##// END OF EJS Templates
Merged r2209 to r2211 from trunk....
Jean-Philippe Lang -
r2227:c9d4d3a2be3d
parent child
Show More
@@ -0,0 +1,41
1 Return-Path: <jsmith@somenet.foo>
2 Received: from osiris ([127.0.0.1])
3 by OSIRIS
4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 From: "John Smith" <jsmith@somenet.foo>
7 To: <redmine@somenet.foo>
8 Subject: New ticket with custom field values
9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 MIME-Version: 1.0
11 Content-Type: text/plain;
12 format=flowed;
13 charset="iso-8859-1";
14 reply-type=original
15 Content-Transfer-Encoding: 7bit
16 X-Priority: 3
17 X-MSMail-Priority: Normal
18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20
21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 platea dictumst.
32
33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
39
40 category: Stock management
41 searchable field: Value for a custom field
@@ -1,72 +1,74
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 AttachmentsController < ApplicationController
18 class AttachmentsController < ApplicationController
19 before_filter :find_project
19 before_filter :find_project
20 before_filter :read_authorize, :except => :destroy
20 before_filter :read_authorize, :except => :destroy
21 before_filter :delete_authorize, :only => :destroy
21 before_filter :delete_authorize, :only => :destroy
22
22
23 verify :method => :post, :only => :destroy
23 verify :method => :post, :only => :destroy
24
24
25 def show
25 def show
26 if @attachment.is_diff?
26 if @attachment.is_diff?
27 @diff = File.new(@attachment.diskfile, "rb").read
27 @diff = File.new(@attachment.diskfile, "rb").read
28 render :action => 'diff'
28 render :action => 'diff'
29 elsif @attachment.is_text?
29 elsif @attachment.is_text?
30 @content = File.new(@attachment.diskfile, "rb").read
30 @content = File.new(@attachment.diskfile, "rb").read
31 render :action => 'file'
31 render :action => 'file'
32 elsif
32 elsif
33 download
33 download
34 end
34 end
35 end
35 end
36
36
37 def download
37 def download
38 @attachment.increment_download if @attachment.container.is_a?(Version)
38 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
39 @attachment.increment_download
40 end
39
41
40 # images are sent inline
42 # images are sent inline
41 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
43 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
42 :type => @attachment.content_type,
44 :type => @attachment.content_type,
43 :disposition => (@attachment.image? ? 'inline' : 'attachment')
45 :disposition => (@attachment.image? ? 'inline' : 'attachment')
44
46
45 end
47 end
46
48
47 def destroy
49 def destroy
48 # Make sure association callbacks are called
50 # Make sure association callbacks are called
49 @attachment.container.attachments.delete(@attachment)
51 @attachment.container.attachments.delete(@attachment)
50 redirect_to :back
52 redirect_to :back
51 rescue ::ActionController::RedirectBackError
53 rescue ::ActionController::RedirectBackError
52 redirect_to :controller => 'projects', :action => 'show', :id => @project
54 redirect_to :controller => 'projects', :action => 'show', :id => @project
53 end
55 end
54
56
55 private
57 private
56 def find_project
58 def find_project
57 @attachment = Attachment.find(params[:id])
59 @attachment = Attachment.find(params[:id])
58 # Show 404 if the filename in the url is wrong
60 # Show 404 if the filename in the url is wrong
59 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
61 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
60 @project = @attachment.project
62 @project = @attachment.project
61 rescue ActiveRecord::RecordNotFound
63 rescue ActiveRecord::RecordNotFound
62 render_404
64 render_404
63 end
65 end
64
66
65 def read_authorize
67 def read_authorize
66 @attachment.visible? ? true : deny_access
68 @attachment.visible? ? true : deny_access
67 end
69 end
68
70
69 def delete_authorize
71 def delete_authorize
70 @attachment.deletable? ? true : deny_access
72 @attachment.deletable? ? true : deny_access
71 end
73 end
72 end
74 end
@@ -1,293 +1,298
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 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 helper :sort
32 helper :sort
33 include SortHelper
33 include SortHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :ifpdf
36 helper :ifpdf
37 include IfpdfHelper
37 include IfpdfHelper
38 helper :issues
38 helper :issues
39 helper IssuesHelper
39 helper IssuesHelper
40 helper :queries
40 helper :queries
41 include QueriesHelper
41 include QueriesHelper
42 helper :repositories
42 helper :repositories
43 include RepositoriesHelper
43 include RepositoriesHelper
44 include ProjectsHelper
44 include ProjectsHelper
45
45
46 # Lists visible projects
46 # Lists visible projects
47 def index
47 def index
48 projects = Project.find :all,
48 projects = Project.find :all,
49 :conditions => Project.visible_by(User.current),
49 :conditions => Project.visible_by(User.current),
50 :include => :parent
50 :include => :parent
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @project_tree = projects.group_by {|p| p.parent || p}
53 @project_tree = projects.group_by {|p| p.parent || p}
54 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
54 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
55 }
55 }
56 format.atom {
56 format.atom {
57 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
57 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
58 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
58 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 }
59 }
60 end
60 end
61 end
61 end
62
62
63 # Add a new project
63 # Add a new project
64 def add
64 def add
65 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
65 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @trackers = Tracker.all
66 @trackers = Tracker.all
67 @root_projects = Project.find(:all,
67 @root_projects = Project.find(:all,
68 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
68 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 :order => 'name')
69 :order => 'name')
70 @project = Project.new(params[:project])
70 @project = Project.new(params[:project])
71 if request.get?
71 if request.get?
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
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 if params[:jump]
88 # try to redirect to the requested menu item
89 redirect_to_project_menu_item(@project, params[:jump]) && return
90 end
91
87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
92 @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))
93 @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")
94 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
90 @trackers = @project.rolled_up_trackers
95 @trackers = @project.rolled_up_trackers
91
96
92 cond = @project.project_condition(Setting.display_subprojects_issues?)
97 cond = @project.project_condition(Setting.display_subprojects_issues?)
93 Issue.visible_by(User.current) do
98 Issue.visible_by(User.current) do
94 @open_issues_by_tracker = Issue.count(:group => :tracker,
99 @open_issues_by_tracker = Issue.count(:group => :tracker,
95 :include => [:project, :status, :tracker],
100 :include => [:project, :status, :tracker],
96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
101 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
97 @total_issues_by_tracker = Issue.count(:group => :tracker,
102 @total_issues_by_tracker = Issue.count(:group => :tracker,
98 :include => [:project, :status, :tracker],
103 :include => [:project, :status, :tracker],
99 :conditions => cond)
104 :conditions => cond)
100 end
105 end
101 TimeEntry.visible_by(User.current) do
106 TimeEntry.visible_by(User.current) do
102 @total_hours = TimeEntry.sum(:hours,
107 @total_hours = TimeEntry.sum(:hours,
103 :include => :project,
108 :include => :project,
104 :conditions => cond).to_f
109 :conditions => cond).to_f
105 end
110 end
106 @key = User.current.rss_key
111 @key = User.current.rss_key
107 end
112 end
108
113
109 def settings
114 def settings
110 @root_projects = Project.find(:all,
115 @root_projects = Project.find(:all,
111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
116 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
112 :order => 'name')
117 :order => 'name')
113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
118 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
114 @issue_category ||= IssueCategory.new
119 @issue_category ||= IssueCategory.new
115 @member ||= @project.members.new
120 @member ||= @project.members.new
116 @trackers = Tracker.all
121 @trackers = Tracker.all
117 @repository ||= @project.repository
122 @repository ||= @project.repository
118 @wiki ||= @project.wiki
123 @wiki ||= @project.wiki
119 end
124 end
120
125
121 # Edit @project
126 # Edit @project
122 def edit
127 def edit
123 if request.post?
128 if request.post?
124 @project.attributes = params[:project]
129 @project.attributes = params[:project]
125 if @project.save
130 if @project.save
126 flash[:notice] = l(:notice_successful_update)
131 flash[:notice] = l(:notice_successful_update)
127 redirect_to :action => 'settings', :id => @project
132 redirect_to :action => 'settings', :id => @project
128 else
133 else
129 settings
134 settings
130 render :action => 'settings'
135 render :action => 'settings'
131 end
136 end
132 end
137 end
133 end
138 end
134
139
135 def modules
140 def modules
136 @project.enabled_module_names = params[:enabled_modules]
141 @project.enabled_module_names = params[:enabled_modules]
137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
142 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
138 end
143 end
139
144
140 def archive
145 def archive
141 @project.archive if request.post? && @project.active?
146 @project.archive if request.post? && @project.active?
142 redirect_to :controller => 'admin', :action => 'projects'
147 redirect_to :controller => 'admin', :action => 'projects'
143 end
148 end
144
149
145 def unarchive
150 def unarchive
146 @project.unarchive if request.post? && !@project.active?
151 @project.unarchive if request.post? && !@project.active?
147 redirect_to :controller => 'admin', :action => 'projects'
152 redirect_to :controller => 'admin', :action => 'projects'
148 end
153 end
149
154
150 # Delete @project
155 # Delete @project
151 def destroy
156 def destroy
152 @project_to_destroy = @project
157 @project_to_destroy = @project
153 if request.post? and params[:confirm]
158 if request.post? and params[:confirm]
154 @project_to_destroy.destroy
159 @project_to_destroy.destroy
155 redirect_to :controller => 'admin', :action => 'projects'
160 redirect_to :controller => 'admin', :action => 'projects'
156 end
161 end
157 # hide project in layout
162 # hide project in layout
158 @project = nil
163 @project = nil
159 end
164 end
160
165
161 # Add a new issue category to @project
166 # Add a new issue category to @project
162 def add_issue_category
167 def add_issue_category
163 @category = @project.issue_categories.build(params[:category])
168 @category = @project.issue_categories.build(params[:category])
164 if request.post? and @category.save
169 if request.post? and @category.save
165 respond_to do |format|
170 respond_to do |format|
166 format.html do
171 format.html do
167 flash[:notice] = l(:notice_successful_create)
172 flash[:notice] = l(:notice_successful_create)
168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
173 redirect_to :action => 'settings', :tab => 'categories', :id => @project
169 end
174 end
170 format.js do
175 format.js do
171 # IE doesn't support the replace_html rjs method for select box options
176 # IE doesn't support the replace_html rjs method for select box options
172 render(:update) {|page| page.replace "issue_category_id",
177 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]')
178 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 }
179 }
175 end
180 end
176 end
181 end
177 end
182 end
178 end
183 end
179
184
180 # Add a new version to @project
185 # Add a new version to @project
181 def add_version
186 def add_version
182 @version = @project.versions.build(params[:version])
187 @version = @project.versions.build(params[:version])
183 if request.post? and @version.save
188 if request.post? and @version.save
184 flash[:notice] = l(:notice_successful_create)
189 flash[:notice] = l(:notice_successful_create)
185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
190 redirect_to :action => 'settings', :tab => 'versions', :id => @project
186 end
191 end
187 end
192 end
188
193
189 def add_file
194 def add_file
190 if request.post?
195 if request.post?
191 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
196 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
192 attachments = attach_files(container, params[:attachments])
197 attachments = attach_files(container, params[:attachments])
193 if !attachments.empty? && Setting.notified_events.include?('file_added')
198 if !attachments.empty? && Setting.notified_events.include?('file_added')
194 Mailer.deliver_attachments_added(attachments)
199 Mailer.deliver_attachments_added(attachments)
195 end
200 end
196 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
201 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
197 return
202 return
198 end
203 end
199 @versions = @project.versions.sort
204 @versions = @project.versions.sort
200 end
205 end
201
206
202 def list_files
207 def list_files
203 sort_init 'filename', 'asc'
208 sort_init 'filename', 'asc'
204 sort_update 'filename' => "#{Attachment.table_name}.filename",
209 sort_update 'filename' => "#{Attachment.table_name}.filename",
205 'created_on' => "#{Attachment.table_name}.created_on",
210 'created_on' => "#{Attachment.table_name}.created_on",
206 'size' => "#{Attachment.table_name}.filesize",
211 'size' => "#{Attachment.table_name}.filesize",
207 'downloads' => "#{Attachment.table_name}.downloads"
212 'downloads' => "#{Attachment.table_name}.downloads"
208
213
209 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
214 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
210 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
215 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
211 render :layout => !request.xhr?
216 render :layout => !request.xhr?
212 end
217 end
213
218
214 # Show changelog for @project
219 # Show changelog for @project
215 def changelog
220 def changelog
216 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
221 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
217 retrieve_selected_tracker_ids(@trackers)
222 retrieve_selected_tracker_ids(@trackers)
218 @versions = @project.versions.sort
223 @versions = @project.versions.sort
219 end
224 end
220
225
221 def roadmap
226 def roadmap
222 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
227 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
223 retrieve_selected_tracker_ids(@trackers)
228 retrieve_selected_tracker_ids(@trackers)
224 @versions = @project.versions.sort
229 @versions = @project.versions.sort
225 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
230 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
226 end
231 end
227
232
228 def activity
233 def activity
229 @days = Setting.activity_days_default.to_i
234 @days = Setting.activity_days_default.to_i
230
235
231 if params[:from]
236 if params[:from]
232 begin; @date_to = params[:from].to_date + 1; rescue; end
237 begin; @date_to = params[:from].to_date + 1; rescue; end
233 end
238 end
234
239
235 @date_to ||= Date.today + 1
240 @date_to ||= Date.today + 1
236 @date_from = @date_to - @days
241 @date_from = @date_to - @days
237 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
242 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
238 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
243 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
239
244
240 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
245 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
241 :with_subprojects => @with_subprojects,
246 :with_subprojects => @with_subprojects,
242 :author => @author)
247 :author => @author)
243 @activity.scope_select {|t| !params["show_#{t}"].nil?}
248 @activity.scope_select {|t| !params["show_#{t}"].nil?}
244 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
249 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
245
250
246 events = @activity.events(@date_from, @date_to)
251 events = @activity.events(@date_from, @date_to)
247
252
248 respond_to do |format|
253 respond_to do |format|
249 format.html {
254 format.html {
250 @events_by_day = events.group_by(&:event_date)
255 @events_by_day = events.group_by(&:event_date)
251 render :layout => false if request.xhr?
256 render :layout => false if request.xhr?
252 }
257 }
253 format.atom {
258 format.atom {
254 title = l(:label_activity)
259 title = l(:label_activity)
255 if @author
260 if @author
256 title = @author.name
261 title = @author.name
257 elsif @activity.scope.size == 1
262 elsif @activity.scope.size == 1
258 title = l("label_#{@activity.scope.first.singularize}_plural")
263 title = l("label_#{@activity.scope.first.singularize}_plural")
259 end
264 end
260 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
265 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
261 }
266 }
262 end
267 end
263
268
264 rescue ActiveRecord::RecordNotFound
269 rescue ActiveRecord::RecordNotFound
265 render_404
270 render_404
266 end
271 end
267
272
268 private
273 private
269 # Find project of id params[:id]
274 # Find project of id params[:id]
270 # if not found, redirect to project list
275 # if not found, redirect to project list
271 # Used as a before_filter
276 # Used as a before_filter
272 def find_project
277 def find_project
273 @project = Project.find(params[:id])
278 @project = Project.find(params[:id])
274 rescue ActiveRecord::RecordNotFound
279 rescue ActiveRecord::RecordNotFound
275 render_404
280 render_404
276 end
281 end
277
282
278 def find_optional_project
283 def find_optional_project
279 return true unless params[:id]
284 return true unless params[:id]
280 @project = Project.find(params[:id])
285 @project = Project.find(params[:id])
281 authorize
286 authorize
282 rescue ActiveRecord::RecordNotFound
287 rescue ActiveRecord::RecordNotFound
283 render_404
288 render_404
284 end
289 end
285
290
286 def retrieve_selected_tracker_ids(selectable_trackers)
291 def retrieve_selected_tracker_ids(selectable_trackers)
287 if ids = params[:tracker_ids]
292 if ids = params[:tracker_ids]
288 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
293 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
289 else
294 else
290 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
295 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
291 end
296 end
292 end
297 end
293 end
298 end
@@ -1,185 +1,192
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 MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20
20
21 class UnauthorizedAction < StandardError; end
21 class UnauthorizedAction < StandardError; end
22 class MissingInformation < StandardError; end
22 class MissingInformation < StandardError; end
23
23
24 attr_reader :email, :user
24 attr_reader :email, :user
25
25
26 def self.receive(email, options={})
26 def self.receive(email, options={})
27 @@handler_options = options.dup
27 @@handler_options = options.dup
28
28
29 @@handler_options[:issue] ||= {}
29 @@handler_options[:issue] ||= {}
30
30
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] ||= []
32 @@handler_options[:allow_override] ||= []
33 # Project needs to be overridable if not specified
33 # Project needs to be overridable if not specified
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 # Status overridable by default
35 # Status overridable by default
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 super email
37 super email
38 end
38 end
39
39
40 # Processes incoming emails
40 # Processes incoming emails
41 def receive(email)
41 def receive(email)
42 @email = email
42 @email = email
43 @user = User.active.find(:first, :conditions => ["LOWER(mail) = ?", email.from.first.to_s.strip.downcase])
43 @user = User.active.find(:first, :conditions => ["LOWER(mail) = ?", email.from.first.to_s.strip.downcase])
44 unless @user
44 unless @user
45 # Unknown user => the email is ignored
45 # Unknown user => the email is ignored
46 # TODO: ability to create the user's account
46 # TODO: ability to create the user's account
47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
48 return false
48 return false
49 end
49 end
50 User.current = @user
50 User.current = @user
51 dispatch
51 dispatch
52 end
52 end
53
53
54 private
54 private
55
55
56 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
56 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
57
57
58 def dispatch
58 def dispatch
59 if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
59 if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
60 receive_issue_update(m[1].to_i)
60 receive_issue_update(m[1].to_i)
61 else
61 else
62 receive_issue
62 receive_issue
63 end
63 end
64 rescue ActiveRecord::RecordInvalid => e
64 rescue ActiveRecord::RecordInvalid => e
65 # TODO: send a email to the user
65 # TODO: send a email to the user
66 logger.error e.message if logger
66 logger.error e.message if logger
67 false
67 false
68 rescue MissingInformation => e
68 rescue MissingInformation => e
69 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
69 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
70 false
70 false
71 rescue UnauthorizedAction => e
71 rescue UnauthorizedAction => e
72 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
72 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
73 false
73 false
74 end
74 end
75
75
76 # Creates a new issue
76 # Creates a new issue
77 def receive_issue
77 def receive_issue
78 project = target_project
78 project = target_project
79 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
79 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
80 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
80 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
81 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
81 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
82 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
82 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
83
83
84 # check permission
84 # check permission
85 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
85 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
86 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
86 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
87 # check workflow
87 # check workflow
88 if status && issue.new_statuses_allowed_to(user).include?(status)
88 if status && issue.new_statuses_allowed_to(user).include?(status)
89 issue.status = status
89 issue.status = status
90 end
90 end
91 issue.subject = email.subject.chomp.toutf8
91 issue.subject = email.subject.chomp.toutf8
92 issue.description = plain_text_body
92 issue.description = plain_text_body
93 # custom fields
94 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
95 if value = get_keyword(c.name, :override => true)
96 h[c.id] = value
97 end
98 h
99 end
93 issue.save!
100 issue.save!
94 add_attachments(issue)
101 add_attachments(issue)
95 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
102 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
96 # add To and Cc as watchers
103 # add To and Cc as watchers
97 add_watchers(issue)
104 add_watchers(issue)
98 # send notification after adding watchers so that they can reply to Redmine
105 # send notification after adding watchers so that they can reply to Redmine
99 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
106 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
100 issue
107 issue
101 end
108 end
102
109
103 def target_project
110 def target_project
104 # TODO: other ways to specify project:
111 # TODO: other ways to specify project:
105 # * parse the email To field
112 # * parse the email To field
106 # * specific project (eg. Setting.mail_handler_target_project)
113 # * specific project (eg. Setting.mail_handler_target_project)
107 target = Project.find_by_identifier(get_keyword(:project))
114 target = Project.find_by_identifier(get_keyword(:project))
108 raise MissingInformation.new('Unable to determine target project') if target.nil?
115 raise MissingInformation.new('Unable to determine target project') if target.nil?
109 target
116 target
110 end
117 end
111
118
112 # Adds a note to an existing issue
119 # Adds a note to an existing issue
113 def receive_issue_update(issue_id)
120 def receive_issue_update(issue_id)
114 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
121 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
115
122
116 issue = Issue.find_by_id(issue_id)
123 issue = Issue.find_by_id(issue_id)
117 return unless issue
124 return unless issue
118 # check permission
125 # check permission
119 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
126 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
120 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
127 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
121
128
122 # add the note
129 # add the note
123 journal = issue.init_journal(user, plain_text_body)
130 journal = issue.init_journal(user, plain_text_body)
124 add_attachments(issue)
131 add_attachments(issue)
125 # check workflow
132 # check workflow
126 if status && issue.new_statuses_allowed_to(user).include?(status)
133 if status && issue.new_statuses_allowed_to(user).include?(status)
127 issue.status = status
134 issue.status = status
128 end
135 end
129 issue.save!
136 issue.save!
130 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
137 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
131 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
138 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
132 journal
139 journal
133 end
140 end
134
141
135 def add_attachments(obj)
142 def add_attachments(obj)
136 if email.has_attachments?
143 if email.has_attachments?
137 email.attachments.each do |attachment|
144 email.attachments.each do |attachment|
138 Attachment.create(:container => obj,
145 Attachment.create(:container => obj,
139 :file => attachment,
146 :file => attachment,
140 :author => user,
147 :author => user,
141 :content_type => attachment.content_type)
148 :content_type => attachment.content_type)
142 end
149 end
143 end
150 end
144 end
151 end
145
152
146 # Adds To and Cc as watchers of the given object if the sender has the
153 # Adds To and Cc as watchers of the given object if the sender has the
147 # appropriate permission
154 # appropriate permission
148 def add_watchers(obj)
155 def add_watchers(obj)
149 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
156 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
150 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
157 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
151 unless addresses.empty?
158 unless addresses.empty?
152 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
159 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
153 watchers.each {|w| obj.add_watcher(w)}
160 watchers.each {|w| obj.add_watcher(w)}
154 end
161 end
155 end
162 end
156 end
163 end
157
164
158 def get_keyword(attr)
165 def get_keyword(attr, options={})
159 if @@handler_options[:allow_override].include?(attr.to_s) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
166 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
160 $1.strip
167 $1.strip
161 elsif !@@handler_options[:issue][attr].blank?
168 elsif !@@handler_options[:issue][attr].blank?
162 @@handler_options[:issue][attr]
169 @@handler_options[:issue][attr]
163 end
170 end
164 end
171 end
165
172
166 # Returns the text/plain part of the email
173 # Returns the text/plain part of the email
167 # If not found (eg. HTML-only email), returns the body with tags removed
174 # If not found (eg. HTML-only email), returns the body with tags removed
168 def plain_text_body
175 def plain_text_body
169 return @plain_text_body unless @plain_text_body.nil?
176 return @plain_text_body unless @plain_text_body.nil?
170 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
177 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
171 if parts.empty?
178 if parts.empty?
172 parts << @email
179 parts << @email
173 end
180 end
174 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
181 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
175 if plain_text_part.nil?
182 if plain_text_part.nil?
176 # no text/plain part found, assuming html-only email
183 # no text/plain part found, assuming html-only email
177 # strip html tags and remove doctype directive
184 # strip html tags and remove doctype directive
178 @plain_text_body = strip_tags(@email.body.to_s)
185 @plain_text_body = strip_tags(@email.body.to_s)
179 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
186 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
180 else
187 else
181 @plain_text_body = plain_text_part.body.to_s
188 @plain_text_body = plain_text_part.body.to_s
182 end
189 end
183 @plain_text_body.strip!
190 @plain_text_body.strip!
184 end
191 end
185 end
192 end
@@ -1,12 +1,12
1 <% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %>
1 <% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %>
2 <select onchange="if (this.value != '') { window.location = this.value; }">
2 <select onchange="if (this.value != '') { window.location = this.value; }">
3 <option selected="selected"><%= l(:label_jump_to_a_project) %></option>
3 <option selected="selected"><%= l(:label_jump_to_a_project) %></option>
4 <option disabled="disabled">---</option>
4 <option disabled="disabled">---</option>
5 <% user_projects_by_root.keys.sort.each do |root| %>
5 <% user_projects_by_root.keys.sort.each do |root| %>
6 <%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root)) %>
6 <%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item)) %>
7 <% user_projects_by_root[root].sort.each do |project| %>
7 <% user_projects_by_root[root].sort.each do |project| %>
8 <% next if project == root %>
8 <% next if project == root %>
9 <%= content_tag('option', ('&#187; ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project)) %>
9 <%= content_tag('option', ('&#187; ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project, :jump => current_menu_item)) %>
10 <% end %>
10 <% end %>
11 <% end %>
11 <% end %>
12 </select>
12 </select>
@@ -1,208 +1,219
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 'gloc'
18 require 'gloc'
19
19
20 module Redmine
20 module Redmine
21 module MenuManager
21 module MenuManager
22 module MenuController
22 module MenuController
23 def self.included(base)
23 def self.included(base)
24 base.extend(ClassMethods)
24 base.extend(ClassMethods)
25 end
25 end
26
26
27 module ClassMethods
27 module ClassMethods
28 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
28 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
29 mattr_accessor :menu_items
29 mattr_accessor :menu_items
30
30
31 # Set the menu item name for a controller or specific actions
31 # Set the menu item name for a controller or specific actions
32 # Examples:
32 # Examples:
33 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
33 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
34 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
34 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
35 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
35 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
36 #
36 #
37 # The default menu item name for a controller is controller_name by default
37 # The default menu item name for a controller is controller_name by default
38 # Eg. the default menu item name for ProjectsController is :projects
38 # Eg. the default menu item name for ProjectsController is :projects
39 def menu_item(id, options = {})
39 def menu_item(id, options = {})
40 if actions = options[:only]
40 if actions = options[:only]
41 actions = [] << actions unless actions.is_a?(Array)
41 actions = [] << actions unless actions.is_a?(Array)
42 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
42 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
43 else
43 else
44 menu_items[controller_name.to_sym][:default] = id
44 menu_items[controller_name.to_sym][:default] = id
45 end
45 end
46 end
46 end
47 end
47 end
48
48
49 def menu_items
49 def menu_items
50 self.class.menu_items
50 self.class.menu_items
51 end
51 end
52
52
53 # Returns the menu item name according to the current action
53 # Returns the menu item name according to the current action
54 def current_menu_item
54 def current_menu_item
55 menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
55 @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
56 menu_items[controller_name.to_sym][:default]
56 menu_items[controller_name.to_sym][:default]
57 end
58
59 # Redirects user to the menu item of the given project
60 # Returns false if user is not authorized
61 def redirect_to_project_menu_item(project, name)
62 item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
63 if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
64 redirect_to({item.param => project}.merge(item.url))
65 return true
66 end
67 false
57 end
68 end
58 end
69 end
59
70
60 module MenuHelper
71 module MenuHelper
61 # Returns the current menu item name
72 # Returns the current menu item name
62 def current_menu_item
73 def current_menu_item
63 @controller.current_menu_item
74 @controller.current_menu_item
64 end
75 end
65
76
66 # Renders the application main menu
77 # Renders the application main menu
67 def render_main_menu(project)
78 def render_main_menu(project)
68 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
79 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
69 end
80 end
70
81
71 def render_menu(menu, project=nil)
82 def render_menu(menu, project=nil)
72 links = []
83 links = []
73 menu_items_for(menu, project) do |item, caption, url, selected|
84 menu_items_for(menu, project) do |item, caption, url, selected|
74 links << content_tag('li',
85 links << content_tag('li',
75 link_to(h(caption), url, item.html_options(:selected => selected)))
86 link_to(h(caption), url, item.html_options(:selected => selected)))
76 end
87 end
77 links.empty? ? nil : content_tag('ul', links.join("\n"))
88 links.empty? ? nil : content_tag('ul', links.join("\n"))
78 end
89 end
79
90
80 def menu_items_for(menu, project=nil)
91 def menu_items_for(menu, project=nil)
81 items = []
92 items = []
82 Redmine::MenuManager.allowed_items(menu, User.current, project).each do |item|
93 Redmine::MenuManager.allowed_items(menu, User.current, project).each do |item|
83 unless item.condition && !item.condition.call(project)
94 unless item.condition && !item.condition.call(project)
84 url = case item.url
95 url = case item.url
85 when Hash
96 when Hash
86 project.nil? ? item.url : {item.param => project}.merge(item.url)
97 project.nil? ? item.url : {item.param => project}.merge(item.url)
87 when Symbol
98 when Symbol
88 send(item.url)
99 send(item.url)
89 else
100 else
90 item.url
101 item.url
91 end
102 end
92 caption = item.caption(project)
103 caption = item.caption(project)
93 caption = l(caption) if caption.is_a?(Symbol)
104 caption = l(caption) if caption.is_a?(Symbol)
94 if block_given?
105 if block_given?
95 yield item, caption, url, (current_menu_item == item.name)
106 yield item, caption, url, (current_menu_item == item.name)
96 else
107 else
97 items << [item, caption, url, (current_menu_item == item.name)]
108 items << [item, caption, url, (current_menu_item == item.name)]
98 end
109 end
99 end
110 end
100 end
111 end
101 return block_given? ? nil : items
112 return block_given? ? nil : items
102 end
113 end
103 end
114 end
104
115
105 class << self
116 class << self
106 def map(menu_name)
117 def map(menu_name)
107 @items ||= {}
118 @items ||= {}
108 mapper = Mapper.new(menu_name.to_sym, @items)
119 mapper = Mapper.new(menu_name.to_sym, @items)
109 if block_given?
120 if block_given?
110 yield mapper
121 yield mapper
111 else
122 else
112 mapper
123 mapper
113 end
124 end
114 end
125 end
115
126
116 def items(menu_name)
127 def items(menu_name)
117 @items[menu_name.to_sym] || []
128 @items[menu_name.to_sym] || []
118 end
129 end
119
130
120 def allowed_items(menu_name, user, project)
131 def allowed_items(menu_name, user, project)
121 project ? items(menu_name).select {|item| user && user.allowed_to?(item.url, project)} : items(menu_name)
132 project ? items(menu_name).select {|item| user && user.allowed_to?(item.url, project)} : items(menu_name)
122 end
133 end
123 end
134 end
124
135
125 class Mapper
136 class Mapper
126 def initialize(menu, items)
137 def initialize(menu, items)
127 items[menu] ||= []
138 items[menu] ||= []
128 @menu = menu
139 @menu = menu
129 @menu_items = items[menu]
140 @menu_items = items[menu]
130 end
141 end
131
142
132 @@last_items_count = Hash.new {|h,k| h[k] = 0}
143 @@last_items_count = Hash.new {|h,k| h[k] = 0}
133
144
134 # Adds an item at the end of the menu. Available options:
145 # Adds an item at the end of the menu. Available options:
135 # * param: the parameter name that is used for the project id (default is :id)
146 # * param: the parameter name that is used for the project id (default is :id)
136 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
147 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
137 # * caption that can be:
148 # * caption that can be:
138 # * a localized string Symbol
149 # * a localized string Symbol
139 # * a String
150 # * a String
140 # * a Proc that can take the project as argument
151 # * a Proc that can take the project as argument
141 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
152 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
142 # * last: menu item will stay at the end (eg. :last => true)
153 # * last: menu item will stay at the end (eg. :last => true)
143 # * html_options: a hash of html options that are passed to link_to
154 # * html_options: a hash of html options that are passed to link_to
144 def push(name, url, options={})
155 def push(name, url, options={})
145 options = options.dup
156 options = options.dup
146
157
147 # menu item position
158 # menu item position
148 if before = options.delete(:before)
159 if before = options.delete(:before)
149 position = @menu_items.collect(&:name).index(before)
160 position = @menu_items.collect(&:name).index(before)
150 elsif after = options.delete(:after)
161 elsif after = options.delete(:after)
151 position = @menu_items.collect(&:name).index(after)
162 position = @menu_items.collect(&:name).index(after)
152 position += 1 unless position.nil?
163 position += 1 unless position.nil?
153 elsif options.delete(:last)
164 elsif options.delete(:last)
154 position = @menu_items.size
165 position = @menu_items.size
155 @@last_items_count[@menu] += 1
166 @@last_items_count[@menu] += 1
156 end
167 end
157 # default position
168 # default position
158 position ||= @menu_items.size - @@last_items_count[@menu]
169 position ||= @menu_items.size - @@last_items_count[@menu]
159
170
160 @menu_items.insert(position, MenuItem.new(name, url, options))
171 @menu_items.insert(position, MenuItem.new(name, url, options))
161 end
172 end
162
173
163 # Removes a menu item
174 # Removes a menu item
164 def delete(name)
175 def delete(name)
165 @menu_items.delete_if {|i| i.name == name}
176 @menu_items.delete_if {|i| i.name == name}
166 end
177 end
167 end
178 end
168
179
169 class MenuItem
180 class MenuItem
170 include GLoc
181 include GLoc
171 attr_reader :name, :url, :param, :condition
182 attr_reader :name, :url, :param, :condition
172
183
173 def initialize(name, url, options)
184 def initialize(name, url, options)
174 raise "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
185 raise "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
175 raise "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
186 raise "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
176 @name = name
187 @name = name
177 @url = url
188 @url = url
178 @condition = options[:if]
189 @condition = options[:if]
179 @param = options[:param] || :id
190 @param = options[:param] || :id
180 @caption = options[:caption]
191 @caption = options[:caption]
181 @html_options = options[:html] || {}
192 @html_options = options[:html] || {}
182 # Adds a unique class to each menu item based on its name
193 # Adds a unique class to each menu item based on its name
183 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
194 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
184 end
195 end
185
196
186 def caption(project=nil)
197 def caption(project=nil)
187 if @caption.is_a?(Proc)
198 if @caption.is_a?(Proc)
188 c = @caption.call(project).to_s
199 c = @caption.call(project).to_s
189 c = @name.to_s.humanize if c.blank?
200 c = @name.to_s.humanize if c.blank?
190 c
201 c
191 else
202 else
192 # check if localized string exists on first render (after GLoc strings are loaded)
203 # check if localized string exists on first render (after GLoc strings are loaded)
193 @caption_key ||= (@caption || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize))
204 @caption_key ||= (@caption || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize))
194 end
205 end
195 end
206 end
196
207
197 def html_options(options={})
208 def html_options(options={})
198 if options[:selected]
209 if options[:selected]
199 o = @html_options.dup
210 o = @html_options.dup
200 o[:class] += ' selected'
211 o[:class] += ' selected'
201 o
212 o
202 else
213 else
203 @html_options
214 @html_options
204 end
215 end
205 end
216 end
206 end
217 end
207 end
218 end
208 end
219 end
@@ -1,340 +1,357
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 :attachments
27 :attachments
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:project_tree)
41 assert_not_nil assigns(:project_tree)
42 # Root project as hash key
42 # Root project as hash key
43 assert assigns(:project_tree).keys.include?(Project.find(1))
43 assert assigns(:project_tree).keys.include?(Project.find(1))
44 # Subproject in corresponding value
44 # Subproject in corresponding value
45 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
45 assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
46 end
46 end
47
47
48 def test_index_atom
48 def test_index_atom
49 get :index, :format => 'atom'
49 get :index, :format => 'atom'
50 assert_response :success
50 assert_response :success
51 assert_template 'common/feed.atom.rxml'
51 assert_template 'common/feed.atom.rxml'
52 assert_select 'feed>title', :text => 'Redmine: Latest projects'
52 assert_select 'feed>title', :text => 'Redmine: Latest projects'
53 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
53 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
54 end
54 end
55
55
56 def test_show_by_id
56 def test_show_by_id
57 get :show, :id => 1
57 get :show, :id => 1
58 assert_response :success
58 assert_response :success
59 assert_template 'show'
59 assert_template 'show'
60 assert_not_nil assigns(:project)
60 assert_not_nil assigns(:project)
61 end
61 end
62
62
63 def test_show_by_identifier
63 def test_show_by_identifier
64 get :show, :id => 'ecookbook'
64 get :show, :id => 'ecookbook'
65 assert_response :success
65 assert_response :success
66 assert_template 'show'
66 assert_template 'show'
67 assert_not_nil assigns(:project)
67 assert_not_nil assigns(:project)
68 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
68 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
69 end
69 end
70
70
71 def test_private_subprojects_hidden
71 def test_private_subprojects_hidden
72 get :show, :id => 'ecookbook'
72 get :show, :id => 'ecookbook'
73 assert_response :success
73 assert_response :success
74 assert_template 'show'
74 assert_template 'show'
75 assert_no_tag :tag => 'a', :content => /Private child/
75 assert_no_tag :tag => 'a', :content => /Private child/
76 end
76 end
77
77
78 def test_private_subprojects_visible
78 def test_private_subprojects_visible
79 @request.session[:user_id] = 2 # manager who is a member of the private subproject
79 @request.session[:user_id] = 2 # manager who is a member of the private subproject
80 get :show, :id => 'ecookbook'
80 get :show, :id => 'ecookbook'
81 assert_response :success
81 assert_response :success
82 assert_template 'show'
82 assert_template 'show'
83 assert_tag :tag => 'a', :content => /Private child/
83 assert_tag :tag => 'a', :content => /Private child/
84 end
84 end
85
85
86 def test_settings
86 def test_settings
87 @request.session[:user_id] = 2 # manager
87 @request.session[:user_id] = 2 # manager
88 get :settings, :id => 1
88 get :settings, :id => 1
89 assert_response :success
89 assert_response :success
90 assert_template 'settings'
90 assert_template 'settings'
91 end
91 end
92
92
93 def test_edit
93 def test_edit
94 @request.session[:user_id] = 2 # manager
94 @request.session[:user_id] = 2 # manager
95 post :edit, :id => 1, :project => {:name => 'Test changed name',
95 post :edit, :id => 1, :project => {:name => 'Test changed name',
96 :issue_custom_field_ids => ['']}
96 :issue_custom_field_ids => ['']}
97 assert_redirected_to 'projects/settings/ecookbook'
97 assert_redirected_to 'projects/settings/ecookbook'
98 project = Project.find(1)
98 project = Project.find(1)
99 assert_equal 'Test changed name', project.name
99 assert_equal 'Test changed name', project.name
100 end
100 end
101
101
102 def test_get_destroy
102 def test_get_destroy
103 @request.session[:user_id] = 1 # admin
103 @request.session[:user_id] = 1 # admin
104 get :destroy, :id => 1
104 get :destroy, :id => 1
105 assert_response :success
105 assert_response :success
106 assert_template 'destroy'
106 assert_template 'destroy'
107 assert_not_nil Project.find_by_id(1)
107 assert_not_nil Project.find_by_id(1)
108 end
108 end
109
109
110 def test_post_destroy
110 def test_post_destroy
111 @request.session[:user_id] = 1 # admin
111 @request.session[:user_id] = 1 # admin
112 post :destroy, :id => 1, :confirm => 1
112 post :destroy, :id => 1, :confirm => 1
113 assert_redirected_to 'admin/projects'
113 assert_redirected_to 'admin/projects'
114 assert_nil Project.find_by_id(1)
114 assert_nil Project.find_by_id(1)
115 end
115 end
116
116
117 def test_add_file
117 def test_add_file
118 set_tmp_attachments_directory
118 set_tmp_attachments_directory
119 @request.session[:user_id] = 2
119 @request.session[:user_id] = 2
120 Setting.notified_events = ['file_added']
120 Setting.notified_events = ['file_added']
121 ActionMailer::Base.deliveries.clear
121 ActionMailer::Base.deliveries.clear
122
122
123 assert_difference 'Attachment.count' do
123 assert_difference 'Attachment.count' do
124 post :add_file, :id => 1, :version_id => '',
124 post :add_file, :id => 1, :version_id => '',
125 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
125 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
126 end
126 end
127 assert_redirected_to 'projects/list_files/ecookbook'
127 assert_redirected_to 'projects/list_files/ecookbook'
128 a = Attachment.find(:first, :order => 'created_on DESC')
128 a = Attachment.find(:first, :order => 'created_on DESC')
129 assert_equal 'testfile.txt', a.filename
129 assert_equal 'testfile.txt', a.filename
130 assert_equal Project.find(1), a.container
130 assert_equal Project.find(1), a.container
131
131
132 mail = ActionMailer::Base.deliveries.last
132 mail = ActionMailer::Base.deliveries.last
133 assert_kind_of TMail::Mail, mail
133 assert_kind_of TMail::Mail, mail
134 assert_equal "[eCookbook] New file", mail.subject
134 assert_equal "[eCookbook] New file", mail.subject
135 assert mail.body.include?('testfile.txt')
135 assert mail.body.include?('testfile.txt')
136 end
136 end
137
137
138 def test_add_version_file
138 def test_add_version_file
139 set_tmp_attachments_directory
139 set_tmp_attachments_directory
140 @request.session[:user_id] = 2
140 @request.session[:user_id] = 2
141 Setting.notified_events = ['file_added']
141 Setting.notified_events = ['file_added']
142
142
143 assert_difference 'Attachment.count' do
143 assert_difference 'Attachment.count' do
144 post :add_file, :id => 1, :version_id => '2',
144 post :add_file, :id => 1, :version_id => '2',
145 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
145 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
146 end
146 end
147 assert_redirected_to 'projects/list_files/ecookbook'
147 assert_redirected_to 'projects/list_files/ecookbook'
148 a = Attachment.find(:first, :order => 'created_on DESC')
148 a = Attachment.find(:first, :order => 'created_on DESC')
149 assert_equal 'testfile.txt', a.filename
149 assert_equal 'testfile.txt', a.filename
150 assert_equal Version.find(2), a.container
150 assert_equal Version.find(2), a.container
151 end
151 end
152
152
153 def test_list_files
153 def test_list_files
154 get :list_files, :id => 1
154 get :list_files, :id => 1
155 assert_response :success
155 assert_response :success
156 assert_template 'list_files'
156 assert_template 'list_files'
157 assert_not_nil assigns(:containers)
157 assert_not_nil assigns(:containers)
158
158
159 # file attached to the project
159 # file attached to the project
160 assert_tag :a, :content => 'project_file.zip',
160 assert_tag :a, :content => 'project_file.zip',
161 :attributes => { :href => '/attachments/download/8/project_file.zip' }
161 :attributes => { :href => '/attachments/download/8/project_file.zip' }
162
162
163 # file attached to a project's version
163 # file attached to a project's version
164 assert_tag :a, :content => 'version_file.zip',
164 assert_tag :a, :content => 'version_file.zip',
165 :attributes => { :href => '/attachments/download/9/version_file.zip' }
165 :attributes => { :href => '/attachments/download/9/version_file.zip' }
166 end
166 end
167
167
168 def test_changelog
168 def test_changelog
169 get :changelog, :id => 1
169 get :changelog, :id => 1
170 assert_response :success
170 assert_response :success
171 assert_template 'changelog'
171 assert_template 'changelog'
172 assert_not_nil assigns(:versions)
172 assert_not_nil assigns(:versions)
173 end
173 end
174
174
175 def test_roadmap
175 def test_roadmap
176 get :roadmap, :id => 1
176 get :roadmap, :id => 1
177 assert_response :success
177 assert_response :success
178 assert_template 'roadmap'
178 assert_template 'roadmap'
179 assert_not_nil assigns(:versions)
179 assert_not_nil assigns(:versions)
180 # Version with no date set appears
180 # Version with no date set appears
181 assert assigns(:versions).include?(Version.find(3))
181 assert assigns(:versions).include?(Version.find(3))
182 # Completed version doesn't appear
182 # Completed version doesn't appear
183 assert !assigns(:versions).include?(Version.find(1))
183 assert !assigns(:versions).include?(Version.find(1))
184 end
184 end
185
185
186 def test_roadmap_with_completed_versions
186 def test_roadmap_with_completed_versions
187 get :roadmap, :id => 1, :completed => 1
187 get :roadmap, :id => 1, :completed => 1
188 assert_response :success
188 assert_response :success
189 assert_template 'roadmap'
189 assert_template 'roadmap'
190 assert_not_nil assigns(:versions)
190 assert_not_nil assigns(:versions)
191 # Version with no date set appears
191 # Version with no date set appears
192 assert assigns(:versions).include?(Version.find(3))
192 assert assigns(:versions).include?(Version.find(3))
193 # Completed version appears
193 # Completed version appears
194 assert assigns(:versions).include?(Version.find(1))
194 assert assigns(:versions).include?(Version.find(1))
195 end
195 end
196
196
197 def test_project_activity
197 def test_project_activity
198 get :activity, :id => 1, :with_subprojects => 0
198 get :activity, :id => 1, :with_subprojects => 0
199 assert_response :success
199 assert_response :success
200 assert_template 'activity'
200 assert_template 'activity'
201 assert_not_nil assigns(:events_by_day)
201 assert_not_nil assigns(:events_by_day)
202
202
203 assert_tag :tag => "h3",
203 assert_tag :tag => "h3",
204 :content => /#{2.days.ago.to_date.day}/,
204 :content => /#{2.days.ago.to_date.day}/,
205 :sibling => { :tag => "dl",
205 :sibling => { :tag => "dl",
206 :child => { :tag => "dt",
206 :child => { :tag => "dt",
207 :attributes => { :class => /issue-edit/ },
207 :attributes => { :class => /issue-edit/ },
208 :child => { :tag => "a",
208 :child => { :tag => "a",
209 :content => /(#{IssueStatus.find(2).name})/,
209 :content => /(#{IssueStatus.find(2).name})/,
210 }
210 }
211 }
211 }
212 }
212 }
213 end
213 end
214
214
215 def test_previous_project_activity
215 def test_previous_project_activity
216 get :activity, :id => 1, :from => 3.days.ago.to_date
216 get :activity, :id => 1, :from => 3.days.ago.to_date
217 assert_response :success
217 assert_response :success
218 assert_template 'activity'
218 assert_template 'activity'
219 assert_not_nil assigns(:events_by_day)
219 assert_not_nil assigns(:events_by_day)
220
220
221 assert_tag :tag => "h3",
221 assert_tag :tag => "h3",
222 :content => /#{3.day.ago.to_date.day}/,
222 :content => /#{3.day.ago.to_date.day}/,
223 :sibling => { :tag => "dl",
223 :sibling => { :tag => "dl",
224 :child => { :tag => "dt",
224 :child => { :tag => "dt",
225 :attributes => { :class => /issue/ },
225 :attributes => { :class => /issue/ },
226 :child => { :tag => "a",
226 :child => { :tag => "a",
227 :content => /#{Issue.find(1).subject}/,
227 :content => /#{Issue.find(1).subject}/,
228 }
228 }
229 }
229 }
230 }
230 }
231 end
231 end
232
232
233 def test_global_activity
233 def test_global_activity
234 get :activity
234 get :activity
235 assert_response :success
235 assert_response :success
236 assert_template 'activity'
236 assert_template 'activity'
237 assert_not_nil assigns(:events_by_day)
237 assert_not_nil assigns(:events_by_day)
238
238
239 assert_tag :tag => "h3",
239 assert_tag :tag => "h3",
240 :content => /#{5.day.ago.to_date.day}/,
240 :content => /#{5.day.ago.to_date.day}/,
241 :sibling => { :tag => "dl",
241 :sibling => { :tag => "dl",
242 :child => { :tag => "dt",
242 :child => { :tag => "dt",
243 :attributes => { :class => /issue/ },
243 :attributes => { :class => /issue/ },
244 :child => { :tag => "a",
244 :child => { :tag => "a",
245 :content => /#{Issue.find(5).subject}/,
245 :content => /#{Issue.find(5).subject}/,
246 }
246 }
247 }
247 }
248 }
248 }
249 end
249 end
250
250
251 def test_user_activity
251 def test_user_activity
252 get :activity, :user_id => 2
252 get :activity, :user_id => 2
253 assert_response :success
253 assert_response :success
254 assert_template 'activity'
254 assert_template 'activity'
255 assert_not_nil assigns(:events_by_day)
255 assert_not_nil assigns(:events_by_day)
256
256
257 assert_tag :tag => "h3",
257 assert_tag :tag => "h3",
258 :content => /#{3.day.ago.to_date.day}/,
258 :content => /#{3.day.ago.to_date.day}/,
259 :sibling => { :tag => "dl",
259 :sibling => { :tag => "dl",
260 :child => { :tag => "dt",
260 :child => { :tag => "dt",
261 :attributes => { :class => /issue/ },
261 :attributes => { :class => /issue/ },
262 :child => { :tag => "a",
262 :child => { :tag => "a",
263 :content => /#{Issue.find(1).subject}/,
263 :content => /#{Issue.find(1).subject}/,
264 }
264 }
265 }
265 }
266 }
266 }
267 end
267 end
268
268
269 def test_activity_atom_feed
269 def test_activity_atom_feed
270 get :activity, :format => 'atom'
270 get :activity, :format => 'atom'
271 assert_response :success
271 assert_response :success
272 assert_template 'common/feed.atom.rxml'
272 assert_template 'common/feed.atom.rxml'
273 end
273 end
274
274
275 def test_archive
275 def test_archive
276 @request.session[:user_id] = 1 # admin
276 @request.session[:user_id] = 1 # admin
277 post :archive, :id => 1
277 post :archive, :id => 1
278 assert_redirected_to 'admin/projects'
278 assert_redirected_to 'admin/projects'
279 assert !Project.find(1).active?
279 assert !Project.find(1).active?
280 end
280 end
281
281
282 def test_unarchive
282 def test_unarchive
283 @request.session[:user_id] = 1 # admin
283 @request.session[:user_id] = 1 # admin
284 Project.find(1).archive
284 Project.find(1).archive
285 post :unarchive, :id => 1
285 post :unarchive, :id => 1
286 assert_redirected_to 'admin/projects'
286 assert_redirected_to 'admin/projects'
287 assert Project.find(1).active?
287 assert Project.find(1).active?
288 end
288 end
289
289
290 def test_jump_should_redirect_to_active_tab
291 get :show, :id => 1, :jump => 'issues'
292 assert_redirected_to 'projects/ecookbook/issues'
293 end
294
295 def test_jump_should_not_redirect_to_inactive_tab
296 get :show, :id => 3, :jump => 'documents'
297 assert_response :success
298 assert_template 'show'
299 end
300
301 def test_jump_should_not_redirect_to_unknown_tab
302 get :show, :id => 3, :jump => 'foobar'
303 assert_response :success
304 assert_template 'show'
305 end
306
290 def test_project_menu
307 def test_project_menu
291 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
308 assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
292 Redmine::MenuManager.map :project_menu do |menu|
309 Redmine::MenuManager.map :project_menu do |menu|
293 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
310 menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
294 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
311 menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
295 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
312 menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
296 end
313 end
297
314
298 get :show, :id => 1
315 get :show, :id => 1
299 assert_tag :div, :attributes => { :id => 'main-menu' },
316 assert_tag :div, :attributes => { :id => 'main-menu' },
300 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
317 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo',
301 :attributes => { :class => 'foo' } } }
318 :attributes => { :class => 'foo' } } }
302
319
303 assert_tag :div, :attributes => { :id => 'main-menu' },
320 assert_tag :div, :attributes => { :id => 'main-menu' },
304 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
321 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar',
305 :attributes => { :class => 'bar' } },
322 :attributes => { :class => 'bar' } },
306 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
323 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
307
324
308 assert_tag :div, :attributes => { :id => 'main-menu' },
325 assert_tag :div, :attributes => { :id => 'main-menu' },
309 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
326 :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK',
310 :attributes => { :class => 'hello' } },
327 :attributes => { :class => 'hello' } },
311 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
328 :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
312
329
313 # Remove the menu items
330 # Remove the menu items
314 Redmine::MenuManager.map :project_menu do |menu|
331 Redmine::MenuManager.map :project_menu do |menu|
315 menu.delete :foo
332 menu.delete :foo
316 menu.delete :bar
333 menu.delete :bar
317 menu.delete :hello
334 menu.delete :hello
318 end
335 end
319 end
336 end
320 end
337 end
321
338
322 # A hook that is manually registered later
339 # A hook that is manually registered later
323 class ProjectBasedTemplate < Redmine::Hook::ViewListener
340 class ProjectBasedTemplate < Redmine::Hook::ViewListener
324 def view_layouts_base_html_head(context)
341 def view_layouts_base_html_head(context)
325 # Adds a project stylesheet
342 # Adds a project stylesheet
326 stylesheet_link_tag(context[:project].identifier) if context[:project]
343 stylesheet_link_tag(context[:project].identifier) if context[:project]
327 end
344 end
328 end
345 end
329 # Don't use this hook now
346 # Don't use this hook now
330 Redmine::Hook.clear_listeners
347 Redmine::Hook.clear_listeners
331
348
332 def test_hook_response
349 def test_hook_response
333 Redmine::Hook.add_listener(ProjectBasedTemplate)
350 Redmine::Hook.add_listener(ProjectBasedTemplate)
334 get :show, :id => 1
351 get :show, :id => 1
335 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
352 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
336 :parent => {:tag => 'head'}
353 :parent => {:tag => 'head'}
337
354
338 Redmine::Hook.clear_listeners
355 Redmine::Hook.clear_listeners
339 end
356 end
340 end
357 end
@@ -1,148 +1,159
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
19
20 class MailHandlerTest < Test::Unit::TestCase
20 class MailHandlerTest < Test::Unit::TestCase
21 fixtures :users, :projects,
21 fixtures :users, :projects,
22 :enabled_modules,
22 :enabled_modules,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :issues,
25 :issues,
26 :issue_statuses,
26 :issue_statuses,
27 :workflows,
27 :workflows,
28 :trackers,
28 :trackers,
29 :projects_trackers,
29 :projects_trackers,
30 :enumerations,
30 :enumerations,
31 :issue_categories
31 :issue_categories,
32 :custom_fields,
33 :custom_fields_trackers
32
34
33 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
35 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
34
36
35 def setup
37 def setup
36 ActionMailer::Base.deliveries.clear
38 ActionMailer::Base.deliveries.clear
37 end
39 end
38
40
39 def test_add_issue
41 def test_add_issue
40 # This email contains: 'Project: onlinestore'
42 # This email contains: 'Project: onlinestore'
41 issue = submit_email('ticket_on_given_project.eml')
43 issue = submit_email('ticket_on_given_project.eml')
42 assert issue.is_a?(Issue)
44 assert issue.is_a?(Issue)
43 assert !issue.new_record?
45 assert !issue.new_record?
44 issue.reload
46 issue.reload
45 assert_equal 'New ticket on a given project', issue.subject
47 assert_equal 'New ticket on a given project', issue.subject
46 assert_equal User.find_by_login('jsmith'), issue.author
48 assert_equal User.find_by_login('jsmith'), issue.author
47 assert_equal Project.find(2), issue.project
49 assert_equal Project.find(2), issue.project
48 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
50 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
49 end
51 end
50
52
51 def test_add_issue_with_status
53 def test_add_issue_with_status
52 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
54 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
53 issue = submit_email('ticket_on_given_project.eml')
55 issue = submit_email('ticket_on_given_project.eml')
54 assert issue.is_a?(Issue)
56 assert issue.is_a?(Issue)
55 assert !issue.new_record?
57 assert !issue.new_record?
56 issue.reload
58 issue.reload
57 assert_equal Project.find(2), issue.project
59 assert_equal Project.find(2), issue.project
58 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
60 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
59 end
61 end
60
62
61 def test_add_issue_with_attributes_override
63 def test_add_issue_with_attributes_override
62 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
64 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
63 assert issue.is_a?(Issue)
65 assert issue.is_a?(Issue)
64 assert !issue.new_record?
66 assert !issue.new_record?
65 issue.reload
67 issue.reload
66 assert_equal 'New ticket on a given project', issue.subject
68 assert_equal 'New ticket on a given project', issue.subject
67 assert_equal User.find_by_login('jsmith'), issue.author
69 assert_equal User.find_by_login('jsmith'), issue.author
68 assert_equal Project.find(2), issue.project
70 assert_equal Project.find(2), issue.project
69 assert_equal 'Feature request', issue.tracker.to_s
71 assert_equal 'Feature request', issue.tracker.to_s
70 assert_equal 'Stock management', issue.category.to_s
72 assert_equal 'Stock management', issue.category.to_s
71 assert_equal 'Urgent', issue.priority.to_s
73 assert_equal 'Urgent', issue.priority.to_s
72 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
74 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
73 end
75 end
74
76
75 def test_add_issue_with_partial_attributes_override
77 def test_add_issue_with_partial_attributes_override
76 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
78 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
77 assert issue.is_a?(Issue)
79 assert issue.is_a?(Issue)
78 assert !issue.new_record?
80 assert !issue.new_record?
79 issue.reload
81 issue.reload
80 assert_equal 'New ticket on a given project', issue.subject
82 assert_equal 'New ticket on a given project', issue.subject
81 assert_equal User.find_by_login('jsmith'), issue.author
83 assert_equal User.find_by_login('jsmith'), issue.author
82 assert_equal Project.find(2), issue.project
84 assert_equal Project.find(2), issue.project
83 assert_equal 'Feature request', issue.tracker.to_s
85 assert_equal 'Feature request', issue.tracker.to_s
84 assert_nil issue.category
86 assert_nil issue.category
85 assert_equal 'High', issue.priority.to_s
87 assert_equal 'High', issue.priority.to_s
86 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
88 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
87 end
89 end
88
90
89 def test_add_issue_with_attachment_to_specific_project
91 def test_add_issue_with_attachment_to_specific_project
90 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
92 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
91 assert issue.is_a?(Issue)
93 assert issue.is_a?(Issue)
92 assert !issue.new_record?
94 assert !issue.new_record?
93 issue.reload
95 issue.reload
94 assert_equal 'Ticket created by email with attachment', issue.subject
96 assert_equal 'Ticket created by email with attachment', issue.subject
95 assert_equal User.find_by_login('jsmith'), issue.author
97 assert_equal User.find_by_login('jsmith'), issue.author
96 assert_equal Project.find(2), issue.project
98 assert_equal Project.find(2), issue.project
97 assert_equal 'This is a new ticket with attachments', issue.description
99 assert_equal 'This is a new ticket with attachments', issue.description
98 # Attachment properties
100 # Attachment properties
99 assert_equal 1, issue.attachments.size
101 assert_equal 1, issue.attachments.size
100 assert_equal 'Paella.jpg', issue.attachments.first.filename
102 assert_equal 'Paella.jpg', issue.attachments.first.filename
101 assert_equal 'image/jpeg', issue.attachments.first.content_type
103 assert_equal 'image/jpeg', issue.attachments.first.content_type
102 assert_equal 10790, issue.attachments.first.filesize
104 assert_equal 10790, issue.attachments.first.filesize
103 end
105 end
104
106
107 def test_add_issue_with_custom_fields
108 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
109 assert issue.is_a?(Issue)
110 assert !issue.new_record?
111 issue.reload
112 assert_equal 'New ticket with custom field values', issue.subject
113 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
114 end
115
105 def test_add_issue_with_cc
116 def test_add_issue_with_cc
106 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
117 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
107 assert issue.is_a?(Issue)
118 assert issue.is_a?(Issue)
108 assert !issue.new_record?
119 assert !issue.new_record?
109 issue.reload
120 issue.reload
110 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
121 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
111 assert_equal 1, issue.watchers.size
122 assert_equal 1, issue.watchers.size
112 end
123 end
113
124
114 def test_add_issue_note
125 def test_add_issue_note
115 journal = submit_email('ticket_reply.eml')
126 journal = submit_email('ticket_reply.eml')
116 assert journal.is_a?(Journal)
127 assert journal.is_a?(Journal)
117 assert_equal User.find_by_login('jsmith'), journal.user
128 assert_equal User.find_by_login('jsmith'), journal.user
118 assert_equal Issue.find(2), journal.journalized
129 assert_equal Issue.find(2), journal.journalized
119 assert_match /This is reply/, journal.notes
130 assert_match /This is reply/, journal.notes
120 end
131 end
121
132
122 def test_add_issue_note_with_status_change
133 def test_add_issue_note_with_status_change
123 # This email contains: 'Status: Resolved'
134 # This email contains: 'Status: Resolved'
124 journal = submit_email('ticket_reply_with_status.eml')
135 journal = submit_email('ticket_reply_with_status.eml')
125 assert journal.is_a?(Journal)
136 assert journal.is_a?(Journal)
126 issue = Issue.find(journal.issue.id)
137 issue = Issue.find(journal.issue.id)
127 assert_equal User.find_by_login('jsmith'), journal.user
138 assert_equal User.find_by_login('jsmith'), journal.user
128 assert_equal Issue.find(2), journal.journalized
139 assert_equal Issue.find(2), journal.journalized
129 assert_match /This is reply/, journal.notes
140 assert_match /This is reply/, journal.notes
130 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
141 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
131 end
142 end
132
143
133 def test_should_strip_tags_of_html_only_emails
144 def test_should_strip_tags_of_html_only_emails
134 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
145 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
135 assert issue.is_a?(Issue)
146 assert issue.is_a?(Issue)
136 assert !issue.new_record?
147 assert !issue.new_record?
137 issue.reload
148 issue.reload
138 assert_equal 'HTML email', issue.subject
149 assert_equal 'HTML email', issue.subject
139 assert_equal 'This is a html-only email.', issue.description
150 assert_equal 'This is a html-only email.', issue.description
140 end
151 end
141
152
142 private
153 private
143
154
144 def submit_email(filename, options={})
155 def submit_email(filename, options={})
145 raw = IO.read(File.join(FIXTURES_PATH, filename))
156 raw = IO.read(File.join(FIXTURES_PATH, filename))
146 MailHandler.receive(raw, options)
157 MailHandler.receive(raw, options)
147 end
158 end
148 end
159 end
General Comments 0
You need to be logged in to leave comments. Login now