##// END OF EJS Templates
Activity refactoring....
Jean-Philippe Lang -
r1692:a774c5c48b5e
parent child
Show More
@@ -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
@@ -1,437 +1,365
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 layout 'base'
20 20 menu_item :overview
21 21 menu_item :activity, :only => :activity
22 22 menu_item :roadmap, :only => :roadmap
23 23 menu_item :files, :only => [:list_files, :add_file]
24 24 menu_item :settings, :only => :settings
25 25 menu_item :issues, :only => [:changelog]
26 26
27 27 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
28 28 before_filter :find_optional_project, :only => :activity
29 29 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
30 30 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
31 31 accept_key_auth :activity, :calendar
32 32
33 33 helper :sort
34 34 include SortHelper
35 35 helper :custom_fields
36 36 include CustomFieldsHelper
37 37 helper :ifpdf
38 38 include IfpdfHelper
39 39 helper :issues
40 40 helper IssuesHelper
41 41 helper :queries
42 42 include QueriesHelper
43 43 helper :repositories
44 44 include RepositoriesHelper
45 45 include ProjectsHelper
46 46
47 47 # Lists visible projects
48 48 def index
49 49 projects = Project.find :all,
50 50 :conditions => Project.visible_by(User.current),
51 51 :include => :parent
52 52 respond_to do |format|
53 53 format.html {
54 54 @project_tree = projects.group_by {|p| p.parent || p}
55 55 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
56 56 }
57 57 format.atom {
58 58 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
59 59 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 60 }
61 61 end
62 62 end
63 63
64 64 # Add a new project
65 65 def add
66 66 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 67 @trackers = Tracker.all
68 68 @root_projects = Project.find(:all,
69 69 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
70 70 :order => 'name')
71 71 @project = Project.new(params[:project])
72 72 if request.get?
73 73 @project.trackers = Tracker.all
74 74 @project.is_public = Setting.default_projects_public?
75 75 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
76 76 else
77 77 @project.enabled_module_names = params[:enabled_modules]
78 78 if @project.save
79 79 flash[:notice] = l(:notice_successful_create)
80 80 redirect_to :controller => 'admin', :action => 'projects'
81 81 end
82 82 end
83 83 end
84 84
85 85 # Show @project
86 86 def show
87 87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
88 88 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
89 89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
90 90 @trackers = @project.rolled_up_trackers
91 91
92 92 cond = @project.project_condition(Setting.display_subprojects_issues?)
93 93 Issue.visible_by(User.current) do
94 94 @open_issues_by_tracker = Issue.count(:group => :tracker,
95 95 :include => [:project, :status, :tracker],
96 96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
97 97 @total_issues_by_tracker = Issue.count(:group => :tracker,
98 98 :include => [:project, :status, :tracker],
99 99 :conditions => cond)
100 100 end
101 101 TimeEntry.visible_by(User.current) do
102 102 @total_hours = TimeEntry.sum(:hours,
103 103 :include => :project,
104 104 :conditions => cond).to_f
105 105 end
106 106 @key = User.current.rss_key
107 107 end
108 108
109 109 def settings
110 110 @root_projects = Project.find(:all,
111 111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
112 112 :order => 'name')
113 113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
114 114 @issue_category ||= IssueCategory.new
115 115 @member ||= @project.members.new
116 116 @trackers = Tracker.all
117 117 @repository ||= @project.repository
118 118 @wiki ||= @project.wiki
119 119 end
120 120
121 121 # Edit @project
122 122 def edit
123 123 if request.post?
124 124 @project.attributes = params[:project]
125 125 if @project.save
126 126 flash[:notice] = l(:notice_successful_update)
127 127 redirect_to :action => 'settings', :id => @project
128 128 else
129 129 settings
130 130 render :action => 'settings'
131 131 end
132 132 end
133 133 end
134 134
135 135 def modules
136 136 @project.enabled_module_names = params[:enabled_modules]
137 137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
138 138 end
139 139
140 140 def archive
141 141 @project.archive if request.post? && @project.active?
142 142 redirect_to :controller => 'admin', :action => 'projects'
143 143 end
144 144
145 145 def unarchive
146 146 @project.unarchive if request.post? && !@project.active?
147 147 redirect_to :controller => 'admin', :action => 'projects'
148 148 end
149 149
150 150 # Delete @project
151 151 def destroy
152 152 @project_to_destroy = @project
153 153 if request.post? and params[:confirm]
154 154 @project_to_destroy.destroy
155 155 redirect_to :controller => 'admin', :action => 'projects'
156 156 end
157 157 # hide project in layout
158 158 @project = nil
159 159 end
160 160
161 161 # Add a new issue category to @project
162 162 def add_issue_category
163 163 @category = @project.issue_categories.build(params[:category])
164 164 if request.post? and @category.save
165 165 respond_to do |format|
166 166 format.html do
167 167 flash[:notice] = l(:notice_successful_create)
168 168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
169 169 end
170 170 format.js do
171 171 # IE doesn't support the replace_html rjs method for select box options
172 172 render(:update) {|page| page.replace "issue_category_id",
173 173 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
174 174 }
175 175 end
176 176 end
177 177 end
178 178 end
179 179
180 180 # Add a new version to @project
181 181 def add_version
182 182 @version = @project.versions.build(params[:version])
183 183 if request.post? and @version.save
184 184 flash[:notice] = l(:notice_successful_create)
185 185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
186 186 end
187 187 end
188 188
189 189 def add_file
190 190 if request.post?
191 191 @version = @project.versions.find_by_id(params[:version_id])
192 192 attachments = attach_files(@version, params[:attachments])
193 193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
194 194 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
195 195 end
196 196 @versions = @project.versions.sort
197 197 end
198 198
199 199 def list_files
200 200 sort_init "#{Attachment.table_name}.filename", "asc"
201 201 sort_update
202 202 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
203 203 render :layout => !request.xhr?
204 204 end
205 205
206 206 # Show changelog for @project
207 207 def changelog
208 208 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
209 209 retrieve_selected_tracker_ids(@trackers)
210 210 @versions = @project.versions.sort
211 211 end
212 212
213 213 def roadmap
214 214 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
215 215 retrieve_selected_tracker_ids(@trackers)
216 216 @versions = @project.versions.sort
217 217 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
218 218 end
219 219
220 220 def activity
221 221 @days = Setting.activity_days_default.to_i
222 222
223 223 if params[:from]
224 224 begin; @date_to = params[:from].to_date; rescue; end
225 225 end
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 @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 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(@events, :title => "#{@project || Setting.app_title}: #{title}")
244 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
317 245 }
318 246 end
319 247 end
320 248
321 249 def calendar
322 250 @trackers = @project.rolled_up_trackers
323 251 retrieve_selected_tracker_ids(@trackers)
324 252
325 253 if params[:year] and params[:year].to_i > 1900
326 254 @year = params[:year].to_i
327 255 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
328 256 @month = params[:month].to_i
329 257 end
330 258 end
331 259 @year ||= Date.today.year
332 260 @month ||= Date.today.month
333 261 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
334 262 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
335 263 events = []
336 264 @project.issues_with_subprojects(@with_subprojects) do
337 265 events += Issue.find(:all,
338 266 :include => [:tracker, :status, :assigned_to, :priority, :project],
339 267 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
340 268 ) unless @selected_tracker_ids.empty?
341 269 events += Version.find(:all, :include => :project,
342 270 :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
343 271 end
344 272 @calendar.events = events
345 273
346 274 render :layout => false if request.xhr?
347 275 end
348 276
349 277 def gantt
350 278 @trackers = @project.rolled_up_trackers
351 279 retrieve_selected_tracker_ids(@trackers)
352 280
353 281 if params[:year] and params[:year].to_i >0
354 282 @year_from = params[:year].to_i
355 283 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
356 284 @month_from = params[:month].to_i
357 285 else
358 286 @month_from = 1
359 287 end
360 288 else
361 289 @month_from ||= Date.today.month
362 290 @year_from ||= Date.today.year
363 291 end
364 292
365 293 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
366 294 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
367 295 months = (params[:months] || User.current.pref[:gantt_months]).to_i
368 296 @months = (months > 0 && months < 25) ? months : 6
369 297
370 298 # Save gantt paramters as user preference (zoom and months count)
371 299 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
372 300 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
373 301 User.current.preference.save
374 302 end
375 303
376 304 @date_from = Date.civil(@year_from, @month_from, 1)
377 305 @date_to = (@date_from >> @months) - 1
378 306 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
379 307
380 308 @events = []
381 309 @project.issues_with_subprojects(@with_subprojects) do
382 310 # Issues that have start and due dates
383 311 @events += Issue.find(:all,
384 312 :order => "start_date, due_date",
385 313 :include => [:tracker, :status, :assigned_to, :priority, :project],
386 314 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
387 315 ) unless @selected_tracker_ids.empty?
388 316 # Issues that don't have a due date but that are assigned to a version with a date
389 317 @events += Issue.find(:all,
390 318 :order => "start_date, effective_date",
391 319 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
392 320 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
393 321 ) unless @selected_tracker_ids.empty?
394 322 @events += Version.find(:all, :include => :project,
395 323 :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
396 324 end
397 325 @events.sort! {|x,y| x.start_date <=> y.start_date }
398 326
399 327 if params[:format]=='pdf'
400 328 @options_for_rfpdf ||= {}
401 329 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
402 330 render :template => "projects/gantt.rfpdf", :layout => false
403 331 elsif params[:format]=='png' && respond_to?('gantt_image')
404 332 image = gantt_image(@events, @date_from, @months, @zoom)
405 333 image.format = 'PNG'
406 334 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
407 335 else
408 336 render :template => "projects/gantt.rhtml"
409 337 end
410 338 end
411 339
412 340 private
413 341 # Find project of id params[:id]
414 342 # if not found, redirect to project list
415 343 # Used as a before_filter
416 344 def find_project
417 345 @project = Project.find(params[:id])
418 346 rescue ActiveRecord::RecordNotFound
419 347 render_404
420 348 end
421 349
422 350 def find_optional_project
423 351 return true unless params[:id]
424 352 @project = Project.find(params[:id])
425 353 authorize
426 354 rescue ActiveRecord::RecordNotFound
427 355 render_404
428 356 end
429 357
430 358 def retrieve_selected_tracker_ids(selectable_trackers)
431 359 if ids = params[:tracker_ids]
432 360 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
433 361 else
434 362 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
435 363 end
436 364 end
437 365 end
@@ -1,122 +1,134
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 23
24 24 validates_presence_of :container, :filename, :author
25 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27
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
34 46 def validate
35 47 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
36 48 end
37 49
38 50 def file=(incoming_file)
39 51 unless incoming_file.nil?
40 52 @temp_file = incoming_file
41 53 if @temp_file.size > 0
42 54 self.filename = sanitize_filename(@temp_file.original_filename)
43 55 self.disk_filename = Attachment.disk_filename(filename)
44 56 self.content_type = @temp_file.content_type.to_s.chomp
45 57 self.filesize = @temp_file.size
46 58 end
47 59 end
48 60 end
49 61
50 62 def file
51 63 nil
52 64 end
53 65
54 66 # Copy temp file to its final location
55 67 def before_save
56 68 if @temp_file && (@temp_file.size > 0)
57 69 logger.debug("saving '#{self.diskfile}'")
58 70 File.open(diskfile, "wb") do |f|
59 71 f.write(@temp_file.read)
60 72 end
61 73 self.digest = Digest::MD5.hexdigest(File.read(diskfile))
62 74 end
63 75 # Don't save the content type if it's longer than the authorized length
64 76 if self.content_type && self.content_type.length > 255
65 77 self.content_type = nil
66 78 end
67 79 end
68 80
69 81 # Deletes file on the disk
70 82 def after_destroy
71 83 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
72 84 end
73 85
74 86 # Returns file's location on disk
75 87 def diskfile
76 88 "#{@@storage_path}/#{self.disk_filename}"
77 89 end
78 90
79 91 def increment_download
80 92 increment!(:downloads)
81 93 end
82 94
83 95 def project
84 96 container.project
85 97 end
86 98
87 99 def image?
88 100 self.filename =~ /\.(jpe?g|gif|png)$/i
89 101 end
90 102
91 103 def is_text?
92 104 Redmine::MimeType.is_type?('text', filename)
93 105 end
94 106
95 107 def is_diff?
96 108 self.filename =~ /\.(patch|diff)$/i
97 109 end
98 110
99 111 private
100 112 def sanitize_filename(value)
101 113 # get only the filename, not the whole path
102 114 just_filename = value.gsub(/^.*(\\|\/)/, '')
103 115 # NOTE: File.basename doesn't work right with Windows paths on Unix
104 116 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
105 117
106 118 # Finally, replace all non alphanumeric, hyphens or periods with underscore
107 119 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
108 120 end
109 121
110 122 # Returns an ASCII or hashed filename
111 123 def self.disk_filename(filename)
112 124 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
113 125 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
114 126 df << filename
115 127 else
116 128 df << Digest::MD5.hexdigest(filename)
117 129 # keep the extension if any
118 130 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
119 131 end
120 132 df
121 133 end
122 134 end
@@ -1,131 +1,134
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Changeset < ActiveRecord::Base
19 19 belongs_to :repository
20 20 has_many :changes, :dependent => :delete_all
21 21 has_and_belongs_to_many :issues
22 22
23 23 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
24 24 :description => :comments,
25 25 :datetime => :committed_on,
26 26 :author => :committer,
27 27 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
28 28
29 29 acts_as_searchable :columns => 'comments',
30 30 :include => {:repository => :project},
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
37 40
38 41 def revision=(r)
39 42 write_attribute :revision, (r.nil? ? nil : r.to_s)
40 43 end
41 44
42 45 def comments=(comment)
43 46 write_attribute(:comments, comment.strip)
44 47 end
45 48
46 49 def committed_on=(date)
47 50 self.commit_date = date
48 51 super
49 52 end
50 53
51 54 def project
52 55 repository.project
53 56 end
54 57
55 58 def after_create
56 59 scan_comment_for_issue_ids
57 60 end
58 61 require 'pp'
59 62
60 63 def scan_comment_for_issue_ids
61 64 return if comments.blank?
62 65 # keywords used to reference issues
63 66 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
64 67 # keywords used to fix issues
65 68 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
66 69 # status and optional done ratio applied
67 70 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
68 71 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
69 72
70 73 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
71 74 return if kw_regexp.blank?
72 75
73 76 referenced_issues = []
74 77
75 78 if ref_keywords.delete('*')
76 79 # find any issue ID in the comments
77 80 target_issue_ids = []
78 81 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
79 82 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
80 83 end
81 84
82 85 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
83 86 action = match[0]
84 87 target_issue_ids = match[1].scan(/\d+/)
85 88 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
86 89 if fix_status && fix_keywords.include?(action.downcase)
87 90 # update status of issues
88 91 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
89 92 target_issues.each do |issue|
90 93 # the issue may have been updated by the closure of another one (eg. duplicate)
91 94 issue.reload
92 95 # don't change the status is the issue is closed
93 96 next if issue.status.is_closed?
94 97 user = committer_user || User.anonymous
95 98 csettext = "r#{self.revision}"
96 99 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
97 100 csettext = "commit:\"#{self.scmid}\""
98 101 end
99 102 journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
100 103 issue.status = fix_status
101 104 issue.done_ratio = done_ratio if done_ratio
102 105 issue.save
103 106 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
104 107 end
105 108 end
106 109 referenced_issues += target_issues
107 110 end
108 111
109 112 self.issues = referenced_issues.uniq
110 113 end
111 114
112 115 # Returns the Redmine User corresponding to the committer
113 116 def committer_user
114 117 if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
115 118 username, email = $1.strip, $3
116 119 u = User.find_by_login(username)
117 120 u ||= User.find_by_mail(email) unless email.blank?
118 121 u
119 122 end
120 123 end
121 124
122 125 # Returns the previous changeset
123 126 def previous
124 127 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
125 128 end
126 129
127 130 # Returns the next changeset
128 131 def next
129 132 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
130 133 end
131 134 end
@@ -1,30 +1,31
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Document < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
21 21 has_many :attachments, :as => :container, :dependent => :destroy
22 22
23 23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
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
30 31 end
@@ -1,259 +1,261
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :attachments, :as => :container, :dependent => :destroy
30 30 has_many :time_entries, :dependent => :delete_all
31 31 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 32
33 33 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 34 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 35
36 36 acts_as_customizable
37 37 acts_as_watchable
38 38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 39 :include => [:project, :journals],
40 40 # sort by id so that limited eager loading doesn't break with postgresql
41 41 :order_column => "#{table_name}.id"
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
48 50 validates_numericality_of :estimated_hours, :allow_nil => true
49 51
50 52 def after_initialize
51 53 if new_record?
52 54 # set default values for new records only
53 55 self.status ||= IssueStatus.default
54 56 self.priority ||= Enumeration.default('IPRI')
55 57 end
56 58 end
57 59
58 60 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
59 61 def available_custom_fields
60 62 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
61 63 end
62 64
63 65 def copy_from(arg)
64 66 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
65 67 self.attributes = issue.attributes.dup
66 68 self.custom_values = issue.custom_values.collect {|v| v.clone}
67 69 self
68 70 end
69 71
70 72 # Move an issue to a new project and tracker
71 73 def move_to(new_project, new_tracker = nil)
72 74 transaction do
73 75 if new_project && project_id != new_project.id
74 76 # delete issue relations
75 77 unless Setting.cross_project_issue_relations?
76 78 self.relations_from.clear
77 79 self.relations_to.clear
78 80 end
79 81 # issue is moved to another project
80 82 # reassign to the category with same name if any
81 83 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
82 84 self.category = new_category
83 85 self.fixed_version = nil
84 86 self.project = new_project
85 87 end
86 88 if new_tracker
87 89 self.tracker = new_tracker
88 90 end
89 91 if save
90 92 # Manually update project_id on related time entries
91 93 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
92 94 else
93 95 rollback_db_transaction
94 96 return false
95 97 end
96 98 end
97 99 return true
98 100 end
99 101
100 102 def priority_id=(pid)
101 103 self.priority = nil
102 104 write_attribute(:priority_id, pid)
103 105 end
104 106
105 107 def estimated_hours=(h)
106 108 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
107 109 end
108 110
109 111 def validate
110 112 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
111 113 errors.add :due_date, :activerecord_error_not_a_date
112 114 end
113 115
114 116 if self.due_date and self.start_date and self.due_date < self.start_date
115 117 errors.add :due_date, :activerecord_error_greater_than_start_date
116 118 end
117 119
118 120 if start_date && soonest_start && start_date < soonest_start
119 121 errors.add :start_date, :activerecord_error_invalid
120 122 end
121 123 end
122 124
123 125 def validate_on_create
124 126 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
125 127 end
126 128
127 129 def before_create
128 130 # default assignment based on category
129 131 if assigned_to.nil? && category && category.assigned_to
130 132 self.assigned_to = category.assigned_to
131 133 end
132 134 end
133 135
134 136 def before_save
135 137 if @current_journal
136 138 # attributes changes
137 139 (Issue.column_names - %w(id description)).each {|c|
138 140 @current_journal.details << JournalDetail.new(:property => 'attr',
139 141 :prop_key => c,
140 142 :old_value => @issue_before_change.send(c),
141 143 :value => send(c)) unless send(c)==@issue_before_change.send(c)
142 144 }
143 145 # custom fields changes
144 146 custom_values.each {|c|
145 147 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
146 148 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
147 149 @current_journal.details << JournalDetail.new(:property => 'cf',
148 150 :prop_key => c.custom_field_id,
149 151 :old_value => @custom_values_before_change[c.custom_field_id],
150 152 :value => c.value)
151 153 }
152 154 @current_journal.save
153 155 end
154 156 # Save the issue even if the journal is not saved (because empty)
155 157 true
156 158 end
157 159
158 160 def after_save
159 161 # Reload is needed in order to get the right status
160 162 reload
161 163
162 164 # Update start/due dates of following issues
163 165 relations_from.each(&:set_issue_to_dates)
164 166
165 167 # Close duplicates if the issue was closed
166 168 if @issue_before_change && !@issue_before_change.closed? && self.closed?
167 169 duplicates.each do |duplicate|
168 170 # Reload is need in case the duplicate was updated by a previous duplicate
169 171 duplicate.reload
170 172 # Don't re-close it if it's already closed
171 173 next if duplicate.closed?
172 174 # Same user and notes
173 175 duplicate.init_journal(@current_journal.user, @current_journal.notes)
174 176 duplicate.update_attribute :status, self.status
175 177 end
176 178 end
177 179 end
178 180
179 181 def init_journal(user, notes = "")
180 182 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
181 183 @issue_before_change = self.clone
182 184 @issue_before_change.status = self.status
183 185 @custom_values_before_change = {}
184 186 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
185 187 @current_journal
186 188 end
187 189
188 190 # Return true if the issue is closed, otherwise false
189 191 def closed?
190 192 self.status.is_closed?
191 193 end
192 194
193 195 # Users the issue can be assigned to
194 196 def assignable_users
195 197 project.assignable_users
196 198 end
197 199
198 200 # Returns an array of status that user is able to apply
199 201 def new_statuses_allowed_to(user)
200 202 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
201 203 statuses << status unless statuses.empty?
202 204 statuses.uniq.sort
203 205 end
204 206
205 207 # Returns the mail adresses of users that should be notified for the issue
206 208 def recipients
207 209 recipients = project.recipients
208 210 # Author and assignee are always notified unless they have been locked
209 211 recipients << author.mail if author && author.active?
210 212 recipients << assigned_to.mail if assigned_to && assigned_to.active?
211 213 recipients.compact.uniq
212 214 end
213 215
214 216 def spent_hours
215 217 @spent_hours ||= time_entries.sum(:hours) || 0
216 218 end
217 219
218 220 def relations
219 221 (relations_from + relations_to).sort
220 222 end
221 223
222 224 def all_dependent_issues
223 225 dependencies = []
224 226 relations_from.each do |relation|
225 227 dependencies << relation.issue_to
226 228 dependencies += relation.issue_to.all_dependent_issues
227 229 end
228 230 dependencies
229 231 end
230 232
231 233 # Returns an array of issues that duplicate this one
232 234 def duplicates
233 235 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
234 236 end
235 237
236 238 # Returns the due date or the target due date if any
237 239 # Used on gantt chart
238 240 def due_before
239 241 due_date || (fixed_version ? fixed_version.effective_date : nil)
240 242 end
241 243
242 244 def duration
243 245 (start_date && due_date) ? due_date - start_date : 0
244 246 end
245 247
246 248 def soonest_start
247 249 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
248 250 end
249 251
250 252 def self.visible_by(usr)
251 253 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
252 254 yield
253 255 end
254 256 end
255 257
256 258 def to_s
257 259 "#{tracker} ##{id}: #{subject}"
258 260 end
259 261 end
@@ -1,61 +1,67
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Journal < ActiveRecord::Base
19 19 belongs_to :journalized, :polymorphic => true
20 20 # added as a quick fix to allow eager loading of the polymorphic association
21 21 # since always associated to an issue, for now
22 22 belongs_to :issue, :foreign_key => :journalized_id
23 23
24 24 belongs_to :user
25 25 has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
26 26 attr_accessor :indice
27 27
28 28 acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
29 29 :description => :notes,
30 30 :author => :user,
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
37 43 end
38 44
39 45 # Returns the new status if the journal contains a status change, otherwise nil
40 46 def new_status
41 47 c = details.detect {|detail| detail.prop_key == 'status_id'}
42 48 (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
43 49 end
44 50
45 51 def new_value_for(prop)
46 52 c = details.detect {|detail| detail.prop_key == prop}
47 53 c ? c.value : nil
48 54 end
49 55
50 56 def editable_by?(usr)
51 57 usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
52 58 end
53 59
54 60 def project
55 61 journalized.respond_to?(:project) ? journalized.project : nil
56 62 end
57 63
58 64 def attachments
59 65 journalized.respond_to?(:attachments) ? journalized.attachments : nil
60 66 end
61 67 end
@@ -1,69 +1,71
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Message < ActiveRecord::Base
19 19 belongs_to :board
20 20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 22 has_many :attachments, :as => :container, :dependent => :destroy
23 23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24 24
25 25 acts_as_searchable :columns => ['subject', 'content'],
26 26 :include => {:board, :project},
27 27 :project_key => 'project_id',
28 28 :date_column => "#{table_name}.created_on"
29 29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 30 :description => :content,
31 31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
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
38 40
39 41 def validate_on_create
40 42 # Can not reply to a locked topic
41 43 errors.add_to_base 'Topic is locked' if root.locked? && self != root
42 44 end
43 45
44 46 def after_create
45 47 board.update_attribute(:last_message_id, self.id)
46 48 board.increment! :messages_count
47 49 if parent
48 50 parent.reload.update_attribute(:last_reply_id, self.id)
49 51 else
50 52 board.increment! :topics_count
51 53 end
52 54 end
53 55
54 56 def after_destroy
55 57 # The following line is required so that the previous counter
56 58 # updates (due to children removal) are not overwritten
57 59 board.reload
58 60 board.decrement! :messages_count
59 61 board.decrement! :topics_count unless parent
60 62 end
61 63
62 64 def sticky?
63 65 sticky == 1
64 66 end
65 67
66 68 def project
67 69 board.project
68 70 end
69 71 end
@@ -1,34 +1,35
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class News < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 21 has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
22 22
23 23 validates_presence_of :title, :description
24 24 validates_length_of :title, :maximum => 60
25 25 validates_length_of :summary, :maximum => 255
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)
32 33 find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
33 34 end
34 35 end
@@ -1,78 +1,89
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'zlib'
19 19
20 20 class WikiContent < ActiveRecord::Base
21 21 set_locking_column :version
22 22 belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
23 23 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 24 validates_presence_of :text
25 25
26 26 acts_as_versioned
27 27 class Version
28 28 belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
29 29 belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
30 30 attr_protected :data
31 31
32 32 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
33 33 :description => :comments,
34 34 :datetime => :updated_on,
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'
41 52 begin
42 53 self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
43 54 self.compression = 'gzip'
44 55 rescue
45 56 self.data = plain
46 57 self.compression = ''
47 58 end
48 59 else
49 60 self.data = plain
50 61 self.compression = ''
51 62 end
52 63 plain
53 64 end
54 65
55 66 def text
56 67 @text ||= case compression
57 68 when 'gzip'
58 69 Zlib::Inflate.inflate(data)
59 70 else
60 71 # uncompressed data
61 72 data
62 73 end
63 74 end
64 75
65 76 def project
66 77 page.project
67 78 end
68 79
69 80 # Returns the previous version or nil
70 81 def previous
71 82 @previous ||= WikiContent::Version.find(:first,
72 83 :order => 'version DESC',
73 84 :include => :author,
74 85 :conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
75 86 end
76 87 end
77 88
78 89 end
@@ -1,58 +1,58
1 1 <h2><%= l(:label_activity) %></h2>
2 2 <p class="subtitle"><%= "#{l(:label_date_from)} #{format_date(@date_to - @days)} #{l(:label_date_to).downcase} #{format_date(@date_to-1)}" %></p>
3 3
4 4 <div id="activity">
5 5 <% @events_by_day.keys.sort.reverse.each do |day| %>
6 6 <h3><%= format_activity_day(day) %></h3>
7 7 <dl>
8 8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
9 9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
10 10 <span class="time"><%= format_time(e.event_datetime, false) %></span>
11 11 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
12 12 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
13 13 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
14 14 <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
15 15 <% end -%>
16 16 </dl>
17 17 <% end -%>
18 18 </div>
19 19
20 20 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
21 21
22 22 <div style="float:left;">
23 23 <%= link_to_remote(('&#171; ' + l(:label_previous)),
24 24 {:update => "content", :url => params.merge(:from => @date_to - @days), :complete => 'window.scrollTo(0,0)'},
25 25 {:href => url_for(params.merge(:from => @date_to - @days)),
26 26 :title => "#{l(:label_date_from)} #{format_date(@date_to - 2*@days)} #{l(:label_date_to).downcase} #{format_date(@date_to - @days - 1)}"}) %>
27 27 </div>
28 28 <div style="float:right;">
29 29 <%= link_to_remote((l(:label_next) + ' &#187;'),
30 30 {:update => "content", :url => params.merge(:from => @date_to + @days), :complete => 'window.scrollTo(0,0)'},
31 31 {:href => url_for(params.merge(:from => @date_to + @days)),
32 32 :title => "#{l(:label_date_from)} #{format_date(@date_to)} #{l(:label_date_to).downcase} #{format_date(@date_to + @days - 1)}"}) unless @date_to >= Date.today %>
33 33 </div>
34 34 &nbsp;
35 35 <p class="other-formats">
36 36 <%= l(:label_export_to) %>
37 37 <%= link_to 'Atom', params.merge(:format => :atom, :key => User.current.rss_key).delete_if{|k,v|k=="commit"}, :class => 'feed' %>
38 38 </p>
39 39
40 40 <% content_for :header_tags do %>
41 41 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :year => nil, :month => nil, :key => User.current.rss_key)) %>
42 42 <% end %>
43 43
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>
52 52 <%= hidden_field_tag 'with_subprojects', 0 %>
53 53 <% end %>
54 54 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
55 55 <% end %>
56 56 <% end %>
57 57
58 58 <% html_title(l(:label_activity)) -%>
@@ -1,134 +1,145
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'
6 7 require 'redmine/plugin'
7 8
8 9 begin
9 10 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
10 11 rescue LoadError
11 12 # RMagick is not available
12 13 end
13 14
14 15 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
15 16
16 17 # Permissions
17 18 Redmine::AccessControl.map do |map|
18 19 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
19 20 map.permission :search_project, {:search => :index}, :public => true
20 21 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
21 22 map.permission :select_project_modules, {:projects => :modules}, :require => :member
22 23 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
23 24 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
24 25
25 26 map.project_module :issue_tracking do |map|
26 27 # Issue categories
27 28 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
28 29 # Issues
29 30 map.permission :view_issues, {:projects => [:changelog, :roadmap],
30 31 :issues => [:index, :changes, :show, :context_menu],
31 32 :versions => [:show, :status_by],
32 33 :queries => :index,
33 34 :reports => :issue_report}, :public => true
34 35 map.permission :add_issues, {:issues => :new}
35 36 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
36 37 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
37 38 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
38 39 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
39 40 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
40 41 map.permission :move_issues, {:issues => :move}, :require => :loggedin
41 42 map.permission :delete_issues, {:issues => :destroy}, :require => :member
42 43 # Queries
43 44 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
44 45 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
45 46 # Gantt & calendar
46 47 map.permission :view_gantt, :projects => :gantt
47 48 map.permission :view_calendar, :projects => :calendar
48 49 end
49 50
50 51 map.project_module :time_tracking do |map|
51 52 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
52 53 map.permission :view_time_entries, :timelog => [:details, :report]
53 54 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
54 55 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
55 56 end
56 57
57 58 map.project_module :news do |map|
58 59 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
59 60 map.permission :view_news, {:news => [:index, :show]}, :public => true
60 61 map.permission :comment_news, {:news => :add_comment}
61 62 end
62 63
63 64 map.project_module :documents do |map|
64 65 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
65 66 map.permission :view_documents, :documents => [:index, :show, :download]
66 67 end
67 68
68 69 map.project_module :files do |map|
69 70 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
70 71 map.permission :view_files, :projects => :list_files, :versions => :download
71 72 end
72 73
73 74 map.project_module :wiki do |map|
74 75 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
75 76 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
76 77 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
77 78 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
78 79 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
79 80 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
80 81 end
81 82
82 83 map.project_module :repository do |map|
83 84 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
84 85 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
85 86 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
86 87 end
87 88
88 89 map.project_module :boards do |map|
89 90 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
90 91 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
91 92 map.permission :add_messages, {:messages => [:new, :reply]}
92 93 map.permission :edit_messages, {:messages => :edit}, :require => :member
93 94 map.permission :delete_messages, {:messages => :destroy}, :require => :member
94 95 end
95 96 end
96 97
97 98 Redmine::MenuManager.map :top_menu do |menu|
98 99 menu.push :home, :home_path, :html => { :class => 'home' }
99 100 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
100 101 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
101 102 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }, :last => true
102 103 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }, :last => true
103 104 end
104 105
105 106 Redmine::MenuManager.map :account_menu do |menu|
106 107 menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
107 108 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
108 109 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
109 110 menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
110 111 end
111 112
112 113 Redmine::MenuManager.map :application_menu do |menu|
113 114 # Empty
114 115 end
115 116
116 117 Redmine::MenuManager.map :project_menu do |menu|
117 118 menu.push :overview, { :controller => 'projects', :action => 'show' }
118 119 menu.push :activity, { :controller => 'projects', :action => 'activity' }
119 120 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
120 121 :if => Proc.new { |p| p.versions.any? }
121 122 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
122 123 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
123 124 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
124 125 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
125 126 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
126 127 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
127 128 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
128 129 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
129 130 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
130 131 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
131 132 menu.push :repository, { :controller => 'repositories', :action => 'show' },
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
@@ -1,344 +1,313
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'projects_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class ProjectsController; def rescue_action(e) raise e end; end
23 23
24 24 class ProjectsControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
26 26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
27 27
28 28 def setup
29 29 @controller = ProjectsController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 @request.session[:user_id] = nil
33 33 end
34 34
35 35 def test_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'index'
39 39 assert_not_nil assigns(:project_tree)
40 40 # Root project as hash key
41 41 assert assigns(:project_tree).keys.include?(Project.find(1))
42 42 # Subproject in corresponding value
43 43 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
44 44 end
45 45
46 46 def test_index_atom
47 47 get :index, :format => 'atom'
48 48 assert_response :success
49 49 assert_template 'common/feed.atom.rxml'
50 50 assert_select 'feed>title', :text => 'Redmine: Latest projects'
51 51 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
52 52 end
53 53
54 54 def test_show_by_id
55 55 get :show, :id => 1
56 56 assert_response :success
57 57 assert_template 'show'
58 58 assert_not_nil assigns(:project)
59 59 end
60 60
61 61 def test_show_by_identifier
62 62 get :show, :id => 'ecookbook'
63 63 assert_response :success
64 64 assert_template 'show'
65 65 assert_not_nil assigns(:project)
66 66 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
67 67 end
68 68
69 69 def test_private_subprojects_hidden
70 70 get :show, :id => 'ecookbook'
71 71 assert_response :success
72 72 assert_template 'show'
73 73 assert_no_tag :tag => 'a', :content => /Private child/
74 74 end
75 75
76 76 def test_private_subprojects_visible
77 77 @request.session[:user_id] = 2 # manager who is a member of the private subproject
78 78 get :show, :id => 'ecookbook'
79 79 assert_response :success
80 80 assert_template 'show'
81 81 assert_tag :tag => 'a', :content => /Private child/
82 82 end
83 83
84 84 def test_settings
85 85 @request.session[:user_id] = 2 # manager
86 86 get :settings, :id => 1
87 87 assert_response :success
88 88 assert_template 'settings'
89 89 end
90 90
91 91 def test_edit
92 92 @request.session[:user_id] = 2 # manager
93 93 post :edit, :id => 1, :project => {:name => 'Test changed name',
94 94 :issue_custom_field_ids => ['']}
95 95 assert_redirected_to 'projects/settings/ecookbook'
96 96 project = Project.find(1)
97 97 assert_equal 'Test changed name', project.name
98 98 end
99 99
100 100 def test_get_destroy
101 101 @request.session[:user_id] = 1 # admin
102 102 get :destroy, :id => 1
103 103 assert_response :success
104 104 assert_template 'destroy'
105 105 assert_not_nil Project.find_by_id(1)
106 106 end
107 107
108 108 def test_post_destroy
109 109 @request.session[:user_id] = 1 # admin
110 110 post :destroy, :id => 1, :confirm => 1
111 111 assert_redirected_to 'admin/projects'
112 112 assert_nil Project.find_by_id(1)
113 113 end
114 114
115 115 def test_list_files
116 116 get :list_files, :id => 1
117 117 assert_response :success
118 118 assert_template 'list_files'
119 119 assert_not_nil assigns(:versions)
120 120 end
121 121
122 122 def test_changelog
123 123 get :changelog, :id => 1
124 124 assert_response :success
125 125 assert_template 'changelog'
126 126 assert_not_nil assigns(:versions)
127 127 end
128 128
129 129 def test_roadmap
130 130 get :roadmap, :id => 1
131 131 assert_response :success
132 132 assert_template 'roadmap'
133 133 assert_not_nil assigns(:versions)
134 134 # Version with no date set appears
135 135 assert assigns(:versions).include?(Version.find(3))
136 136 # Completed version doesn't appear
137 137 assert !assigns(:versions).include?(Version.find(1))
138 138 end
139 139
140 140 def test_roadmap_with_completed_versions
141 141 get :roadmap, :id => 1, :completed => 1
142 142 assert_response :success
143 143 assert_template 'roadmap'
144 144 assert_not_nil assigns(:versions)
145 145 # Version with no date set appears
146 146 assert assigns(:versions).include?(Version.find(3))
147 147 # Completed version appears
148 148 assert assigns(:versions).include?(Version.find(1))
149 149 end
150 150
151 151 def test_project_activity
152 152 get :activity, :id => 1, :with_subprojects => 0
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}/,
163 159 :sibling => { :tag => "dl",
164 160 :child => { :tag => "dt",
165 161 :attributes => { :class => /issue-edit/ },
166 162 :child => { :tag => "a",
167 163 :content => /(#{IssueStatus.find(2).name})/,
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'
175 173 assert_not_nil assigns(:events_by_day)
176 174
177 175 assert_tag :tag => "h3",
178 176 :content => /#{3.day.ago.to_date.day}/,
179 177 :sibling => { :tag => "dl",
180 178 :child => { :tag => "dt",
181 179 :attributes => { :class => /issue/ },
182 180 :child => { :tag => "a",
183 181 :content => /#{Issue.find(1).subject}/,
184 182 }
185 183 }
186 184 }
187 185 end
188 186
189 def test_activity_with_subprojects
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
237 206 get :calendar, :id => 1
238 207 assert_response :success
239 208 assert_template 'calendar'
240 209 assert_not_nil assigns(:calendar)
241 210 end
242 211
243 212 def test_calendar_with_subprojects_should_not_show_private_subprojects
244 213 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
245 214 assert_response :success
246 215 assert_template 'calendar'
247 216 assert_not_nil assigns(:calendar)
248 217 assert_no_tag :tag => 'a', :content => /#6/
249 218 end
250 219
251 220 def test_calendar_with_subprojects_should_show_private_subprojects
252 221 @request.session[:user_id] = 2
253 222 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
254 223 assert_response :success
255 224 assert_template 'calendar'
256 225 assert_not_nil assigns(:calendar)
257 226 assert_tag :tag => 'a', :content => /#6/
258 227 end
259 228
260 229 def test_gantt
261 230 get :gantt, :id => 1
262 231 assert_response :success
263 232 assert_template 'gantt.rhtml'
264 233 events = assigns(:events)
265 234 assert_not_nil events
266 235 # Issue with start and due dates
267 236 i = Issue.find(1)
268 237 assert_not_nil i.due_date
269 238 assert events.include?(Issue.find(1))
270 239 # Issue with without due date but targeted to a version with date
271 240 i = Issue.find(2)
272 241 assert_nil i.due_date
273 242 assert events.include?(i)
274 243 end
275 244
276 245 def test_gantt_with_subprojects_should_not_show_private_subprojects
277 246 get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
278 247 assert_response :success
279 248 assert_template 'gantt.rhtml'
280 249 assert_not_nil assigns(:events)
281 250 assert_no_tag :tag => 'a', :content => /#6/
282 251 end
283 252
284 253 def test_gantt_with_subprojects_should_show_private_subprojects
285 254 @request.session[:user_id] = 2
286 255 get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
287 256 assert_response :success
288 257 assert_template 'gantt.rhtml'
289 258 assert_not_nil assigns(:events)
290 259 assert_tag :tag => 'a', :content => /#6/
291 260 end
292 261
293 262 def test_gantt_export_to_pdf
294 263 get :gantt, :id => 1, :format => 'pdf'
295 264 assert_response :success
296 265 assert_template 'gantt.rfpdf'
297 266 assert_equal 'application/pdf', @response.content_type
298 267 assert_not_nil assigns(:events)
299 268 end
300 269
301 270 def test_archive
302 271 @request.session[:user_id] = 1 # admin
303 272 post :archive, :id => 1
304 273 assert_redirected_to 'admin/projects'
305 274 assert !Project.find(1).active?
306 275 end
307 276
308 277 def test_unarchive
309 278 @request.session[:user_id] = 1 # admin
310 279 Project.find(1).archive
311 280 post :unarchive, :id => 1
312 281 assert_redirected_to 'admin/projects'
313 282 assert Project.find(1).active?
314 283 end
315 284
316 285 def test_project_menu
317 286 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
318 287 Redmine::MenuManager.map :project_menu do |menu|
319 288 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
320 289 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
321 290 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
322 291 end
323 292
324 293 get :show, :id => 1
325 294 assert_tag :div, :attributes => { :id => 'main-menu' },
326 295 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo' } }
327 296
328 297 assert_tag :div, :attributes => { :id => 'main-menu' },
329 298 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar' },
330 299 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
331 300
332 301 assert_tag :div, :attributes => { :id => 'main-menu' },
333 302 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' },
334 303 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
335 304
336 305 # Remove the menu items
337 306 Redmine::MenuManager.map :project_menu do |menu|
338 307 menu.delete :foo
339 308 menu.delete :bar
340 309 menu.delete :hello
341 310 end
342 311 end
343 312 end
344 313 end
General Comments 0
You need to be logged in to leave comments. Login now