@@ -0,0 +1,54 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
15 | # along with this program; if not, write to the Free Software | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | module Redmine | |||
|
19 | module Activity | |||
|
20 | ||||
|
21 | mattr_accessor :available_event_types, :default_event_types, :providers | |||
|
22 | ||||
|
23 | @@available_event_types = [] | |||
|
24 | @@default_event_types = [] | |||
|
25 | @@providers = Hash.new {|h,k| h[k]=[] } | |||
|
26 | ||||
|
27 | class << self | |||
|
28 | def map(&block) | |||
|
29 | yield self | |||
|
30 | end | |||
|
31 | ||||
|
32 | # Registers an activity provider | |||
|
33 | # | |||
|
34 | # Options: | |||
|
35 | # * :class_name - one or more model(s) that provide these events (inferred from event_type by default) | |||
|
36 | # * :default - setting this option to false will make the events not displayed by default | |||
|
37 | # | |||
|
38 | # Examples: | |||
|
39 | # register :issues | |||
|
40 | # register :myevents, :class_name => 'Meeting' | |||
|
41 | def register(event_type, options={}) | |||
|
42 | options.assert_valid_keys(:class_name, :default) | |||
|
43 | ||||
|
44 | event_type = event_type.to_s | |||
|
45 | providers = options[:class_name] || event_type.classify | |||
|
46 | providers = ([] << providers) unless providers.is_a?(Array) | |||
|
47 | ||||
|
48 | @@available_event_types << event_type unless @@available_event_types.include?(event_type) | |||
|
49 | @@default_event_types << event_type unless options[:default] == false | |||
|
50 | @@providers[event_type] += providers | |||
|
51 | end | |||
|
52 | end | |||
|
53 | end | |||
|
54 | end |
@@ -0,0 +1,79 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
15 | # along with this program; if not, write to the Free Software | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | module Redmine | |||
|
19 | module Activity | |||
|
20 | # Class used to retrieve activity events | |||
|
21 | class Fetcher | |||
|
22 | attr_reader :user, :project, :scope | |||
|
23 | ||||
|
24 | # Needs to be unloaded in development mode | |||
|
25 | @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } } | |||
|
26 | ||||
|
27 | def initialize(user, options={}) | |||
|
28 | options.assert_valid_keys(:project, :with_subprojects) | |||
|
29 | @user = user | |||
|
30 | @project = options[:project] | |||
|
31 | @options = options | |||
|
32 | ||||
|
33 | @scope = event_types | |||
|
34 | end | |||
|
35 | ||||
|
36 | # Returns an array of available event types | |||
|
37 | def event_types | |||
|
38 | return @event_types unless @event_types.nil? | |||
|
39 | ||||
|
40 | @event_types = Redmine::Activity.available_event_types | |||
|
41 | @event_types = @event_types.select {|o| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project | |||
|
42 | @event_types | |||
|
43 | end | |||
|
44 | ||||
|
45 | # Yields to filter the activity scope | |||
|
46 | def scope_select(&block) | |||
|
47 | @scope = @scope.select {|t| yield t } | |||
|
48 | end | |||
|
49 | ||||
|
50 | # Sets the scope | |||
|
51 | def scope=(s) | |||
|
52 | @scope = s & event_types | |||
|
53 | end | |||
|
54 | ||||
|
55 | # Resets the scope to the default scope | |||
|
56 | def default_scope! | |||
|
57 | @scope = Redmine::Activity.default_event_types | |||
|
58 | end | |||
|
59 | ||||
|
60 | # Returns an array of events for the given date range | |||
|
61 | def events(from, to) | |||
|
62 | e = [] | |||
|
63 | ||||
|
64 | @scope.each do |event_type| | |||
|
65 | constantized_providers(event_type).each do |provider| | |||
|
66 | e += provider.find_events(event_type, @user, from, to, @options) | |||
|
67 | end | |||
|
68 | end | |||
|
69 | e | |||
|
70 | end | |||
|
71 | ||||
|
72 | private | |||
|
73 | ||||
|
74 | def constantized_providers(event_type) | |||
|
75 | @@constantized_providers[event_type] | |||
|
76 | end | |||
|
77 | end | |||
|
78 | end | |||
|
79 | end |
@@ -0,0 +1,71 | |||||
|
1 | # redMine - project management software | |||
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
15 | # along with this program; if not, write to the Free Software | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | require File.dirname(__FILE__) + '/../test_helper' | |||
|
19 | ||||
|
20 | class ActivityTest < Test::Unit::TestCase | |||
|
21 | fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details, | |||
|
22 | :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages | |||
|
23 | ||||
|
24 | def setup | |||
|
25 | @project = Project.find(1) | |||
|
26 | end | |||
|
27 | ||||
|
28 | def test_activity_without_subprojects | |||
|
29 | events = find_events(User.anonymous, :project => @project) | |||
|
30 | assert_not_nil events | |||
|
31 | ||||
|
32 | assert events.include?(Issue.find(1)) | |||
|
33 | assert !events.include?(Issue.find(4)) | |||
|
34 | # subproject issue | |||
|
35 | assert !events.include?(Issue.find(5)) | |||
|
36 | end | |||
|
37 | ||||
|
38 | def test_activity_with_subprojects | |||
|
39 | events = find_events(User.anonymous, :project => @project, :with_subprojects => 1) | |||
|
40 | assert_not_nil events | |||
|
41 | ||||
|
42 | assert events.include?(Issue.find(1)) | |||
|
43 | # subproject issue | |||
|
44 | assert events.include?(Issue.find(5)) | |||
|
45 | end | |||
|
46 | ||||
|
47 | def test_global_activity_anonymous | |||
|
48 | events = find_events(User.anonymous) | |||
|
49 | assert_not_nil events | |||
|
50 | ||||
|
51 | assert events.include?(Issue.find(1)) | |||
|
52 | assert events.include?(Message.find(5)) | |||
|
53 | # Issue of a private project | |||
|
54 | assert !events.include?(Issue.find(4)) | |||
|
55 | end | |||
|
56 | ||||
|
57 | def test_global_activity_logged_user | |||
|
58 | events = find_events(User.find(2)) # manager | |||
|
59 | assert_not_nil events | |||
|
60 | ||||
|
61 | assert events.include?(Issue.find(1)) | |||
|
62 | # Issue of a private project the user belongs to | |||
|
63 | assert events.include?(Issue.find(4)) | |||
|
64 | end | |||
|
65 | ||||
|
66 | private | |||
|
67 | ||||
|
68 | def find_events(user, options={}) | |||
|
69 | Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1) | |||
|
70 | end | |||
|
71 | end |
@@ -0,0 +1,2 | |||||
|
1 | require File.dirname(__FILE__) + '/lib/acts_as_activity_provider' | |||
|
2 | ActiveRecord::Base.send(:include, Redmine::Acts::ActivityProvider) |
@@ -0,0 +1,68 | |||||
|
1 | # redMine - project management software | |||
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
15 | # along with this program; if not, write to the Free Software | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | module Redmine | |||
|
19 | module Acts | |||
|
20 | module ActivityProvider | |||
|
21 | def self.included(base) | |||
|
22 | base.extend ClassMethods | |||
|
23 | end | |||
|
24 | ||||
|
25 | module ClassMethods | |||
|
26 | def acts_as_activity_provider(options = {}) | |||
|
27 | unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods) | |||
|
28 | cattr_accessor :activity_provider_options | |||
|
29 | send :include, Redmine::Acts::ActivityProvider::InstanceMethods | |||
|
30 | end | |||
|
31 | ||||
|
32 | options.assert_valid_keys(:type, :permission, :timestamp, :find_options) | |||
|
33 | self.activity_provider_options ||= {} | |||
|
34 | ||||
|
35 | # One model can provide different event types | |||
|
36 | # We store these options in activity_provider_options hash | |||
|
37 | event_type = options.delete(:type) || self.name.underscore.pluralize | |||
|
38 | ||||
|
39 | options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless options.has_key?(:permission) | |||
|
40 | options[:timestamp] ||= "#{table_name}.created_on" | |||
|
41 | options[:find_options] ||= {} | |||
|
42 | self.activity_provider_options[event_type] = options | |||
|
43 | end | |||
|
44 | end | |||
|
45 | ||||
|
46 | module InstanceMethods | |||
|
47 | def self.included(base) | |||
|
48 | base.extend ClassMethods | |||
|
49 | end | |||
|
50 | ||||
|
51 | module ClassMethods | |||
|
52 | # Returns events of type event_type visible by user that occured between from and to | |||
|
53 | def find_events(event_type, user, from, to, options) | |||
|
54 | provider_options = activity_provider_options[event_type] | |||
|
55 | raise "#{self.name} can not provide #{event_type} events." if provider_options.nil? | |||
|
56 | ||||
|
57 | cond = ARCondition.new(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to]) | |||
|
58 | cond.add(Project.allowed_to_condition(user, provider_options[:permission], options)) if provider_options[:permission] | |||
|
59 | ||||
|
60 | with_scope(:find => { :conditions => cond.conditions }) do | |||
|
61 | find(:all, provider_options[:find_options]) | |||
|
62 | end | |||
|
63 | end | |||
|
64 | end | |||
|
65 | end | |||
|
66 | end | |||
|
67 | end | |||
|
68 | end |
@@ -226,94 +226,22 class ProjectsController < ApplicationController | |||||
226 |
|
226 | |||
227 | @date_to ||= Date.today + 1 |
|
227 | @date_to ||= Date.today + 1 | |
228 | @date_from = @date_to - @days |
|
228 | @date_from = @date_to - @days | |
|
229 | @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') | |||
229 |
|
230 | |||
230 | @event_types = %w(issues news files documents changesets wiki_pages messages) |
|
231 | @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects) | |
231 | if @project |
|
232 | @activity.scope_select {|t| !params["show_#{t}"].nil?} | |
232 | @event_types.delete('wiki_pages') unless @project.wiki |
|
233 | @activity.default_scope! if @activity.scope.empty? | |
233 | @event_types.delete('changesets') unless @project.repository |
|
|||
234 | @event_types.delete('messages') unless @project.boards.any? |
|
|||
235 | # only show what the user is allowed to view |
|
|||
236 | @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} |
|
|||
237 | @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') |
|
|||
238 | end |
|
|||
239 | @scope = @event_types.select {|t| params["show_#{t}"]} |
|
|||
240 | # default events if none is specified in parameters |
|
|||
241 | @scope = (@event_types - %w(wiki_pages messages))if @scope.empty? |
|
|||
242 |
|
||||
243 | @events = [] |
|
|||
244 |
|
||||
245 | if @scope.include?('issues') |
|
|||
246 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
247 | cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
248 | @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions) |
|
|||
249 |
|
||||
250 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
251 | cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
252 | cond.add("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''") |
|
|||
253 | @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions) |
|
|||
254 | end |
|
|||
255 |
|
||||
256 | if @scope.include?('news') |
|
|||
257 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
258 | cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
259 | @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions) |
|
|||
260 | end |
|
|||
261 |
|
||||
262 | if @scope.include?('files') |
|
|||
263 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
264 | cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
265 | @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", |
|
|||
266 | :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + |
|
|||
267 | "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id", |
|
|||
268 | :conditions => cond.conditions) |
|
|||
269 | end |
|
|||
270 |
|
||||
271 | if @scope.include?('documents') |
|
|||
272 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
273 | cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
274 | @events += Document.find(:all, :include => :project, :conditions => cond.conditions) |
|
|||
275 |
|
||||
276 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
277 | cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
278 | @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", |
|
|||
279 | :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + |
|
|||
280 | "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id", |
|
|||
281 | :conditions => cond.conditions) |
|
|||
282 | end |
|
|||
283 |
|
||||
284 | if @scope.include?('wiki_pages') |
|
|||
285 | select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + |
|
|||
286 | "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + |
|
|||
287 | "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + |
|
|||
288 | "#{WikiContent.versioned_table_name}.id" |
|
|||
289 | joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + |
|
|||
290 | "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + |
|
|||
291 | "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id" |
|
|||
292 |
|
234 | |||
293 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects)) |
|
235 | events = @activity.events(@date_from, @date_to) | |
294 | cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
295 | @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions) |
|
|||
296 | end |
|
|||
297 |
|
||||
298 | if @scope.include?('changesets') |
|
|||
299 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
300 | cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
301 | @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions) |
|
|||
302 | end |
|
|||
303 |
|
||||
304 | if @scope.include?('messages') |
|
|||
305 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects)) |
|
|||
306 | cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) |
|
|||
307 | @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions) |
|
|||
308 | end |
|
|||
309 |
|
||||
310 | @events_by_day = @events.group_by(&:event_date) |
|
|||
311 |
|
236 | |||
312 | respond_to do |format| |
|
237 | respond_to do |format| | |
313 | format.html { render :layout => false if request.xhr? } |
|
238 | format.html { | |
|
239 | @events_by_day = events.group_by(&:event_date) | |||
|
240 | render :layout => false if request.xhr? | |||
|
241 | } | |||
314 | format.atom { |
|
242 | format.atom { | |
315 | title = (@scope.size == 1) ? l("label_#{@scope.first.singularize}_plural") : l(:label_activity) |
|
243 | title = (@scope.size == 1) ? l("label_#{@scope.first.singularize}_plural") : l(:label_activity) | |
316 |
render_feed( |
|
244 | render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") | |
317 | } |
|
245 | } | |
318 | end |
|
246 | end | |
319 | end |
|
247 | end |
@@ -28,6 +28,18 class Attachment < ActiveRecord::Base | |||||
28 | acts_as_event :title => :filename, |
|
28 | acts_as_event :title => :filename, | |
29 | :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} |
|
29 | :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} | |
30 |
|
30 | |||
|
31 | acts_as_activity_provider :type => 'files', | |||
|
32 | :permission => :view_files, | |||
|
33 | :find_options => {:select => "#{Attachment.table_name}.*", | |||
|
34 | :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + | |||
|
35 | "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"} | |||
|
36 | ||||
|
37 | acts_as_activity_provider :type => 'documents', | |||
|
38 | :permission => :view_documents, | |||
|
39 | :find_options => {:select => "#{Attachment.table_name}.*", | |||
|
40 | :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + | |||
|
41 | "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} | |||
|
42 | ||||
31 | cattr_accessor :storage_path |
|
43 | cattr_accessor :storage_path | |
32 | @@storage_path = "#{RAILS_ROOT}/files" |
|
44 | @@storage_path = "#{RAILS_ROOT}/files" | |
33 |
|
45 |
@@ -30,6 +30,9 class Changeset < ActiveRecord::Base | |||||
30 | :include => {:repository => :project}, |
|
30 | :include => {:repository => :project}, | |
31 | :project_key => "#{Repository.table_name}.project_id", |
|
31 | :project_key => "#{Repository.table_name}.project_id", | |
32 | :date_column => 'committed_on' |
|
32 | :date_column => 'committed_on' | |
|
33 | ||||
|
34 | acts_as_activity_provider :timestamp => "#{table_name}.committed_on", | |||
|
35 | :find_options => {:include => {:repository => :project}} | |||
33 |
|
36 | |||
34 | validates_presence_of :repository_id, :revision, :committed_on, :commit_date |
|
37 | validates_presence_of :repository_id, :revision, :committed_on, :commit_date | |
35 | validates_uniqueness_of :revision, :scope => :repository_id |
|
38 | validates_uniqueness_of :revision, :scope => :repository_id |
@@ -24,7 +24,8 class Document < ActiveRecord::Base | |||||
24 | acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, |
|
24 | acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, | |
25 | :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, |
|
25 | :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, | |
26 | :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} |
|
26 | :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} | |
27 |
|
27 | acts_as_activity_provider :find_options => {:include => :project} | ||
|
28 | ||||
28 | validates_presence_of :project, :title, :category |
|
29 | validates_presence_of :project, :title, :category | |
29 | validates_length_of :title, :maximum => 60 |
|
30 | validates_length_of :title, :maximum => 60 | |
30 | end |
|
31 | end |
@@ -42,6 +42,8 class Issue < ActiveRecord::Base | |||||
42 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, |
|
42 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, | |
43 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} |
|
43 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} | |
44 |
|
44 | |||
|
45 | acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]} | |||
|
46 | ||||
45 | validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status |
|
47 | validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status | |
46 | validates_length_of :subject, :maximum => 255 |
|
48 | validates_length_of :subject, :maximum => 255 | |
47 | validates_inclusion_of :done_ratio, :in => 0..100 |
|
49 | validates_inclusion_of :done_ratio, :in => 0..100 |
@@ -31,6 +31,12 class Journal < ActiveRecord::Base | |||||
31 | :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, |
|
31 | :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, | |
32 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} |
|
32 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} | |
33 |
|
33 | |||
|
34 | acts_as_activity_provider :type => 'issues', | |||
|
35 | :permission => :view_issues, | |||
|
36 | :find_options => {:include => [{:issue => :project}, :details, :user], | |||
|
37 | :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + | |||
|
38 | " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} | |||
|
39 | ||||
34 | def save |
|
40 | def save | |
35 | # Do not save an empty journal |
|
41 | # Do not save an empty journal | |
36 | (details.empty? && notes.blank?) ? false : super |
|
42 | (details.empty? && notes.blank?) ? false : super |
@@ -31,7 +31,9 class Message < ActiveRecord::Base | |||||
31 | :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, |
|
31 | :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, | |
32 | :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : |
|
32 | :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : | |
33 | {:id => o.parent_id, :anchor => "message-#{o.id}"})} |
|
33 | {:id => o.parent_id, :anchor => "message-#{o.id}"})} | |
34 |
|
34 | |||
|
35 | acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} | |||
|
36 | ||||
35 | attr_protected :locked, :sticky |
|
37 | attr_protected :locked, :sticky | |
36 | validates_presence_of :subject, :content |
|
38 | validates_presence_of :subject, :content | |
37 | validates_length_of :subject, :maximum => 255 |
|
39 | validates_length_of :subject, :maximum => 255 |
@@ -26,7 +26,8 class News < ActiveRecord::Base | |||||
26 |
|
26 | |||
27 | acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project |
|
27 | acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project | |
28 | acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} |
|
28 | acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} | |
29 |
|
29 | acts_as_activity_provider :find_options => {:include => [:project, :author]} | ||
|
30 | ||||
30 | # returns latest news for projects visible by user |
|
31 | # returns latest news for projects visible by user | |
31 | def self.latest(user=nil, count=5) |
|
32 | def self.latest(user=nil, count=5) | |
32 | find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") |
|
33 | find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") |
@@ -35,6 +35,17 class WikiContent < ActiveRecord::Base | |||||
35 | :type => 'wiki-page', |
|
35 | :type => 'wiki-page', | |
36 | :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} |
|
36 | :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} | |
37 |
|
37 | |||
|
38 | acts_as_activity_provider :type => 'wiki_pages', | |||
|
39 | :timestamp => "#{WikiContent.versioned_table_name}.updated_on", | |||
|
40 | :permission => :view_wiki_pages, | |||
|
41 | :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + | |||
|
42 | "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + | |||
|
43 | "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + | |||
|
44 | "#{WikiContent.versioned_table_name}.id", | |||
|
45 | :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + | |||
|
46 | "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + | |||
|
47 | "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"} | |||
|
48 | ||||
38 | def text=(plain) |
|
49 | def text=(plain) | |
39 | case Setting.wiki_compression |
|
50 | case Setting.wiki_compression | |
40 | when 'gzip' |
|
51 | when 'gzip' |
@@ -44,8 +44,8 | |||||
44 | <% content_for :sidebar do %> |
|
44 | <% content_for :sidebar do %> | |
45 | <% form_tag({}, :method => :get) do %> |
|
45 | <% form_tag({}, :method => :get) do %> | |
46 | <h3><%= l(:label_activity) %></h3> |
|
46 | <h3><%= l(:label_activity) %></h3> | |
47 | <p><% @event_types.each do |t| %> |
|
47 | <p><% @activity.event_types.each do |t| %> | |
48 | <label><%= check_box_tag "show_#{t}", 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> |
|
48 | <label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> | |
49 | <% end %></p> |
|
49 | <% end %></p> | |
50 | <% if @project && @project.active_children.any? %> |
|
50 | <% if @project && @project.active_children.any? %> | |
51 | <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> |
|
51 | <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> |
@@ -1,5 +1,6 | |||||
1 | require 'redmine/access_control' |
|
1 | require 'redmine/access_control' | |
2 | require 'redmine/menu_manager' |
|
2 | require 'redmine/menu_manager' | |
|
3 | require 'redmine/activity' | |||
3 | require 'redmine/mime_type' |
|
4 | require 'redmine/mime_type' | |
4 | require 'redmine/core_ext' |
|
5 | require 'redmine/core_ext' | |
5 | require 'redmine/themes' |
|
6 | require 'redmine/themes' | |
@@ -132,3 +133,13 Redmine::MenuManager.map :project_menu do |menu| | |||||
132 | :if => Proc.new { |p| p.repository && !p.repository.new_record? } |
|
133 | :if => Proc.new { |p| p.repository && !p.repository.new_record? } | |
133 | menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true |
|
134 | menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true | |
134 | end |
|
135 | end | |
|
136 | ||||
|
137 | Redmine::Activity.map do |activity| | |||
|
138 | activity.register :issues, :class_name => %w(Issue Journal) | |||
|
139 | activity.register :changesets | |||
|
140 | activity.register :news | |||
|
141 | activity.register :documents, :class_name => %w(Document Attachment) | |||
|
142 | activity.register :files, :class_name => 'Attachment' | |||
|
143 | activity.register :wiki_pages, :class_name => 'WikiContent::Version', :default => false | |||
|
144 | activity.register :messages, :default => false | |||
|
145 | end |
@@ -153,10 +153,6 class ProjectsControllerTest < Test::Unit::TestCase | |||||
153 | assert_response :success |
|
153 | assert_response :success | |
154 | assert_template 'activity' |
|
154 | assert_template 'activity' | |
155 | assert_not_nil assigns(:events_by_day) |
|
155 | assert_not_nil assigns(:events_by_day) | |
156 | assert_not_nil assigns(:events) |
|
|||
157 |
|
||||
158 | # subproject issue not included by default |
|
|||
159 | assert !assigns(:events).include?(Issue.find(5)) |
|
|||
160 |
|
156 | |||
161 | assert_tag :tag => "h3", |
|
157 | assert_tag :tag => "h3", | |
162 | :content => /#{2.days.ago.to_date.day}/, |
|
158 | :content => /#{2.days.ago.to_date.day}/, | |
@@ -168,7 +164,9 class ProjectsControllerTest < Test::Unit::TestCase | |||||
168 | } |
|
164 | } | |
169 | } |
|
165 | } | |
170 | } |
|
166 | } | |
171 |
|
167 | end | ||
|
168 | ||||
|
169 | def test_previous_project_activity | |||
172 | get :activity, :id => 1, :from => 3.days.ago.to_date |
|
170 | get :activity, :id => 1, :from => 3.days.ago.to_date | |
173 | assert_response :success |
|
171 | assert_response :success | |
174 | assert_template 'activity' |
|
172 | assert_template 'activity' | |
@@ -186,53 +184,24 class ProjectsControllerTest < Test::Unit::TestCase | |||||
186 | } |
|
184 | } | |
187 | end |
|
185 | end | |
188 |
|
186 | |||
189 |
def test_activity |
|
187 | def test_global_activity | |
190 | get :activity, :id => 1, :with_subprojects => 1 |
|
|||
191 | assert_response :success |
|
|||
192 | assert_template 'activity' |
|
|||
193 | assert_not_nil assigns(:events) |
|
|||
194 |
|
||||
195 | assert assigns(:events).include?(Issue.find(1)) |
|
|||
196 | assert !assigns(:events).include?(Issue.find(4)) |
|
|||
197 | # subproject issue |
|
|||
198 | assert assigns(:events).include?(Issue.find(5)) |
|
|||
199 | end |
|
|||
200 |
|
||||
201 | def test_global_activity_anonymous |
|
|||
202 | get :activity |
|
|||
203 | assert_response :success |
|
|||
204 | assert_template 'activity' |
|
|||
205 | assert_not_nil assigns(:events) |
|
|||
206 |
|
||||
207 | assert assigns(:events).include?(Issue.find(1)) |
|
|||
208 | # Issue of a private project |
|
|||
209 | assert !assigns(:events).include?(Issue.find(4)) |
|
|||
210 | end |
|
|||
211 |
|
||||
212 | def test_global_activity_logged_user |
|
|||
213 | @request.session[:user_id] = 2 # manager |
|
|||
214 | get :activity |
|
188 | get :activity | |
215 | assert_response :success |
|
189 | assert_response :success | |
216 | assert_template 'activity' |
|
190 | assert_template 'activity' | |
217 | assert_not_nil assigns(:events) |
|
191 | assert_not_nil assigns(:events_by_day) | |
218 |
|
192 | |||
219 | assert assigns(:events).include?(Issue.find(1)) |
|
193 | assert_tag :tag => "h3", | |
220 | # Issue of a private project the user belongs to |
|
194 | :content => /#{5.day.ago.to_date.day}/, | |
221 | assert assigns(:events).include?(Issue.find(4)) |
|
195 | :sibling => { :tag => "dl", | |
|
196 | :child => { :tag => "dt", | |||
|
197 | :attributes => { :class => /issue/ }, | |||
|
198 | :child => { :tag => "a", | |||
|
199 | :content => /#{Issue.find(5).subject}/, | |||
|
200 | } | |||
|
201 | } | |||
|
202 | } | |||
222 | end |
|
203 | end | |
223 |
|
||||
224 |
|
204 | |||
225 | def test_global_activity_with_all_types |
|
|||
226 | get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1 |
|
|||
227 | assert_response :success |
|
|||
228 | assert_template 'activity' |
|
|||
229 | assert_not_nil assigns(:events) |
|
|||
230 |
|
||||
231 | assert assigns(:events).include?(Issue.find(1)) |
|
|||
232 | assert !assigns(:events).include?(Issue.find(4)) |
|
|||
233 | assert assigns(:events).include?(Message.find(5)) |
|
|||
234 | end |
|
|||
235 |
|
||||
236 | def test_calendar |
|
205 | def test_calendar | |
237 | get :calendar, :id => 1 |
|
206 | get :calendar, :id => 1 | |
238 | assert_response :success |
|
207 | assert_response :success |
General Comments 0
You need to be logged in to leave comments.
Login now