##// END OF EJS Templates
Let the new time_entry form be submitted without project (#17954)....
Jean-Philippe Lang -
r13058:15d5c331ebc1
parent child
Show More
@@ -1,297 +1,280
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20
20
21 before_filter :find_project_for_new_time_entry, :only => [:create]
22 before_filter :find_time_entry, :only => [:show, :edit, :update]
21 before_filter :find_time_entry, :only => [:show, :edit, :update]
23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :authorize, :except => [:new, :index, :report]
23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
25
24
26 before_filter :find_optional_project, :only => [:index, :report]
25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
28 before_filter :authorize_global, :only => [:new, :index, :report]
29
27
30 accept_rss_auth :index
28 accept_rss_auth :index
31 accept_api_auth :index, :show, :create, :update, :destroy
29 accept_api_auth :index, :show, :create, :update, :destroy
32
30
33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
34
32
35 helper :sort
33 helper :sort
36 include SortHelper
34 include SortHelper
37 helper :issues
35 helper :issues
38 include TimelogHelper
36 include TimelogHelper
39 helper :custom_fields
37 helper :custom_fields
40 include CustomFieldsHelper
38 include CustomFieldsHelper
41 helper :queries
39 helper :queries
42 include QueriesHelper
40 include QueriesHelper
43
41
44 def index
42 def index
45 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
43 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
46
44
47 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
48 sort_update(@query.sortable_columns)
46 sort_update(@query.sortable_columns)
49 scope = time_entry_scope(:order => sort_clause).
47 scope = time_entry_scope(:order => sort_clause).
50 includes(:project, :user, :issue).
48 includes(:project, :user, :issue).
51 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
52
50
53 respond_to do |format|
51 respond_to do |format|
54 format.html {
52 format.html {
55 @entry_count = scope.count
53 @entry_count = scope.count
56 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
57 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).all
55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).all
58 @total_hours = scope.sum(:hours).to_f
56 @total_hours = scope.sum(:hours).to_f
59
57
60 render :layout => !request.xhr?
58 render :layout => !request.xhr?
61 }
59 }
62 format.api {
60 format.api {
63 @entry_count = scope.count
61 @entry_count = scope.count
64 @offset, @limit = api_offset_and_limit
62 @offset, @limit = api_offset_and_limit
65 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).all
63 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).all
66 }
64 }
67 format.atom {
65 format.atom {
68 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").all
66 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").all
69 render_feed(entries, :title => l(:label_spent_time))
67 render_feed(entries, :title => l(:label_spent_time))
70 }
68 }
71 format.csv {
69 format.csv {
72 # Export all entries
70 # Export all entries
73 @entries = scope.all
71 @entries = scope.all
74 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
72 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
75 }
73 }
76 end
74 end
77 end
75 end
78
76
79 def report
77 def report
80 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
78 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
81 scope = time_entry_scope
79 scope = time_entry_scope
82
80
83 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
81 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
84
82
85 respond_to do |format|
83 respond_to do |format|
86 format.html { render :layout => !request.xhr? }
84 format.html { render :layout => !request.xhr? }
87 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
85 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
88 end
86 end
89 end
87 end
90
88
91 def show
89 def show
92 respond_to do |format|
90 respond_to do |format|
93 # TODO: Implement html response
91 # TODO: Implement html response
94 format.html { render :nothing => true, :status => 406 }
92 format.html { render :nothing => true, :status => 406 }
95 format.api
93 format.api
96 end
94 end
97 end
95 end
98
96
99 def new
97 def new
100 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
98 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
101 @time_entry.safe_attributes = params[:time_entry]
99 @time_entry.safe_attributes = params[:time_entry]
102 end
100 end
103
101
104 def create
102 def create
105 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
103 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
106 @time_entry.safe_attributes = params[:time_entry]
104 @time_entry.safe_attributes = params[:time_entry]
105 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
106 render_403
107 return
108 end
107
109
108 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
109
111
110 if @time_entry.save
112 if @time_entry.save
111 respond_to do |format|
113 respond_to do |format|
112 format.html {
114 format.html {
113 flash[:notice] = l(:notice_successful_create)
115 flash[:notice] = l(:notice_successful_create)
114 if params[:continue]
116 if params[:continue]
115 if params[:project_id]
116 options = {
117 options = {
117 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
118 :time_entry => {
119 :project_id => params[:time_entry][:project_id],
120 :issue_id => @time_entry.issue_id,
121 :activity_id => @time_entry.activity_id
122 },
118 :back_url => params[:back_url]
123 :back_url => params[:back_url]
119 }
124 }
120 if @time_entry.issue
125 if params[:project_id] && @time_entry.project
121 redirect_to new_project_issue_time_entry_path(@time_entry.project, @time_entry.issue, options)
122 else
123 redirect_to new_project_time_entry_path(@time_entry.project, options)
126 redirect_to new_project_time_entry_path(@time_entry.project, options)
124 end
127 elsif params[:issue_id] && @time_entry.issue
128 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
125 else
129 else
126 options = {
127 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
128 :back_url => params[:back_url]
129 }
130 redirect_to new_time_entry_path(options)
130 redirect_to new_time_entry_path(options)
131 end
131 end
132 else
132 else
133 redirect_back_or_default project_time_entries_path(@time_entry.project)
133 redirect_back_or_default project_time_entries_path(@time_entry.project)
134 end
134 end
135 }
135 }
136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 end
137 end
138 else
138 else
139 respond_to do |format|
139 respond_to do |format|
140 format.html { render :action => 'new' }
140 format.html { render :action => 'new' }
141 format.api { render_validation_errors(@time_entry) }
141 format.api { render_validation_errors(@time_entry) }
142 end
142 end
143 end
143 end
144 end
144 end
145
145
146 def edit
146 def edit
147 @time_entry.safe_attributes = params[:time_entry]
147 @time_entry.safe_attributes = params[:time_entry]
148 end
148 end
149
149
150 def update
150 def update
151 @time_entry.safe_attributes = params[:time_entry]
151 @time_entry.safe_attributes = params[:time_entry]
152
152
153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
154
154
155 if @time_entry.save
155 if @time_entry.save
156 respond_to do |format|
156 respond_to do |format|
157 format.html {
157 format.html {
158 flash[:notice] = l(:notice_successful_update)
158 flash[:notice] = l(:notice_successful_update)
159 redirect_back_or_default project_time_entries_path(@time_entry.project)
159 redirect_back_or_default project_time_entries_path(@time_entry.project)
160 }
160 }
161 format.api { render_api_ok }
161 format.api { render_api_ok }
162 end
162 end
163 else
163 else
164 respond_to do |format|
164 respond_to do |format|
165 format.html { render :action => 'edit' }
165 format.html { render :action => 'edit' }
166 format.api { render_validation_errors(@time_entry) }
166 format.api { render_validation_errors(@time_entry) }
167 end
167 end
168 end
168 end
169 end
169 end
170
170
171 def bulk_edit
171 def bulk_edit
172 @available_activities = TimeEntryActivity.shared.active
172 @available_activities = TimeEntryActivity.shared.active
173 @custom_fields = TimeEntry.first.available_custom_fields
173 @custom_fields = TimeEntry.first.available_custom_fields
174 end
174 end
175
175
176 def bulk_update
176 def bulk_update
177 attributes = parse_params_for_bulk_time_entry_attributes(params)
177 attributes = parse_params_for_bulk_time_entry_attributes(params)
178
178
179 unsaved_time_entry_ids = []
179 unsaved_time_entry_ids = []
180 @time_entries.each do |time_entry|
180 @time_entries.each do |time_entry|
181 time_entry.reload
181 time_entry.reload
182 time_entry.safe_attributes = attributes
182 time_entry.safe_attributes = attributes
183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
184 unless time_entry.save
184 unless time_entry.save
185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info
185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info
186 # Keep unsaved time_entry ids to display them in flash error
186 # Keep unsaved time_entry ids to display them in flash error
187 unsaved_time_entry_ids << time_entry.id
187 unsaved_time_entry_ids << time_entry.id
188 end
188 end
189 end
189 end
190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
191 redirect_back_or_default project_time_entries_path(@projects.first)
191 redirect_back_or_default project_time_entries_path(@projects.first)
192 end
192 end
193
193
194 def destroy
194 def destroy
195 destroyed = TimeEntry.transaction do
195 destroyed = TimeEntry.transaction do
196 @time_entries.each do |t|
196 @time_entries.each do |t|
197 unless t.destroy && t.destroyed?
197 unless t.destroy && t.destroyed?
198 raise ActiveRecord::Rollback
198 raise ActiveRecord::Rollback
199 end
199 end
200 end
200 end
201 end
201 end
202
202
203 respond_to do |format|
203 respond_to do |format|
204 format.html {
204 format.html {
205 if destroyed
205 if destroyed
206 flash[:notice] = l(:notice_successful_delete)
206 flash[:notice] = l(:notice_successful_delete)
207 else
207 else
208 flash[:error] = l(:notice_unable_delete_time_entry)
208 flash[:error] = l(:notice_unable_delete_time_entry)
209 end
209 end
210 redirect_back_or_default project_time_entries_path(@projects.first)
210 redirect_back_or_default project_time_entries_path(@projects.first)
211 }
211 }
212 format.api {
212 format.api {
213 if destroyed
213 if destroyed
214 render_api_ok
214 render_api_ok
215 else
215 else
216 render_validation_errors(@time_entries)
216 render_validation_errors(@time_entries)
217 end
217 end
218 }
218 }
219 end
219 end
220 end
220 end
221
221
222 private
222 private
223 def find_time_entry
223 def find_time_entry
224 @time_entry = TimeEntry.find(params[:id])
224 @time_entry = TimeEntry.find(params[:id])
225 unless @time_entry.editable_by?(User.current)
225 unless @time_entry.editable_by?(User.current)
226 render_403
226 render_403
227 return false
227 return false
228 end
228 end
229 @project = @time_entry.project
229 @project = @time_entry.project
230 rescue ActiveRecord::RecordNotFound
230 rescue ActiveRecord::RecordNotFound
231 render_404
231 render_404
232 end
232 end
233
233
234 def find_time_entries
234 def find_time_entries
235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).all
235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).all
236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 @projects = @time_entries.collect(&:project).compact.uniq
237 @projects = @time_entries.collect(&:project).compact.uniq
238 @project = @projects.first if @projects.size == 1
238 @project = @projects.first if @projects.size == 1
239 rescue ActiveRecord::RecordNotFound
239 rescue ActiveRecord::RecordNotFound
240 render_404
240 render_404
241 end
241 end
242
242
243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
243 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
244 if unsaved_time_entry_ids.empty?
244 if unsaved_time_entry_ids.empty?
245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
245 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
246 else
246 else
247 flash[:error] = l(:notice_failed_to_save_time_entries,
247 flash[:error] = l(:notice_failed_to_save_time_entries,
248 :count => unsaved_time_entry_ids.size,
248 :count => unsaved_time_entry_ids.size,
249 :total => time_entries.size,
249 :total => time_entries.size,
250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
250 :ids => '#' + unsaved_time_entry_ids.join(', #'))
251 end
251 end
252 end
252 end
253
253
254 def find_optional_project_for_new_time_entry
255 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
256 @project = Project.find(project_id)
257 end
258 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
259 @issue = Issue.find(issue_id)
260 @project ||= @issue.project
261 end
262 rescue ActiveRecord::RecordNotFound
263 render_404
264 end
265
266 def find_project_for_new_time_entry
267 find_optional_project_for_new_time_entry
268 if @project.nil?
269 render_404
270 end
271 end
272
273 def find_optional_project
254 def find_optional_project
274 if !params[:issue_id].blank?
255 if params[:issue_id].present?
275 @issue = Issue.find(params[:issue_id])
256 @issue = Issue.find(params[:issue_id])
276 @project = @issue.project
257 @project = @issue.project
277 elsif !params[:project_id].blank?
258 elsif params[:project_id].present?
278 @project = Project.find(params[:project_id])
259 @project = Project.find(params[:project_id])
279 end
260 end
261 rescue ActiveRecord::RecordNotFound
262 render_404
280 end
263 end
281
264
282 # Returns the TimeEntry scope for index and report actions
265 # Returns the TimeEntry scope for index and report actions
283 def time_entry_scope(options={})
266 def time_entry_scope(options={})
284 scope = @query.results_scope(options)
267 scope = @query.results_scope(options)
285 if @issue
268 if @issue
286 scope = scope.on_issue(@issue)
269 scope = scope.on_issue(@issue)
287 end
270 end
288 scope
271 scope
289 end
272 end
290
273
291 def parse_params_for_bulk_time_entry_attributes(params)
274 def parse_params_for_bulk_time_entry_attributes(params)
292 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
275 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
293 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
276 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
294 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
277 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
295 attributes
278 attributes
296 end
279 end
297 end
280 end
@@ -1,1360 +1,1363
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = issue.subject.truncate(60)
75 title = issue.subject.truncate(60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if truncate_length = options[:truncate]
78 if truncate_length = options[:truncate]
79 subject = subject.truncate(truncate_length)
79 subject = subject.truncate(truncate_length)
80 end
80 end
81 end
81 end
82 only_path = options[:only_path].nil? ? true : options[:only_path]
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_path(issue, :only_path => only_path),
83 s = link_to(text, issue_path(issue, :only_path => only_path),
84 :class => issue.css_classes, :title => title)
84 :class => issue.css_classes, :title => title)
85 s << h(": #{subject}") if subject
85 s << h(": #{subject}") if subject
86 s = h("#{issue.project} - ") + s if options[:project]
86 s = h("#{issue.project} - ") + s if options[:project]
87 s
87 s
88 end
88 end
89
89
90 # Generates a link to an attachment.
90 # Generates a link to an attachment.
91 # Options:
91 # Options:
92 # * :text - Link text (default to attachment filename)
92 # * :text - Link text (default to attachment filename)
93 # * :download - Force download (default: false)
93 # * :download - Force download (default: false)
94 def link_to_attachment(attachment, options={})
94 def link_to_attachment(attachment, options={})
95 text = options.delete(:text) || attachment.filename
95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 html_options = options.slice!(:only_path)
97 html_options = options.slice!(:only_path)
98 url = send(route_method, attachment, attachment.filename, options)
98 url = send(route_method, attachment, attachment.filename, options)
99 link_to text, url, html_options
99 link_to text, url, html_options
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, repository, options={})
105 def link_to_revision(revision, repository, options={})
106 if repository.is_a?(Project)
106 if repository.is_a?(Project)
107 repository = repository.repository
107 repository = repository.repository
108 end
108 end
109 text = options.delete(:text) || format_revision(revision)
109 text = options.delete(:text) || format_revision(revision)
110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 link_to(
111 link_to(
112 h(text),
112 h(text),
113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 :title => l(:label_revision_id, format_revision(revision))
114 :title => l(:label_revision_id, format_revision(revision))
115 )
115 )
116 end
116 end
117
117
118 # Generates a link to a message
118 # Generates a link to a message
119 def link_to_message(message, options={}, html_options = nil)
119 def link_to_message(message, options={}, html_options = nil)
120 link_to(
120 link_to(
121 message.subject.truncate(60),
121 message.subject.truncate(60),
122 board_message_path(message.board_id, message.parent_id || message.id, {
122 board_message_path(message.board_id, message.parent_id || message.id, {
123 :r => (message.parent_id && message.id),
123 :r => (message.parent_id && message.id),
124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 }.merge(options)),
125 }.merge(options)),
126 html_options
126 html_options
127 )
127 )
128 end
128 end
129
129
130 # Generates a link to a project if active
130 # Generates a link to a project if active
131 # Examples:
131 # Examples:
132 #
132 #
133 # link_to_project(project) # => link to the specified project overview
133 # link_to_project(project) # => link to the specified project overview
134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 #
136 #
137 def link_to_project(project, options={}, html_options = nil)
137 def link_to_project(project, options={}, html_options = nil)
138 if project.archived?
138 if project.archived?
139 h(project.name)
139 h(project.name)
140 elsif options.key?(:action)
140 elsif options.key?(:action)
141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143 link_to project.name, url, html_options
143 link_to project.name, url, html_options
144 else
144 else
145 link_to project.name, project_path(project, options), html_options
145 link_to project.name, project_path(project, options), html_options
146 end
146 end
147 end
147 end
148
148
149 # Generates a link to a project settings if active
149 # Generates a link to a project settings if active
150 def link_to_project_settings(project, options={}, html_options=nil)
150 def link_to_project_settings(project, options={}, html_options=nil)
151 if project.active?
151 if project.active?
152 link_to project.name, settings_project_path(project, options), html_options
152 link_to project.name, settings_project_path(project, options), html_options
153 elsif project.archived?
153 elsif project.archived?
154 h(project.name)
154 h(project.name)
155 else
155 else
156 link_to project.name, project_path(project, options), html_options
156 link_to project.name, project_path(project, options), html_options
157 end
157 end
158 end
158 end
159
159
160 # Generates a link to a version
160 # Generates a link to a version
161 def link_to_version(version, options = {})
161 def link_to_version(version, options = {})
162 return '' unless version && version.is_a?(Version)
162 return '' unless version && version.is_a?(Version)
163 options = {:title => format_date(version.effective_date)}.merge(options)
163 options = {:title => format_date(version.effective_date)}.merge(options)
164 link_to_if version.visible?, format_version_name(version), version_path(version), options
164 link_to_if version.visible?, format_version_name(version), version_path(version), options
165 end
165 end
166
166
167 # Helper that formats object for html or text rendering
167 # Helper that formats object for html or text rendering
168 def format_object(object, html=true, &block)
168 def format_object(object, html=true, &block)
169 if block_given?
169 if block_given?
170 object = yield object
170 object = yield object
171 end
171 end
172 case object.class.name
172 case object.class.name
173 when 'Array'
173 when 'Array'
174 object.map {|o| format_object(o, html)}.join(', ').html_safe
174 object.map {|o| format_object(o, html)}.join(', ').html_safe
175 when 'Time'
175 when 'Time'
176 format_time(object)
176 format_time(object)
177 when 'Date'
177 when 'Date'
178 format_date(object)
178 format_date(object)
179 when 'Fixnum'
179 when 'Fixnum'
180 object.to_s
180 object.to_s
181 when 'Float'
181 when 'Float'
182 sprintf "%.2f", object
182 sprintf "%.2f", object
183 when 'User'
183 when 'User'
184 html ? link_to_user(object) : object.to_s
184 html ? link_to_user(object) : object.to_s
185 when 'Project'
185 when 'Project'
186 html ? link_to_project(object) : object.to_s
186 html ? link_to_project(object) : object.to_s
187 when 'Version'
187 when 'Version'
188 html ? link_to_version(object) : object.to_s
188 html ? link_to_version(object) : object.to_s
189 when 'TrueClass'
189 when 'TrueClass'
190 l(:general_text_Yes)
190 l(:general_text_Yes)
191 when 'FalseClass'
191 when 'FalseClass'
192 l(:general_text_No)
192 l(:general_text_No)
193 when 'Issue'
193 when 'Issue'
194 object.visible? && html ? link_to_issue(object) : "##{object.id}"
194 object.visible? && html ? link_to_issue(object) : "##{object.id}"
195 when 'CustomValue', 'CustomFieldValue'
195 when 'CustomValue', 'CustomFieldValue'
196 if object.custom_field
196 if object.custom_field
197 f = object.custom_field.format.formatted_custom_value(self, object, html)
197 f = object.custom_field.format.formatted_custom_value(self, object, html)
198 if f.nil? || f.is_a?(String)
198 if f.nil? || f.is_a?(String)
199 f
199 f
200 else
200 else
201 format_object(f, html, &block)
201 format_object(f, html, &block)
202 end
202 end
203 else
203 else
204 object.value.to_s
204 object.value.to_s
205 end
205 end
206 else
206 else
207 html ? h(object) : object.to_s
207 html ? h(object) : object.to_s
208 end
208 end
209 end
209 end
210
210
211 def wiki_page_path(page, options={})
211 def wiki_page_path(page, options={})
212 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
212 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
213 end
213 end
214
214
215 def thumbnail_tag(attachment)
215 def thumbnail_tag(attachment)
216 link_to image_tag(thumbnail_path(attachment)),
216 link_to image_tag(thumbnail_path(attachment)),
217 named_attachment_path(attachment, attachment.filename),
217 named_attachment_path(attachment, attachment.filename),
218 :title => attachment.filename
218 :title => attachment.filename
219 end
219 end
220
220
221 def toggle_link(name, id, options={})
221 def toggle_link(name, id, options={})
222 onclick = "$('##{id}').toggle(); "
222 onclick = "$('##{id}').toggle(); "
223 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
223 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
224 onclick << "return false;"
224 onclick << "return false;"
225 link_to(name, "#", :onclick => onclick)
225 link_to(name, "#", :onclick => onclick)
226 end
226 end
227
227
228 def image_to_function(name, function, html_options = {})
228 def image_to_function(name, function, html_options = {})
229 html_options.symbolize_keys!
229 html_options.symbolize_keys!
230 tag(:input, html_options.merge({
230 tag(:input, html_options.merge({
231 :type => "image", :src => image_path(name),
231 :type => "image", :src => image_path(name),
232 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
232 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
233 }))
233 }))
234 end
234 end
235
235
236 def format_activity_title(text)
236 def format_activity_title(text)
237 h(truncate_single_line_raw(text, 100))
237 h(truncate_single_line_raw(text, 100))
238 end
238 end
239
239
240 def format_activity_day(date)
240 def format_activity_day(date)
241 date == User.current.today ? l(:label_today).titleize : format_date(date)
241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 end
242 end
243
243
244 def format_activity_description(text)
244 def format_activity_description(text)
245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 ).gsub(/[\r\n]+/, "<br />").html_safe
246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 end
247 end
248
248
249 def format_version_name(version)
249 def format_version_name(version)
250 if !version.shared? || version.project == @project
250 if !version.shared? || version.project == @project
251 h(version)
251 h(version)
252 else
252 else
253 h("#{version.project} - #{version}")
253 h("#{version.project} - #{version}")
254 end
254 end
255 end
255 end
256
256
257 def due_date_distance_in_words(date)
257 def due_date_distance_in_words(date)
258 if date
258 if date
259 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
259 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
260 end
260 end
261 end
261 end
262
262
263 # Renders a tree of projects as a nested set of unordered lists
263 # Renders a tree of projects as a nested set of unordered lists
264 # The given collection may be a subset of the whole project tree
264 # The given collection may be a subset of the whole project tree
265 # (eg. some intermediate nodes are private and can not be seen)
265 # (eg. some intermediate nodes are private and can not be seen)
266 def render_project_nested_lists(projects)
266 def render_project_nested_lists(projects)
267 s = ''
267 s = ''
268 if projects.any?
268 if projects.any?
269 ancestors = []
269 ancestors = []
270 original_project = @project
270 original_project = @project
271 projects.sort_by(&:lft).each do |project|
271 projects.sort_by(&:lft).each do |project|
272 # set the project environment to please macros.
272 # set the project environment to please macros.
273 @project = project
273 @project = project
274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
276 else
276 else
277 ancestors.pop
277 ancestors.pop
278 s << "</li>"
278 s << "</li>"
279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
280 ancestors.pop
280 ancestors.pop
281 s << "</ul></li>\n"
281 s << "</ul></li>\n"
282 end
282 end
283 end
283 end
284 classes = (ancestors.empty? ? 'root' : 'child')
284 classes = (ancestors.empty? ? 'root' : 'child')
285 s << "<li class='#{classes}'><div class='#{classes}'>"
285 s << "<li class='#{classes}'><div class='#{classes}'>"
286 s << h(block_given? ? yield(project) : project.name)
286 s << h(block_given? ? yield(project) : project.name)
287 s << "</div>\n"
287 s << "</div>\n"
288 ancestors << project
288 ancestors << project
289 end
289 end
290 s << ("</li></ul>\n" * ancestors.size)
290 s << ("</li></ul>\n" * ancestors.size)
291 @project = original_project
291 @project = original_project
292 end
292 end
293 s.html_safe
293 s.html_safe
294 end
294 end
295
295
296 def render_page_hierarchy(pages, node=nil, options={})
296 def render_page_hierarchy(pages, node=nil, options={})
297 content = ''
297 content = ''
298 if pages[node]
298 if pages[node]
299 content << "<ul class=\"pages-hierarchy\">\n"
299 content << "<ul class=\"pages-hierarchy\">\n"
300 pages[node].each do |page|
300 pages[node].each do |page|
301 content << "<li>"
301 content << "<li>"
302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
305 content << "</li>\n"
305 content << "</li>\n"
306 end
306 end
307 content << "</ul>\n"
307 content << "</ul>\n"
308 end
308 end
309 content.html_safe
309 content.html_safe
310 end
310 end
311
311
312 # Renders flash messages
312 # Renders flash messages
313 def render_flash_messages
313 def render_flash_messages
314 s = ''
314 s = ''
315 flash.each do |k,v|
315 flash.each do |k,v|
316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
317 end
317 end
318 s.html_safe
318 s.html_safe
319 end
319 end
320
320
321 # Renders tabs and their content
321 # Renders tabs and their content
322 def render_tabs(tabs, selected=params[:tab])
322 def render_tabs(tabs, selected=params[:tab])
323 if tabs.any?
323 if tabs.any?
324 unless tabs.detect {|tab| tab[:name] == selected}
324 unless tabs.detect {|tab| tab[:name] == selected}
325 selected = nil
325 selected = nil
326 end
326 end
327 selected ||= tabs.first[:name]
327 selected ||= tabs.first[:name]
328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
329 else
329 else
330 content_tag 'p', l(:label_no_data), :class => "nodata"
330 content_tag 'p', l(:label_no_data), :class => "nodata"
331 end
331 end
332 end
332 end
333
333
334 # Renders the project quick-jump box
334 # Renders the project quick-jump box
335 def render_project_jump_box
335 def render_project_jump_box
336 return unless User.current.logged?
336 return unless User.current.logged?
337 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
337 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
338 if projects.any?
338 if projects.any?
339 options =
339 options =
340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
341 '<option value="" disabled="disabled">---</option>').html_safe
341 '<option value="" disabled="disabled">---</option>').html_safe
342
342
343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
344 { :value => project_path(:id => p, :jump => current_menu_item) }
344 { :value => project_path(:id => p, :jump => current_menu_item) }
345 end
345 end
346
346
347 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
347 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
348 end
348 end
349 end
349 end
350
350
351 def project_tree_options_for_select(projects, options = {})
351 def project_tree_options_for_select(projects, options = {})
352 s = ''
352 s = ''.html_safe
353 if options[:include_blank]
354 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
355 end
353 project_tree(projects) do |project, level|
356 project_tree(projects) do |project, level|
354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 tag_options = {:value => project.id}
358 tag_options = {:value => project.id}
356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 tag_options[:selected] = 'selected'
360 tag_options[:selected] = 'selected'
358 else
361 else
359 tag_options[:selected] = nil
362 tag_options[:selected] = nil
360 end
363 end
361 tag_options.merge!(yield(project)) if block_given?
364 tag_options.merge!(yield(project)) if block_given?
362 s << content_tag('option', name_prefix + h(project), tag_options)
365 s << content_tag('option', name_prefix + h(project), tag_options)
363 end
366 end
364 s.html_safe
367 s.html_safe
365 end
368 end
366
369
367 # Yields the given block for each project with its level in the tree
370 # Yields the given block for each project with its level in the tree
368 #
371 #
369 # Wrapper for Project#project_tree
372 # Wrapper for Project#project_tree
370 def project_tree(projects, &block)
373 def project_tree(projects, &block)
371 Project.project_tree(projects, &block)
374 Project.project_tree(projects, &block)
372 end
375 end
373
376
374 def principals_check_box_tags(name, principals)
377 def principals_check_box_tags(name, principals)
375 s = ''
378 s = ''
376 principals.each do |principal|
379 principals.each do |principal|
377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 end
381 end
379 s.html_safe
382 s.html_safe
380 end
383 end
381
384
382 # Returns a string for users/groups option tags
385 # Returns a string for users/groups option tags
383 def principals_options_for_select(collection, selected=nil)
386 def principals_options_for_select(collection, selected=nil)
384 s = ''
387 s = ''
385 if collection.include?(User.current)
388 if collection.include?(User.current)
386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 end
390 end
388 groups = ''
391 groups = ''
389 collection.sort.each do |element|
392 collection.sort.each do |element|
390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 end
395 end
393 unless groups.empty?
396 unless groups.empty?
394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 end
398 end
396 s.html_safe
399 s.html_safe
397 end
400 end
398
401
399 # Options for the new membership projects combo-box
402 # Options for the new membership projects combo-box
400 def options_for_membership_project_select(principal, projects)
403 def options_for_membership_project_select(principal, projects)
401 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
404 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
402 options << project_tree_options_for_select(projects) do |p|
405 options << project_tree_options_for_select(projects) do |p|
403 {:disabled => principal.projects.to_a.include?(p)}
406 {:disabled => principal.projects.to_a.include?(p)}
404 end
407 end
405 options
408 options
406 end
409 end
407
410
408 def option_tag(name, text, value, selected=nil, options={})
411 def option_tag(name, text, value, selected=nil, options={})
409 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
412 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
410 end
413 end
411
414
412 # Truncates and returns the string as a single line
415 # Truncates and returns the string as a single line
413 def truncate_single_line(string, *args)
416 def truncate_single_line(string, *args)
414 ActiveSupport::Deprecation.warn(
417 ActiveSupport::Deprecation.warn(
415 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
418 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
416 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
419 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
417 # So, result is broken.
420 # So, result is broken.
418 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
421 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
419 end
422 end
420
423
421 def truncate_single_line_raw(string, length)
424 def truncate_single_line_raw(string, length)
422 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
425 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
423 end
426 end
424
427
425 # Truncates at line break after 250 characters or options[:length]
428 # Truncates at line break after 250 characters or options[:length]
426 def truncate_lines(string, options={})
429 def truncate_lines(string, options={})
427 length = options[:length] || 250
430 length = options[:length] || 250
428 if string.to_s =~ /\A(.{#{length}}.*?)$/m
431 if string.to_s =~ /\A(.{#{length}}.*?)$/m
429 "#{$1}..."
432 "#{$1}..."
430 else
433 else
431 string
434 string
432 end
435 end
433 end
436 end
434
437
435 def anchor(text)
438 def anchor(text)
436 text.to_s.gsub(' ', '_')
439 text.to_s.gsub(' ', '_')
437 end
440 end
438
441
439 def html_hours(text)
442 def html_hours(text)
440 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
443 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
441 end
444 end
442
445
443 def authoring(created, author, options={})
446 def authoring(created, author, options={})
444 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
447 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
445 end
448 end
446
449
447 def time_tag(time)
450 def time_tag(time)
448 text = distance_of_time_in_words(Time.now, time)
451 text = distance_of_time_in_words(Time.now, time)
449 if @project
452 if @project
450 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
453 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
451 else
454 else
452 content_tag('abbr', text, :title => format_time(time))
455 content_tag('abbr', text, :title => format_time(time))
453 end
456 end
454 end
457 end
455
458
456 def syntax_highlight_lines(name, content)
459 def syntax_highlight_lines(name, content)
457 lines = []
460 lines = []
458 syntax_highlight(name, content).each_line { |line| lines << line }
461 syntax_highlight(name, content).each_line { |line| lines << line }
459 lines
462 lines
460 end
463 end
461
464
462 def syntax_highlight(name, content)
465 def syntax_highlight(name, content)
463 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
466 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
464 end
467 end
465
468
466 def to_path_param(path)
469 def to_path_param(path)
467 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
470 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
468 str.blank? ? nil : str
471 str.blank? ? nil : str
469 end
472 end
470
473
471 def reorder_links(name, url, method = :post)
474 def reorder_links(name, url, method = :post)
472 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
475 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
473 url.merge({"#{name}[move_to]" => 'highest'}),
476 url.merge({"#{name}[move_to]" => 'highest'}),
474 :method => method, :title => l(:label_sort_highest)) +
477 :method => method, :title => l(:label_sort_highest)) +
475 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
478 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
476 url.merge({"#{name}[move_to]" => 'higher'}),
479 url.merge({"#{name}[move_to]" => 'higher'}),
477 :method => method, :title => l(:label_sort_higher)) +
480 :method => method, :title => l(:label_sort_higher)) +
478 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
481 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
479 url.merge({"#{name}[move_to]" => 'lower'}),
482 url.merge({"#{name}[move_to]" => 'lower'}),
480 :method => method, :title => l(:label_sort_lower)) +
483 :method => method, :title => l(:label_sort_lower)) +
481 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
484 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
482 url.merge({"#{name}[move_to]" => 'lowest'}),
485 url.merge({"#{name}[move_to]" => 'lowest'}),
483 :method => method, :title => l(:label_sort_lowest))
486 :method => method, :title => l(:label_sort_lowest))
484 end
487 end
485
488
486 def breadcrumb(*args)
489 def breadcrumb(*args)
487 elements = args.flatten
490 elements = args.flatten
488 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
491 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
489 end
492 end
490
493
491 def other_formats_links(&block)
494 def other_formats_links(&block)
492 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
495 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
493 yield Redmine::Views::OtherFormatsBuilder.new(self)
496 yield Redmine::Views::OtherFormatsBuilder.new(self)
494 concat('</p>'.html_safe)
497 concat('</p>'.html_safe)
495 end
498 end
496
499
497 def page_header_title
500 def page_header_title
498 if @project.nil? || @project.new_record?
501 if @project.nil? || @project.new_record?
499 h(Setting.app_title)
502 h(Setting.app_title)
500 else
503 else
501 b = []
504 b = []
502 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
505 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
503 if ancestors.any?
506 if ancestors.any?
504 root = ancestors.shift
507 root = ancestors.shift
505 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
508 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
506 if ancestors.size > 2
509 if ancestors.size > 2
507 b << "\xe2\x80\xa6"
510 b << "\xe2\x80\xa6"
508 ancestors = ancestors[-2, 2]
511 ancestors = ancestors[-2, 2]
509 end
512 end
510 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
513 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
511 end
514 end
512 b << h(@project)
515 b << h(@project)
513 b.join(" \xc2\xbb ").html_safe
516 b.join(" \xc2\xbb ").html_safe
514 end
517 end
515 end
518 end
516
519
517 # Returns a h2 tag and sets the html title with the given arguments
520 # Returns a h2 tag and sets the html title with the given arguments
518 def title(*args)
521 def title(*args)
519 strings = args.map do |arg|
522 strings = args.map do |arg|
520 if arg.is_a?(Array) && arg.size >= 2
523 if arg.is_a?(Array) && arg.size >= 2
521 link_to(*arg)
524 link_to(*arg)
522 else
525 else
523 h(arg.to_s)
526 h(arg.to_s)
524 end
527 end
525 end
528 end
526 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
529 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
527 content_tag('h2', strings.join(' &#187; ').html_safe)
530 content_tag('h2', strings.join(' &#187; ').html_safe)
528 end
531 end
529
532
530 # Sets the html title
533 # Sets the html title
531 # Returns the html title when called without arguments
534 # Returns the html title when called without arguments
532 # Current project name and app_title and automatically appended
535 # Current project name and app_title and automatically appended
533 # Exemples:
536 # Exemples:
534 # html_title 'Foo', 'Bar'
537 # html_title 'Foo', 'Bar'
535 # html_title # => 'Foo - Bar - My Project - Redmine'
538 # html_title # => 'Foo - Bar - My Project - Redmine'
536 def html_title(*args)
539 def html_title(*args)
537 if args.empty?
540 if args.empty?
538 title = @html_title || []
541 title = @html_title || []
539 title << @project.name if @project
542 title << @project.name if @project
540 title << Setting.app_title unless Setting.app_title == title.last
543 title << Setting.app_title unless Setting.app_title == title.last
541 title.reject(&:blank?).join(' - ')
544 title.reject(&:blank?).join(' - ')
542 else
545 else
543 @html_title ||= []
546 @html_title ||= []
544 @html_title += args
547 @html_title += args
545 end
548 end
546 end
549 end
547
550
548 # Returns the theme, controller name, and action as css classes for the
551 # Returns the theme, controller name, and action as css classes for the
549 # HTML body.
552 # HTML body.
550 def body_css_classes
553 def body_css_classes
551 css = []
554 css = []
552 if theme = Redmine::Themes.theme(Setting.ui_theme)
555 if theme = Redmine::Themes.theme(Setting.ui_theme)
553 css << 'theme-' + theme.name
556 css << 'theme-' + theme.name
554 end
557 end
555
558
556 css << 'project-' + @project.identifier if @project && @project.identifier.present?
559 css << 'project-' + @project.identifier if @project && @project.identifier.present?
557 css << 'controller-' + controller_name
560 css << 'controller-' + controller_name
558 css << 'action-' + action_name
561 css << 'action-' + action_name
559 css.join(' ')
562 css.join(' ')
560 end
563 end
561
564
562 def accesskey(s)
565 def accesskey(s)
563 @used_accesskeys ||= []
566 @used_accesskeys ||= []
564 key = Redmine::AccessKeys.key_for(s)
567 key = Redmine::AccessKeys.key_for(s)
565 return nil if @used_accesskeys.include?(key)
568 return nil if @used_accesskeys.include?(key)
566 @used_accesskeys << key
569 @used_accesskeys << key
567 key
570 key
568 end
571 end
569
572
570 # Formats text according to system settings.
573 # Formats text according to system settings.
571 # 2 ways to call this method:
574 # 2 ways to call this method:
572 # * with a String: textilizable(text, options)
575 # * with a String: textilizable(text, options)
573 # * with an object and one of its attribute: textilizable(issue, :description, options)
576 # * with an object and one of its attribute: textilizable(issue, :description, options)
574 def textilizable(*args)
577 def textilizable(*args)
575 options = args.last.is_a?(Hash) ? args.pop : {}
578 options = args.last.is_a?(Hash) ? args.pop : {}
576 case args.size
579 case args.size
577 when 1
580 when 1
578 obj = options[:object]
581 obj = options[:object]
579 text = args.shift
582 text = args.shift
580 when 2
583 when 2
581 obj = args.shift
584 obj = args.shift
582 attr = args.shift
585 attr = args.shift
583 text = obj.send(attr).to_s
586 text = obj.send(attr).to_s
584 else
587 else
585 raise ArgumentError, 'invalid arguments to textilizable'
588 raise ArgumentError, 'invalid arguments to textilizable'
586 end
589 end
587 return '' if text.blank?
590 return '' if text.blank?
588 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
591 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
589 only_path = options.delete(:only_path) == false ? false : true
592 only_path = options.delete(:only_path) == false ? false : true
590
593
591 text = text.dup
594 text = text.dup
592 macros = catch_macros(text)
595 macros = catch_macros(text)
593 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
596 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
594
597
595 @parsed_headings = []
598 @parsed_headings = []
596 @heading_anchors = {}
599 @heading_anchors = {}
597 @current_section = 0 if options[:edit_section_links]
600 @current_section = 0 if options[:edit_section_links]
598
601
599 parse_sections(text, project, obj, attr, only_path, options)
602 parse_sections(text, project, obj, attr, only_path, options)
600 text = parse_non_pre_blocks(text, obj, macros) do |text|
603 text = parse_non_pre_blocks(text, obj, macros) do |text|
601 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
604 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
602 send method_name, text, project, obj, attr, only_path, options
605 send method_name, text, project, obj, attr, only_path, options
603 end
606 end
604 end
607 end
605 parse_headings(text, project, obj, attr, only_path, options)
608 parse_headings(text, project, obj, attr, only_path, options)
606
609
607 if @parsed_headings.any?
610 if @parsed_headings.any?
608 replace_toc(text, @parsed_headings)
611 replace_toc(text, @parsed_headings)
609 end
612 end
610
613
611 text.html_safe
614 text.html_safe
612 end
615 end
613
616
614 def parse_non_pre_blocks(text, obj, macros)
617 def parse_non_pre_blocks(text, obj, macros)
615 s = StringScanner.new(text)
618 s = StringScanner.new(text)
616 tags = []
619 tags = []
617 parsed = ''
620 parsed = ''
618 while !s.eos?
621 while !s.eos?
619 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
622 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
620 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
623 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
621 if tags.empty?
624 if tags.empty?
622 yield text
625 yield text
623 inject_macros(text, obj, macros) if macros.any?
626 inject_macros(text, obj, macros) if macros.any?
624 else
627 else
625 inject_macros(text, obj, macros, false) if macros.any?
628 inject_macros(text, obj, macros, false) if macros.any?
626 end
629 end
627 parsed << text
630 parsed << text
628 if tag
631 if tag
629 if closing
632 if closing
630 if tags.last == tag.downcase
633 if tags.last == tag.downcase
631 tags.pop
634 tags.pop
632 end
635 end
633 else
636 else
634 tags << tag.downcase
637 tags << tag.downcase
635 end
638 end
636 parsed << full_tag
639 parsed << full_tag
637 end
640 end
638 end
641 end
639 # Close any non closing tags
642 # Close any non closing tags
640 while tag = tags.pop
643 while tag = tags.pop
641 parsed << "</#{tag}>"
644 parsed << "</#{tag}>"
642 end
645 end
643 parsed
646 parsed
644 end
647 end
645
648
646 def parse_inline_attachments(text, project, obj, attr, only_path, options)
649 def parse_inline_attachments(text, project, obj, attr, only_path, options)
647 # when using an image link, try to use an attachment, if possible
650 # when using an image link, try to use an attachment, if possible
648 attachments = options[:attachments] || []
651 attachments = options[:attachments] || []
649 attachments += obj.attachments if obj.respond_to?(:attachments)
652 attachments += obj.attachments if obj.respond_to?(:attachments)
650 if attachments.present?
653 if attachments.present?
651 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
654 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
652 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
655 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
653 # search for the picture in attachments
656 # search for the picture in attachments
654 if found = Attachment.latest_attach(attachments, filename)
657 if found = Attachment.latest_attach(attachments, filename)
655 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
658 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
656 desc = found.description.to_s.gsub('"', '')
659 desc = found.description.to_s.gsub('"', '')
657 if !desc.blank? && alttext.blank?
660 if !desc.blank? && alttext.blank?
658 alt = " title=\"#{desc}\" alt=\"#{desc}\""
661 alt = " title=\"#{desc}\" alt=\"#{desc}\""
659 end
662 end
660 "src=\"#{image_url}\"#{alt}"
663 "src=\"#{image_url}\"#{alt}"
661 else
664 else
662 m
665 m
663 end
666 end
664 end
667 end
665 end
668 end
666 end
669 end
667
670
668 # Wiki links
671 # Wiki links
669 #
672 #
670 # Examples:
673 # Examples:
671 # [[mypage]]
674 # [[mypage]]
672 # [[mypage|mytext]]
675 # [[mypage|mytext]]
673 # wiki links can refer other project wikis, using project name or identifier:
676 # wiki links can refer other project wikis, using project name or identifier:
674 # [[project:]] -> wiki starting page
677 # [[project:]] -> wiki starting page
675 # [[project:|mytext]]
678 # [[project:|mytext]]
676 # [[project:mypage]]
679 # [[project:mypage]]
677 # [[project:mypage|mytext]]
680 # [[project:mypage|mytext]]
678 def parse_wiki_links(text, project, obj, attr, only_path, options)
681 def parse_wiki_links(text, project, obj, attr, only_path, options)
679 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
682 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
680 link_project = project
683 link_project = project
681 esc, all, page, title = $1, $2, $3, $5
684 esc, all, page, title = $1, $2, $3, $5
682 if esc.nil?
685 if esc.nil?
683 if page =~ /^([^\:]+)\:(.*)$/
686 if page =~ /^([^\:]+)\:(.*)$/
684 identifier, page = $1, $2
687 identifier, page = $1, $2
685 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
688 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
686 title ||= identifier if page.blank?
689 title ||= identifier if page.blank?
687 end
690 end
688
691
689 if link_project && link_project.wiki
692 if link_project && link_project.wiki
690 # extract anchor
693 # extract anchor
691 anchor = nil
694 anchor = nil
692 if page =~ /^(.+?)\#(.+)$/
695 if page =~ /^(.+?)\#(.+)$/
693 page, anchor = $1, $2
696 page, anchor = $1, $2
694 end
697 end
695 anchor = sanitize_anchor_name(anchor) if anchor.present?
698 anchor = sanitize_anchor_name(anchor) if anchor.present?
696 # check if page exists
699 # check if page exists
697 wiki_page = link_project.wiki.find_page(page)
700 wiki_page = link_project.wiki.find_page(page)
698 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
701 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
699 "##{anchor}"
702 "##{anchor}"
700 else
703 else
701 case options[:wiki_links]
704 case options[:wiki_links]
702 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
705 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
703 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
706 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
704 else
707 else
705 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
708 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
706 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
709 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
707 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
710 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
708 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
711 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
709 end
712 end
710 end
713 end
711 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
714 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
712 else
715 else
713 # project or wiki doesn't exist
716 # project or wiki doesn't exist
714 all
717 all
715 end
718 end
716 else
719 else
717 all
720 all
718 end
721 end
719 end
722 end
720 end
723 end
721
724
722 # Redmine links
725 # Redmine links
723 #
726 #
724 # Examples:
727 # Examples:
725 # Issues:
728 # Issues:
726 # #52 -> Link to issue #52
729 # #52 -> Link to issue #52
727 # Changesets:
730 # Changesets:
728 # r52 -> Link to revision 52
731 # r52 -> Link to revision 52
729 # commit:a85130f -> Link to scmid starting with a85130f
732 # commit:a85130f -> Link to scmid starting with a85130f
730 # Documents:
733 # Documents:
731 # document#17 -> Link to document with id 17
734 # document#17 -> Link to document with id 17
732 # document:Greetings -> Link to the document with title "Greetings"
735 # document:Greetings -> Link to the document with title "Greetings"
733 # document:"Some document" -> Link to the document with title "Some document"
736 # document:"Some document" -> Link to the document with title "Some document"
734 # Versions:
737 # Versions:
735 # version#3 -> Link to version with id 3
738 # version#3 -> Link to version with id 3
736 # version:1.0.0 -> Link to version named "1.0.0"
739 # version:1.0.0 -> Link to version named "1.0.0"
737 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
740 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
738 # Attachments:
741 # Attachments:
739 # attachment:file.zip -> Link to the attachment of the current object named file.zip
742 # attachment:file.zip -> Link to the attachment of the current object named file.zip
740 # Source files:
743 # Source files:
741 # source:some/file -> Link to the file located at /some/file in the project's repository
744 # source:some/file -> Link to the file located at /some/file in the project's repository
742 # source:some/file@52 -> Link to the file's revision 52
745 # source:some/file@52 -> Link to the file's revision 52
743 # source:some/file#L120 -> Link to line 120 of the file
746 # source:some/file#L120 -> Link to line 120 of the file
744 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
747 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
745 # export:some/file -> Force the download of the file
748 # export:some/file -> Force the download of the file
746 # Forum messages:
749 # Forum messages:
747 # message#1218 -> Link to message with id 1218
750 # message#1218 -> Link to message with id 1218
748 # Projects:
751 # Projects:
749 # project:someproject -> Link to project named "someproject"
752 # project:someproject -> Link to project named "someproject"
750 # project#3 -> Link to project with id 3
753 # project#3 -> Link to project with id 3
751 #
754 #
752 # Links can refer other objects from other projects, using project identifier:
755 # Links can refer other objects from other projects, using project identifier:
753 # identifier:r52
756 # identifier:r52
754 # identifier:document:"Some document"
757 # identifier:document:"Some document"
755 # identifier:version:1.0.0
758 # identifier:version:1.0.0
756 # identifier:source:some/file
759 # identifier:source:some/file
757 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
760 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
758 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
761 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
759 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
762 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
760 link = nil
763 link = nil
761 project = default_project
764 project = default_project
762 if project_identifier
765 if project_identifier
763 project = Project.visible.find_by_identifier(project_identifier)
766 project = Project.visible.find_by_identifier(project_identifier)
764 end
767 end
765 if esc.nil?
768 if esc.nil?
766 if prefix.nil? && sep == 'r'
769 if prefix.nil? && sep == 'r'
767 if project
770 if project
768 repository = nil
771 repository = nil
769 if repo_identifier
772 if repo_identifier
770 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
773 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
771 else
774 else
772 repository = project.repository
775 repository = project.repository
773 end
776 end
774 # project.changesets.visible raises an SQL error because of a double join on repositories
777 # project.changesets.visible raises an SQL error because of a double join on repositories
775 if repository &&
778 if repository &&
776 (changeset = Changeset.visible.
779 (changeset = Changeset.visible.
777 find_by_repository_id_and_revision(repository.id, identifier))
780 find_by_repository_id_and_revision(repository.id, identifier))
778 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
781 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
779 {:only_path => only_path, :controller => 'repositories',
782 {:only_path => only_path, :controller => 'repositories',
780 :action => 'revision', :id => project,
783 :action => 'revision', :id => project,
781 :repository_id => repository.identifier_param,
784 :repository_id => repository.identifier_param,
782 :rev => changeset.revision},
785 :rev => changeset.revision},
783 :class => 'changeset',
786 :class => 'changeset',
784 :title => truncate_single_line_raw(changeset.comments, 100))
787 :title => truncate_single_line_raw(changeset.comments, 100))
785 end
788 end
786 end
789 end
787 elsif sep == '#'
790 elsif sep == '#'
788 oid = identifier.to_i
791 oid = identifier.to_i
789 case prefix
792 case prefix
790 when nil
793 when nil
791 if oid.to_s == identifier &&
794 if oid.to_s == identifier &&
792 issue = Issue.visible.includes(:status).find_by_id(oid)
795 issue = Issue.visible.includes(:status).find_by_id(oid)
793 anchor = comment_id ? "note-#{comment_id}" : nil
796 anchor = comment_id ? "note-#{comment_id}" : nil
794 link = link_to(h("##{oid}#{comment_suffix}"),
797 link = link_to(h("##{oid}#{comment_suffix}"),
795 {:only_path => only_path, :controller => 'issues',
798 {:only_path => only_path, :controller => 'issues',
796 :action => 'show', :id => oid, :anchor => anchor},
799 :action => 'show', :id => oid, :anchor => anchor},
797 :class => issue.css_classes,
800 :class => issue.css_classes,
798 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
801 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
799 end
802 end
800 when 'document'
803 when 'document'
801 if document = Document.visible.find_by_id(oid)
804 if document = Document.visible.find_by_id(oid)
802 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
805 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
803 :class => 'document'
806 :class => 'document'
804 end
807 end
805 when 'version'
808 when 'version'
806 if version = Version.visible.find_by_id(oid)
809 if version = Version.visible.find_by_id(oid)
807 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
810 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
808 :class => 'version'
811 :class => 'version'
809 end
812 end
810 when 'message'
813 when 'message'
811 if message = Message.visible.includes(:parent).find_by_id(oid)
814 if message = Message.visible.includes(:parent).find_by_id(oid)
812 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
815 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
813 end
816 end
814 when 'forum'
817 when 'forum'
815 if board = Board.visible.find_by_id(oid)
818 if board = Board.visible.find_by_id(oid)
816 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
819 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
817 :class => 'board'
820 :class => 'board'
818 end
821 end
819 when 'news'
822 when 'news'
820 if news = News.visible.find_by_id(oid)
823 if news = News.visible.find_by_id(oid)
821 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
824 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
822 :class => 'news'
825 :class => 'news'
823 end
826 end
824 when 'project'
827 when 'project'
825 if p = Project.visible.find_by_id(oid)
828 if p = Project.visible.find_by_id(oid)
826 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
829 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
827 end
830 end
828 end
831 end
829 elsif sep == ':'
832 elsif sep == ':'
830 # removes the double quotes if any
833 # removes the double quotes if any
831 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
834 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
832 name = CGI.unescapeHTML(name)
835 name = CGI.unescapeHTML(name)
833 case prefix
836 case prefix
834 when 'document'
837 when 'document'
835 if project && document = project.documents.visible.find_by_title(name)
838 if project && document = project.documents.visible.find_by_title(name)
836 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
839 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
837 :class => 'document'
840 :class => 'document'
838 end
841 end
839 when 'version'
842 when 'version'
840 if project && version = project.versions.visible.find_by_name(name)
843 if project && version = project.versions.visible.find_by_name(name)
841 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
844 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
842 :class => 'version'
845 :class => 'version'
843 end
846 end
844 when 'forum'
847 when 'forum'
845 if project && board = project.boards.visible.find_by_name(name)
848 if project && board = project.boards.visible.find_by_name(name)
846 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
849 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
847 :class => 'board'
850 :class => 'board'
848 end
851 end
849 when 'news'
852 when 'news'
850 if project && news = project.news.visible.find_by_title(name)
853 if project && news = project.news.visible.find_by_title(name)
851 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
854 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
852 :class => 'news'
855 :class => 'news'
853 end
856 end
854 when 'commit', 'source', 'export'
857 when 'commit', 'source', 'export'
855 if project
858 if project
856 repository = nil
859 repository = nil
857 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
860 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
858 repo_prefix, repo_identifier, name = $1, $2, $3
861 repo_prefix, repo_identifier, name = $1, $2, $3
859 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
862 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
860 else
863 else
861 repository = project.repository
864 repository = project.repository
862 end
865 end
863 if prefix == 'commit'
866 if prefix == 'commit'
864 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
867 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
865 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
868 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
866 :class => 'changeset',
869 :class => 'changeset',
867 :title => truncate_single_line_raw(changeset.comments, 100)
870 :title => truncate_single_line_raw(changeset.comments, 100)
868 end
871 end
869 else
872 else
870 if repository && User.current.allowed_to?(:browse_repository, project)
873 if repository && User.current.allowed_to?(:browse_repository, project)
871 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
874 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
872 path, rev, anchor = $1, $3, $5
875 path, rev, anchor = $1, $3, $5
873 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
876 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
874 :path => to_path_param(path),
877 :path => to_path_param(path),
875 :rev => rev,
878 :rev => rev,
876 :anchor => anchor},
879 :anchor => anchor},
877 :class => (prefix == 'export' ? 'source download' : 'source')
880 :class => (prefix == 'export' ? 'source download' : 'source')
878 end
881 end
879 end
882 end
880 repo_prefix = nil
883 repo_prefix = nil
881 end
884 end
882 when 'attachment'
885 when 'attachment'
883 attachments = options[:attachments] || []
886 attachments = options[:attachments] || []
884 attachments += obj.attachments if obj.respond_to?(:attachments)
887 attachments += obj.attachments if obj.respond_to?(:attachments)
885 if attachments && attachment = Attachment.latest_attach(attachments, name)
888 if attachments && attachment = Attachment.latest_attach(attachments, name)
886 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
889 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
887 end
890 end
888 when 'project'
891 when 'project'
889 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
892 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
890 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
893 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
891 end
894 end
892 end
895 end
893 end
896 end
894 end
897 end
895 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
898 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
896 end
899 end
897 end
900 end
898
901
899 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
902 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
900
903
901 def parse_sections(text, project, obj, attr, only_path, options)
904 def parse_sections(text, project, obj, attr, only_path, options)
902 return unless options[:edit_section_links]
905 return unless options[:edit_section_links]
903 text.gsub!(HEADING_RE) do
906 text.gsub!(HEADING_RE) do
904 heading = $1
907 heading = $1
905 @current_section += 1
908 @current_section += 1
906 if @current_section > 1
909 if @current_section > 1
907 content_tag('div',
910 content_tag('div',
908 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
911 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
909 :class => 'contextual',
912 :class => 'contextual',
910 :title => l(:button_edit_section),
913 :title => l(:button_edit_section),
911 :id => "section-#{@current_section}") + heading.html_safe
914 :id => "section-#{@current_section}") + heading.html_safe
912 else
915 else
913 heading
916 heading
914 end
917 end
915 end
918 end
916 end
919 end
917
920
918 # Headings and TOC
921 # Headings and TOC
919 # Adds ids and links to headings unless options[:headings] is set to false
922 # Adds ids and links to headings unless options[:headings] is set to false
920 def parse_headings(text, project, obj, attr, only_path, options)
923 def parse_headings(text, project, obj, attr, only_path, options)
921 return if options[:headings] == false
924 return if options[:headings] == false
922
925
923 text.gsub!(HEADING_RE) do
926 text.gsub!(HEADING_RE) do
924 level, attrs, content = $2.to_i, $3, $4
927 level, attrs, content = $2.to_i, $3, $4
925 item = strip_tags(content).strip
928 item = strip_tags(content).strip
926 anchor = sanitize_anchor_name(item)
929 anchor = sanitize_anchor_name(item)
927 # used for single-file wiki export
930 # used for single-file wiki export
928 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
931 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
929 @heading_anchors[anchor] ||= 0
932 @heading_anchors[anchor] ||= 0
930 idx = (@heading_anchors[anchor] += 1)
933 idx = (@heading_anchors[anchor] += 1)
931 if idx > 1
934 if idx > 1
932 anchor = "#{anchor}-#{idx}"
935 anchor = "#{anchor}-#{idx}"
933 end
936 end
934 @parsed_headings << [level, anchor, item]
937 @parsed_headings << [level, anchor, item]
935 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
938 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
936 end
939 end
937 end
940 end
938
941
939 MACROS_RE = /(
942 MACROS_RE = /(
940 (!)? # escaping
943 (!)? # escaping
941 (
944 (
942 \{\{ # opening tag
945 \{\{ # opening tag
943 ([\w]+) # macro name
946 ([\w]+) # macro name
944 (\(([^\n\r]*?)\))? # optional arguments
947 (\(([^\n\r]*?)\))? # optional arguments
945 ([\n\r].*?[\n\r])? # optional block of text
948 ([\n\r].*?[\n\r])? # optional block of text
946 \}\} # closing tag
949 \}\} # closing tag
947 )
950 )
948 )/mx unless const_defined?(:MACROS_RE)
951 )/mx unless const_defined?(:MACROS_RE)
949
952
950 MACRO_SUB_RE = /(
953 MACRO_SUB_RE = /(
951 \{\{
954 \{\{
952 macro\((\d+)\)
955 macro\((\d+)\)
953 \}\}
956 \}\}
954 )/x unless const_defined?(:MACRO_SUB_RE)
957 )/x unless const_defined?(:MACRO_SUB_RE)
955
958
956 # Extracts macros from text
959 # Extracts macros from text
957 def catch_macros(text)
960 def catch_macros(text)
958 macros = {}
961 macros = {}
959 text.gsub!(MACROS_RE) do
962 text.gsub!(MACROS_RE) do
960 all, macro = $1, $4.downcase
963 all, macro = $1, $4.downcase
961 if macro_exists?(macro) || all =~ MACRO_SUB_RE
964 if macro_exists?(macro) || all =~ MACRO_SUB_RE
962 index = macros.size
965 index = macros.size
963 macros[index] = all
966 macros[index] = all
964 "{{macro(#{index})}}"
967 "{{macro(#{index})}}"
965 else
968 else
966 all
969 all
967 end
970 end
968 end
971 end
969 macros
972 macros
970 end
973 end
971
974
972 # Executes and replaces macros in text
975 # Executes and replaces macros in text
973 def inject_macros(text, obj, macros, execute=true)
976 def inject_macros(text, obj, macros, execute=true)
974 text.gsub!(MACRO_SUB_RE) do
977 text.gsub!(MACRO_SUB_RE) do
975 all, index = $1, $2.to_i
978 all, index = $1, $2.to_i
976 orig = macros.delete(index)
979 orig = macros.delete(index)
977 if execute && orig && orig =~ MACROS_RE
980 if execute && orig && orig =~ MACROS_RE
978 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
981 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
979 if esc.nil?
982 if esc.nil?
980 h(exec_macro(macro, obj, args, block) || all)
983 h(exec_macro(macro, obj, args, block) || all)
981 else
984 else
982 h(all)
985 h(all)
983 end
986 end
984 elsif orig
987 elsif orig
985 h(orig)
988 h(orig)
986 else
989 else
987 h(all)
990 h(all)
988 end
991 end
989 end
992 end
990 end
993 end
991
994
992 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
995 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
993
996
994 # Renders the TOC with given headings
997 # Renders the TOC with given headings
995 def replace_toc(text, headings)
998 def replace_toc(text, headings)
996 text.gsub!(TOC_RE) do
999 text.gsub!(TOC_RE) do
997 left_align, right_align = $2, $3
1000 left_align, right_align = $2, $3
998 # Keep only the 4 first levels
1001 # Keep only the 4 first levels
999 headings = headings.select{|level, anchor, item| level <= 4}
1002 headings = headings.select{|level, anchor, item| level <= 4}
1000 if headings.empty?
1003 if headings.empty?
1001 ''
1004 ''
1002 else
1005 else
1003 div_class = 'toc'
1006 div_class = 'toc'
1004 div_class << ' right' if right_align
1007 div_class << ' right' if right_align
1005 div_class << ' left' if left_align
1008 div_class << ' left' if left_align
1006 out = "<ul class=\"#{div_class}\"><li>"
1009 out = "<ul class=\"#{div_class}\"><li>"
1007 root = headings.map(&:first).min
1010 root = headings.map(&:first).min
1008 current = root
1011 current = root
1009 started = false
1012 started = false
1010 headings.each do |level, anchor, item|
1013 headings.each do |level, anchor, item|
1011 if level > current
1014 if level > current
1012 out << '<ul><li>' * (level - current)
1015 out << '<ul><li>' * (level - current)
1013 elsif level < current
1016 elsif level < current
1014 out << "</li></ul>\n" * (current - level) + "</li><li>"
1017 out << "</li></ul>\n" * (current - level) + "</li><li>"
1015 elsif started
1018 elsif started
1016 out << '</li><li>'
1019 out << '</li><li>'
1017 end
1020 end
1018 out << "<a href=\"##{anchor}\">#{item}</a>"
1021 out << "<a href=\"##{anchor}\">#{item}</a>"
1019 current = level
1022 current = level
1020 started = true
1023 started = true
1021 end
1024 end
1022 out << '</li></ul>' * (current - root)
1025 out << '</li></ul>' * (current - root)
1023 out << '</li></ul>'
1026 out << '</li></ul>'
1024 end
1027 end
1025 end
1028 end
1026 end
1029 end
1027
1030
1028 # Same as Rails' simple_format helper without using paragraphs
1031 # Same as Rails' simple_format helper without using paragraphs
1029 def simple_format_without_paragraph(text)
1032 def simple_format_without_paragraph(text)
1030 text.to_s.
1033 text.to_s.
1031 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1034 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1032 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1035 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1033 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1036 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1034 html_safe
1037 html_safe
1035 end
1038 end
1036
1039
1037 def lang_options_for_select(blank=true)
1040 def lang_options_for_select(blank=true)
1038 (blank ? [["(auto)", ""]] : []) + languages_options
1041 (blank ? [["(auto)", ""]] : []) + languages_options
1039 end
1042 end
1040
1043
1041 def label_tag_for(name, option_tags = nil, options = {})
1044 def label_tag_for(name, option_tags = nil, options = {})
1042 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1045 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1043 content_tag("label", label_text)
1046 content_tag("label", label_text)
1044 end
1047 end
1045
1048
1046 def labelled_form_for(*args, &proc)
1049 def labelled_form_for(*args, &proc)
1047 args << {} unless args.last.is_a?(Hash)
1050 args << {} unless args.last.is_a?(Hash)
1048 options = args.last
1051 options = args.last
1049 if args.first.is_a?(Symbol)
1052 if args.first.is_a?(Symbol)
1050 options.merge!(:as => args.shift)
1053 options.merge!(:as => args.shift)
1051 end
1054 end
1052 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1055 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1053 form_for(*args, &proc)
1056 form_for(*args, &proc)
1054 end
1057 end
1055
1058
1056 def labelled_fields_for(*args, &proc)
1059 def labelled_fields_for(*args, &proc)
1057 args << {} unless args.last.is_a?(Hash)
1060 args << {} unless args.last.is_a?(Hash)
1058 options = args.last
1061 options = args.last
1059 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1062 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1060 fields_for(*args, &proc)
1063 fields_for(*args, &proc)
1061 end
1064 end
1062
1065
1063 def labelled_remote_form_for(*args, &proc)
1066 def labelled_remote_form_for(*args, &proc)
1064 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1067 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1065 args << {} unless args.last.is_a?(Hash)
1068 args << {} unless args.last.is_a?(Hash)
1066 options = args.last
1069 options = args.last
1067 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1070 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1068 form_for(*args, &proc)
1071 form_for(*args, &proc)
1069 end
1072 end
1070
1073
1071 def error_messages_for(*objects)
1074 def error_messages_for(*objects)
1072 html = ""
1075 html = ""
1073 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1076 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1074 errors = objects.map {|o| o.errors.full_messages}.flatten
1077 errors = objects.map {|o| o.errors.full_messages}.flatten
1075 if errors.any?
1078 if errors.any?
1076 html << "<div id='errorExplanation'><ul>\n"
1079 html << "<div id='errorExplanation'><ul>\n"
1077 errors.each do |error|
1080 errors.each do |error|
1078 html << "<li>#{h error}</li>\n"
1081 html << "<li>#{h error}</li>\n"
1079 end
1082 end
1080 html << "</ul></div>\n"
1083 html << "</ul></div>\n"
1081 end
1084 end
1082 html.html_safe
1085 html.html_safe
1083 end
1086 end
1084
1087
1085 def delete_link(url, options={})
1088 def delete_link(url, options={})
1086 options = {
1089 options = {
1087 :method => :delete,
1090 :method => :delete,
1088 :data => {:confirm => l(:text_are_you_sure)},
1091 :data => {:confirm => l(:text_are_you_sure)},
1089 :class => 'icon icon-del'
1092 :class => 'icon icon-del'
1090 }.merge(options)
1093 }.merge(options)
1091
1094
1092 link_to l(:button_delete), url, options
1095 link_to l(:button_delete), url, options
1093 end
1096 end
1094
1097
1095 def preview_link(url, form, target='preview', options={})
1098 def preview_link(url, form, target='preview', options={})
1096 content_tag 'a', l(:label_preview), {
1099 content_tag 'a', l(:label_preview), {
1097 :href => "#",
1100 :href => "#",
1098 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1101 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1099 :accesskey => accesskey(:preview)
1102 :accesskey => accesskey(:preview)
1100 }.merge(options)
1103 }.merge(options)
1101 end
1104 end
1102
1105
1103 def link_to_function(name, function, html_options={})
1106 def link_to_function(name, function, html_options={})
1104 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1107 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1105 end
1108 end
1106
1109
1107 # Helper to render JSON in views
1110 # Helper to render JSON in views
1108 def raw_json(arg)
1111 def raw_json(arg)
1109 arg.to_json.to_s.gsub('/', '\/').html_safe
1112 arg.to_json.to_s.gsub('/', '\/').html_safe
1110 end
1113 end
1111
1114
1112 def back_url
1115 def back_url
1113 url = params[:back_url]
1116 url = params[:back_url]
1114 if url.nil? && referer = request.env['HTTP_REFERER']
1117 if url.nil? && referer = request.env['HTTP_REFERER']
1115 url = CGI.unescape(referer.to_s)
1118 url = CGI.unescape(referer.to_s)
1116 end
1119 end
1117 url
1120 url
1118 end
1121 end
1119
1122
1120 def back_url_hidden_field_tag
1123 def back_url_hidden_field_tag
1121 url = back_url
1124 url = back_url
1122 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1125 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1123 end
1126 end
1124
1127
1125 def check_all_links(form_name)
1128 def check_all_links(form_name)
1126 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1129 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1127 " | ".html_safe +
1130 " | ".html_safe +
1128 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1131 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1129 end
1132 end
1130
1133
1131 def progress_bar(pcts, options={})
1134 def progress_bar(pcts, options={})
1132 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1135 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1133 pcts = pcts.collect(&:round)
1136 pcts = pcts.collect(&:round)
1134 pcts[1] = pcts[1] - pcts[0]
1137 pcts[1] = pcts[1] - pcts[0]
1135 pcts << (100 - pcts[1] - pcts[0])
1138 pcts << (100 - pcts[1] - pcts[0])
1136 width = options[:width] || '100px;'
1139 width = options[:width] || '100px;'
1137 legend = options[:legend] || ''
1140 legend = options[:legend] || ''
1138 content_tag('table',
1141 content_tag('table',
1139 content_tag('tr',
1142 content_tag('tr',
1140 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1143 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1141 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1144 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1142 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1145 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1143 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1146 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1144 content_tag('p', legend, :class => 'percent').html_safe
1147 content_tag('p', legend, :class => 'percent').html_safe
1145 end
1148 end
1146
1149
1147 def checked_image(checked=true)
1150 def checked_image(checked=true)
1148 if checked
1151 if checked
1149 image_tag 'toggle_check.png'
1152 image_tag 'toggle_check.png'
1150 end
1153 end
1151 end
1154 end
1152
1155
1153 def context_menu(url)
1156 def context_menu(url)
1154 unless @context_menu_included
1157 unless @context_menu_included
1155 content_for :header_tags do
1158 content_for :header_tags do
1156 javascript_include_tag('context_menu') +
1159 javascript_include_tag('context_menu') +
1157 stylesheet_link_tag('context_menu')
1160 stylesheet_link_tag('context_menu')
1158 end
1161 end
1159 if l(:direction) == 'rtl'
1162 if l(:direction) == 'rtl'
1160 content_for :header_tags do
1163 content_for :header_tags do
1161 stylesheet_link_tag('context_menu_rtl')
1164 stylesheet_link_tag('context_menu_rtl')
1162 end
1165 end
1163 end
1166 end
1164 @context_menu_included = true
1167 @context_menu_included = true
1165 end
1168 end
1166 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1169 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1167 end
1170 end
1168
1171
1169 def calendar_for(field_id)
1172 def calendar_for(field_id)
1170 include_calendar_headers_tags
1173 include_calendar_headers_tags
1171 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1174 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1172 end
1175 end
1173
1176
1174 def include_calendar_headers_tags
1177 def include_calendar_headers_tags
1175 unless @calendar_headers_tags_included
1178 unless @calendar_headers_tags_included
1176 tags = javascript_include_tag("datepicker")
1179 tags = javascript_include_tag("datepicker")
1177 @calendar_headers_tags_included = true
1180 @calendar_headers_tags_included = true
1178 content_for :header_tags do
1181 content_for :header_tags do
1179 start_of_week = Setting.start_of_week
1182 start_of_week = Setting.start_of_week
1180 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1183 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1181 # Redmine uses 1..7 (monday..sunday) in settings and locales
1184 # Redmine uses 1..7 (monday..sunday) in settings and locales
1182 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1185 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1183 start_of_week = start_of_week.to_i % 7
1186 start_of_week = start_of_week.to_i % 7
1184 tags << javascript_tag(
1187 tags << javascript_tag(
1185 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1188 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1186 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1189 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1187 path_to_image('/images/calendar.png') +
1190 path_to_image('/images/calendar.png') +
1188 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1191 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1189 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1192 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1190 "beforeShow: beforeShowDatePicker};")
1193 "beforeShow: beforeShowDatePicker};")
1191 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1194 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1192 unless jquery_locale == 'en'
1195 unless jquery_locale == 'en'
1193 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1196 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1194 end
1197 end
1195 tags
1198 tags
1196 end
1199 end
1197 end
1200 end
1198 end
1201 end
1199
1202
1200 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1203 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1201 # Examples:
1204 # Examples:
1202 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1205 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1203 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1206 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1204 #
1207 #
1205 def stylesheet_link_tag(*sources)
1208 def stylesheet_link_tag(*sources)
1206 options = sources.last.is_a?(Hash) ? sources.pop : {}
1209 options = sources.last.is_a?(Hash) ? sources.pop : {}
1207 plugin = options.delete(:plugin)
1210 plugin = options.delete(:plugin)
1208 sources = sources.map do |source|
1211 sources = sources.map do |source|
1209 if plugin
1212 if plugin
1210 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1213 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1211 elsif current_theme && current_theme.stylesheets.include?(source)
1214 elsif current_theme && current_theme.stylesheets.include?(source)
1212 current_theme.stylesheet_path(source)
1215 current_theme.stylesheet_path(source)
1213 else
1216 else
1214 source
1217 source
1215 end
1218 end
1216 end
1219 end
1217 super sources, options
1220 super sources, options
1218 end
1221 end
1219
1222
1220 # Overrides Rails' image_tag with themes and plugins support.
1223 # Overrides Rails' image_tag with themes and plugins support.
1221 # Examples:
1224 # Examples:
1222 # image_tag('image.png') # => picks image.png from the current theme or defaults
1225 # image_tag('image.png') # => picks image.png from the current theme or defaults
1223 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1226 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1224 #
1227 #
1225 def image_tag(source, options={})
1228 def image_tag(source, options={})
1226 if plugin = options.delete(:plugin)
1229 if plugin = options.delete(:plugin)
1227 source = "/plugin_assets/#{plugin}/images/#{source}"
1230 source = "/plugin_assets/#{plugin}/images/#{source}"
1228 elsif current_theme && current_theme.images.include?(source)
1231 elsif current_theme && current_theme.images.include?(source)
1229 source = current_theme.image_path(source)
1232 source = current_theme.image_path(source)
1230 end
1233 end
1231 super source, options
1234 super source, options
1232 end
1235 end
1233
1236
1234 # Overrides Rails' javascript_include_tag with plugins support
1237 # Overrides Rails' javascript_include_tag with plugins support
1235 # Examples:
1238 # Examples:
1236 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1239 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1237 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1240 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1238 #
1241 #
1239 def javascript_include_tag(*sources)
1242 def javascript_include_tag(*sources)
1240 options = sources.last.is_a?(Hash) ? sources.pop : {}
1243 options = sources.last.is_a?(Hash) ? sources.pop : {}
1241 if plugin = options.delete(:plugin)
1244 if plugin = options.delete(:plugin)
1242 sources = sources.map do |source|
1245 sources = sources.map do |source|
1243 if plugin
1246 if plugin
1244 "/plugin_assets/#{plugin}/javascripts/#{source}"
1247 "/plugin_assets/#{plugin}/javascripts/#{source}"
1245 else
1248 else
1246 source
1249 source
1247 end
1250 end
1248 end
1251 end
1249 end
1252 end
1250 super sources, options
1253 super sources, options
1251 end
1254 end
1252
1255
1253 # TODO: remove this in 2.5.0
1256 # TODO: remove this in 2.5.0
1254 def has_content?(name)
1257 def has_content?(name)
1255 content_for?(name)
1258 content_for?(name)
1256 end
1259 end
1257
1260
1258 def sidebar_content?
1261 def sidebar_content?
1259 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1262 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1260 end
1263 end
1261
1264
1262 def view_layouts_base_sidebar_hook_response
1265 def view_layouts_base_sidebar_hook_response
1263 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1266 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1264 end
1267 end
1265
1268
1266 def email_delivery_enabled?
1269 def email_delivery_enabled?
1267 !!ActionMailer::Base.perform_deliveries
1270 !!ActionMailer::Base.perform_deliveries
1268 end
1271 end
1269
1272
1270 # Returns the avatar image tag for the given +user+ if avatars are enabled
1273 # Returns the avatar image tag for the given +user+ if avatars are enabled
1271 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1274 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1272 def avatar(user, options = { })
1275 def avatar(user, options = { })
1273 if Setting.gravatar_enabled?
1276 if Setting.gravatar_enabled?
1274 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1277 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1275 email = nil
1278 email = nil
1276 if user.respond_to?(:mail)
1279 if user.respond_to?(:mail)
1277 email = user.mail
1280 email = user.mail
1278 elsif user.to_s =~ %r{<(.+?)>}
1281 elsif user.to_s =~ %r{<(.+?)>}
1279 email = $1
1282 email = $1
1280 end
1283 end
1281 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1284 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1282 else
1285 else
1283 ''
1286 ''
1284 end
1287 end
1285 end
1288 end
1286
1289
1287 def sanitize_anchor_name(anchor)
1290 def sanitize_anchor_name(anchor)
1288 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1291 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1289 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1292 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1290 else
1293 else
1291 # TODO: remove when ruby1.8 is no longer supported
1294 # TODO: remove when ruby1.8 is no longer supported
1292 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1295 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1293 end
1296 end
1294 end
1297 end
1295
1298
1296 # Returns the javascript tags that are included in the html layout head
1299 # Returns the javascript tags that are included in the html layout head
1297 def javascript_heads
1300 def javascript_heads
1298 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1301 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1299 unless User.current.pref.warn_on_leaving_unsaved == '0'
1302 unless User.current.pref.warn_on_leaving_unsaved == '0'
1300 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1303 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1301 end
1304 end
1302 tags
1305 tags
1303 end
1306 end
1304
1307
1305 def favicon
1308 def favicon
1306 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1309 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1307 end
1310 end
1308
1311
1309 # Returns the path to the favicon
1312 # Returns the path to the favicon
1310 def favicon_path
1313 def favicon_path
1311 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1314 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1312 image_path(icon)
1315 image_path(icon)
1313 end
1316 end
1314
1317
1315 # Returns the full URL to the favicon
1318 # Returns the full URL to the favicon
1316 def favicon_url
1319 def favicon_url
1317 # TODO: use #image_url introduced in Rails4
1320 # TODO: use #image_url introduced in Rails4
1318 path = favicon_path
1321 path = favicon_path
1319 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1322 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1320 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1323 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1321 end
1324 end
1322
1325
1323 def robot_exclusion_tag
1326 def robot_exclusion_tag
1324 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1327 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1325 end
1328 end
1326
1329
1327 # Returns true if arg is expected in the API response
1330 # Returns true if arg is expected in the API response
1328 def include_in_api_response?(arg)
1331 def include_in_api_response?(arg)
1329 unless @included_in_api_response
1332 unless @included_in_api_response
1330 param = params[:include]
1333 param = params[:include]
1331 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1334 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1332 @included_in_api_response.collect!(&:strip)
1335 @included_in_api_response.collect!(&:strip)
1333 end
1336 end
1334 @included_in_api_response.include?(arg.to_s)
1337 @included_in_api_response.include?(arg.to_s)
1335 end
1338 end
1336
1339
1337 # Returns options or nil if nometa param or X-Redmine-Nometa header
1340 # Returns options or nil if nometa param or X-Redmine-Nometa header
1338 # was set in the request
1341 # was set in the request
1339 def api_meta(options)
1342 def api_meta(options)
1340 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1343 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1341 # compatibility mode for activeresource clients that raise
1344 # compatibility mode for activeresource clients that raise
1342 # an error when deserializing an array with attributes
1345 # an error when deserializing an array with attributes
1343 nil
1346 nil
1344 else
1347 else
1345 options
1348 options
1346 end
1349 end
1347 end
1350 end
1348
1351
1349 private
1352 private
1350
1353
1351 def wiki_helper
1354 def wiki_helper
1352 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1355 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1353 extend helper
1356 extend helper
1354 return self
1357 return self
1355 end
1358 end
1356
1359
1357 def link_to_content_update(text, url_params = {}, html_options = {})
1360 def link_to_content_update(text, url_params = {}, html_options = {})
1358 link_to(text, url_params, html_options)
1361 link_to(text, url_params, html_options)
1359 end
1362 end
1360 end
1363 end
@@ -1,139 +1,141
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 TimeEntry < ActiveRecord::Base
18 class TimeEntry < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 # could have used polymorphic association
20 # could have used polymorphic association
21 # project association here allows easy loading of time entries at project level with one database trip
21 # project association here allows easy loading of time entries at project level with one database trip
22 belongs_to :project
22 belongs_to :project
23 belongs_to :issue
23 belongs_to :issue
24 belongs_to :user
24 belongs_to :user
25 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
25 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
26
26
27 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27 attr_protected :user_id, :tyear, :tmonth, :tweek
28
28
29 acts_as_customizable
29 acts_as_customizable
30 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
30 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
31 :url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
31 :url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
32 :author => :user,
32 :author => :user,
33 :group => :issue,
33 :group => :issue,
34 :description => :comments
34 :description => :comments
35
35
36 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
36 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
37 :author_key => :user_id,
37 :author_key => :user_id,
38 :find_options => {:include => :project}
38 :find_options => {:include => :project}
39
39
40 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
40 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
41 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
41 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
42 validates_length_of :comments, :maximum => 255, :allow_nil => true
42 validates_length_of :comments, :maximum => 255, :allow_nil => true
43 validates :spent_on, :date => true
43 validates :spent_on, :date => true
44 before_validation :set_project_if_nil
44 before_validation :set_project_if_nil
45 validate :validate_time_entry
45 validate :validate_time_entry
46
46
47 scope :visible, lambda {|*args|
47 scope :visible, lambda {|*args|
48 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args))
48 includes(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args))
49 }
49 }
50 scope :on_issue, lambda {|issue|
50 scope :on_issue, lambda {|issue|
51 includes(:issue).where("#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}")
51 includes(:issue).where("#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}")
52 }
52 }
53 scope :on_project, lambda {|project, include_subprojects|
53 scope :on_project, lambda {|project, include_subprojects|
54 includes(:project).where(project.project_condition(include_subprojects))
54 includes(:project).where(project.project_condition(include_subprojects))
55 }
55 }
56 scope :spent_between, lambda {|from, to|
56 scope :spent_between, lambda {|from, to|
57 if from && to
57 if from && to
58 where("#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to)
58 where("#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to)
59 elsif from
59 elsif from
60 where("#{TimeEntry.table_name}.spent_on >= ?", from)
60 where("#{TimeEntry.table_name}.spent_on >= ?", from)
61 elsif to
61 elsif to
62 where("#{TimeEntry.table_name}.spent_on <= ?", to)
62 where("#{TimeEntry.table_name}.spent_on <= ?", to)
63 else
63 else
64 where(nil)
64 where(nil)
65 end
65 end
66 }
66 }
67
67
68 safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
68 safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields'
69
69
70 def initialize(attributes=nil, *args)
70 def initialize(attributes=nil, *args)
71 super
71 super
72 if new_record? && self.activity.nil?
72 if new_record? && self.activity.nil?
73 if default_activity = TimeEntryActivity.default
73 if default_activity = TimeEntryActivity.default
74 self.activity_id = default_activity.id
74 self.activity_id = default_activity.id
75 end
75 end
76 self.hours = nil if hours == 0
76 self.hours = nil if hours == 0
77 end
77 end
78 end
78 end
79
79
80 def safe_attributes=(attrs, user=User.current)
80 def safe_attributes=(attrs, user=User.current)
81 attrs = super
81 if attrs
82 if !new_record? && issue && issue.project_id != project_id
82 attrs = super(attrs)
83 if issue_id_changed? && attrs[:project_id].blank? && issue && issue.project_id != project_id
83 if user.allowed_to?(:log_time, issue.project)
84 if user.allowed_to?(:log_time, issue.project)
84 self.project_id = issue.project_id
85 self.project_id = issue.project_id
85 end
86 end
86 end
87 end
88 end
87 attrs
89 attrs
88 end
90 end
89
91
90 def set_project_if_nil
92 def set_project_if_nil
91 self.project = issue.project if issue && project.nil?
93 self.project = issue.project if issue && project.nil?
92 end
94 end
93
95
94 def validate_time_entry
96 def validate_time_entry
95 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
97 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
96 errors.add :project_id, :invalid if project.nil?
98 errors.add :project_id, :invalid if project.nil?
97 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
99 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
98 end
100 end
99
101
100 def hours=(h)
102 def hours=(h)
101 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
103 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
102 end
104 end
103
105
104 def hours
106 def hours
105 h = read_attribute(:hours)
107 h = read_attribute(:hours)
106 if h.is_a?(Float)
108 if h.is_a?(Float)
107 h.round(2)
109 h.round(2)
108 else
110 else
109 h
111 h
110 end
112 end
111 end
113 end
112
114
113 # tyear, tmonth, tweek assigned where setting spent_on attributes
115 # tyear, tmonth, tweek assigned where setting spent_on attributes
114 # these attributes make time aggregations easier
116 # these attributes make time aggregations easier
115 def spent_on=(date)
117 def spent_on=(date)
116 super
118 super
117 if spent_on.is_a?(Time)
119 if spent_on.is_a?(Time)
118 self.spent_on = spent_on.to_date
120 self.spent_on = spent_on.to_date
119 end
121 end
120 self.tyear = spent_on ? spent_on.year : nil
122 self.tyear = spent_on ? spent_on.year : nil
121 self.tmonth = spent_on ? spent_on.month : nil
123 self.tmonth = spent_on ? spent_on.month : nil
122 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
124 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
123 end
125 end
124
126
125 # Returns true if the time entry can be edited by usr, otherwise false
127 # Returns true if the time entry can be edited by usr, otherwise false
126 def editable_by?(usr)
128 def editable_by?(usr)
127 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
129 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
128 end
130 end
129
131
130 # Returns the custom_field_values that can be edited by the given user
132 # Returns the custom_field_values that can be edited by the given user
131 def editable_custom_field_values(user=nil)
133 def editable_custom_field_values(user=nil)
132 visible_custom_field_values
134 visible_custom_field_values
133 end
135 end
134
136
135 # Returns the custom fields that can be edited by the given user
137 # Returns the custom fields that can be edited by the given user
136 def editable_custom_fields(user=nil)
138 def editable_custom_fields(user=nil)
137 editable_custom_field_values(user).map(&:custom_field).uniq
139 editable_custom_field_values(user).map(&:custom_field).uniq
138 end
140 end
139 end
141 end
@@ -1,158 +1,158
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <h2><%= issue_heading(@issue) %></h2>
3 <h2><%= issue_heading(@issue) %></h2>
4
4
5 <div class="<%= @issue.css_classes %> details">
5 <div class="<%= @issue.css_classes %> details">
6 <% if @prev_issue_id || @next_issue_id %>
6 <% if @prev_issue_id || @next_issue_id %>
7 <div class="next-prev-links contextual">
7 <div class="next-prev-links contextual">
8 <%= link_to_if @prev_issue_id,
8 <%= link_to_if @prev_issue_id,
9 "\xc2\xab #{l(:label_previous)}",
9 "\xc2\xab #{l(:label_previous)}",
10 (@prev_issue_id ? issue_path(@prev_issue_id) : nil),
10 (@prev_issue_id ? issue_path(@prev_issue_id) : nil),
11 :title => "##{@prev_issue_id}" %> |
11 :title => "##{@prev_issue_id}" %> |
12 <% if @issue_position && @issue_count %>
12 <% if @issue_position && @issue_count %>
13 <span class="position"><%= l(:label_item_position, :position => @issue_position, :count => @issue_count) %></span> |
13 <span class="position"><%= l(:label_item_position, :position => @issue_position, :count => @issue_count) %></span> |
14 <% end %>
14 <% end %>
15 <%= link_to_if @next_issue_id,
15 <%= link_to_if @next_issue_id,
16 "#{l(:label_next)} \xc2\xbb",
16 "#{l(:label_next)} \xc2\xbb",
17 (@next_issue_id ? issue_path(@next_issue_id) : nil),
17 (@next_issue_id ? issue_path(@next_issue_id) : nil),
18 :title => "##{@next_issue_id}" %>
18 :title => "##{@next_issue_id}" %>
19 </div>
19 </div>
20 <% end %>
20 <% end %>
21
21
22 <%= avatar(@issue.author, :size => "50") %>
22 <%= avatar(@issue.author, :size => "50") %>
23
23
24 <div class="subject">
24 <div class="subject">
25 <%= render_issue_subject_with_tree(@issue) %>
25 <%= render_issue_subject_with_tree(@issue) %>
26 </div>
26 </div>
27 <p class="author">
27 <p class="author">
28 <%= authoring @issue.created_on, @issue.author %>.
28 <%= authoring @issue.created_on, @issue.author %>.
29 <% if @issue.created_on != @issue.updated_on %>
29 <% if @issue.created_on != @issue.updated_on %>
30 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
30 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
31 <% end %>
31 <% end %>
32 </p>
32 </p>
33
33
34 <table class="attributes">
34 <table class="attributes">
35 <%= issue_fields_rows do |rows|
35 <%= issue_fields_rows do |rows|
36 rows.left l(:field_status), h(@issue.status.name), :class => 'status'
36 rows.left l(:field_status), h(@issue.status.name), :class => 'status'
37 rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority'
37 rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority'
38
38
39 unless @issue.disabled_core_fields.include?('assigned_to_id')
39 unless @issue.disabled_core_fields.include?('assigned_to_id')
40 rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
40 rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
41 end
41 end
42 unless @issue.disabled_core_fields.include?('category_id')
42 unless @issue.disabled_core_fields.include?('category_id')
43 rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category'
43 rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category'
44 end
44 end
45 unless @issue.disabled_core_fields.include?('fixed_version_id')
45 unless @issue.disabled_core_fields.include?('fixed_version_id')
46 rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
46 rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
47 end
47 end
48
48
49 unless @issue.disabled_core_fields.include?('start_date')
49 unless @issue.disabled_core_fields.include?('start_date')
50 rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
50 rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
51 end
51 end
52 unless @issue.disabled_core_fields.include?('due_date')
52 unless @issue.disabled_core_fields.include?('due_date')
53 rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
53 rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
54 end
54 end
55 unless @issue.disabled_core_fields.include?('done_ratio')
55 unless @issue.disabled_core_fields.include?('done_ratio')
56 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
56 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress'
57 end
57 end
58 unless @issue.disabled_core_fields.include?('estimated_hours')
58 unless @issue.disabled_core_fields.include?('estimated_hours')
59 unless @issue.estimated_hours.nil?
59 unless @issue.estimated_hours.nil?
60 rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
60 rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours'
61 end
61 end
62 end
62 end
63 if User.current.allowed_to?(:view_time_entries, @project)
63 if User.current.allowed_to?(:view_time_entries, @project)
64 rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), project_issue_time_entries_path(@project, @issue)) : "-"), :class => 'spent-time'
64 rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time'
65 end
65 end
66 end %>
66 end %>
67 <%= render_custom_fields_rows(@issue) %>
67 <%= render_custom_fields_rows(@issue) %>
68 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
68 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
69 </table>
69 </table>
70
70
71 <% if @issue.description? || @issue.attachments.any? -%>
71 <% if @issue.description? || @issue.attachments.any? -%>
72 <hr />
72 <hr />
73 <% if @issue.description? %>
73 <% if @issue.description? %>
74 <div class="description">
74 <div class="description">
75 <div class="contextual">
75 <div class="contextual">
76 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
76 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if authorize_for('issues', 'edit') %>
77 </div>
77 </div>
78
78
79 <p><strong><%=l(:field_description)%></strong></p>
79 <p><strong><%=l(:field_description)%></strong></p>
80 <div class="wiki">
80 <div class="wiki">
81 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
81 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
82 </div>
82 </div>
83 </div>
83 </div>
84 <% end %>
84 <% end %>
85 <%= link_to_attachments @issue, :thumbnails => true %>
85 <%= link_to_attachments @issue, :thumbnails => true %>
86 <% end -%>
86 <% end -%>
87
87
88 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
88 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
89
89
90 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
90 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
91 <hr />
91 <hr />
92 <div id="issue_tree">
92 <div id="issue_tree">
93 <div class="contextual">
93 <div class="contextual">
94 <%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
94 <%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
95 </div>
95 </div>
96 <p><strong><%=l(:label_subtask_plural)%></strong></p>
96 <p><strong><%=l(:label_subtask_plural)%></strong></p>
97 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
97 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
98 </div>
98 </div>
99 <% end %>
99 <% end %>
100
100
101 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
101 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
102 <hr />
102 <hr />
103 <div id="relations">
103 <div id="relations">
104 <%= render :partial => 'relations' %>
104 <%= render :partial => 'relations' %>
105 </div>
105 </div>
106 <% end %>
106 <% end %>
107
107
108 </div>
108 </div>
109
109
110 <% if @changesets.present? %>
110 <% if @changesets.present? %>
111 <div id="issue-changesets">
111 <div id="issue-changesets">
112 <h3><%=l(:label_associated_revisions)%></h3>
112 <h3><%=l(:label_associated_revisions)%></h3>
113 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
113 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
114 </div>
114 </div>
115 <% end %>
115 <% end %>
116
116
117 <% if @journals.present? %>
117 <% if @journals.present? %>
118 <div id="history">
118 <div id="history">
119 <h3><%=l(:label_history)%></h3>
119 <h3><%=l(:label_history)%></h3>
120 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
120 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
121 </div>
121 </div>
122 <% end %>
122 <% end %>
123
123
124
124
125 <div style="clear: both;"></div>
125 <div style="clear: both;"></div>
126 <%= render :partial => 'action_menu' %>
126 <%= render :partial => 'action_menu' %>
127
127
128 <div style="clear: both;"></div>
128 <div style="clear: both;"></div>
129 <% if @issue.editable? %>
129 <% if @issue.editable? %>
130 <div id="update" style="display:none;">
130 <div id="update" style="display:none;">
131 <h3><%= l(:button_edit) %></h3>
131 <h3><%= l(:button_edit) %></h3>
132 <%= render :partial => 'edit' %>
132 <%= render :partial => 'edit' %>
133 </div>
133 </div>
134 <% end %>
134 <% end %>
135
135
136 <% other_formats_links do |f| %>
136 <% other_formats_links do |f| %>
137 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
137 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
138 <%= f.link_to 'PDF' %>
138 <%= f.link_to 'PDF' %>
139 <% end %>
139 <% end %>
140
140
141 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
141 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
142
142
143 <% content_for :sidebar do %>
143 <% content_for :sidebar do %>
144 <%= render :partial => 'issues/sidebar' %>
144 <%= render :partial => 'issues/sidebar' %>
145
145
146 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
146 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
147 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
147 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
148 <div id="watchers">
148 <div id="watchers">
149 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
149 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
150 </div>
150 </div>
151 <% end %>
151 <% end %>
152 <% end %>
152 <% end %>
153
153
154 <% content_for :header_tags do %>
154 <% content_for :header_tags do %>
155 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
155 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
156 <% end %>
156 <% end %>
157
157
158 <%= context_menu issues_context_menu_path %>
158 <%= context_menu issues_context_menu_path %>
@@ -1,32 +1,34
1 <%= error_messages_for 'time_entry' %>
1 <%= error_messages_for 'time_entry' %>
2 <%= back_url_hidden_field_tag %>
2 <%= back_url_hidden_field_tag %>
3
3
4 <div class="box tabular">
4 <div class="box tabular">
5 <% if @time_entry.new_record? %>
5 <% if @time_entry.new_record? %>
6 <% if params[:project_id] || @time_entry.issue %>
6 <% if params[:project_id] %>
7 <%= f.hidden_field :project_id %>
7 <%= hidden_field_tag 'project_id', params[:project_id] %>
8 <% elsif params[:issue_id] %>
9 <%= hidden_field_tag 'issue_id', params[:issue_id] %>
8 <% else %>
10 <% else %>
9 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).all, :selected => @time_entry.project), :required => true %></p>
11 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).all, :selected => @time_entry.project, :include_blank => true) %></p>
10 <% end %>
12 <% end %>
11 <% end %>
13 <% end %>
12 <p>
14 <p>
13 <%= f.text_field :issue_id, :size => 6 %>
15 <%= f.text_field :issue_id, :size => 6 %>
14 <span id="time_entry_issue"><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></span>
16 <span id="time_entry_issue"><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></span>
15 </p>
17 </p>
16 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
18 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
17 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
19 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
18 <p><%= f.text_field :comments, :size => 100, :maxlength => 255 %></p>
20 <p><%= f.text_field :comments, :size => 100, :maxlength => 255 %></p>
19 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
21 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
20 <% @time_entry.custom_field_values.each do |value| %>
22 <% @time_entry.custom_field_values.each do |value| %>
21 <p><%= custom_field_tag_with_label :time_entry, value %></p>
23 <p><%= custom_field_tag_with_label :time_entry, value %></p>
22 <% end %>
24 <% end %>
23 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
25 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
24 </div>
26 </div>
25
27
26 <%= javascript_tag do %>
28 <%= javascript_tag do %>
27 observeAutocompleteField('time_entry_issue_id', '<%= escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (@project ? nil : 'all'))%>', {
29 observeAutocompleteField('time_entry_issue_id', '<%= escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (@project ? nil : 'all'))%>', {
28 select: function(event, ui) {
30 select: function(event, ui) {
29 $('#time_entry_issue').text(ui.item.label);
31 $('#time_entry_issue').text(ui.item.label);
30 }
32 }
31 });
33 });
32 <% end %>
34 <% end %>
@@ -1,8 +1,7
1 <h2><%= l(:label_spent_time) %></h2>
1 <h2><%= l(:label_spent_time) %></h2>
2
2
3 <%= labelled_form_for @time_entry, :url => time_entries_path do |f| %>
3 <%= labelled_form_for @time_entry, :url => time_entries_path do |f| %>
4 <%= hidden_field_tag 'project_id', params[:project_id] if params[:project_id] %>
5 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
6 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
7 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
8 <% end %>
7 <% end %>
@@ -1,685 +1,717
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require File.expand_path('../../test_helper', __FILE__)
19 require File.expand_path('../../test_helper', __FILE__)
20
20
21 class TimelogControllerTest < ActionController::TestCase
21 class TimelogControllerTest < ActionController::TestCase
22 fixtures :projects, :enabled_modules, :roles, :members,
22 fixtures :projects, :enabled_modules, :roles, :members,
23 :member_roles, :issues, :time_entries, :users,
23 :member_roles, :issues, :time_entries, :users,
24 :trackers, :enumerations, :issue_statuses,
24 :trackers, :enumerations, :issue_statuses,
25 :custom_fields, :custom_values,
25 :custom_fields, :custom_values,
26 :projects_trackers, :custom_fields_trackers,
26 :projects_trackers, :custom_fields_trackers,
27 :custom_fields_projects
27 :custom_fields_projects
28
28
29 include Redmine::I18n
29 include Redmine::I18n
30
30
31 def test_new_with_project_id
31 def test_new
32 @request.session[:user_id] = 3
32 @request.session[:user_id] = 3
33 get :new, :project_id => 1
33 get :new
34 assert_response :success
34 assert_response :success
35 assert_template 'new'
35 assert_template 'new'
36 assert_select 'select[name=?]', 'time_entry[project_id]', 0
36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
37 assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
38 assert_select 'select[name=?]', 'time_entry[project_id]' do
39 # blank option for project
40 assert_select 'option[value=]'
41 end
38 end
42 end
39
43
40 def test_new_with_issue_id
44 def test_new_with_project_id
41 @request.session[:user_id] = 3
45 @request.session[:user_id] = 3
42 get :new, :issue_id => 2
46 get :new, :project_id => 1
43 assert_response :success
47 assert_response :success
44 assert_template 'new'
48 assert_template 'new'
49 assert_select 'input[name=?][type=hidden]', 'project_id'
50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
45 assert_select 'select[name=?]', 'time_entry[project_id]', 0
51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
46 assert_select 'input[name=?][value=1][type=hidden]', 'time_entry[project_id]'
47 end
52 end
48
53
49 def test_new_without_project
54 def test_new_with_issue_id
50 @request.session[:user_id] = 3
55 @request.session[:user_id] = 3
51 get :new
56 get :new, :issue_id => 2
52 assert_response :success
57 assert_response :success
53 assert_template 'new'
58 assert_template 'new'
54 assert_select 'select[name=?]', 'time_entry[project_id]'
59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
55 assert_select 'input[name=?]', 'time_entry[project_id]', 0
60 assert_select 'input[name=?][type=hidden]', 'issue_id'
61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
56 end
62 end
57
63
58 def test_new_without_project_should_prefill_the_form
64 def test_new_without_project_should_prefill_the_form
59 @request.session[:user_id] = 3
65 @request.session[:user_id] = 3
60 get :new, :time_entry => {:project_id => '1'}
66 get :new, :time_entry => {:project_id => '1'}
61 assert_response :success
67 assert_response :success
62 assert_template 'new'
68 assert_template 'new'
63 assert_select 'select[name=?]', 'time_entry[project_id]' do
69 assert_select 'select[name=?]', 'time_entry[project_id]' do
64 assert_select 'option[value=1][selected=selected]'
70 assert_select 'option[value=1][selected=selected]'
65 end
71 end
66 assert_select 'input[name=?]', 'time_entry[project_id]', 0
67 end
72 end
68
73
69 def test_new_without_project_should_deny_without_permission
74 def test_new_without_project_should_deny_without_permission
70 Role.all.each {|role| role.remove_permission! :log_time}
75 Role.all.each {|role| role.remove_permission! :log_time}
71 @request.session[:user_id] = 3
76 @request.session[:user_id] = 3
72
77
73 get :new
78 get :new
74 assert_response 403
79 assert_response 403
75 end
80 end
76
81
77 def test_new_should_select_default_activity
82 def test_new_should_select_default_activity
78 @request.session[:user_id] = 3
83 @request.session[:user_id] = 3
79 get :new, :project_id => 1
84 get :new, :project_id => 1
80 assert_response :success
85 assert_response :success
81 assert_select 'select[name=?]', 'time_entry[activity_id]' do
86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
82 assert_select 'option[selected=selected]', :text => 'Development'
87 assert_select 'option[selected=selected]', :text => 'Development'
83 end
88 end
84 end
89 end
85
90
86 def test_new_should_only_show_active_time_entry_activities
91 def test_new_should_only_show_active_time_entry_activities
87 @request.session[:user_id] = 3
92 @request.session[:user_id] = 3
88 get :new, :project_id => 1
93 get :new, :project_id => 1
89 assert_response :success
94 assert_response :success
90 assert_no_tag 'option', :content => 'Inactive Activity'
95 assert_no_tag 'option', :content => 'Inactive Activity'
91 end
96 end
92
97
93 def test_get_edit_existing_time
98 def test_get_edit_existing_time
94 @request.session[:user_id] = 2
99 @request.session[:user_id] = 2
95 get :edit, :id => 2, :project_id => nil
100 get :edit, :id => 2, :project_id => nil
96 assert_response :success
101 assert_response :success
97 assert_template 'edit'
102 assert_template 'edit'
98 # Default activity selected
103 # Default activity selected
99 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
104 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
100 end
105 end
101
106
102 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
107 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
103 te = TimeEntry.find(1)
108 te = TimeEntry.find(1)
104 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
109 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
105 te.save!
110 te.save!
106
111
107 @request.session[:user_id] = 1
112 @request.session[:user_id] = 1
108 get :edit, :project_id => 1, :id => 1
113 get :edit, :project_id => 1, :id => 1
109 assert_response :success
114 assert_response :success
110 assert_template 'edit'
115 assert_template 'edit'
111 # Blank option since nothing is pre-selected
116 # Blank option since nothing is pre-selected
112 assert_tag :tag => 'option', :content => '--- Please select ---'
117 assert_tag :tag => 'option', :content => '--- Please select ---'
113 end
118 end
114
119
115 def test_post_create
120 def test_post_create
116 # TODO: should POST to issues’ time log instead of project. change form
117 # and routing
118 @request.session[:user_id] = 3
121 @request.session[:user_id] = 3
122 assert_difference 'TimeEntry.count' do
119 post :create, :project_id => 1,
123 post :create, :project_id => 1,
120 :time_entry => {:comments => 'Some work on TimelogControllerTest',
124 :time_entry => {:comments => 'Some work on TimelogControllerTest',
121 # Not the default activity
125 # Not the default activity
122 :activity_id => '11',
126 :activity_id => '11',
123 :spent_on => '2008-03-14',
127 :spent_on => '2008-03-14',
124 :issue_id => '1',
128 :issue_id => '1',
125 :hours => '7.3'}
129 :hours => '7.3'}
126 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
130 assert_redirected_to '/projects/ecookbook/time_entries'
131 end
127
132
128 i = Issue.find(1)
133 t = TimeEntry.order('id DESC').first
129 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
130 assert_not_nil t
134 assert_not_nil t
135 assert_equal 'Some work on TimelogControllerTest', t.comments
136 assert_equal 1, t.project_id
137 assert_equal 1, t.issue_id
131 assert_equal 11, t.activity_id
138 assert_equal 11, t.activity_id
132 assert_equal 7.3, t.hours
139 assert_equal 7.3, t.hours
133 assert_equal 3, t.user_id
140 assert_equal 3, t.user_id
134 assert_equal i, t.issue
135 assert_equal i.project, t.project
136 end
141 end
137
142
138 def test_post_create_with_blank_issue
143 def test_post_create_with_blank_issue
139 # TODO: should POST to issues’ time log instead of project. change form
140 # and routing
141 @request.session[:user_id] = 3
144 @request.session[:user_id] = 3
145 assert_difference 'TimeEntry.count' do
142 post :create, :project_id => 1,
146 post :create, :project_id => 1,
143 :time_entry => {:comments => 'Some work on TimelogControllerTest',
147 :time_entry => {:comments => 'Some work on TimelogControllerTest',
144 # Not the default activity
148 # Not the default activity
145 :activity_id => '11',
149 :activity_id => '11',
146 :issue_id => '',
150 :issue_id => '',
147 :spent_on => '2008-03-14',
151 :spent_on => '2008-03-14',
148 :hours => '7.3'}
152 :hours => '7.3'}
149 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
153 assert_redirected_to '/projects/ecookbook/time_entries'
154 end
150
155
151 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
156 t = TimeEntry.order('id DESC').first
152 assert_not_nil t
157 assert_not_nil t
158 assert_equal 'Some work on TimelogControllerTest', t.comments
159 assert_equal 1, t.project_id
160 assert_nil t.issue_id
153 assert_equal 11, t.activity_id
161 assert_equal 11, t.activity_id
154 assert_equal 7.3, t.hours
162 assert_equal 7.3, t.hours
155 assert_equal 3, t.user_id
163 assert_equal 3, t.user_id
156 end
164 end
157
165
158 def test_create_and_continue
166 def test_create_and_continue_at_project_level
159 @request.session[:user_id] = 2
167 @request.session[:user_id] = 2
160 post :create, :project_id => 1,
168 assert_difference 'TimeEntry.count' do
161 :time_entry => {:activity_id => '11',
169 post :create, :time_entry => {:project_id => '1',
170 :activity_id => '11',
162 :issue_id => '',
171 :issue_id => '',
163 :spent_on => '2008-03-14',
172 :spent_on => '2008-03-14',
164 :hours => '7.3'},
173 :hours => '7.3'},
165 :continue => '1'
174 :continue => '1'
166 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D='
175 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
176 end
167 end
177 end
168
178
169 def test_create_and_continue_with_issue_id
179 def test_create_and_continue_at_issue_level
170 @request.session[:user_id] = 2
180 @request.session[:user_id] = 2
171 post :create, :project_id => 1,
181 assert_difference 'TimeEntry.count' do
172 :time_entry => {:activity_id => '11',
182 post :create, :time_entry => {:project_id => '',
183 :activity_id => '11',
173 :issue_id => '1',
184 :issue_id => '1',
174 :spent_on => '2008-03-14',
185 :spent_on => '2008-03-14',
175 :hours => '7.3'},
186 :hours => '7.3'},
176 :continue => '1'
187 :continue => '1'
177 assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1'
188 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
189 end
178 end
190 end
179
191
180 def test_create_and_continue_without_project
192 def test_create_and_continue_with_project_id
181 @request.session[:user_id] = 2
193 @request.session[:user_id] = 2
182 post :create, :time_entry => {:project_id => '1',
194 assert_difference 'TimeEntry.count' do
183 :activity_id => '11',
195 post :create, :project_id => 1,
196 :time_entry => {:activity_id => '11',
184 :issue_id => '',
197 :issue_id => '',
185 :spent_on => '2008-03-14',
198 :spent_on => '2008-03-14',
186 :hours => '7.3'},
199 :hours => '7.3'},
187 :continue => '1'
200 :continue => '1'
201 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
202 end
203 end
188
204
189 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
205 def test_create_and_continue_with_issue_id
206 @request.session[:user_id] = 2
207 assert_difference 'TimeEntry.count' do
208 post :create, :issue_id => 1,
209 :time_entry => {:activity_id => '11',
210 :issue_id => '1',
211 :spent_on => '2008-03-14',
212 :hours => '7.3'},
213 :continue => '1'
214 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
215 end
190 end
216 end
191
217
192 def test_create_without_log_time_permission_should_be_denied
218 def test_create_without_log_time_permission_should_be_denied
193 @request.session[:user_id] = 2
219 @request.session[:user_id] = 2
194 Role.find_by_name('Manager').remove_permission! :log_time
220 Role.find_by_name('Manager').remove_permission! :log_time
195 post :create, :project_id => 1,
221 post :create, :project_id => 1,
196 :time_entry => {:activity_id => '11',
222 :time_entry => {:activity_id => '11',
197 :issue_id => '',
223 :issue_id => '',
198 :spent_on => '2008-03-14',
224 :spent_on => '2008-03-14',
199 :hours => '7.3'}
225 :hours => '7.3'}
200
226
201 assert_response 403
227 assert_response 403
202 end
228 end
203
229
230 def test_create_without_project_and_issue_should_fail
231 @request.session[:user_id] = 2
232 post :create, :time_entry => {:issue_id => ''}
233
234 assert_response :success
235 assert_template 'new'
236 end
237
204 def test_create_with_failure
238 def test_create_with_failure
205 @request.session[:user_id] = 2
239 @request.session[:user_id] = 2
206 post :create, :project_id => 1,
240 post :create, :project_id => 1,
207 :time_entry => {:activity_id => '',
241 :time_entry => {:activity_id => '',
208 :issue_id => '',
242 :issue_id => '',
209 :spent_on => '2008-03-14',
243 :spent_on => '2008-03-14',
210 :hours => '7.3'}
244 :hours => '7.3'}
211
245
212 assert_response :success
246 assert_response :success
213 assert_template 'new'
247 assert_template 'new'
214 end
248 end
215
249
216 def test_create_without_project
250 def test_create_without_project
217 @request.session[:user_id] = 2
251 @request.session[:user_id] = 2
218 assert_difference 'TimeEntry.count' do
252 assert_difference 'TimeEntry.count' do
219 post :create, :time_entry => {:project_id => '1',
253 post :create, :time_entry => {:project_id => '1',
220 :activity_id => '11',
254 :activity_id => '11',
221 :issue_id => '',
255 :issue_id => '',
222 :spent_on => '2008-03-14',
256 :spent_on => '2008-03-14',
223 :hours => '7.3'}
257 :hours => '7.3'}
224 end
258 end
225
259
226 assert_redirected_to '/projects/ecookbook/time_entries'
260 assert_redirected_to '/projects/ecookbook/time_entries'
227 time_entry = TimeEntry.order('id DESC').first
261 time_entry = TimeEntry.order('id DESC').first
228 assert_equal 1, time_entry.project_id
262 assert_equal 1, time_entry.project_id
229 end
263 end
230
264
231 def test_create_without_project_should_fail_with_issue_not_inside_project
265 def test_create_without_project_should_fail_with_issue_not_inside_project
232 @request.session[:user_id] = 2
266 @request.session[:user_id] = 2
233 assert_no_difference 'TimeEntry.count' do
267 assert_no_difference 'TimeEntry.count' do
234 post :create, :time_entry => {:project_id => '1',
268 post :create, :time_entry => {:project_id => '1',
235 :activity_id => '11',
269 :activity_id => '11',
236 :issue_id => '5',
270 :issue_id => '5',
237 :spent_on => '2008-03-14',
271 :spent_on => '2008-03-14',
238 :hours => '7.3'}
272 :hours => '7.3'}
239 end
273 end
240
274
241 assert_response :success
275 assert_response :success
242 assert assigns(:time_entry).errors[:issue_id].present?
276 assert assigns(:time_entry).errors[:issue_id].present?
243 end
277 end
244
278
245 def test_create_without_project_should_deny_without_permission
279 def test_create_without_project_should_deny_without_permission
246 @request.session[:user_id] = 2
280 @request.session[:user_id] = 2
247 Project.find(3).disable_module!(:time_tracking)
281 Project.find(3).disable_module!(:time_tracking)
248
282
249 assert_no_difference 'TimeEntry.count' do
283 assert_no_difference 'TimeEntry.count' do
250 post :create, :time_entry => {:project_id => '3',
284 post :create, :time_entry => {:project_id => '3',
251 :activity_id => '11',
285 :activity_id => '11',
252 :issue_id => '',
286 :issue_id => '',
253 :spent_on => '2008-03-14',
287 :spent_on => '2008-03-14',
254 :hours => '7.3'}
288 :hours => '7.3'}
255 end
289 end
256
290
257 assert_response 403
291 assert_response 403
258 end
292 end
259
293
260 def test_create_without_project_with_failure
294 def test_create_without_project_with_failure
261 @request.session[:user_id] = 2
295 @request.session[:user_id] = 2
262 assert_no_difference 'TimeEntry.count' do
296 assert_no_difference 'TimeEntry.count' do
263 post :create, :time_entry => {:project_id => '1',
297 post :create, :time_entry => {:project_id => '1',
264 :activity_id => '11',
298 :activity_id => '11',
265 :issue_id => '',
299 :issue_id => '',
266 :spent_on => '2008-03-14',
300 :spent_on => '2008-03-14',
267 :hours => ''}
301 :hours => ''}
268 end
302 end
269
303
270 assert_response :success
304 assert_response :success
271 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
305 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
272 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
306 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
273 end
307 end
274
308
275 def test_update
309 def test_update
276 entry = TimeEntry.find(1)
310 entry = TimeEntry.find(1)
277 assert_equal 1, entry.issue_id
311 assert_equal 1, entry.issue_id
278 assert_equal 2, entry.user_id
312 assert_equal 2, entry.user_id
279
313
280 @request.session[:user_id] = 1
314 @request.session[:user_id] = 1
281 put :update, :id => 1,
315 put :update, :id => 1,
282 :time_entry => {:issue_id => '2',
316 :time_entry => {:issue_id => '2',
283 :hours => '8'}
317 :hours => '8'}
284 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
318 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
285 entry.reload
319 entry.reload
286
320
287 assert_equal 8, entry.hours
321 assert_equal 8, entry.hours
288 assert_equal 2, entry.issue_id
322 assert_equal 2, entry.issue_id
289 assert_equal 2, entry.user_id
323 assert_equal 2, entry.user_id
290 end
324 end
291
325
292 def test_update_should_allow_to_change_issue_to_another_project
326 def test_update_should_allow_to_change_issue_to_another_project
293 entry = TimeEntry.generate!(:issue_id => 1)
327 entry = TimeEntry.generate!(:issue_id => 1)
294
328
295 @request.session[:user_id] = 1
329 @request.session[:user_id] = 1
296 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
330 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
297 assert_response 302
331 assert_response 302
298 entry.reload
332 entry.reload
299
333
300 assert_equal 5, entry.issue_id
334 assert_equal 5, entry.issue_id
301 assert_equal 3, entry.project_id
335 assert_equal 3, entry.project_id
302 end
336 end
303
337
304 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
338 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
305 entry = TimeEntry.generate!(:issue_id => 1)
339 entry = TimeEntry.generate!(:issue_id => 1)
306 Project.find(3).disable_module!(:time_tracking)
340 Project.find(3).disable_module!(:time_tracking)
307
341
308 @request.session[:user_id] = 1
342 @request.session[:user_id] = 1
309 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
343 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
310 assert_response 200
344 assert_response 200
311 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
345 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
312 end
346 end
313
347
314 def test_get_bulk_edit
348 def test_get_bulk_edit
315 @request.session[:user_id] = 2
349 @request.session[:user_id] = 2
316 get :bulk_edit, :ids => [1, 2]
350 get :bulk_edit, :ids => [1, 2]
317 assert_response :success
351 assert_response :success
318 assert_template 'bulk_edit'
352 assert_template 'bulk_edit'
319
353
320 assert_select 'ul#bulk-selection' do
354 assert_select 'ul#bulk-selection' do
321 assert_select 'li', 2
355 assert_select 'li', 2
322 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
356 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
323 end
357 end
324
358
325 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
359 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
326 # System wide custom field
360 # System wide custom field
327 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
361 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
328
362
329 # Activities
363 # Activities
330 assert_select 'select[name=?]', 'time_entry[activity_id]' do
364 assert_select 'select[name=?]', 'time_entry[activity_id]' do
331 assert_select 'option[value=]', :text => '(No change)'
365 assert_select 'option[value=]', :text => '(No change)'
332 assert_select 'option[value=9]', :text => 'Design'
366 assert_select 'option[value=9]', :text => 'Design'
333 end
367 end
334 end
368 end
335 end
369 end
336
370
337 def test_get_bulk_edit_on_different_projects
371 def test_get_bulk_edit_on_different_projects
338 @request.session[:user_id] = 2
372 @request.session[:user_id] = 2
339 get :bulk_edit, :ids => [1, 2, 6]
373 get :bulk_edit, :ids => [1, 2, 6]
340 assert_response :success
374 assert_response :success
341 assert_template 'bulk_edit'
375 assert_template 'bulk_edit'
342 end
376 end
343
377
344 def test_bulk_update
378 def test_bulk_update
345 @request.session[:user_id] = 2
379 @request.session[:user_id] = 2
346 # update time entry activity
380 # update time entry activity
347 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
381 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
348
382
349 assert_response 302
383 assert_response 302
350 # check that the issues were updated
384 # check that the issues were updated
351 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
385 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
352 end
386 end
353
387
354 def test_bulk_update_with_failure
388 def test_bulk_update_with_failure
355 @request.session[:user_id] = 2
389 @request.session[:user_id] = 2
356 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
390 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
357
391
358 assert_response 302
392 assert_response 302
359 assert_match /Failed to save 2 time entrie/, flash[:error]
393 assert_match /Failed to save 2 time entrie/, flash[:error]
360 end
394 end
361
395
362 def test_bulk_update_on_different_projects
396 def test_bulk_update_on_different_projects
363 @request.session[:user_id] = 2
397 @request.session[:user_id] = 2
364 # makes user a manager on the other project
398 # makes user a manager on the other project
365 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
399 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
366
400
367 # update time entry activity
401 # update time entry activity
368 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
402 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
369
403
370 assert_response 302
404 assert_response 302
371 # check that the issues were updated
405 # check that the issues were updated
372 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
406 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
373 end
407 end
374
408
375 def test_bulk_update_on_different_projects_without_rights
409 def test_bulk_update_on_different_projects_without_rights
376 @request.session[:user_id] = 3
410 @request.session[:user_id] = 3
377 user = User.find(3)
411 user = User.find(3)
378 action = { :controller => "timelog", :action => "bulk_update" }
412 action = { :controller => "timelog", :action => "bulk_update" }
379 assert user.allowed_to?(action, TimeEntry.find(1).project)
413 assert user.allowed_to?(action, TimeEntry.find(1).project)
380 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
414 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
381 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
415 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
382 assert_response 403
416 assert_response 403
383 end
417 end
384
418
385 def test_bulk_update_custom_field
419 def test_bulk_update_custom_field
386 @request.session[:user_id] = 2
420 @request.session[:user_id] = 2
387 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
421 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
388
422
389 assert_response 302
423 assert_response 302
390 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
424 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
391 end
425 end
392
426
393 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
427 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
394 @request.session[:user_id] = 2
428 @request.session[:user_id] = 2
395 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
429 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
396
430
397 assert_response :redirect
431 assert_response :redirect
398 assert_redirected_to '/time_entries'
432 assert_redirected_to '/time_entries'
399 end
433 end
400
434
401 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
435 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
402 @request.session[:user_id] = 2
436 @request.session[:user_id] = 2
403 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
437 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
404
438
405 assert_response :redirect
439 assert_response :redirect
406 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
440 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
407 end
441 end
408
442
409 def test_post_bulk_update_without_edit_permission_should_be_denied
443 def test_post_bulk_update_without_edit_permission_should_be_denied
410 @request.session[:user_id] = 2
444 @request.session[:user_id] = 2
411 Role.find_by_name('Manager').remove_permission! :edit_time_entries
445 Role.find_by_name('Manager').remove_permission! :edit_time_entries
412 post :bulk_update, :ids => [1,2]
446 post :bulk_update, :ids => [1,2]
413
447
414 assert_response 403
448 assert_response 403
415 end
449 end
416
450
417 def test_destroy
451 def test_destroy
418 @request.session[:user_id] = 2
452 @request.session[:user_id] = 2
419 delete :destroy, :id => 1
453 delete :destroy, :id => 1
420 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
454 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
421 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
455 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
422 assert_nil TimeEntry.find_by_id(1)
456 assert_nil TimeEntry.find_by_id(1)
423 end
457 end
424
458
425 def test_destroy_should_fail
459 def test_destroy_should_fail
426 # simulate that this fails (e.g. due to a plugin), see #5700
460 # simulate that this fails (e.g. due to a plugin), see #5700
427 TimeEntry.any_instance.expects(:destroy).returns(false)
461 TimeEntry.any_instance.expects(:destroy).returns(false)
428
462
429 @request.session[:user_id] = 2
463 @request.session[:user_id] = 2
430 delete :destroy, :id => 1
464 delete :destroy, :id => 1
431 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
465 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
432 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
466 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
433 assert_not_nil TimeEntry.find_by_id(1)
467 assert_not_nil TimeEntry.find_by_id(1)
434 end
468 end
435
469
436 def test_index_all_projects
470 def test_index_all_projects
437 get :index
471 get :index
438 assert_response :success
472 assert_response :success
439 assert_template 'index'
473 assert_template 'index'
440 assert_not_nil assigns(:total_hours)
474 assert_not_nil assigns(:total_hours)
441 assert_equal "162.90", "%.2f" % assigns(:total_hours)
475 assert_equal "162.90", "%.2f" % assigns(:total_hours)
442 assert_tag :form,
476 assert_tag :form,
443 :attributes => {:action => "/time_entries", :id => 'query_form'}
477 :attributes => {:action => "/time_entries", :id => 'query_form'}
444 end
478 end
445
479
446 def test_index_all_projects_should_show_log_time_link
480 def test_index_all_projects_should_show_log_time_link
447 @request.session[:user_id] = 2
481 @request.session[:user_id] = 2
448 get :index
482 get :index
449 assert_response :success
483 assert_response :success
450 assert_template 'index'
484 assert_template 'index'
451 assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
485 assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
452 end
486 end
453
487
454 def test_index_my_spent_time
488 def test_index_my_spent_time
455 @request.session[:user_id] = 2
489 @request.session[:user_id] = 2
456 get :index, :user_id => 'me'
490 get :index, :user_id => 'me'
457 assert_response :success
491 assert_response :success
458 assert_template 'index'
492 assert_template 'index'
459 assert assigns(:entries).all? {|entry| entry.user_id == 2}
493 assert assigns(:entries).all? {|entry| entry.user_id == 2}
460 end
494 end
461
495
462 def test_index_at_project_level
496 def test_index_at_project_level
463 get :index, :project_id => 'ecookbook'
497 get :index, :project_id => 'ecookbook'
464 assert_response :success
498 assert_response :success
465 assert_template 'index'
499 assert_template 'index'
466 assert_not_nil assigns(:entries)
500 assert_not_nil assigns(:entries)
467 assert_equal 4, assigns(:entries).size
501 assert_equal 4, assigns(:entries).size
468 # project and subproject
502 # project and subproject
469 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
503 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
470 assert_not_nil assigns(:total_hours)
504 assert_not_nil assigns(:total_hours)
471 assert_equal "162.90", "%.2f" % assigns(:total_hours)
505 assert_equal "162.90", "%.2f" % assigns(:total_hours)
472 assert_tag :form,
506 assert_tag :form,
473 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
507 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
474 end
508 end
475
509
476 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
510 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
477 entry = TimeEntry.generate!(:project => Project.find(3))
511 entry = TimeEntry.generate!(:project => Project.find(3))
478
512
479 with_settings :display_subprojects_issues => '0' do
513 with_settings :display_subprojects_issues => '0' do
480 get :index, :project_id => 'ecookbook'
514 get :index, :project_id => 'ecookbook'
481 assert_response :success
515 assert_response :success
482 assert_template 'index'
516 assert_template 'index'
483 assert_not_include entry, assigns(:entries)
517 assert_not_include entry, assigns(:entries)
484 end
518 end
485 end
519 end
486
520
487 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
521 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
488 entry = TimeEntry.generate!(:project => Project.find(3))
522 entry = TimeEntry.generate!(:project => Project.find(3))
489
523
490 with_settings :display_subprojects_issues => '0' do
524 with_settings :display_subprojects_issues => '0' do
491 get :index, :project_id => 'ecookbook', :subproject_id => 3
525 get :index, :project_id => 'ecookbook', :subproject_id => 3
492 assert_response :success
526 assert_response :success
493 assert_template 'index'
527 assert_template 'index'
494 assert_include entry, assigns(:entries)
528 assert_include entry, assigns(:entries)
495 end
529 end
496 end
530 end
497
531
498 def test_index_at_project_level_with_date_range
532 def test_index_at_project_level_with_date_range
499 get :index, :project_id => 'ecookbook',
533 get :index, :project_id => 'ecookbook',
500 :f => ['spent_on'],
534 :f => ['spent_on'],
501 :op => {'spent_on' => '><'},
535 :op => {'spent_on' => '><'},
502 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
536 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
503 assert_response :success
537 assert_response :success
504 assert_template 'index'
538 assert_template 'index'
505 assert_not_nil assigns(:entries)
539 assert_not_nil assigns(:entries)
506 assert_equal 3, assigns(:entries).size
540 assert_equal 3, assigns(:entries).size
507 assert_not_nil assigns(:total_hours)
541 assert_not_nil assigns(:total_hours)
508 assert_equal "12.90", "%.2f" % assigns(:total_hours)
542 assert_equal "12.90", "%.2f" % assigns(:total_hours)
509 assert_tag :form,
543 assert_tag :form,
510 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
544 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
511 end
545 end
512
546
513 def test_index_at_project_level_with_date_range_using_from_and_to_params
547 def test_index_at_project_level_with_date_range_using_from_and_to_params
514 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
548 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
515 assert_response :success
549 assert_response :success
516 assert_template 'index'
550 assert_template 'index'
517 assert_not_nil assigns(:entries)
551 assert_not_nil assigns(:entries)
518 assert_equal 3, assigns(:entries).size
552 assert_equal 3, assigns(:entries).size
519 assert_not_nil assigns(:total_hours)
553 assert_not_nil assigns(:total_hours)
520 assert_equal "12.90", "%.2f" % assigns(:total_hours)
554 assert_equal "12.90", "%.2f" % assigns(:total_hours)
521 assert_tag :form,
555 assert_tag :form,
522 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
556 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
523 end
557 end
524
558
525 def test_index_at_project_level_with_period
559 def test_index_at_project_level_with_period
526 get :index, :project_id => 'ecookbook',
560 get :index, :project_id => 'ecookbook',
527 :f => ['spent_on'],
561 :f => ['spent_on'],
528 :op => {'spent_on' => '>t-'},
562 :op => {'spent_on' => '>t-'},
529 :v => {'spent_on' => ['7']}
563 :v => {'spent_on' => ['7']}
530 assert_response :success
564 assert_response :success
531 assert_template 'index'
565 assert_template 'index'
532 assert_not_nil assigns(:entries)
566 assert_not_nil assigns(:entries)
533 assert_not_nil assigns(:total_hours)
567 assert_not_nil assigns(:total_hours)
534 assert_tag :form,
568 assert_tag :form,
535 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
569 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
536 end
570 end
537
571
538 def test_index_at_issue_level
572 def test_index_at_issue_level
539 get :index, :issue_id => 1
573 get :index, :issue_id => 1
540 assert_response :success
574 assert_response :success
541 assert_template 'index'
575 assert_template 'index'
542 assert_not_nil assigns(:entries)
576 assert_not_nil assigns(:entries)
543 assert_equal 2, assigns(:entries).size
577 assert_equal 2, assigns(:entries).size
544 assert_not_nil assigns(:total_hours)
578 assert_not_nil assigns(:total_hours)
545 assert_equal 154.25, assigns(:total_hours)
579 assert_equal 154.25, assigns(:total_hours)
546 # display all time
580 # display all time
547 assert_nil assigns(:from)
581 assert_nil assigns(:from)
548 assert_nil assigns(:to)
582 assert_nil assigns(:to)
549 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
550 # to use /issues/:issue_id/time_entries
551 assert_tag :form,
583 assert_tag :form,
552 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
584 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
553 end
585 end
554
586
555 def test_index_should_sort_by_spent_on_and_created_on
587 def test_index_should_sort_by_spent_on_and_created_on
556 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
588 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
557 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
589 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
558 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
590 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
559
591
560 get :index, :project_id => 1,
592 get :index, :project_id => 1,
561 :f => ['spent_on'],
593 :f => ['spent_on'],
562 :op => {'spent_on' => '><'},
594 :op => {'spent_on' => '><'},
563 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
595 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
564 assert_response :success
596 assert_response :success
565 assert_equal [t2, t1, t3], assigns(:entries)
597 assert_equal [t2, t1, t3], assigns(:entries)
566
598
567 get :index, :project_id => 1,
599 get :index, :project_id => 1,
568 :f => ['spent_on'],
600 :f => ['spent_on'],
569 :op => {'spent_on' => '><'},
601 :op => {'spent_on' => '><'},
570 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
602 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
571 :sort => 'spent_on'
603 :sort => 'spent_on'
572 assert_response :success
604 assert_response :success
573 assert_equal [t3, t1, t2], assigns(:entries)
605 assert_equal [t3, t1, t2], assigns(:entries)
574 end
606 end
575
607
576 def test_index_with_filter_on_issue_custom_field
608 def test_index_with_filter_on_issue_custom_field
577 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
609 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
578 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
610 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
579
611
580 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
612 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
581 assert_response :success
613 assert_response :success
582 assert_equal [entry], assigns(:entries)
614 assert_equal [entry], assigns(:entries)
583 end
615 end
584
616
585 def test_index_with_issue_custom_field_column
617 def test_index_with_issue_custom_field_column
586 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
618 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
587 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
619 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
588
620
589 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
621 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
590 assert_response :success
622 assert_response :success
591 assert_include :'issue.cf_2', assigns(:query).column_names
623 assert_include :'issue.cf_2', assigns(:query).column_names
592 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
624 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
593 end
625 end
594
626
595 def test_index_with_time_entry_custom_field_column
627 def test_index_with_time_entry_custom_field_column
596 field = TimeEntryCustomField.generate!(:field_format => 'string')
628 field = TimeEntryCustomField.generate!(:field_format => 'string')
597 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
629 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
598 field_name = "cf_#{field.id}"
630 field_name = "cf_#{field.id}"
599
631
600 get :index, :c => ["hours", field_name]
632 get :index, :c => ["hours", field_name]
601 assert_response :success
633 assert_response :success
602 assert_include field_name.to_sym, assigns(:query).column_names
634 assert_include field_name.to_sym, assigns(:query).column_names
603 assert_select "td.#{field_name}", :text => 'CF Value'
635 assert_select "td.#{field_name}", :text => 'CF Value'
604 end
636 end
605
637
606 def test_index_with_time_entry_custom_field_sorting
638 def test_index_with_time_entry_custom_field_sorting
607 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
639 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
608 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
640 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
609 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
641 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
610 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
642 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
611 field_name = "cf_#{field.id}"
643 field_name = "cf_#{field.id}"
612
644
613 get :index, :c => ["hours", field_name], :sort => field_name
645 get :index, :c => ["hours", field_name], :sort => field_name
614 assert_response :success
646 assert_response :success
615 assert_include field_name.to_sym, assigns(:query).column_names
647 assert_include field_name.to_sym, assigns(:query).column_names
616 assert_select "th a.sort", :text => 'String Field'
648 assert_select "th a.sort", :text => 'String Field'
617
649
618 # Make sure that values are properly sorted
650 # Make sure that values are properly sorted
619 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
651 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
620 assert_equal 3, values.size
652 assert_equal 3, values.size
621 assert_equal values.sort, values
653 assert_equal values.sort, values
622 end
654 end
623
655
624 def test_index_atom_feed
656 def test_index_atom_feed
625 get :index, :project_id => 1, :format => 'atom'
657 get :index, :project_id => 1, :format => 'atom'
626 assert_response :success
658 assert_response :success
627 assert_equal 'application/atom+xml', @response.content_type
659 assert_equal 'application/atom+xml', @response.content_type
628 assert_not_nil assigns(:items)
660 assert_not_nil assigns(:items)
629 assert assigns(:items).first.is_a?(TimeEntry)
661 assert assigns(:items).first.is_a?(TimeEntry)
630 end
662 end
631
663
632 def test_index_at_project_level_should_include_csv_export_dialog
664 def test_index_at_project_level_should_include_csv_export_dialog
633 get :index, :project_id => 'ecookbook',
665 get :index, :project_id => 'ecookbook',
634 :f => ['spent_on'],
666 :f => ['spent_on'],
635 :op => {'spent_on' => '>='},
667 :op => {'spent_on' => '>='},
636 :v => {'spent_on' => ['2007-04-01']},
668 :v => {'spent_on' => ['2007-04-01']},
637 :c => ['spent_on', 'user']
669 :c => ['spent_on', 'user']
638 assert_response :success
670 assert_response :success
639
671
640 assert_select '#csv-export-options' do
672 assert_select '#csv-export-options' do
641 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
673 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
642 # filter
674 # filter
643 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
675 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
644 assert_select 'input[name=?][value=?]', 'op[spent_on]', '&gt;='
676 assert_select 'input[name=?][value=?]', 'op[spent_on]', '&gt;='
645 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
677 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
646 # columns
678 # columns
647 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
679 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
648 assert_select 'input[name=?][value=?]', 'c[]', 'user'
680 assert_select 'input[name=?][value=?]', 'c[]', 'user'
649 assert_select 'input[name=?]', 'c[]', 2
681 assert_select 'input[name=?]', 'c[]', 2
650 end
682 end
651 end
683 end
652 end
684 end
653
685
654 def test_index_cross_project_should_include_csv_export_dialog
686 def test_index_cross_project_should_include_csv_export_dialog
655 get :index
687 get :index
656 assert_response :success
688 assert_response :success
657
689
658 assert_select '#csv-export-options' do
690 assert_select '#csv-export-options' do
659 assert_select 'form[action=?][method=get]', '/time_entries.csv'
691 assert_select 'form[action=?][method=get]', '/time_entries.csv'
660 end
692 end
661 end
693 end
662
694
663 def test_index_at_issue_level_should_include_csv_export_dialog
695 def test_index_at_issue_level_should_include_csv_export_dialog
664 get :index, :project_id => 'ecookbook', :issue_id => 3
696 get :index, :project_id => 'ecookbook', :issue_id => 3
665 assert_response :success
697 assert_response :success
666
698
667 assert_select '#csv-export-options' do
699 assert_select '#csv-export-options' do
668 assert_select 'form[action=?][method=get]', '/projects/ecookbook/issues/3/time_entries.csv'
700 assert_select 'form[action=?][method=get]', '/projects/ecookbook/issues/3/time_entries.csv'
669 end
701 end
670 end
702 end
671
703
672 def test_index_csv_all_projects
704 def test_index_csv_all_projects
673 Setting.date_format = '%m/%d/%Y'
705 Setting.date_format = '%m/%d/%Y'
674 get :index, :format => 'csv'
706 get :index, :format => 'csv'
675 assert_response :success
707 assert_response :success
676 assert_equal 'text/csv; header=present', response.content_type
708 assert_equal 'text/csv; header=present', response.content_type
677 end
709 end
678
710
679 def test_index_csv
711 def test_index_csv
680 Setting.date_format = '%m/%d/%Y'
712 Setting.date_format = '%m/%d/%Y'
681 get :index, :project_id => 1, :format => 'csv'
713 get :index, :project_id => 1, :format => 'csv'
682 assert_response :success
714 assert_response :success
683 assert_equal 'text/csv; header=present', response.content_type
715 assert_equal 'text/csv; header=present', response.content_type
684 end
716 end
685 end
717 end
General Comments 0
You need to be logged in to leave comments. Login now