@@ -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 | 227 | @date_to ||= Date.today + 1 |
|
228 | 228 | @date_from = @date_to - @days |
|
229 | ||
|
230 | @event_types = %w(issues news files documents changesets wiki_pages messages) | |
|
231 | if @project | |
|
232 | @event_types.delete('wiki_pages') unless @project.wiki | |
|
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 | 229 |
|
|
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 | 230 | |
|
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) | |
|
231 | @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects) | |
|
232 | @activity.scope_select {|t| !params["show_#{t}"].nil?} | |
|
233 | @activity.default_scope! if @activity.scope.empty? | |
|
275 | 234 | |
|
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 | ||
|
293 | cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects)) | |
|
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) | |
|
235 | events = @activity.events(@date_from, @date_to) | |
|
311 | 236 | |
|
312 | 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 | 242 | format.atom { |
|
315 | 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 | 246 | end |
|
319 | 247 | end |
@@ -28,6 +28,18 class Attachment < ActiveRecord::Base | |||
|
28 | 28 | acts_as_event :title => :filename, |
|
29 | 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 | 43 | cattr_accessor :storage_path |
|
32 | 44 | @@storage_path = "#{RAILS_ROOT}/files" |
|
33 | 45 |
@@ -31,6 +31,9 class Changeset < ActiveRecord::Base | |||
|
31 | 31 | :project_key => "#{Repository.table_name}.project_id", |
|
32 | 32 | :date_column => 'committed_on' |
|
33 | 33 | |
|
34 | acts_as_activity_provider :timestamp => "#{table_name}.committed_on", | |
|
35 | :find_options => {:include => {:repository => :project}} | |
|
36 | ||
|
34 | 37 | validates_presence_of :repository_id, :revision, :committed_on, :commit_date |
|
35 | 38 | validates_uniqueness_of :revision, :scope => :repository_id |
|
36 | 39 | validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true |
@@ -24,6 +24,7 class Document < ActiveRecord::Base | |||
|
24 | 24 | acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, |
|
25 | 25 | :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, |
|
26 | 26 | :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} |
|
27 | acts_as_activity_provider :find_options => {:include => :project} | |
|
27 | 28 | |
|
28 | 29 | validates_presence_of :project, :title, :category |
|
29 | 30 | validates_length_of :title, :maximum => 60 |
@@ -42,6 +42,8 class Issue < ActiveRecord::Base | |||
|
42 | 42 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, |
|
43 | 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 | 47 | validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status |
|
46 | 48 | validates_length_of :subject, :maximum => 255 |
|
47 | 49 | validates_inclusion_of :done_ratio, :in => 0..100 |
@@ -31,6 +31,12 class Journal < ActiveRecord::Base | |||
|
31 | 31 | :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, |
|
32 | 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 | 40 | def save |
|
35 | 41 | # Do not save an empty journal |
|
36 | 42 | (details.empty? && notes.blank?) ? false : super |
@@ -32,6 +32,8 class Message < ActiveRecord::Base | |||
|
32 | 32 | :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : |
|
33 | 33 | {:id => o.parent_id, :anchor => "message-#{o.id}"})} |
|
34 | 34 | |
|
35 | acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} | |
|
36 | ||
|
35 | 37 | attr_protected :locked, :sticky |
|
36 | 38 | validates_presence_of :subject, :content |
|
37 | 39 | validates_length_of :subject, :maximum => 255 |
@@ -26,6 +26,7 class News < ActiveRecord::Base | |||
|
26 | 26 | |
|
27 | 27 | acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project |
|
28 | 28 | acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} |
|
29 | acts_as_activity_provider :find_options => {:include => [:project, :author]} | |
|
29 | 30 | |
|
30 | 31 | # returns latest news for projects visible by user |
|
31 | 32 | def self.latest(user=nil, count=5) |
@@ -35,6 +35,17 class WikiContent < ActiveRecord::Base | |||
|
35 | 35 | :type => 'wiki-page', |
|
36 | 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 | 49 | def text=(plain) |
|
39 | 50 | case Setting.wiki_compression |
|
40 | 51 | when 'gzip' |
@@ -44,8 +44,8 | |||
|
44 | 44 | <% content_for :sidebar do %> |
|
45 | 45 | <% form_tag({}, :method => :get) do %> |
|
46 | 46 | <h3><%= l(:label_activity) %></h3> |
|
47 | <p><% @event_types.each do |t| %> | |
|
48 | <label><%= check_box_tag "show_#{t}", 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> | |
|
47 | <p><% @activity.event_types.each do |t| %> | |
|
48 | <label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> | |
|
49 | 49 | <% end %></p> |
|
50 | 50 | <% if @project && @project.active_children.any? %> |
|
51 | 51 | <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> |
@@ -1,5 +1,6 | |||
|
1 | 1 | require 'redmine/access_control' |
|
2 | 2 | require 'redmine/menu_manager' |
|
3 | require 'redmine/activity' | |
|
3 | 4 | require 'redmine/mime_type' |
|
4 | 5 | require 'redmine/core_ext' |
|
5 | 6 | require 'redmine/themes' |
@@ -132,3 +133,13 Redmine::MenuManager.map :project_menu do |menu| | |||
|
132 | 133 | :if => Proc.new { |p| p.repository && !p.repository.new_record? } |
|
133 | 134 | menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true |
|
134 | 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 | 153 | assert_response :success |
|
154 | 154 | assert_template 'activity' |
|
155 | 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 | 157 | assert_tag :tag => "h3", |
|
162 | 158 | :content => /#{2.days.ago.to_date.day}/, |
@@ -168,7 +164,9 class ProjectsControllerTest < Test::Unit::TestCase | |||
|
168 | 164 | } |
|
169 | 165 | } |
|
170 | 166 | } |
|
167 | end | |
|
171 | 168 | |
|
169 | def test_previous_project_activity | |
|
172 | 170 | get :activity, :id => 1, :from => 3.days.ago.to_date |
|
173 | 171 | assert_response :success |
|
174 | 172 | assert_template 'activity' |
@@ -186,51 +184,22 class ProjectsControllerTest < Test::Unit::TestCase | |||
|
186 | 184 | } |
|
187 | 185 | end |
|
188 | 186 | |
|
189 |
def test_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 | |
|
187 | def test_global_activity | |
|
214 | 188 | get :activity |
|
215 | 189 | assert_response :success |
|
216 | 190 | assert_template 'activity' |
|
217 | assert_not_nil assigns(:events) | |
|
218 | ||
|
219 | assert assigns(:events).include?(Issue.find(1)) | |
|
220 | # Issue of a private project the user belongs to | |
|
221 | assert assigns(:events).include?(Issue.find(4)) | |
|
222 | end | |
|
223 | ||
|
224 | ||
|
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) | |
|
191 | assert_not_nil assigns(:events_by_day) | |
|
230 | 192 | |
|
231 | assert assigns(:events).include?(Issue.find(1)) | |
|
232 | assert !assigns(:events).include?(Issue.find(4)) | |
|
233 | assert assigns(:events).include?(Message.find(5)) | |
|
193 | assert_tag :tag => "h3", | |
|
194 | :content => /#{5.day.ago.to_date.day}/, | |
|
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 | } | |
|
234 | 203 | end |
|
235 | 204 | |
|
236 | 205 | def test_calendar |
General Comments 0
You need to be logged in to leave comments.
Login now