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