##// END OF EJS Templates
Return to section anchor after wiki section edit (#15182)....
Jean-Philippe Lang -
r12009:ba083225b740
parent child
Show More
@@ -1,353 +1,356
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 # The WikiController follows the Rails REST controller pattern but with
18 # The WikiController follows the Rails REST controller pattern but with
19 # a few differences
19 # a few differences
20 #
20 #
21 # * index - shows a list of WikiPages grouped by page or date
21 # * index - shows a list of WikiPages grouped by page or date
22 # * new - not used
22 # * new - not used
23 # * create - not used
23 # * create - not used
24 # * show - will also show the form for creating a new wiki page
24 # * show - will also show the form for creating a new wiki page
25 # * edit - used to edit an existing or new page
25 # * edit - used to edit an existing or new page
26 # * update - used to save a wiki page update to the database, including new pages
26 # * update - used to save a wiki page update to the database, including new pages
27 # * destroy - normal
27 # * destroy - normal
28 #
28 #
29 # Other member and collection methods are also used
29 # Other member and collection methods are also used
30 #
30 #
31 # TODO: still being worked on
31 # TODO: still being worked on
32 class WikiController < ApplicationController
32 class WikiController < ApplicationController
33 default_search_scope :wiki_pages
33 default_search_scope :wiki_pages
34 before_filter :find_wiki, :authorize
34 before_filter :find_wiki, :authorize
35 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
35 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
36 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
36 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
37 accept_api_auth :index, :show, :update, :destroy
37 accept_api_auth :index, :show, :update, :destroy
38 before_filter :find_attachments, :only => [:preview]
38 before_filter :find_attachments, :only => [:preview]
39
39
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :watchers
42 helper :watchers
43 include Redmine::Export::PDF
43 include Redmine::Export::PDF
44
44
45 # List of pages, sorted alphabetically and by parent (hierarchy)
45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 def index
46 def index
47 load_pages_for_index
47 load_pages_for_index
48
48
49 respond_to do |format|
49 respond_to do |format|
50 format.html {
50 format.html {
51 @pages_by_parent_id = @pages.group_by(&:parent_id)
51 @pages_by_parent_id = @pages.group_by(&:parent_id)
52 }
52 }
53 format.api
53 format.api
54 end
54 end
55 end
55 end
56
56
57 # List of page, by last update
57 # List of page, by last update
58 def date_index
58 def date_index
59 load_pages_for_index
59 load_pages_for_index
60 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
60 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
61 end
61 end
62
62
63 # display a page (in editing mode if it doesn't exist)
63 # display a page (in editing mode if it doesn't exist)
64 def show
64 def show
65 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
65 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
66 deny_access
66 deny_access
67 return
67 return
68 end
68 end
69 @content = @page.content_for_version(params[:version])
69 @content = @page.content_for_version(params[:version])
70 if @content.nil?
70 if @content.nil?
71 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
71 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
72 edit
72 edit
73 render :action => 'edit'
73 render :action => 'edit'
74 else
74 else
75 render_404
75 render_404
76 end
76 end
77 return
77 return
78 end
78 end
79 if User.current.allowed_to?(:export_wiki_pages, @project)
79 if User.current.allowed_to?(:export_wiki_pages, @project)
80 if params[:format] == 'pdf'
80 if params[:format] == 'pdf'
81 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
81 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
82 return
82 return
83 elsif params[:format] == 'html'
83 elsif params[:format] == 'html'
84 export = render_to_string :action => 'export', :layout => false
84 export = render_to_string :action => 'export', :layout => false
85 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
85 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
86 return
86 return
87 elsif params[:format] == 'txt'
87 elsif params[:format] == 'txt'
88 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
88 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
89 return
89 return
90 end
90 end
91 end
91 end
92 @editable = editable?
92 @editable = editable?
93 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
93 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
94 @content.current_version? &&
94 @content.current_version? &&
95 Redmine::WikiFormatting.supports_section_edit?
95 Redmine::WikiFormatting.supports_section_edit?
96
96
97 respond_to do |format|
97 respond_to do |format|
98 format.html
98 format.html
99 format.api
99 format.api
100 end
100 end
101 end
101 end
102
102
103 # edit an existing page or a new one
103 # edit an existing page or a new one
104 def edit
104 def edit
105 return render_403 unless editable?
105 return render_403 unless editable?
106 if @page.new_record?
106 if @page.new_record?
107 if params[:parent].present?
107 if params[:parent].present?
108 @page.parent = @page.wiki.find_page(params[:parent].to_s)
108 @page.parent = @page.wiki.find_page(params[:parent].to_s)
109 end
109 end
110 end
110 end
111
111
112 @content = @page.content_for_version(params[:version])
112 @content = @page.content_for_version(params[:version])
113 @content ||= WikiContent.new(:page => @page)
113 @content ||= WikiContent.new(:page => @page)
114 @content.text = initial_page_content(@page) if @content.text.blank?
114 @content.text = initial_page_content(@page) if @content.text.blank?
115 # don't keep previous comment
115 # don't keep previous comment
116 @content.comments = nil
116 @content.comments = nil
117
117
118 # To prevent StaleObjectError exception when reverting to a previous version
118 # To prevent StaleObjectError exception when reverting to a previous version
119 @content.version = @page.content.version if @page.content
119 @content.version = @page.content.version if @page.content
120
120
121 @text = @content.text
121 @text = @content.text
122 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
122 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
123 @section = params[:section].to_i
123 @section = params[:section].to_i
124 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
124 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
125 render_404 if @text.blank?
125 render_404 if @text.blank?
126 end
126 end
127 end
127 end
128
128
129 # Creates a new page or updates an existing one
129 # Creates a new page or updates an existing one
130 def update
130 def update
131 return render_403 unless editable?
131 return render_403 unless editable?
132 was_new_page = @page.new_record?
132 was_new_page = @page.new_record?
133 @page.safe_attributes = params[:wiki_page]
133 @page.safe_attributes = params[:wiki_page]
134
134
135 @content = @page.content || WikiContent.new(:page => @page)
135 @content = @page.content || WikiContent.new(:page => @page)
136 content_params = params[:content]
136 content_params = params[:content]
137 if content_params.nil? && params[:wiki_page].is_a?(Hash)
137 if content_params.nil? && params[:wiki_page].is_a?(Hash)
138 content_params = params[:wiki_page].slice(:text, :comments, :version)
138 content_params = params[:wiki_page].slice(:text, :comments, :version)
139 end
139 end
140 content_params ||= {}
140 content_params ||= {}
141
141
142 @content.comments = content_params[:comments]
142 @content.comments = content_params[:comments]
143 @text = content_params[:text]
143 @text = content_params[:text]
144 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
144 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
145 @section = params[:section].to_i
145 @section = params[:section].to_i
146 @section_hash = params[:section_hash]
146 @section_hash = params[:section_hash]
147 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
147 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(@section, @text, @section_hash)
148 else
148 else
149 @content.version = content_params[:version] if content_params[:version]
149 @content.version = content_params[:version] if content_params[:version]
150 @content.text = @text
150 @content.text = @text
151 end
151 end
152 @content.author = User.current
152 @content.author = User.current
153
153
154 if @page.save_with_content(@content)
154 if @page.save_with_content(@content)
155 attachments = Attachment.attach_files(@page, params[:attachments])
155 attachments = Attachment.attach_files(@page, params[:attachments])
156 render_attachment_warning_if_needed(@page)
156 render_attachment_warning_if_needed(@page)
157 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
157 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
158
158
159 respond_to do |format|
159 respond_to do |format|
160 format.html { redirect_to project_wiki_page_path(@project, @page.title) }
160 format.html {
161 anchor = @section ? "section-#{@section}" : nil
162 redirect_to project_wiki_page_path(@project, @page.title, :anchor => anchor)
163 }
161 format.api {
164 format.api {
162 if was_new_page
165 if was_new_page
163 render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
166 render :action => 'show', :status => :created, :location => project_wiki_page_path(@project, @page.title)
164 else
167 else
165 render_api_ok
168 render_api_ok
166 end
169 end
167 }
170 }
168 end
171 end
169 else
172 else
170 respond_to do |format|
173 respond_to do |format|
171 format.html { render :action => 'edit' }
174 format.html { render :action => 'edit' }
172 format.api { render_validation_errors(@content) }
175 format.api { render_validation_errors(@content) }
173 end
176 end
174 end
177 end
175
178
176 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
179 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
177 # Optimistic locking exception
180 # Optimistic locking exception
178 respond_to do |format|
181 respond_to do |format|
179 format.html {
182 format.html {
180 flash.now[:error] = l(:notice_locking_conflict)
183 flash.now[:error] = l(:notice_locking_conflict)
181 render :action => 'edit'
184 render :action => 'edit'
182 }
185 }
183 format.api { render_api_head :conflict }
186 format.api { render_api_head :conflict }
184 end
187 end
185 rescue ActiveRecord::RecordNotSaved
188 rescue ActiveRecord::RecordNotSaved
186 respond_to do |format|
189 respond_to do |format|
187 format.html { render :action => 'edit' }
190 format.html { render :action => 'edit' }
188 format.api { render_validation_errors(@content) }
191 format.api { render_validation_errors(@content) }
189 end
192 end
190 end
193 end
191
194
192 # rename a page
195 # rename a page
193 def rename
196 def rename
194 return render_403 unless editable?
197 return render_403 unless editable?
195 @page.redirect_existing_links = true
198 @page.redirect_existing_links = true
196 # used to display the *original* title if some AR validation errors occur
199 # used to display the *original* title if some AR validation errors occur
197 @original_title = @page.pretty_title
200 @original_title = @page.pretty_title
198 if request.post? && @page.update_attributes(params[:wiki_page])
201 if request.post? && @page.update_attributes(params[:wiki_page])
199 flash[:notice] = l(:notice_successful_update)
202 flash[:notice] = l(:notice_successful_update)
200 redirect_to project_wiki_page_path(@project, @page.title)
203 redirect_to project_wiki_page_path(@project, @page.title)
201 end
204 end
202 end
205 end
203
206
204 def protect
207 def protect
205 @page.update_attribute :protected, params[:protected]
208 @page.update_attribute :protected, params[:protected]
206 redirect_to project_wiki_page_path(@project, @page.title)
209 redirect_to project_wiki_page_path(@project, @page.title)
207 end
210 end
208
211
209 # show page history
212 # show page history
210 def history
213 def history
211 @version_count = @page.content.versions.count
214 @version_count = @page.content.versions.count
212 @version_pages = Paginator.new @version_count, per_page_option, params['page']
215 @version_pages = Paginator.new @version_count, per_page_option, params['page']
213 # don't load text
216 # don't load text
214 @versions = @page.content.versions.
217 @versions = @page.content.versions.
215 select("id, author_id, comments, updated_on, version").
218 select("id, author_id, comments, updated_on, version").
216 reorder('version DESC').
219 reorder('version DESC').
217 limit(@version_pages.per_page + 1).
220 limit(@version_pages.per_page + 1).
218 offset(@version_pages.offset).
221 offset(@version_pages.offset).
219 all
222 all
220
223
221 render :layout => false if request.xhr?
224 render :layout => false if request.xhr?
222 end
225 end
223
226
224 def diff
227 def diff
225 @diff = @page.diff(params[:version], params[:version_from])
228 @diff = @page.diff(params[:version], params[:version_from])
226 render_404 unless @diff
229 render_404 unless @diff
227 end
230 end
228
231
229 def annotate
232 def annotate
230 @annotate = @page.annotate(params[:version])
233 @annotate = @page.annotate(params[:version])
231 render_404 unless @annotate
234 render_404 unless @annotate
232 end
235 end
233
236
234 # Removes a wiki page and its history
237 # Removes a wiki page and its history
235 # Children can be either set as root pages, removed or reassigned to another parent page
238 # Children can be either set as root pages, removed or reassigned to another parent page
236 def destroy
239 def destroy
237 return render_403 unless editable?
240 return render_403 unless editable?
238
241
239 @descendants_count = @page.descendants.size
242 @descendants_count = @page.descendants.size
240 if @descendants_count > 0
243 if @descendants_count > 0
241 case params[:todo]
244 case params[:todo]
242 when 'nullify'
245 when 'nullify'
243 # Nothing to do
246 # Nothing to do
244 when 'destroy'
247 when 'destroy'
245 # Removes all its descendants
248 # Removes all its descendants
246 @page.descendants.each(&:destroy)
249 @page.descendants.each(&:destroy)
247 when 'reassign'
250 when 'reassign'
248 # Reassign children to another parent page
251 # Reassign children to another parent page
249 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
252 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
250 return unless reassign_to
253 return unless reassign_to
251 @page.children.each do |child|
254 @page.children.each do |child|
252 child.update_attribute(:parent, reassign_to)
255 child.update_attribute(:parent, reassign_to)
253 end
256 end
254 else
257 else
255 @reassignable_to = @wiki.pages - @page.self_and_descendants
258 @reassignable_to = @wiki.pages - @page.self_and_descendants
256 # display the destroy form if it's a user request
259 # display the destroy form if it's a user request
257 return unless api_request?
260 return unless api_request?
258 end
261 end
259 end
262 end
260 @page.destroy
263 @page.destroy
261 respond_to do |format|
264 respond_to do |format|
262 format.html { redirect_to project_wiki_index_path(@project) }
265 format.html { redirect_to project_wiki_index_path(@project) }
263 format.api { render_api_ok }
266 format.api { render_api_ok }
264 end
267 end
265 end
268 end
266
269
267 def destroy_version
270 def destroy_version
268 return render_403 unless editable?
271 return render_403 unless editable?
269
272
270 @content = @page.content_for_version(params[:version])
273 @content = @page.content_for_version(params[:version])
271 @content.destroy
274 @content.destroy
272 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
275 redirect_to_referer_or history_project_wiki_page_path(@project, @page.title)
273 end
276 end
274
277
275 # Export wiki to a single pdf or html file
278 # Export wiki to a single pdf or html file
276 def export
279 def export
277 @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
280 @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}])
278 respond_to do |format|
281 respond_to do |format|
279 format.html {
282 format.html {
280 export = render_to_string :action => 'export_multiple', :layout => false
283 export = render_to_string :action => 'export_multiple', :layout => false
281 send_data(export, :type => 'text/html', :filename => "wiki.html")
284 send_data(export, :type => 'text/html', :filename => "wiki.html")
282 }
285 }
283 format.pdf {
286 format.pdf {
284 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
287 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
285 }
288 }
286 end
289 end
287 end
290 end
288
291
289 def preview
292 def preview
290 page = @wiki.find_page(params[:id])
293 page = @wiki.find_page(params[:id])
291 # page is nil when previewing a new page
294 # page is nil when previewing a new page
292 return render_403 unless page.nil? || editable?(page)
295 return render_403 unless page.nil? || editable?(page)
293 if page
296 if page
294 @attachments += page.attachments
297 @attachments += page.attachments
295 @previewed = page.content
298 @previewed = page.content
296 end
299 end
297 @text = params[:content][:text]
300 @text = params[:content][:text]
298 render :partial => 'common/preview'
301 render :partial => 'common/preview'
299 end
302 end
300
303
301 def add_attachment
304 def add_attachment
302 return render_403 unless editable?
305 return render_403 unless editable?
303 attachments = Attachment.attach_files(@page, params[:attachments])
306 attachments = Attachment.attach_files(@page, params[:attachments])
304 render_attachment_warning_if_needed(@page)
307 render_attachment_warning_if_needed(@page)
305 redirect_to :action => 'show', :id => @page.title, :project_id => @project
308 redirect_to :action => 'show', :id => @page.title, :project_id => @project
306 end
309 end
307
310
308 private
311 private
309
312
310 def find_wiki
313 def find_wiki
311 @project = Project.find(params[:project_id])
314 @project = Project.find(params[:project_id])
312 @wiki = @project.wiki
315 @wiki = @project.wiki
313 render_404 unless @wiki
316 render_404 unless @wiki
314 rescue ActiveRecord::RecordNotFound
317 rescue ActiveRecord::RecordNotFound
315 render_404
318 render_404
316 end
319 end
317
320
318 # Finds the requested page or a new page if it doesn't exist
321 # Finds the requested page or a new page if it doesn't exist
319 def find_existing_or_new_page
322 def find_existing_or_new_page
320 @page = @wiki.find_or_new_page(params[:id])
323 @page = @wiki.find_or_new_page(params[:id])
321 if @wiki.page_found_with_redirect?
324 if @wiki.page_found_with_redirect?
322 redirect_to params.update(:id => @page.title)
325 redirect_to params.update(:id => @page.title)
323 end
326 end
324 end
327 end
325
328
326 # Finds the requested page and returns a 404 error if it doesn't exist
329 # Finds the requested page and returns a 404 error if it doesn't exist
327 def find_existing_page
330 def find_existing_page
328 @page = @wiki.find_page(params[:id])
331 @page = @wiki.find_page(params[:id])
329 if @page.nil?
332 if @page.nil?
330 render_404
333 render_404
331 return
334 return
332 end
335 end
333 if @wiki.page_found_with_redirect?
336 if @wiki.page_found_with_redirect?
334 redirect_to params.update(:id => @page.title)
337 redirect_to params.update(:id => @page.title)
335 end
338 end
336 end
339 end
337
340
338 # Returns true if the current user is allowed to edit the page, otherwise false
341 # Returns true if the current user is allowed to edit the page, otherwise false
339 def editable?(page = @page)
342 def editable?(page = @page)
340 page.editable_by?(User.current)
343 page.editable_by?(User.current)
341 end
344 end
342
345
343 # Returns the default content of a new wiki page
346 # Returns the default content of a new wiki page
344 def initial_page_content(page)
347 def initial_page_content(page)
345 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
348 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
346 extend helper unless self.instance_of?(helper)
349 extend helper unless self.instance_of?(helper)
347 helper.instance_method(:initial_page_content).bind(self).call(page)
350 helper.instance_method(:initial_page_content).bind(self).call(page)
348 end
351 end
349
352
350 def load_pages_for_index
353 def load_pages_for_index
351 @pages = @wiki.pages.with_updated_on.reorder("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
354 @pages = @wiki.pages.with_updated_on.reorder("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
352 end
355 end
353 end
356 end
@@ -1,1273 +1,1274
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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 = truncate(issue.subject, :length => 60)
75 title = truncate(issue.subject, :length => 60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if options[:truncate]
78 if options[:truncate]
79 subject = truncate(subject, :length => options[:truncate])
79 subject = truncate(subject, :length => options[:truncate])
80 end
80 end
81 end
81 end
82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
82 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
83 s << h(": #{subject}") if subject
83 s << h(": #{subject}") if subject
84 s = h("#{issue.project} - ") + s if options[:project]
84 s = h("#{issue.project} - ") + s if options[:project]
85 s
85 s
86 end
86 end
87
87
88 # Generates a link to an attachment.
88 # Generates a link to an attachment.
89 # Options:
89 # Options:
90 # * :text - Link text (default to attachment filename)
90 # * :text - Link text (default to attachment filename)
91 # * :download - Force download (default: false)
91 # * :download - Force download (default: false)
92 def link_to_attachment(attachment, options={})
92 def link_to_attachment(attachment, options={})
93 text = options.delete(:text) || attachment.filename
93 text = options.delete(:text) || attachment.filename
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
94 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
95 html_options = options.slice!(:only_path)
95 html_options = options.slice!(:only_path)
96 url = send(route_method, attachment, attachment.filename, options)
96 url = send(route_method, attachment, attachment.filename, options)
97 link_to text, url, html_options
97 link_to text, url, html_options
98 end
98 end
99
99
100 # Generates a link to a SCM revision
100 # Generates a link to a SCM revision
101 # Options:
101 # Options:
102 # * :text - Link text (default to the formatted revision)
102 # * :text - Link text (default to the formatted revision)
103 def link_to_revision(revision, repository, options={})
103 def link_to_revision(revision, repository, options={})
104 if repository.is_a?(Project)
104 if repository.is_a?(Project)
105 repository = repository.repository
105 repository = repository.repository
106 end
106 end
107 text = options.delete(:text) || format_revision(revision)
107 text = options.delete(:text) || format_revision(revision)
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
109 link_to(
109 link_to(
110 h(text),
110 h(text),
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
111 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
112 :title => l(:label_revision_id, format_revision(revision))
112 :title => l(:label_revision_id, format_revision(revision))
113 )
113 )
114 end
114 end
115
115
116 # Generates a link to a message
116 # Generates a link to a message
117 def link_to_message(message, options={}, html_options = nil)
117 def link_to_message(message, options={}, html_options = nil)
118 link_to(
118 link_to(
119 truncate(message.subject, :length => 60),
119 truncate(message.subject, :length => 60),
120 board_message_path(message.board_id, message.parent_id || message.id, {
120 board_message_path(message.board_id, message.parent_id || message.id, {
121 :r => (message.parent_id && message.id),
121 :r => (message.parent_id && message.id),
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123 }.merge(options)),
123 }.merge(options)),
124 html_options
124 html_options
125 )
125 )
126 end
126 end
127
127
128 # Generates a link to a project if active
128 # Generates a link to a project if active
129 # Examples:
129 # Examples:
130 #
130 #
131 # link_to_project(project) # => link to the specified project overview
131 # link_to_project(project) # => link to the specified project overview
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 #
134 #
135 def link_to_project(project, options={}, html_options = nil)
135 def link_to_project(project, options={}, html_options = nil)
136 if project.archived?
136 if project.archived?
137 h(project.name)
137 h(project.name)
138 elsif options.key?(:action)
138 elsif options.key?(:action)
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
139 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
140 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 link_to project.name, url, html_options
141 link_to project.name, url, html_options
142 else
142 else
143 link_to project.name, project_path(project, options), html_options
143 link_to project.name, project_path(project, options), html_options
144 end
144 end
145 end
145 end
146
146
147 # Generates a link to a project settings if active
147 # Generates a link to a project settings if active
148 def link_to_project_settings(project, options={}, html_options=nil)
148 def link_to_project_settings(project, options={}, html_options=nil)
149 if project.active?
149 if project.active?
150 link_to project.name, settings_project_path(project, options), html_options
150 link_to project.name, settings_project_path(project, options), html_options
151 elsif project.archived?
151 elsif project.archived?
152 h(project.name)
152 h(project.name)
153 else
153 else
154 link_to project.name, project_path(project, options), html_options
154 link_to project.name, project_path(project, options), html_options
155 end
155 end
156 end
156 end
157
157
158 def wiki_page_path(page, options={})
158 def wiki_page_path(page, options={})
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
159 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
160 end
160 end
161
161
162 def thumbnail_tag(attachment)
162 def thumbnail_tag(attachment)
163 link_to image_tag(thumbnail_path(attachment)),
163 link_to image_tag(thumbnail_path(attachment)),
164 named_attachment_path(attachment, attachment.filename),
164 named_attachment_path(attachment, attachment.filename),
165 :title => attachment.filename
165 :title => attachment.filename
166 end
166 end
167
167
168 def toggle_link(name, id, options={})
168 def toggle_link(name, id, options={})
169 onclick = "$('##{id}').toggle(); "
169 onclick = "$('##{id}').toggle(); "
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
170 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
171 onclick << "return false;"
171 onclick << "return false;"
172 link_to(name, "#", :onclick => onclick)
172 link_to(name, "#", :onclick => onclick)
173 end
173 end
174
174
175 def image_to_function(name, function, html_options = {})
175 def image_to_function(name, function, html_options = {})
176 html_options.symbolize_keys!
176 html_options.symbolize_keys!
177 tag(:input, html_options.merge({
177 tag(:input, html_options.merge({
178 :type => "image", :src => image_path(name),
178 :type => "image", :src => image_path(name),
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
179 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 }))
180 }))
181 end
181 end
182
182
183 def format_activity_title(text)
183 def format_activity_title(text)
184 h(truncate_single_line(text, :length => 100))
184 h(truncate_single_line(text, :length => 100))
185 end
185 end
186
186
187 def format_activity_day(date)
187 def format_activity_day(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
188 date == User.current.today ? l(:label_today).titleize : format_date(date)
189 end
189 end
190
190
191 def format_activity_description(text)
191 def format_activity_description(text)
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
192 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
193 ).gsub(/[\r\n]+/, "<br />").html_safe
193 ).gsub(/[\r\n]+/, "<br />").html_safe
194 end
194 end
195
195
196 def format_version_name(version)
196 def format_version_name(version)
197 if version.project == @project
197 if version.project == @project
198 h(version)
198 h(version)
199 else
199 else
200 h("#{version.project} - #{version}")
200 h("#{version.project} - #{version}")
201 end
201 end
202 end
202 end
203
203
204 def due_date_distance_in_words(date)
204 def due_date_distance_in_words(date)
205 if date
205 if date
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
206 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
207 end
207 end
208 end
208 end
209
209
210 # Renders a tree of projects as a nested set of unordered lists
210 # Renders a tree of projects as a nested set of unordered lists
211 # The given collection may be a subset of the whole project tree
211 # The given collection may be a subset of the whole project tree
212 # (eg. some intermediate nodes are private and can not be seen)
212 # (eg. some intermediate nodes are private and can not be seen)
213 def render_project_nested_lists(projects)
213 def render_project_nested_lists(projects)
214 s = ''
214 s = ''
215 if projects.any?
215 if projects.any?
216 ancestors = []
216 ancestors = []
217 original_project = @project
217 original_project = @project
218 projects.sort_by(&:lft).each do |project|
218 projects.sort_by(&:lft).each do |project|
219 # set the project environment to please macros.
219 # set the project environment to please macros.
220 @project = project
220 @project = project
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
222 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
223 else
223 else
224 ancestors.pop
224 ancestors.pop
225 s << "</li>"
225 s << "</li>"
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
227 ancestors.pop
227 ancestors.pop
228 s << "</ul></li>\n"
228 s << "</ul></li>\n"
229 end
229 end
230 end
230 end
231 classes = (ancestors.empty? ? 'root' : 'child')
231 classes = (ancestors.empty? ? 'root' : 'child')
232 s << "<li class='#{classes}'><div class='#{classes}'>"
232 s << "<li class='#{classes}'><div class='#{classes}'>"
233 s << h(block_given? ? yield(project) : project.name)
233 s << h(block_given? ? yield(project) : project.name)
234 s << "</div>\n"
234 s << "</div>\n"
235 ancestors << project
235 ancestors << project
236 end
236 end
237 s << ("</li></ul>\n" * ancestors.size)
237 s << ("</li></ul>\n" * ancestors.size)
238 @project = original_project
238 @project = original_project
239 end
239 end
240 s.html_safe
240 s.html_safe
241 end
241 end
242
242
243 def render_page_hierarchy(pages, node=nil, options={})
243 def render_page_hierarchy(pages, node=nil, options={})
244 content = ''
244 content = ''
245 if pages[node]
245 if pages[node]
246 content << "<ul class=\"pages-hierarchy\">\n"
246 content << "<ul class=\"pages-hierarchy\">\n"
247 pages[node].each do |page|
247 pages[node].each do |page|
248 content << "<li>"
248 content << "<li>"
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
249 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
250 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
251 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
252 content << "</li>\n"
252 content << "</li>\n"
253 end
253 end
254 content << "</ul>\n"
254 content << "</ul>\n"
255 end
255 end
256 content.html_safe
256 content.html_safe
257 end
257 end
258
258
259 # Renders flash messages
259 # Renders flash messages
260 def render_flash_messages
260 def render_flash_messages
261 s = ''
261 s = ''
262 flash.each do |k,v|
262 flash.each do |k,v|
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
263 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264 end
264 end
265 s.html_safe
265 s.html_safe
266 end
266 end
267
267
268 # Renders tabs and their content
268 # Renders tabs and their content
269 def render_tabs(tabs)
269 def render_tabs(tabs)
270 if tabs.any?
270 if tabs.any?
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
271 render :partial => 'common/tabs', :locals => {:tabs => tabs}
272 else
272 else
273 content_tag 'p', l(:label_no_data), :class => "nodata"
273 content_tag 'p', l(:label_no_data), :class => "nodata"
274 end
274 end
275 end
275 end
276
276
277 # Renders the project quick-jump box
277 # Renders the project quick-jump box
278 def render_project_jump_box
278 def render_project_jump_box
279 return unless User.current.logged?
279 return unless User.current.logged?
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
280 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281 if projects.any?
281 if projects.any?
282 options =
282 options =
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
283 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
284 '<option value="" disabled="disabled">---</option>').html_safe
284 '<option value="" disabled="disabled">---</option>').html_safe
285
285
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
286 options << project_tree_options_for_select(projects, :selected => @project) do |p|
287 { :value => project_path(:id => p, :jump => current_menu_item) }
287 { :value => project_path(:id => p, :jump => current_menu_item) }
288 end
288 end
289
289
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
290 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 end
291 end
292 end
292 end
293
293
294 def project_tree_options_for_select(projects, options = {})
294 def project_tree_options_for_select(projects, options = {})
295 s = ''
295 s = ''
296 project_tree(projects) do |project, level|
296 project_tree(projects) do |project, level|
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
297 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 tag_options = {:value => project.id}
298 tag_options = {:value => project.id}
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
299 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
300 tag_options[:selected] = 'selected'
300 tag_options[:selected] = 'selected'
301 else
301 else
302 tag_options[:selected] = nil
302 tag_options[:selected] = nil
303 end
303 end
304 tag_options.merge!(yield(project)) if block_given?
304 tag_options.merge!(yield(project)) if block_given?
305 s << content_tag('option', name_prefix + h(project), tag_options)
305 s << content_tag('option', name_prefix + h(project), tag_options)
306 end
306 end
307 s.html_safe
307 s.html_safe
308 end
308 end
309
309
310 # Yields the given block for each project with its level in the tree
310 # Yields the given block for each project with its level in the tree
311 #
311 #
312 # Wrapper for Project#project_tree
312 # Wrapper for Project#project_tree
313 def project_tree(projects, &block)
313 def project_tree(projects, &block)
314 Project.project_tree(projects, &block)
314 Project.project_tree(projects, &block)
315 end
315 end
316
316
317 def principals_check_box_tags(name, principals)
317 def principals_check_box_tags(name, principals)
318 s = ''
318 s = ''
319 principals.each do |principal|
319 principals.each do |principal|
320 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
320 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
321 end
321 end
322 s.html_safe
322 s.html_safe
323 end
323 end
324
324
325 # Returns a string for users/groups option tags
325 # Returns a string for users/groups option tags
326 def principals_options_for_select(collection, selected=nil)
326 def principals_options_for_select(collection, selected=nil)
327 s = ''
327 s = ''
328 if collection.include?(User.current)
328 if collection.include?(User.current)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
329 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330 end
330 end
331 groups = ''
331 groups = ''
332 collection.sort.each do |element|
332 collection.sort.each do |element|
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
333 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
334 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
335 end
335 end
336 unless groups.empty?
336 unless groups.empty?
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
337 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
338 end
338 end
339 s.html_safe
339 s.html_safe
340 end
340 end
341
341
342 # Options for the new membership projects combo-box
342 # Options for the new membership projects combo-box
343 def options_for_membership_project_select(principal, projects)
343 def options_for_membership_project_select(principal, projects)
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
344 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
345 options << project_tree_options_for_select(projects) do |p|
345 options << project_tree_options_for_select(projects) do |p|
346 {:disabled => principal.projects.to_a.include?(p)}
346 {:disabled => principal.projects.to_a.include?(p)}
347 end
347 end
348 options
348 options
349 end
349 end
350
350
351 def option_tag(name, text, value, selected=nil, options={})
351 def option_tag(name, text, value, selected=nil, options={})
352 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
352 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
353 end
353 end
354
354
355 # Truncates and returns the string as a single line
355 # Truncates and returns the string as a single line
356 def truncate_single_line(string, *args)
356 def truncate_single_line(string, *args)
357 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
357 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
358 end
358 end
359
359
360 # Truncates at line break after 250 characters or options[:length]
360 # Truncates at line break after 250 characters or options[:length]
361 def truncate_lines(string, options={})
361 def truncate_lines(string, options={})
362 length = options[:length] || 250
362 length = options[:length] || 250
363 if string.to_s =~ /\A(.{#{length}}.*?)$/m
363 if string.to_s =~ /\A(.{#{length}}.*?)$/m
364 "#{$1}..."
364 "#{$1}..."
365 else
365 else
366 string
366 string
367 end
367 end
368 end
368 end
369
369
370 def anchor(text)
370 def anchor(text)
371 text.to_s.gsub(' ', '_')
371 text.to_s.gsub(' ', '_')
372 end
372 end
373
373
374 def html_hours(text)
374 def html_hours(text)
375 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
375 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
376 end
376 end
377
377
378 def authoring(created, author, options={})
378 def authoring(created, author, options={})
379 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
379 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
380 end
380 end
381
381
382 def time_tag(time)
382 def time_tag(time)
383 text = distance_of_time_in_words(Time.now, time)
383 text = distance_of_time_in_words(Time.now, time)
384 if @project
384 if @project
385 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
385 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
386 else
386 else
387 content_tag('abbr', text, :title => format_time(time))
387 content_tag('abbr', text, :title => format_time(time))
388 end
388 end
389 end
389 end
390
390
391 def syntax_highlight_lines(name, content)
391 def syntax_highlight_lines(name, content)
392 lines = []
392 lines = []
393 syntax_highlight(name, content).each_line { |line| lines << line }
393 syntax_highlight(name, content).each_line { |line| lines << line }
394 lines
394 lines
395 end
395 end
396
396
397 def syntax_highlight(name, content)
397 def syntax_highlight(name, content)
398 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
398 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
399 end
399 end
400
400
401 def to_path_param(path)
401 def to_path_param(path)
402 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
402 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
403 str.blank? ? nil : str
403 str.blank? ? nil : str
404 end
404 end
405
405
406 def reorder_links(name, url, method = :post)
406 def reorder_links(name, url, method = :post)
407 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
407 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
408 url.merge({"#{name}[move_to]" => 'highest'}),
408 url.merge({"#{name}[move_to]" => 'highest'}),
409 :method => method, :title => l(:label_sort_highest)) +
409 :method => method, :title => l(:label_sort_highest)) +
410 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
410 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
411 url.merge({"#{name}[move_to]" => 'higher'}),
411 url.merge({"#{name}[move_to]" => 'higher'}),
412 :method => method, :title => l(:label_sort_higher)) +
412 :method => method, :title => l(:label_sort_higher)) +
413 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
413 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
414 url.merge({"#{name}[move_to]" => 'lower'}),
414 url.merge({"#{name}[move_to]" => 'lower'}),
415 :method => method, :title => l(:label_sort_lower)) +
415 :method => method, :title => l(:label_sort_lower)) +
416 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
416 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
417 url.merge({"#{name}[move_to]" => 'lowest'}),
417 url.merge({"#{name}[move_to]" => 'lowest'}),
418 :method => method, :title => l(:label_sort_lowest))
418 :method => method, :title => l(:label_sort_lowest))
419 end
419 end
420
420
421 def breadcrumb(*args)
421 def breadcrumb(*args)
422 elements = args.flatten
422 elements = args.flatten
423 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
423 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424 end
424 end
425
425
426 def other_formats_links(&block)
426 def other_formats_links(&block)
427 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
427 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
428 yield Redmine::Views::OtherFormatsBuilder.new(self)
428 yield Redmine::Views::OtherFormatsBuilder.new(self)
429 concat('</p>'.html_safe)
429 concat('</p>'.html_safe)
430 end
430 end
431
431
432 def page_header_title
432 def page_header_title
433 if @project.nil? || @project.new_record?
433 if @project.nil? || @project.new_record?
434 h(Setting.app_title)
434 h(Setting.app_title)
435 else
435 else
436 b = []
436 b = []
437 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
437 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
438 if ancestors.any?
438 if ancestors.any?
439 root = ancestors.shift
439 root = ancestors.shift
440 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
440 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
441 if ancestors.size > 2
441 if ancestors.size > 2
442 b << "\xe2\x80\xa6"
442 b << "\xe2\x80\xa6"
443 ancestors = ancestors[-2, 2]
443 ancestors = ancestors[-2, 2]
444 end
444 end
445 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
445 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
446 end
446 end
447 b << h(@project)
447 b << h(@project)
448 b.join(" \xc2\xbb ").html_safe
448 b.join(" \xc2\xbb ").html_safe
449 end
449 end
450 end
450 end
451
451
452 # Returns a h2 tag and sets the html title with the given arguments
452 # Returns a h2 tag and sets the html title with the given arguments
453 def title(*args)
453 def title(*args)
454 strings = args.map do |arg|
454 strings = args.map do |arg|
455 if arg.is_a?(Array) && arg.size >= 2
455 if arg.is_a?(Array) && arg.size >= 2
456 link_to(*arg)
456 link_to(*arg)
457 else
457 else
458 h(arg.to_s)
458 h(arg.to_s)
459 end
459 end
460 end
460 end
461 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
461 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
462 content_tag('h2', strings.join(' &#187; ').html_safe)
462 content_tag('h2', strings.join(' &#187; ').html_safe)
463 end
463 end
464
464
465 # Sets the html title
465 # Sets the html title
466 # Returns the html title when called without arguments
466 # Returns the html title when called without arguments
467 # Current project name and app_title and automatically appended
467 # Current project name and app_title and automatically appended
468 # Exemples:
468 # Exemples:
469 # html_title 'Foo', 'Bar'
469 # html_title 'Foo', 'Bar'
470 # html_title # => 'Foo - Bar - My Project - Redmine'
470 # html_title # => 'Foo - Bar - My Project - Redmine'
471 def html_title(*args)
471 def html_title(*args)
472 if args.empty?
472 if args.empty?
473 title = @html_title || []
473 title = @html_title || []
474 title << @project.name if @project
474 title << @project.name if @project
475 title << Setting.app_title unless Setting.app_title == title.last
475 title << Setting.app_title unless Setting.app_title == title.last
476 title.reject(&:blank?).join(' - ')
476 title.reject(&:blank?).join(' - ')
477 else
477 else
478 @html_title ||= []
478 @html_title ||= []
479 @html_title += args
479 @html_title += args
480 end
480 end
481 end
481 end
482
482
483 # Returns the theme, controller name, and action as css classes for the
483 # Returns the theme, controller name, and action as css classes for the
484 # HTML body.
484 # HTML body.
485 def body_css_classes
485 def body_css_classes
486 css = []
486 css = []
487 if theme = Redmine::Themes.theme(Setting.ui_theme)
487 if theme = Redmine::Themes.theme(Setting.ui_theme)
488 css << 'theme-' + theme.name
488 css << 'theme-' + theme.name
489 end
489 end
490
490
491 css << 'project-' + @project.identifier if @project && @project.identifier.present?
491 css << 'project-' + @project.identifier if @project && @project.identifier.present?
492 css << 'controller-' + controller_name
492 css << 'controller-' + controller_name
493 css << 'action-' + action_name
493 css << 'action-' + action_name
494 css.join(' ')
494 css.join(' ')
495 end
495 end
496
496
497 def accesskey(s)
497 def accesskey(s)
498 @used_accesskeys ||= []
498 @used_accesskeys ||= []
499 key = Redmine::AccessKeys.key_for(s)
499 key = Redmine::AccessKeys.key_for(s)
500 return nil if @used_accesskeys.include?(key)
500 return nil if @used_accesskeys.include?(key)
501 @used_accesskeys << key
501 @used_accesskeys << key
502 key
502 key
503 end
503 end
504
504
505 # Formats text according to system settings.
505 # Formats text according to system settings.
506 # 2 ways to call this method:
506 # 2 ways to call this method:
507 # * with a String: textilizable(text, options)
507 # * with a String: textilizable(text, options)
508 # * with an object and one of its attribute: textilizable(issue, :description, options)
508 # * with an object and one of its attribute: textilizable(issue, :description, options)
509 def textilizable(*args)
509 def textilizable(*args)
510 options = args.last.is_a?(Hash) ? args.pop : {}
510 options = args.last.is_a?(Hash) ? args.pop : {}
511 case args.size
511 case args.size
512 when 1
512 when 1
513 obj = options[:object]
513 obj = options[:object]
514 text = args.shift
514 text = args.shift
515 when 2
515 when 2
516 obj = args.shift
516 obj = args.shift
517 attr = args.shift
517 attr = args.shift
518 text = obj.send(attr).to_s
518 text = obj.send(attr).to_s
519 else
519 else
520 raise ArgumentError, 'invalid arguments to textilizable'
520 raise ArgumentError, 'invalid arguments to textilizable'
521 end
521 end
522 return '' if text.blank?
522 return '' if text.blank?
523 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
523 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
524 only_path = options.delete(:only_path) == false ? false : true
524 only_path = options.delete(:only_path) == false ? false : true
525
525
526 text = text.dup
526 text = text.dup
527 macros = catch_macros(text)
527 macros = catch_macros(text)
528 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
528 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
529
529
530 @parsed_headings = []
530 @parsed_headings = []
531 @heading_anchors = {}
531 @heading_anchors = {}
532 @current_section = 0 if options[:edit_section_links]
532 @current_section = 0 if options[:edit_section_links]
533
533
534 parse_sections(text, project, obj, attr, only_path, options)
534 parse_sections(text, project, obj, attr, only_path, options)
535 text = parse_non_pre_blocks(text, obj, macros) do |text|
535 text = parse_non_pre_blocks(text, obj, macros) do |text|
536 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
536 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
537 send method_name, text, project, obj, attr, only_path, options
537 send method_name, text, project, obj, attr, only_path, options
538 end
538 end
539 end
539 end
540 parse_headings(text, project, obj, attr, only_path, options)
540 parse_headings(text, project, obj, attr, only_path, options)
541
541
542 if @parsed_headings.any?
542 if @parsed_headings.any?
543 replace_toc(text, @parsed_headings)
543 replace_toc(text, @parsed_headings)
544 end
544 end
545
545
546 text.html_safe
546 text.html_safe
547 end
547 end
548
548
549 def parse_non_pre_blocks(text, obj, macros)
549 def parse_non_pre_blocks(text, obj, macros)
550 s = StringScanner.new(text)
550 s = StringScanner.new(text)
551 tags = []
551 tags = []
552 parsed = ''
552 parsed = ''
553 while !s.eos?
553 while !s.eos?
554 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
554 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
555 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
555 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
556 if tags.empty?
556 if tags.empty?
557 yield text
557 yield text
558 inject_macros(text, obj, macros) if macros.any?
558 inject_macros(text, obj, macros) if macros.any?
559 else
559 else
560 inject_macros(text, obj, macros, false) if macros.any?
560 inject_macros(text, obj, macros, false) if macros.any?
561 end
561 end
562 parsed << text
562 parsed << text
563 if tag
563 if tag
564 if closing
564 if closing
565 if tags.last == tag.downcase
565 if tags.last == tag.downcase
566 tags.pop
566 tags.pop
567 end
567 end
568 else
568 else
569 tags << tag.downcase
569 tags << tag.downcase
570 end
570 end
571 parsed << full_tag
571 parsed << full_tag
572 end
572 end
573 end
573 end
574 # Close any non closing tags
574 # Close any non closing tags
575 while tag = tags.pop
575 while tag = tags.pop
576 parsed << "</#{tag}>"
576 parsed << "</#{tag}>"
577 end
577 end
578 parsed
578 parsed
579 end
579 end
580
580
581 def parse_inline_attachments(text, project, obj, attr, only_path, options)
581 def parse_inline_attachments(text, project, obj, attr, only_path, options)
582 # when using an image link, try to use an attachment, if possible
582 # when using an image link, try to use an attachment, if possible
583 attachments = options[:attachments] || []
583 attachments = options[:attachments] || []
584 attachments += obj.attachments if obj.respond_to?(:attachments)
584 attachments += obj.attachments if obj.respond_to?(:attachments)
585 if attachments.present?
585 if attachments.present?
586 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
586 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
587 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
587 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
588 # search for the picture in attachments
588 # search for the picture in attachments
589 if found = Attachment.latest_attach(attachments, filename)
589 if found = Attachment.latest_attach(attachments, filename)
590 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
590 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
591 desc = found.description.to_s.gsub('"', '')
591 desc = found.description.to_s.gsub('"', '')
592 if !desc.blank? && alttext.blank?
592 if !desc.blank? && alttext.blank?
593 alt = " title=\"#{desc}\" alt=\"#{desc}\""
593 alt = " title=\"#{desc}\" alt=\"#{desc}\""
594 end
594 end
595 "src=\"#{image_url}\"#{alt}"
595 "src=\"#{image_url}\"#{alt}"
596 else
596 else
597 m
597 m
598 end
598 end
599 end
599 end
600 end
600 end
601 end
601 end
602
602
603 # Wiki links
603 # Wiki links
604 #
604 #
605 # Examples:
605 # Examples:
606 # [[mypage]]
606 # [[mypage]]
607 # [[mypage|mytext]]
607 # [[mypage|mytext]]
608 # wiki links can refer other project wikis, using project name or identifier:
608 # wiki links can refer other project wikis, using project name or identifier:
609 # [[project:]] -> wiki starting page
609 # [[project:]] -> wiki starting page
610 # [[project:|mytext]]
610 # [[project:|mytext]]
611 # [[project:mypage]]
611 # [[project:mypage]]
612 # [[project:mypage|mytext]]
612 # [[project:mypage|mytext]]
613 def parse_wiki_links(text, project, obj, attr, only_path, options)
613 def parse_wiki_links(text, project, obj, attr, only_path, options)
614 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
614 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
615 link_project = project
615 link_project = project
616 esc, all, page, title = $1, $2, $3, $5
616 esc, all, page, title = $1, $2, $3, $5
617 if esc.nil?
617 if esc.nil?
618 if page =~ /^([^\:]+)\:(.*)$/
618 if page =~ /^([^\:]+)\:(.*)$/
619 identifier, page = $1, $2
619 identifier, page = $1, $2
620 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
620 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
621 title ||= identifier if page.blank?
621 title ||= identifier if page.blank?
622 end
622 end
623
623
624 if link_project && link_project.wiki
624 if link_project && link_project.wiki
625 # extract anchor
625 # extract anchor
626 anchor = nil
626 anchor = nil
627 if page =~ /^(.+?)\#(.+)$/
627 if page =~ /^(.+?)\#(.+)$/
628 page, anchor = $1, $2
628 page, anchor = $1, $2
629 end
629 end
630 anchor = sanitize_anchor_name(anchor) if anchor.present?
630 anchor = sanitize_anchor_name(anchor) if anchor.present?
631 # check if page exists
631 # check if page exists
632 wiki_page = link_project.wiki.find_page(page)
632 wiki_page = link_project.wiki.find_page(page)
633 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
633 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
634 "##{anchor}"
634 "##{anchor}"
635 else
635 else
636 case options[:wiki_links]
636 case options[:wiki_links]
637 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
637 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
638 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
638 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
639 else
639 else
640 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
640 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
641 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
641 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
642 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
642 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
643 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
643 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
644 end
644 end
645 end
645 end
646 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
646 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
647 else
647 else
648 # project or wiki doesn't exist
648 # project or wiki doesn't exist
649 all
649 all
650 end
650 end
651 else
651 else
652 all
652 all
653 end
653 end
654 end
654 end
655 end
655 end
656
656
657 # Redmine links
657 # Redmine links
658 #
658 #
659 # Examples:
659 # Examples:
660 # Issues:
660 # Issues:
661 # #52 -> Link to issue #52
661 # #52 -> Link to issue #52
662 # Changesets:
662 # Changesets:
663 # r52 -> Link to revision 52
663 # r52 -> Link to revision 52
664 # commit:a85130f -> Link to scmid starting with a85130f
664 # commit:a85130f -> Link to scmid starting with a85130f
665 # Documents:
665 # Documents:
666 # document#17 -> Link to document with id 17
666 # document#17 -> Link to document with id 17
667 # document:Greetings -> Link to the document with title "Greetings"
667 # document:Greetings -> Link to the document with title "Greetings"
668 # document:"Some document" -> Link to the document with title "Some document"
668 # document:"Some document" -> Link to the document with title "Some document"
669 # Versions:
669 # Versions:
670 # version#3 -> Link to version with id 3
670 # version#3 -> Link to version with id 3
671 # version:1.0.0 -> Link to version named "1.0.0"
671 # version:1.0.0 -> Link to version named "1.0.0"
672 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
672 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
673 # Attachments:
673 # Attachments:
674 # attachment:file.zip -> Link to the attachment of the current object named file.zip
674 # attachment:file.zip -> Link to the attachment of the current object named file.zip
675 # Source files:
675 # Source files:
676 # source:some/file -> Link to the file located at /some/file in the project's repository
676 # source:some/file -> Link to the file located at /some/file in the project's repository
677 # source:some/file@52 -> Link to the file's revision 52
677 # source:some/file@52 -> Link to the file's revision 52
678 # source:some/file#L120 -> Link to line 120 of the file
678 # source:some/file#L120 -> Link to line 120 of the file
679 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
679 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
680 # export:some/file -> Force the download of the file
680 # export:some/file -> Force the download of the file
681 # Forum messages:
681 # Forum messages:
682 # message#1218 -> Link to message with id 1218
682 # message#1218 -> Link to message with id 1218
683 # Projects:
683 # Projects:
684 # project:someproject -> Link to project named "someproject"
684 # project:someproject -> Link to project named "someproject"
685 # project#3 -> Link to project with id 3
685 # project#3 -> Link to project with id 3
686 #
686 #
687 # Links can refer other objects from other projects, using project identifier:
687 # Links can refer other objects from other projects, using project identifier:
688 # identifier:r52
688 # identifier:r52
689 # identifier:document:"Some document"
689 # identifier:document:"Some document"
690 # identifier:version:1.0.0
690 # identifier:version:1.0.0
691 # identifier:source:some/file
691 # identifier:source:some/file
692 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
692 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
693 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|
693 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|
694 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
694 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
695 link = nil
695 link = nil
696 project = default_project
696 project = default_project
697 if project_identifier
697 if project_identifier
698 project = Project.visible.find_by_identifier(project_identifier)
698 project = Project.visible.find_by_identifier(project_identifier)
699 end
699 end
700 if esc.nil?
700 if esc.nil?
701 if prefix.nil? && sep == 'r'
701 if prefix.nil? && sep == 'r'
702 if project
702 if project
703 repository = nil
703 repository = nil
704 if repo_identifier
704 if repo_identifier
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 else
706 else
707 repository = project.repository
707 repository = project.repository
708 end
708 end
709 # project.changesets.visible raises an SQL error because of a double join on repositories
709 # project.changesets.visible raises an SQL error because of a double join on repositories
710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
712 :class => 'changeset',
712 :class => 'changeset',
713 :title => truncate_single_line(changeset.comments, :length => 100))
713 :title => truncate_single_line(changeset.comments, :length => 100))
714 end
714 end
715 end
715 end
716 elsif sep == '#'
716 elsif sep == '#'
717 oid = identifier.to_i
717 oid = identifier.to_i
718 case prefix
718 case prefix
719 when nil
719 when nil
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 anchor = comment_id ? "note-#{comment_id}" : nil
721 anchor = comment_id ? "note-#{comment_id}" : nil
722 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
722 link = link_to(h("##{oid}#{comment_suffix}"), {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
723 :class => issue.css_classes,
723 :class => issue.css_classes,
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 end
725 end
726 when 'document'
726 when 'document'
727 if document = Document.visible.find_by_id(oid)
727 if document = Document.visible.find_by_id(oid)
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
729 :class => 'document'
729 :class => 'document'
730 end
730 end
731 when 'version'
731 when 'version'
732 if version = Version.visible.find_by_id(oid)
732 if version = Version.visible.find_by_id(oid)
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
734 :class => 'version'
734 :class => 'version'
735 end
735 end
736 when 'message'
736 when 'message'
737 if message = Message.visible.find_by_id(oid, :include => :parent)
737 if message = Message.visible.find_by_id(oid, :include => :parent)
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 end
739 end
740 when 'forum'
740 when 'forum'
741 if board = Board.visible.find_by_id(oid)
741 if board = Board.visible.find_by_id(oid)
742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
743 :class => 'board'
743 :class => 'board'
744 end
744 end
745 when 'news'
745 when 'news'
746 if news = News.visible.find_by_id(oid)
746 if news = News.visible.find_by_id(oid)
747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
748 :class => 'news'
748 :class => 'news'
749 end
749 end
750 when 'project'
750 when 'project'
751 if p = Project.visible.find_by_id(oid)
751 if p = Project.visible.find_by_id(oid)
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 end
753 end
754 end
754 end
755 elsif sep == ':'
755 elsif sep == ':'
756 # removes the double quotes if any
756 # removes the double quotes if any
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 case prefix
758 case prefix
759 when 'document'
759 when 'document'
760 if project && document = project.documents.visible.find_by_title(name)
760 if project && document = project.documents.visible.find_by_title(name)
761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
762 :class => 'document'
762 :class => 'document'
763 end
763 end
764 when 'version'
764 when 'version'
765 if project && version = project.versions.visible.find_by_name(name)
765 if project && version = project.versions.visible.find_by_name(name)
766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
767 :class => 'version'
767 :class => 'version'
768 end
768 end
769 when 'forum'
769 when 'forum'
770 if project && board = project.boards.visible.find_by_name(name)
770 if project && board = project.boards.visible.find_by_name(name)
771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
772 :class => 'board'
772 :class => 'board'
773 end
773 end
774 when 'news'
774 when 'news'
775 if project && news = project.news.visible.find_by_title(name)
775 if project && news = project.news.visible.find_by_title(name)
776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
777 :class => 'news'
777 :class => 'news'
778 end
778 end
779 when 'commit', 'source', 'export'
779 when 'commit', 'source', 'export'
780 if project
780 if project
781 repository = nil
781 repository = nil
782 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
782 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
783 repo_prefix, repo_identifier, name = $1, $2, $3
783 repo_prefix, repo_identifier, name = $1, $2, $3
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 else
785 else
786 repository = project.repository
786 repository = project.repository
787 end
787 end
788 if prefix == 'commit'
788 if prefix == 'commit'
789 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
789 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
790 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},
790 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},
791 :class => 'changeset',
791 :class => 'changeset',
792 :title => truncate_single_line(changeset.comments, :length => 100)
792 :title => truncate_single_line(changeset.comments, :length => 100)
793 end
793 end
794 else
794 else
795 if repository && User.current.allowed_to?(:browse_repository, project)
795 if repository && User.current.allowed_to?(:browse_repository, project)
796 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
796 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
797 path, rev, anchor = $1, $3, $5
797 path, rev, anchor = $1, $3, $5
798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
799 :path => to_path_param(path),
799 :path => to_path_param(path),
800 :rev => rev,
800 :rev => rev,
801 :anchor => anchor},
801 :anchor => anchor},
802 :class => (prefix == 'export' ? 'source download' : 'source')
802 :class => (prefix == 'export' ? 'source download' : 'source')
803 end
803 end
804 end
804 end
805 repo_prefix = nil
805 repo_prefix = nil
806 end
806 end
807 when 'attachment'
807 when 'attachment'
808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
809 if attachments && attachment = Attachment.latest_attach(attachments, name)
809 if attachments && attachment = Attachment.latest_attach(attachments, name)
810 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
810 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
811 end
811 end
812 when 'project'
812 when 'project'
813 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
813 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
814 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
814 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
815 end
815 end
816 end
816 end
817 end
817 end
818 end
818 end
819 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
819 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
820 end
820 end
821 end
821 end
822
822
823 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
823 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
824
824
825 def parse_sections(text, project, obj, attr, only_path, options)
825 def parse_sections(text, project, obj, attr, only_path, options)
826 return unless options[:edit_section_links]
826 return unless options[:edit_section_links]
827 text.gsub!(HEADING_RE) do
827 text.gsub!(HEADING_RE) do
828 heading = $1
828 heading = $1
829 @current_section += 1
829 @current_section += 1
830 if @current_section > 1
830 if @current_section > 1
831 content_tag('div',
831 content_tag('div',
832 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
832 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
833 :class => 'contextual',
833 :class => 'contextual',
834 :title => l(:button_edit_section)) + heading.html_safe
834 :title => l(:button_edit_section),
835 :id => "section-#{@current_section}") + heading.html_safe
835 else
836 else
836 heading
837 heading
837 end
838 end
838 end
839 end
839 end
840 end
840
841
841 # Headings and TOC
842 # Headings and TOC
842 # Adds ids and links to headings unless options[:headings] is set to false
843 # Adds ids and links to headings unless options[:headings] is set to false
843 def parse_headings(text, project, obj, attr, only_path, options)
844 def parse_headings(text, project, obj, attr, only_path, options)
844 return if options[:headings] == false
845 return if options[:headings] == false
845
846
846 text.gsub!(HEADING_RE) do
847 text.gsub!(HEADING_RE) do
847 level, attrs, content = $2.to_i, $3, $4
848 level, attrs, content = $2.to_i, $3, $4
848 item = strip_tags(content).strip
849 item = strip_tags(content).strip
849 anchor = sanitize_anchor_name(item)
850 anchor = sanitize_anchor_name(item)
850 # used for single-file wiki export
851 # used for single-file wiki export
851 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
852 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
852 @heading_anchors[anchor] ||= 0
853 @heading_anchors[anchor] ||= 0
853 idx = (@heading_anchors[anchor] += 1)
854 idx = (@heading_anchors[anchor] += 1)
854 if idx > 1
855 if idx > 1
855 anchor = "#{anchor}-#{idx}"
856 anchor = "#{anchor}-#{idx}"
856 end
857 end
857 @parsed_headings << [level, anchor, item]
858 @parsed_headings << [level, anchor, item]
858 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
859 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
859 end
860 end
860 end
861 end
861
862
862 MACROS_RE = /(
863 MACROS_RE = /(
863 (!)? # escaping
864 (!)? # escaping
864 (
865 (
865 \{\{ # opening tag
866 \{\{ # opening tag
866 ([\w]+) # macro name
867 ([\w]+) # macro name
867 (\(([^\n\r]*?)\))? # optional arguments
868 (\(([^\n\r]*?)\))? # optional arguments
868 ([\n\r].*?[\n\r])? # optional block of text
869 ([\n\r].*?[\n\r])? # optional block of text
869 \}\} # closing tag
870 \}\} # closing tag
870 )
871 )
871 )/mx unless const_defined?(:MACROS_RE)
872 )/mx unless const_defined?(:MACROS_RE)
872
873
873 MACRO_SUB_RE = /(
874 MACRO_SUB_RE = /(
874 \{\{
875 \{\{
875 macro\((\d+)\)
876 macro\((\d+)\)
876 \}\}
877 \}\}
877 )/x unless const_defined?(:MACRO_SUB_RE)
878 )/x unless const_defined?(:MACRO_SUB_RE)
878
879
879 # Extracts macros from text
880 # Extracts macros from text
880 def catch_macros(text)
881 def catch_macros(text)
881 macros = {}
882 macros = {}
882 text.gsub!(MACROS_RE) do
883 text.gsub!(MACROS_RE) do
883 all, macro = $1, $4.downcase
884 all, macro = $1, $4.downcase
884 if macro_exists?(macro) || all =~ MACRO_SUB_RE
885 if macro_exists?(macro) || all =~ MACRO_SUB_RE
885 index = macros.size
886 index = macros.size
886 macros[index] = all
887 macros[index] = all
887 "{{macro(#{index})}}"
888 "{{macro(#{index})}}"
888 else
889 else
889 all
890 all
890 end
891 end
891 end
892 end
892 macros
893 macros
893 end
894 end
894
895
895 # Executes and replaces macros in text
896 # Executes and replaces macros in text
896 def inject_macros(text, obj, macros, execute=true)
897 def inject_macros(text, obj, macros, execute=true)
897 text.gsub!(MACRO_SUB_RE) do
898 text.gsub!(MACRO_SUB_RE) do
898 all, index = $1, $2.to_i
899 all, index = $1, $2.to_i
899 orig = macros.delete(index)
900 orig = macros.delete(index)
900 if execute && orig && orig =~ MACROS_RE
901 if execute && orig && orig =~ MACROS_RE
901 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
902 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
902 if esc.nil?
903 if esc.nil?
903 h(exec_macro(macro, obj, args, block) || all)
904 h(exec_macro(macro, obj, args, block) || all)
904 else
905 else
905 h(all)
906 h(all)
906 end
907 end
907 elsif orig
908 elsif orig
908 h(orig)
909 h(orig)
909 else
910 else
910 h(all)
911 h(all)
911 end
912 end
912 end
913 end
913 end
914 end
914
915
915 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
916 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
916
917
917 # Renders the TOC with given headings
918 # Renders the TOC with given headings
918 def replace_toc(text, headings)
919 def replace_toc(text, headings)
919 text.gsub!(TOC_RE) do
920 text.gsub!(TOC_RE) do
920 # Keep only the 4 first levels
921 # Keep only the 4 first levels
921 headings = headings.select{|level, anchor, item| level <= 4}
922 headings = headings.select{|level, anchor, item| level <= 4}
922 if headings.empty?
923 if headings.empty?
923 ''
924 ''
924 else
925 else
925 div_class = 'toc'
926 div_class = 'toc'
926 div_class << ' right' if $1 == '>'
927 div_class << ' right' if $1 == '>'
927 div_class << ' left' if $1 == '<'
928 div_class << ' left' if $1 == '<'
928 out = "<ul class=\"#{div_class}\"><li>"
929 out = "<ul class=\"#{div_class}\"><li>"
929 root = headings.map(&:first).min
930 root = headings.map(&:first).min
930 current = root
931 current = root
931 started = false
932 started = false
932 headings.each do |level, anchor, item|
933 headings.each do |level, anchor, item|
933 if level > current
934 if level > current
934 out << '<ul><li>' * (level - current)
935 out << '<ul><li>' * (level - current)
935 elsif level < current
936 elsif level < current
936 out << "</li></ul>\n" * (current - level) + "</li><li>"
937 out << "</li></ul>\n" * (current - level) + "</li><li>"
937 elsif started
938 elsif started
938 out << '</li><li>'
939 out << '</li><li>'
939 end
940 end
940 out << "<a href=\"##{anchor}\">#{item}</a>"
941 out << "<a href=\"##{anchor}\">#{item}</a>"
941 current = level
942 current = level
942 started = true
943 started = true
943 end
944 end
944 out << '</li></ul>' * (current - root)
945 out << '</li></ul>' * (current - root)
945 out << '</li></ul>'
946 out << '</li></ul>'
946 end
947 end
947 end
948 end
948 end
949 end
949
950
950 # Same as Rails' simple_format helper without using paragraphs
951 # Same as Rails' simple_format helper without using paragraphs
951 def simple_format_without_paragraph(text)
952 def simple_format_without_paragraph(text)
952 text.to_s.
953 text.to_s.
953 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
954 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
954 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
955 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
955 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
956 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
956 html_safe
957 html_safe
957 end
958 end
958
959
959 def lang_options_for_select(blank=true)
960 def lang_options_for_select(blank=true)
960 (blank ? [["(auto)", ""]] : []) + languages_options
961 (blank ? [["(auto)", ""]] : []) + languages_options
961 end
962 end
962
963
963 def label_tag_for(name, option_tags = nil, options = {})
964 def label_tag_for(name, option_tags = nil, options = {})
964 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
965 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
965 content_tag("label", label_text)
966 content_tag("label", label_text)
966 end
967 end
967
968
968 def labelled_form_for(*args, &proc)
969 def labelled_form_for(*args, &proc)
969 args << {} unless args.last.is_a?(Hash)
970 args << {} unless args.last.is_a?(Hash)
970 options = args.last
971 options = args.last
971 if args.first.is_a?(Symbol)
972 if args.first.is_a?(Symbol)
972 options.merge!(:as => args.shift)
973 options.merge!(:as => args.shift)
973 end
974 end
974 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
975 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
975 form_for(*args, &proc)
976 form_for(*args, &proc)
976 end
977 end
977
978
978 def labelled_fields_for(*args, &proc)
979 def labelled_fields_for(*args, &proc)
979 args << {} unless args.last.is_a?(Hash)
980 args << {} unless args.last.is_a?(Hash)
980 options = args.last
981 options = args.last
981 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
982 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
982 fields_for(*args, &proc)
983 fields_for(*args, &proc)
983 end
984 end
984
985
985 def labelled_remote_form_for(*args, &proc)
986 def labelled_remote_form_for(*args, &proc)
986 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
987 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
987 args << {} unless args.last.is_a?(Hash)
988 args << {} unless args.last.is_a?(Hash)
988 options = args.last
989 options = args.last
989 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
990 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
990 form_for(*args, &proc)
991 form_for(*args, &proc)
991 end
992 end
992
993
993 def error_messages_for(*objects)
994 def error_messages_for(*objects)
994 html = ""
995 html = ""
995 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
996 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
996 errors = objects.map {|o| o.errors.full_messages}.flatten
997 errors = objects.map {|o| o.errors.full_messages}.flatten
997 if errors.any?
998 if errors.any?
998 html << "<div id='errorExplanation'><ul>\n"
999 html << "<div id='errorExplanation'><ul>\n"
999 errors.each do |error|
1000 errors.each do |error|
1000 html << "<li>#{h error}</li>\n"
1001 html << "<li>#{h error}</li>\n"
1001 end
1002 end
1002 html << "</ul></div>\n"
1003 html << "</ul></div>\n"
1003 end
1004 end
1004 html.html_safe
1005 html.html_safe
1005 end
1006 end
1006
1007
1007 def delete_link(url, options={})
1008 def delete_link(url, options={})
1008 options = {
1009 options = {
1009 :method => :delete,
1010 :method => :delete,
1010 :data => {:confirm => l(:text_are_you_sure)},
1011 :data => {:confirm => l(:text_are_you_sure)},
1011 :class => 'icon icon-del'
1012 :class => 'icon icon-del'
1012 }.merge(options)
1013 }.merge(options)
1013
1014
1014 link_to l(:button_delete), url, options
1015 link_to l(:button_delete), url, options
1015 end
1016 end
1016
1017
1017 def preview_link(url, form, target='preview', options={})
1018 def preview_link(url, form, target='preview', options={})
1018 content_tag 'a', l(:label_preview), {
1019 content_tag 'a', l(:label_preview), {
1019 :href => "#",
1020 :href => "#",
1020 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1021 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1021 :accesskey => accesskey(:preview)
1022 :accesskey => accesskey(:preview)
1022 }.merge(options)
1023 }.merge(options)
1023 end
1024 end
1024
1025
1025 def link_to_function(name, function, html_options={})
1026 def link_to_function(name, function, html_options={})
1026 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1027 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1027 end
1028 end
1028
1029
1029 # Helper to render JSON in views
1030 # Helper to render JSON in views
1030 def raw_json(arg)
1031 def raw_json(arg)
1031 arg.to_json.to_s.gsub('/', '\/').html_safe
1032 arg.to_json.to_s.gsub('/', '\/').html_safe
1032 end
1033 end
1033
1034
1034 def back_url
1035 def back_url
1035 url = params[:back_url]
1036 url = params[:back_url]
1036 if url.nil? && referer = request.env['HTTP_REFERER']
1037 if url.nil? && referer = request.env['HTTP_REFERER']
1037 url = CGI.unescape(referer.to_s)
1038 url = CGI.unescape(referer.to_s)
1038 end
1039 end
1039 url
1040 url
1040 end
1041 end
1041
1042
1042 def back_url_hidden_field_tag
1043 def back_url_hidden_field_tag
1043 url = back_url
1044 url = back_url
1044 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1045 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1045 end
1046 end
1046
1047
1047 def check_all_links(form_name)
1048 def check_all_links(form_name)
1048 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1049 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1049 " | ".html_safe +
1050 " | ".html_safe +
1050 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1051 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1051 end
1052 end
1052
1053
1053 def progress_bar(pcts, options={})
1054 def progress_bar(pcts, options={})
1054 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1055 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1055 pcts = pcts.collect(&:round)
1056 pcts = pcts.collect(&:round)
1056 pcts[1] = pcts[1] - pcts[0]
1057 pcts[1] = pcts[1] - pcts[0]
1057 pcts << (100 - pcts[1] - pcts[0])
1058 pcts << (100 - pcts[1] - pcts[0])
1058 width = options[:width] || '100px;'
1059 width = options[:width] || '100px;'
1059 legend = options[:legend] || ''
1060 legend = options[:legend] || ''
1060 content_tag('table',
1061 content_tag('table',
1061 content_tag('tr',
1062 content_tag('tr',
1062 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1063 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1063 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1064 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1064 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1065 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1065 ), :class => 'progress progress-#{pcts[0]}', :style => "width: #{width};").html_safe +
1066 ), :class => 'progress progress-#{pcts[0]}', :style => "width: #{width};").html_safe +
1066 content_tag('p', legend, :class => 'percent').html_safe
1067 content_tag('p', legend, :class => 'percent').html_safe
1067 end
1068 end
1068
1069
1069 def checked_image(checked=true)
1070 def checked_image(checked=true)
1070 if checked
1071 if checked
1071 image_tag 'toggle_check.png'
1072 image_tag 'toggle_check.png'
1072 end
1073 end
1073 end
1074 end
1074
1075
1075 def context_menu(url)
1076 def context_menu(url)
1076 unless @context_menu_included
1077 unless @context_menu_included
1077 content_for :header_tags do
1078 content_for :header_tags do
1078 javascript_include_tag('context_menu') +
1079 javascript_include_tag('context_menu') +
1079 stylesheet_link_tag('context_menu')
1080 stylesheet_link_tag('context_menu')
1080 end
1081 end
1081 if l(:direction) == 'rtl'
1082 if l(:direction) == 'rtl'
1082 content_for :header_tags do
1083 content_for :header_tags do
1083 stylesheet_link_tag('context_menu_rtl')
1084 stylesheet_link_tag('context_menu_rtl')
1084 end
1085 end
1085 end
1086 end
1086 @context_menu_included = true
1087 @context_menu_included = true
1087 end
1088 end
1088 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1089 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1089 end
1090 end
1090
1091
1091 def calendar_for(field_id)
1092 def calendar_for(field_id)
1092 include_calendar_headers_tags
1093 include_calendar_headers_tags
1093 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1094 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1094 end
1095 end
1095
1096
1096 def include_calendar_headers_tags
1097 def include_calendar_headers_tags
1097 unless @calendar_headers_tags_included
1098 unless @calendar_headers_tags_included
1098 tags = javascript_include_tag("datepicker")
1099 tags = javascript_include_tag("datepicker")
1099 @calendar_headers_tags_included = true
1100 @calendar_headers_tags_included = true
1100 content_for :header_tags do
1101 content_for :header_tags do
1101 start_of_week = Setting.start_of_week
1102 start_of_week = Setting.start_of_week
1102 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1103 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1103 # Redmine uses 1..7 (monday..sunday) in settings and locales
1104 # Redmine uses 1..7 (monday..sunday) in settings and locales
1104 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1105 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1105 start_of_week = start_of_week.to_i % 7
1106 start_of_week = start_of_week.to_i % 7
1106 tags << javascript_tag(
1107 tags << javascript_tag(
1107 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1108 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1108 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1109 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1109 path_to_image('/images/calendar.png') +
1110 path_to_image('/images/calendar.png') +
1110 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1111 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1111 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1112 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1112 "beforeShow: beforeShowDatePicker};")
1113 "beforeShow: beforeShowDatePicker};")
1113 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1114 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1114 unless jquery_locale == 'en'
1115 unless jquery_locale == 'en'
1115 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1116 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1116 end
1117 end
1117 tags
1118 tags
1118 end
1119 end
1119 end
1120 end
1120 end
1121 end
1121
1122
1122 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1123 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1123 # Examples:
1124 # Examples:
1124 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1125 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1125 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1126 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1126 #
1127 #
1127 def stylesheet_link_tag(*sources)
1128 def stylesheet_link_tag(*sources)
1128 options = sources.last.is_a?(Hash) ? sources.pop : {}
1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1129 plugin = options.delete(:plugin)
1130 plugin = options.delete(:plugin)
1130 sources = sources.map do |source|
1131 sources = sources.map do |source|
1131 if plugin
1132 if plugin
1132 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1133 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1133 elsif current_theme && current_theme.stylesheets.include?(source)
1134 elsif current_theme && current_theme.stylesheets.include?(source)
1134 current_theme.stylesheet_path(source)
1135 current_theme.stylesheet_path(source)
1135 else
1136 else
1136 source
1137 source
1137 end
1138 end
1138 end
1139 end
1139 super sources, options
1140 super sources, options
1140 end
1141 end
1141
1142
1142 # Overrides Rails' image_tag with themes and plugins support.
1143 # Overrides Rails' image_tag with themes and plugins support.
1143 # Examples:
1144 # Examples:
1144 # image_tag('image.png') # => picks image.png from the current theme or defaults
1145 # image_tag('image.png') # => picks image.png from the current theme or defaults
1145 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1146 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1146 #
1147 #
1147 def image_tag(source, options={})
1148 def image_tag(source, options={})
1148 if plugin = options.delete(:plugin)
1149 if plugin = options.delete(:plugin)
1149 source = "/plugin_assets/#{plugin}/images/#{source}"
1150 source = "/plugin_assets/#{plugin}/images/#{source}"
1150 elsif current_theme && current_theme.images.include?(source)
1151 elsif current_theme && current_theme.images.include?(source)
1151 source = current_theme.image_path(source)
1152 source = current_theme.image_path(source)
1152 end
1153 end
1153 super source, options
1154 super source, options
1154 end
1155 end
1155
1156
1156 # Overrides Rails' javascript_include_tag with plugins support
1157 # Overrides Rails' javascript_include_tag with plugins support
1157 # Examples:
1158 # Examples:
1158 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1159 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1159 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1160 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1160 #
1161 #
1161 def javascript_include_tag(*sources)
1162 def javascript_include_tag(*sources)
1162 options = sources.last.is_a?(Hash) ? sources.pop : {}
1163 options = sources.last.is_a?(Hash) ? sources.pop : {}
1163 if plugin = options.delete(:plugin)
1164 if plugin = options.delete(:plugin)
1164 sources = sources.map do |source|
1165 sources = sources.map do |source|
1165 if plugin
1166 if plugin
1166 "/plugin_assets/#{plugin}/javascripts/#{source}"
1167 "/plugin_assets/#{plugin}/javascripts/#{source}"
1167 else
1168 else
1168 source
1169 source
1169 end
1170 end
1170 end
1171 end
1171 end
1172 end
1172 super sources, options
1173 super sources, options
1173 end
1174 end
1174
1175
1175 def content_for(name, content = nil, &block)
1176 def content_for(name, content = nil, &block)
1176 @has_content ||= {}
1177 @has_content ||= {}
1177 @has_content[name] = true
1178 @has_content[name] = true
1178 super(name, content, &block)
1179 super(name, content, &block)
1179 end
1180 end
1180
1181
1181 def has_content?(name)
1182 def has_content?(name)
1182 (@has_content && @has_content[name]) || false
1183 (@has_content && @has_content[name]) || false
1183 end
1184 end
1184
1185
1185 def sidebar_content?
1186 def sidebar_content?
1186 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1187 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1187 end
1188 end
1188
1189
1189 def view_layouts_base_sidebar_hook_response
1190 def view_layouts_base_sidebar_hook_response
1190 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1191 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1191 end
1192 end
1192
1193
1193 def email_delivery_enabled?
1194 def email_delivery_enabled?
1194 !!ActionMailer::Base.perform_deliveries
1195 !!ActionMailer::Base.perform_deliveries
1195 end
1196 end
1196
1197
1197 # Returns the avatar image tag for the given +user+ if avatars are enabled
1198 # Returns the avatar image tag for the given +user+ if avatars are enabled
1198 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1199 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1199 def avatar(user, options = { })
1200 def avatar(user, options = { })
1200 if Setting.gravatar_enabled?
1201 if Setting.gravatar_enabled?
1201 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1202 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1202 email = nil
1203 email = nil
1203 if user.respond_to?(:mail)
1204 if user.respond_to?(:mail)
1204 email = user.mail
1205 email = user.mail
1205 elsif user.to_s =~ %r{<(.+?)>}
1206 elsif user.to_s =~ %r{<(.+?)>}
1206 email = $1
1207 email = $1
1207 end
1208 end
1208 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1209 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1209 else
1210 else
1210 ''
1211 ''
1211 end
1212 end
1212 end
1213 end
1213
1214
1214 def sanitize_anchor_name(anchor)
1215 def sanitize_anchor_name(anchor)
1215 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1216 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1216 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1217 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1217 else
1218 else
1218 # TODO: remove when ruby1.8 is no longer supported
1219 # TODO: remove when ruby1.8 is no longer supported
1219 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1220 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1220 end
1221 end
1221 end
1222 end
1222
1223
1223 # Returns the javascript tags that are included in the html layout head
1224 # Returns the javascript tags that are included in the html layout head
1224 def javascript_heads
1225 def javascript_heads
1225 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1226 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1226 unless User.current.pref.warn_on_leaving_unsaved == '0'
1227 unless User.current.pref.warn_on_leaving_unsaved == '0'
1227 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1228 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1228 end
1229 end
1229 tags
1230 tags
1230 end
1231 end
1231
1232
1232 def favicon
1233 def favicon
1233 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1234 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1234 end
1235 end
1235
1236
1236 def robot_exclusion_tag
1237 def robot_exclusion_tag
1237 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1238 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1238 end
1239 end
1239
1240
1240 # Returns true if arg is expected in the API response
1241 # Returns true if arg is expected in the API response
1241 def include_in_api_response?(arg)
1242 def include_in_api_response?(arg)
1242 unless @included_in_api_response
1243 unless @included_in_api_response
1243 param = params[:include]
1244 param = params[:include]
1244 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1245 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1245 @included_in_api_response.collect!(&:strip)
1246 @included_in_api_response.collect!(&:strip)
1246 end
1247 end
1247 @included_in_api_response.include?(arg.to_s)
1248 @included_in_api_response.include?(arg.to_s)
1248 end
1249 end
1249
1250
1250 # Returns options or nil if nometa param or X-Redmine-Nometa header
1251 # Returns options or nil if nometa param or X-Redmine-Nometa header
1251 # was set in the request
1252 # was set in the request
1252 def api_meta(options)
1253 def api_meta(options)
1253 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1254 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1254 # compatibility mode for activeresource clients that raise
1255 # compatibility mode for activeresource clients that raise
1255 # an error when unserializing an array with attributes
1256 # an error when unserializing an array with attributes
1256 nil
1257 nil
1257 else
1258 else
1258 options
1259 options
1259 end
1260 end
1260 end
1261 end
1261
1262
1262 private
1263 private
1263
1264
1264 def wiki_helper
1265 def wiki_helper
1265 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1266 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1266 extend helper
1267 extend helper
1267 return self
1268 return self
1268 end
1269 end
1269
1270
1270 def link_to_content_update(text, url_params = {}, html_options = {})
1271 def link_to_content_update(text, url_params = {}, html_options = {})
1271 link_to(text, url_params, html_options)
1272 link_to(text, url_params, html_options)
1272 end
1273 end
1273 end
1274 end
@@ -1,956 +1,956
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class WikiControllerTest < ActionController::TestCase
20 class WikiControllerTest < ActionController::TestCase
21 fixtures :projects, :users, :roles, :members, :member_roles,
21 fixtures :projects, :users, :roles, :members, :member_roles,
22 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
22 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
23 :wiki_content_versions, :attachments
23 :wiki_content_versions, :attachments
24
24
25 def setup
25 def setup
26 User.current = nil
26 User.current = nil
27 end
27 end
28
28
29 def test_show_start_page
29 def test_show_start_page
30 get :show, :project_id => 'ecookbook'
30 get :show, :project_id => 'ecookbook'
31 assert_response :success
31 assert_response :success
32 assert_template 'show'
32 assert_template 'show'
33 assert_tag :tag => 'h1', :content => /CookBook documentation/
33 assert_tag :tag => 'h1', :content => /CookBook documentation/
34
34
35 # child_pages macro
35 # child_pages macro
36 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
36 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
37 :child => { :tag => 'li',
37 :child => { :tag => 'li',
38 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
38 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
39 :content => 'Page with an inline image' } }
39 :content => 'Page with an inline image' } }
40 end
40 end
41
41
42 def test_export_link
42 def test_export_link
43 Role.anonymous.add_permission! :export_wiki_pages
43 Role.anonymous.add_permission! :export_wiki_pages
44 get :show, :project_id => 'ecookbook'
44 get :show, :project_id => 'ecookbook'
45 assert_response :success
45 assert_response :success
46 assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
46 assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
47 end
47 end
48
48
49 def test_show_page_with_name
49 def test_show_page_with_name
50 get :show, :project_id => 1, :id => 'Another_page'
50 get :show, :project_id => 1, :id => 'Another_page'
51 assert_response :success
51 assert_response :success
52 assert_template 'show'
52 assert_template 'show'
53 assert_tag :tag => 'h1', :content => /Another page/
53 assert_tag :tag => 'h1', :content => /Another page/
54 # Included page with an inline image
54 # Included page with an inline image
55 assert_tag :tag => 'p', :content => /This is an inline image/
55 assert_tag :tag => 'p', :content => /This is an inline image/
56 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3/logo.gif',
56 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3/logo.gif',
57 :alt => 'This is a logo' }
57 :alt => 'This is a logo' }
58 end
58 end
59
59
60 def test_show_old_version
60 def test_show_old_version
61 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
61 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
62 assert_response :success
62 assert_response :success
63 assert_template 'show'
63 assert_template 'show'
64
64
65 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
65 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
66 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
66 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
67 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
67 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
68 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
68 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
69 end
69 end
70
70
71 def test_show_old_version_with_attachments
71 def test_show_old_version_with_attachments
72 page = WikiPage.find(4)
72 page = WikiPage.find(4)
73 assert page.attachments.any?
73 assert page.attachments.any?
74 content = page.content
74 content = page.content
75 content.text = "update"
75 content.text = "update"
76 content.save!
76 content.save!
77
77
78 get :show, :project_id => 'ecookbook', :id => page.title, :version => '1'
78 get :show, :project_id => 'ecookbook', :id => page.title, :version => '1'
79 assert_kind_of WikiContent::Version, assigns(:content)
79 assert_kind_of WikiContent::Version, assigns(:content)
80 assert_response :success
80 assert_response :success
81 assert_template 'show'
81 assert_template 'show'
82 end
82 end
83
83
84 def test_show_old_version_without_permission_should_be_denied
84 def test_show_old_version_without_permission_should_be_denied
85 Role.anonymous.remove_permission! :view_wiki_edits
85 Role.anonymous.remove_permission! :view_wiki_edits
86
86
87 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
87 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
88 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2'
88 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2'
89 end
89 end
90
90
91 def test_show_first_version
91 def test_show_first_version
92 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
92 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
93 assert_response :success
93 assert_response :success
94 assert_template 'show'
94 assert_template 'show'
95
95
96 assert_select 'a', :text => /Previous/, :count => 0
96 assert_select 'a', :text => /Previous/, :count => 0
97 assert_select 'a', :text => /diff/, :count => 0
97 assert_select 'a', :text => /diff/, :count => 0
98 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
98 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
99 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
99 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
100 end
100 end
101
101
102 def test_show_redirected_page
102 def test_show_redirected_page
103 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
103 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
104
104
105 get :show, :project_id => 'ecookbook', :id => 'Old_title'
105 get :show, :project_id => 'ecookbook', :id => 'Old_title'
106 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
106 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
107 end
107 end
108
108
109 def test_show_with_sidebar
109 def test_show_with_sidebar
110 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
110 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
111 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
111 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
112 page.save!
112 page.save!
113
113
114 get :show, :project_id => 1, :id => 'Another_page'
114 get :show, :project_id => 1, :id => 'Another_page'
115 assert_response :success
115 assert_response :success
116 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
116 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
117 :content => /Side bar content for test_show_with_sidebar/
117 :content => /Side bar content for test_show_with_sidebar/
118 end
118 end
119
119
120 def test_show_should_display_section_edit_links
120 def test_show_should_display_section_edit_links
121 @request.session[:user_id] = 2
121 @request.session[:user_id] = 2
122 get :show, :project_id => 1, :id => 'Page with sections'
122 get :show, :project_id => 1, :id => 'Page with sections'
123 assert_no_tag 'a', :attributes => {
123 assert_no_tag 'a', :attributes => {
124 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
124 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
125 }
125 }
126 assert_tag 'a', :attributes => {
126 assert_tag 'a', :attributes => {
127 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
127 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
128 }
128 }
129 assert_tag 'a', :attributes => {
129 assert_tag 'a', :attributes => {
130 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
130 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
131 }
131 }
132 end
132 end
133
133
134 def test_show_current_version_should_display_section_edit_links
134 def test_show_current_version_should_display_section_edit_links
135 @request.session[:user_id] = 2
135 @request.session[:user_id] = 2
136 get :show, :project_id => 1, :id => 'Page with sections', :version => 3
136 get :show, :project_id => 1, :id => 'Page with sections', :version => 3
137
137
138 assert_tag 'a', :attributes => {
138 assert_tag 'a', :attributes => {
139 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
139 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
140 }
140 }
141 end
141 end
142
142
143 def test_show_old_version_should_not_display_section_edit_links
143 def test_show_old_version_should_not_display_section_edit_links
144 @request.session[:user_id] = 2
144 @request.session[:user_id] = 2
145 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
145 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
146
146
147 assert_no_tag 'a', :attributes => {
147 assert_no_tag 'a', :attributes => {
148 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
148 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
149 }
149 }
150 end
150 end
151
151
152 def test_show_unexistent_page_without_edit_right
152 def test_show_unexistent_page_without_edit_right
153 get :show, :project_id => 1, :id => 'Unexistent page'
153 get :show, :project_id => 1, :id => 'Unexistent page'
154 assert_response 404
154 assert_response 404
155 end
155 end
156
156
157 def test_show_unexistent_page_with_edit_right
157 def test_show_unexistent_page_with_edit_right
158 @request.session[:user_id] = 2
158 @request.session[:user_id] = 2
159 get :show, :project_id => 1, :id => 'Unexistent page'
159 get :show, :project_id => 1, :id => 'Unexistent page'
160 assert_response :success
160 assert_response :success
161 assert_template 'edit'
161 assert_template 'edit'
162 end
162 end
163
163
164 def test_show_unexistent_page_with_parent_should_preselect_parent
164 def test_show_unexistent_page_with_parent_should_preselect_parent
165 @request.session[:user_id] = 2
165 @request.session[:user_id] = 2
166 get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
166 get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
167 assert_response :success
167 assert_response :success
168 assert_template 'edit'
168 assert_template 'edit'
169 assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
169 assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
170 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
170 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
171 end
171 end
172
172
173 def test_show_should_not_show_history_without_permission
173 def test_show_should_not_show_history_without_permission
174 Role.anonymous.remove_permission! :view_wiki_edits
174 Role.anonymous.remove_permission! :view_wiki_edits
175 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
175 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
176
176
177 assert_response 302
177 assert_response 302
178 end
178 end
179
179
180 def test_show_page_without_content_should_display_the_edit_form
180 def test_show_page_without_content_should_display_the_edit_form
181 @request.session[:user_id] = 2
181 @request.session[:user_id] = 2
182 WikiPage.create!(:title => 'NoContent', :wiki => Project.find(1).wiki)
182 WikiPage.create!(:title => 'NoContent', :wiki => Project.find(1).wiki)
183
183
184 get :show, :project_id => 1, :id => 'NoContent'
184 get :show, :project_id => 1, :id => 'NoContent'
185 assert_response :success
185 assert_response :success
186 assert_template 'edit'
186 assert_template 'edit'
187 assert_select 'textarea[name=?]', 'content[text]'
187 assert_select 'textarea[name=?]', 'content[text]'
188 end
188 end
189
189
190 def test_create_page
190 def test_create_page
191 @request.session[:user_id] = 2
191 @request.session[:user_id] = 2
192 assert_difference 'WikiPage.count' do
192 assert_difference 'WikiPage.count' do
193 assert_difference 'WikiContent.count' do
193 assert_difference 'WikiContent.count' do
194 put :update, :project_id => 1,
194 put :update, :project_id => 1,
195 :id => 'New page',
195 :id => 'New page',
196 :content => {:comments => 'Created the page',
196 :content => {:comments => 'Created the page',
197 :text => "h1. New page\n\nThis is a new page",
197 :text => "h1. New page\n\nThis is a new page",
198 :version => 0}
198 :version => 0}
199 end
199 end
200 end
200 end
201 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
201 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
202 page = Project.find(1).wiki.find_page('New page')
202 page = Project.find(1).wiki.find_page('New page')
203 assert !page.new_record?
203 assert !page.new_record?
204 assert_not_nil page.content
204 assert_not_nil page.content
205 assert_nil page.parent
205 assert_nil page.parent
206 assert_equal 'Created the page', page.content.comments
206 assert_equal 'Created the page', page.content.comments
207 end
207 end
208
208
209 def test_create_page_with_attachments
209 def test_create_page_with_attachments
210 @request.session[:user_id] = 2
210 @request.session[:user_id] = 2
211 assert_difference 'WikiPage.count' do
211 assert_difference 'WikiPage.count' do
212 assert_difference 'Attachment.count' do
212 assert_difference 'Attachment.count' do
213 put :update, :project_id => 1,
213 put :update, :project_id => 1,
214 :id => 'New page',
214 :id => 'New page',
215 :content => {:comments => 'Created the page',
215 :content => {:comments => 'Created the page',
216 :text => "h1. New page\n\nThis is a new page",
216 :text => "h1. New page\n\nThis is a new page",
217 :version => 0},
217 :version => 0},
218 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
218 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
219 end
219 end
220 end
220 end
221 page = Project.find(1).wiki.find_page('New page')
221 page = Project.find(1).wiki.find_page('New page')
222 assert_equal 1, page.attachments.count
222 assert_equal 1, page.attachments.count
223 assert_equal 'testfile.txt', page.attachments.first.filename
223 assert_equal 'testfile.txt', page.attachments.first.filename
224 end
224 end
225
225
226 def test_create_page_with_parent
226 def test_create_page_with_parent
227 @request.session[:user_id] = 2
227 @request.session[:user_id] = 2
228 assert_difference 'WikiPage.count' do
228 assert_difference 'WikiPage.count' do
229 put :update, :project_id => 1, :id => 'New page',
229 put :update, :project_id => 1, :id => 'New page',
230 :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
230 :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
231 :wiki_page => {:parent_id => 2}
231 :wiki_page => {:parent_id => 2}
232 end
232 end
233 page = Project.find(1).wiki.find_page('New page')
233 page = Project.find(1).wiki.find_page('New page')
234 assert_equal WikiPage.find(2), page.parent
234 assert_equal WikiPage.find(2), page.parent
235 end
235 end
236
236
237 def test_edit_page
237 def test_edit_page
238 @request.session[:user_id] = 2
238 @request.session[:user_id] = 2
239 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
239 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
240
240
241 assert_response :success
241 assert_response :success
242 assert_template 'edit'
242 assert_template 'edit'
243
243
244 assert_tag 'textarea',
244 assert_tag 'textarea',
245 :attributes => { :name => 'content[text]' },
245 :attributes => { :name => 'content[text]' },
246 :content => "\n"+WikiPage.find_by_title('Another_page').content.text
246 :content => "\n"+WikiPage.find_by_title('Another_page').content.text
247 end
247 end
248
248
249 def test_edit_section
249 def test_edit_section
250 @request.session[:user_id] = 2
250 @request.session[:user_id] = 2
251 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
251 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
252
252
253 assert_response :success
253 assert_response :success
254 assert_template 'edit'
254 assert_template 'edit'
255
255
256 page = WikiPage.find_by_title('Page_with_sections')
256 page = WikiPage.find_by_title('Page_with_sections')
257 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
257 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
258
258
259 assert_tag 'textarea',
259 assert_tag 'textarea',
260 :attributes => { :name => 'content[text]' },
260 :attributes => { :name => 'content[text]' },
261 :content => "\n"+section
261 :content => "\n"+section
262 assert_tag 'input',
262 assert_tag 'input',
263 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
263 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
264 assert_tag 'input',
264 assert_tag 'input',
265 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
265 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
266 end
266 end
267
267
268 def test_edit_invalid_section_should_respond_with_404
268 def test_edit_invalid_section_should_respond_with_404
269 @request.session[:user_id] = 2
269 @request.session[:user_id] = 2
270 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
270 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
271
271
272 assert_response 404
272 assert_response 404
273 end
273 end
274
274
275 def test_update_page
275 def test_update_page
276 @request.session[:user_id] = 2
276 @request.session[:user_id] = 2
277 assert_no_difference 'WikiPage.count' do
277 assert_no_difference 'WikiPage.count' do
278 assert_no_difference 'WikiContent.count' do
278 assert_no_difference 'WikiContent.count' do
279 assert_difference 'WikiContent::Version.count' do
279 assert_difference 'WikiContent::Version.count' do
280 put :update, :project_id => 1,
280 put :update, :project_id => 1,
281 :id => 'Another_page',
281 :id => 'Another_page',
282 :content => {
282 :content => {
283 :comments => "my comments",
283 :comments => "my comments",
284 :text => "edited",
284 :text => "edited",
285 :version => 1
285 :version => 1
286 }
286 }
287 end
287 end
288 end
288 end
289 end
289 end
290 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
290 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
291
291
292 page = Wiki.find(1).pages.find_by_title('Another_page')
292 page = Wiki.find(1).pages.find_by_title('Another_page')
293 assert_equal "edited", page.content.text
293 assert_equal "edited", page.content.text
294 assert_equal 2, page.content.version
294 assert_equal 2, page.content.version
295 assert_equal "my comments", page.content.comments
295 assert_equal "my comments", page.content.comments
296 end
296 end
297
297
298 def test_update_page_with_parent
298 def test_update_page_with_parent
299 @request.session[:user_id] = 2
299 @request.session[:user_id] = 2
300 assert_no_difference 'WikiPage.count' do
300 assert_no_difference 'WikiPage.count' do
301 assert_no_difference 'WikiContent.count' do
301 assert_no_difference 'WikiContent.count' do
302 assert_difference 'WikiContent::Version.count' do
302 assert_difference 'WikiContent::Version.count' do
303 put :update, :project_id => 1,
303 put :update, :project_id => 1,
304 :id => 'Another_page',
304 :id => 'Another_page',
305 :content => {
305 :content => {
306 :comments => "my comments",
306 :comments => "my comments",
307 :text => "edited",
307 :text => "edited",
308 :version => 1
308 :version => 1
309 },
309 },
310 :wiki_page => {:parent_id => '1'}
310 :wiki_page => {:parent_id => '1'}
311 end
311 end
312 end
312 end
313 end
313 end
314 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
314 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
315
315
316 page = Wiki.find(1).pages.find_by_title('Another_page')
316 page = Wiki.find(1).pages.find_by_title('Another_page')
317 assert_equal "edited", page.content.text
317 assert_equal "edited", page.content.text
318 assert_equal 2, page.content.version
318 assert_equal 2, page.content.version
319 assert_equal "my comments", page.content.comments
319 assert_equal "my comments", page.content.comments
320 assert_equal WikiPage.find(1), page.parent
320 assert_equal WikiPage.find(1), page.parent
321 end
321 end
322
322
323 def test_update_page_with_failure
323 def test_update_page_with_failure
324 @request.session[:user_id] = 2
324 @request.session[:user_id] = 2
325 assert_no_difference 'WikiPage.count' do
325 assert_no_difference 'WikiPage.count' do
326 assert_no_difference 'WikiContent.count' do
326 assert_no_difference 'WikiContent.count' do
327 assert_no_difference 'WikiContent::Version.count' do
327 assert_no_difference 'WikiContent::Version.count' do
328 put :update, :project_id => 1,
328 put :update, :project_id => 1,
329 :id => 'Another_page',
329 :id => 'Another_page',
330 :content => {
330 :content => {
331 :comments => 'a' * 300, # failure here, comment is too long
331 :comments => 'a' * 300, # failure here, comment is too long
332 :text => 'edited',
332 :text => 'edited',
333 :version => 1
333 :version => 1
334 }
334 }
335 end
335 end
336 end
336 end
337 end
337 end
338 assert_response :success
338 assert_response :success
339 assert_template 'edit'
339 assert_template 'edit'
340
340
341 assert_error_tag :descendant => {:content => /Comment is too long/}
341 assert_error_tag :descendant => {:content => /Comment is too long/}
342 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
342 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
343 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
343 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
344 end
344 end
345
345
346 def test_update_page_with_parent_change_only_should_not_create_content_version
346 def test_update_page_with_parent_change_only_should_not_create_content_version
347 @request.session[:user_id] = 2
347 @request.session[:user_id] = 2
348 assert_no_difference 'WikiPage.count' do
348 assert_no_difference 'WikiPage.count' do
349 assert_no_difference 'WikiContent.count' do
349 assert_no_difference 'WikiContent.count' do
350 assert_no_difference 'WikiContent::Version.count' do
350 assert_no_difference 'WikiContent::Version.count' do
351 put :update, :project_id => 1,
351 put :update, :project_id => 1,
352 :id => 'Another_page',
352 :id => 'Another_page',
353 :content => {
353 :content => {
354 :comments => '',
354 :comments => '',
355 :text => Wiki.find(1).find_page('Another_page').content.text,
355 :text => Wiki.find(1).find_page('Another_page').content.text,
356 :version => 1
356 :version => 1
357 },
357 },
358 :wiki_page => {:parent_id => '1'}
358 :wiki_page => {:parent_id => '1'}
359 end
359 end
360 end
360 end
361 end
361 end
362 page = Wiki.find(1).pages.find_by_title('Another_page')
362 page = Wiki.find(1).pages.find_by_title('Another_page')
363 assert_equal 1, page.content.version
363 assert_equal 1, page.content.version
364 assert_equal WikiPage.find(1), page.parent
364 assert_equal WikiPage.find(1), page.parent
365 end
365 end
366
366
367 def test_update_page_with_attachments_only_should_not_create_content_version
367 def test_update_page_with_attachments_only_should_not_create_content_version
368 @request.session[:user_id] = 2
368 @request.session[:user_id] = 2
369 assert_no_difference 'WikiPage.count' do
369 assert_no_difference 'WikiPage.count' do
370 assert_no_difference 'WikiContent.count' do
370 assert_no_difference 'WikiContent.count' do
371 assert_no_difference 'WikiContent::Version.count' do
371 assert_no_difference 'WikiContent::Version.count' do
372 assert_difference 'Attachment.count' do
372 assert_difference 'Attachment.count' do
373 put :update, :project_id => 1,
373 put :update, :project_id => 1,
374 :id => 'Another_page',
374 :id => 'Another_page',
375 :content => {
375 :content => {
376 :comments => '',
376 :comments => '',
377 :text => Wiki.find(1).find_page('Another_page').content.text,
377 :text => Wiki.find(1).find_page('Another_page').content.text,
378 :version => 1
378 :version => 1
379 },
379 },
380 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
380 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
381 end
381 end
382 end
382 end
383 end
383 end
384 end
384 end
385 page = Wiki.find(1).pages.find_by_title('Another_page')
385 page = Wiki.find(1).pages.find_by_title('Another_page')
386 assert_equal 1, page.content.version
386 assert_equal 1, page.content.version
387 end
387 end
388
388
389 def test_update_stale_page_should_not_raise_an_error
389 def test_update_stale_page_should_not_raise_an_error
390 @request.session[:user_id] = 2
390 @request.session[:user_id] = 2
391 c = Wiki.find(1).find_page('Another_page').content
391 c = Wiki.find(1).find_page('Another_page').content
392 c.text = 'Previous text'
392 c.text = 'Previous text'
393 c.save!
393 c.save!
394 assert_equal 2, c.version
394 assert_equal 2, c.version
395
395
396 assert_no_difference 'WikiPage.count' do
396 assert_no_difference 'WikiPage.count' do
397 assert_no_difference 'WikiContent.count' do
397 assert_no_difference 'WikiContent.count' do
398 assert_no_difference 'WikiContent::Version.count' do
398 assert_no_difference 'WikiContent::Version.count' do
399 put :update, :project_id => 1,
399 put :update, :project_id => 1,
400 :id => 'Another_page',
400 :id => 'Another_page',
401 :content => {
401 :content => {
402 :comments => 'My comments',
402 :comments => 'My comments',
403 :text => 'Text should not be lost',
403 :text => 'Text should not be lost',
404 :version => 1
404 :version => 1
405 }
405 }
406 end
406 end
407 end
407 end
408 end
408 end
409 assert_response :success
409 assert_response :success
410 assert_template 'edit'
410 assert_template 'edit'
411 assert_tag :div,
411 assert_tag :div,
412 :attributes => { :class => /error/ },
412 :attributes => { :class => /error/ },
413 :content => /Data has been updated by another user/
413 :content => /Data has been updated by another user/
414 assert_tag 'textarea',
414 assert_tag 'textarea',
415 :attributes => { :name => 'content[text]' },
415 :attributes => { :name => 'content[text]' },
416 :content => /Text should not be lost/
416 :content => /Text should not be lost/
417 assert_tag 'input',
417 assert_tag 'input',
418 :attributes => { :name => 'content[comments]', :value => 'My comments' }
418 :attributes => { :name => 'content[comments]', :value => 'My comments' }
419
419
420 c.reload
420 c.reload
421 assert_equal 'Previous text', c.text
421 assert_equal 'Previous text', c.text
422 assert_equal 2, c.version
422 assert_equal 2, c.version
423 end
423 end
424
424
425 def test_update_page_without_content_should_create_content
425 def test_update_page_without_content_should_create_content
426 @request.session[:user_id] = 2
426 @request.session[:user_id] = 2
427 page = WikiPage.create!(:title => 'NoContent', :wiki => Project.find(1).wiki)
427 page = WikiPage.create!(:title => 'NoContent', :wiki => Project.find(1).wiki)
428
428
429 assert_no_difference 'WikiPage.count' do
429 assert_no_difference 'WikiPage.count' do
430 assert_difference 'WikiContent.count' do
430 assert_difference 'WikiContent.count' do
431 put :update, :project_id => 1, :id => 'NoContent', :content => {:text => 'Some content'}
431 put :update, :project_id => 1, :id => 'NoContent', :content => {:text => 'Some content'}
432 assert_response 302
432 assert_response 302
433 end
433 end
434 end
434 end
435 assert_equal 'Some content', page.reload.content.text
435 assert_equal 'Some content', page.reload.content.text
436 end
436 end
437
437
438 def test_update_section
438 def test_update_section
439 @request.session[:user_id] = 2
439 @request.session[:user_id] = 2
440 page = WikiPage.find_by_title('Page_with_sections')
440 page = WikiPage.find_by_title('Page_with_sections')
441 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
441 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
442 text = page.content.text
442 text = page.content.text
443
443
444 assert_no_difference 'WikiPage.count' do
444 assert_no_difference 'WikiPage.count' do
445 assert_no_difference 'WikiContent.count' do
445 assert_no_difference 'WikiContent.count' do
446 assert_difference 'WikiContent::Version.count' do
446 assert_difference 'WikiContent::Version.count' do
447 put :update, :project_id => 1, :id => 'Page_with_sections',
447 put :update, :project_id => 1, :id => 'Page_with_sections',
448 :content => {
448 :content => {
449 :text => "New section content",
449 :text => "New section content",
450 :version => 3
450 :version => 3
451 },
451 },
452 :section => 2,
452 :section => 2,
453 :section_hash => hash
453 :section_hash => hash
454 end
454 end
455 end
455 end
456 end
456 end
457 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
457 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections#section-2'
458 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
458 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
459 end
459 end
460
460
461 def test_update_section_should_allow_stale_page_update
461 def test_update_section_should_allow_stale_page_update
462 @request.session[:user_id] = 2
462 @request.session[:user_id] = 2
463 page = WikiPage.find_by_title('Page_with_sections')
463 page = WikiPage.find_by_title('Page_with_sections')
464 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
464 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
465 text = page.content.text
465 text = page.content.text
466
466
467 assert_no_difference 'WikiPage.count' do
467 assert_no_difference 'WikiPage.count' do
468 assert_no_difference 'WikiContent.count' do
468 assert_no_difference 'WikiContent.count' do
469 assert_difference 'WikiContent::Version.count' do
469 assert_difference 'WikiContent::Version.count' do
470 put :update, :project_id => 1, :id => 'Page_with_sections',
470 put :update, :project_id => 1, :id => 'Page_with_sections',
471 :content => {
471 :content => {
472 :text => "New section content",
472 :text => "New section content",
473 :version => 2 # Current version is 3
473 :version => 2 # Current version is 3
474 },
474 },
475 :section => 2,
475 :section => 2,
476 :section_hash => hash
476 :section_hash => hash
477 end
477 end
478 end
478 end
479 end
479 end
480 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
480 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections#section-2'
481 page.reload
481 page.reload
482 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
482 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
483 assert_equal 4, page.content.version
483 assert_equal 4, page.content.version
484 end
484 end
485
485
486 def test_update_section_should_not_allow_stale_section_update
486 def test_update_section_should_not_allow_stale_section_update
487 @request.session[:user_id] = 2
487 @request.session[:user_id] = 2
488
488
489 assert_no_difference 'WikiPage.count' do
489 assert_no_difference 'WikiPage.count' do
490 assert_no_difference 'WikiContent.count' do
490 assert_no_difference 'WikiContent.count' do
491 assert_no_difference 'WikiContent::Version.count' do
491 assert_no_difference 'WikiContent::Version.count' do
492 put :update, :project_id => 1, :id => 'Page_with_sections',
492 put :update, :project_id => 1, :id => 'Page_with_sections',
493 :content => {
493 :content => {
494 :comments => 'My comments',
494 :comments => 'My comments',
495 :text => "Text should not be lost",
495 :text => "Text should not be lost",
496 :version => 3
496 :version => 3
497 },
497 },
498 :section => 2,
498 :section => 2,
499 :section_hash => Digest::MD5.hexdigest("wrong hash")
499 :section_hash => Digest::MD5.hexdigest("wrong hash")
500 end
500 end
501 end
501 end
502 end
502 end
503 assert_response :success
503 assert_response :success
504 assert_template 'edit'
504 assert_template 'edit'
505 assert_tag :div,
505 assert_tag :div,
506 :attributes => { :class => /error/ },
506 :attributes => { :class => /error/ },
507 :content => /Data has been updated by another user/
507 :content => /Data has been updated by another user/
508 assert_tag 'textarea',
508 assert_tag 'textarea',
509 :attributes => { :name => 'content[text]' },
509 :attributes => { :name => 'content[text]' },
510 :content => /Text should not be lost/
510 :content => /Text should not be lost/
511 assert_tag 'input',
511 assert_tag 'input',
512 :attributes => { :name => 'content[comments]', :value => 'My comments' }
512 :attributes => { :name => 'content[comments]', :value => 'My comments' }
513 end
513 end
514
514
515 def test_preview
515 def test_preview
516 @request.session[:user_id] = 2
516 @request.session[:user_id] = 2
517 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
517 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
518 :content => { :comments => '',
518 :content => { :comments => '',
519 :text => 'this is a *previewed text*',
519 :text => 'this is a *previewed text*',
520 :version => 3 }
520 :version => 3 }
521 assert_response :success
521 assert_response :success
522 assert_template 'common/_preview'
522 assert_template 'common/_preview'
523 assert_tag :tag => 'strong', :content => /previewed text/
523 assert_tag :tag => 'strong', :content => /previewed text/
524 end
524 end
525
525
526 def test_preview_new_page
526 def test_preview_new_page
527 @request.session[:user_id] = 2
527 @request.session[:user_id] = 2
528 xhr :post, :preview, :project_id => 1, :id => 'New page',
528 xhr :post, :preview, :project_id => 1, :id => 'New page',
529 :content => { :text => 'h1. New page',
529 :content => { :text => 'h1. New page',
530 :comments => '',
530 :comments => '',
531 :version => 0 }
531 :version => 0 }
532 assert_response :success
532 assert_response :success
533 assert_template 'common/_preview'
533 assert_template 'common/_preview'
534 assert_tag :tag => 'h1', :content => /New page/
534 assert_tag :tag => 'h1', :content => /New page/
535 end
535 end
536
536
537 def test_history
537 def test_history
538 @request.session[:user_id] = 2
538 @request.session[:user_id] = 2
539 get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
539 get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
540 assert_response :success
540 assert_response :success
541 assert_template 'history'
541 assert_template 'history'
542 assert_not_nil assigns(:versions)
542 assert_not_nil assigns(:versions)
543 assert_equal 3, assigns(:versions).size
543 assert_equal 3, assigns(:versions).size
544
544
545 assert_select "input[type=submit][name=commit]"
545 assert_select "input[type=submit][name=commit]"
546 assert_select 'td' do
546 assert_select 'td' do
547 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
547 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
548 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate'
548 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate'
549 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete'
549 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete'
550 end
550 end
551 end
551 end
552
552
553 def test_history_with_one_version
553 def test_history_with_one_version
554 @request.session[:user_id] = 2
554 @request.session[:user_id] = 2
555 get :history, :project_id => 'ecookbook', :id => 'Another_page'
555 get :history, :project_id => 'ecookbook', :id => 'Another_page'
556 assert_response :success
556 assert_response :success
557 assert_template 'history'
557 assert_template 'history'
558 assert_not_nil assigns(:versions)
558 assert_not_nil assigns(:versions)
559 assert_equal 1, assigns(:versions).size
559 assert_equal 1, assigns(:versions).size
560 assert_select "input[type=submit][name=commit]", false
560 assert_select "input[type=submit][name=commit]", false
561 assert_select 'td' do
561 assert_select 'td' do
562 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1'
562 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1'
563 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate'
563 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate'
564 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0
564 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0
565 end
565 end
566 end
566 end
567
567
568 def test_diff
568 def test_diff
569 content = WikiPage.find(1).content
569 content = WikiPage.find(1).content
570 assert_difference 'WikiContent::Version.count', 2 do
570 assert_difference 'WikiContent::Version.count', 2 do
571 content.text = "Line removed\nThis is a sample text for testing diffs"
571 content.text = "Line removed\nThis is a sample text for testing diffs"
572 content.save!
572 content.save!
573 content.text = "This is a sample text for testing diffs\nLine added"
573 content.text = "This is a sample text for testing diffs\nLine added"
574 content.save!
574 content.save!
575 end
575 end
576
576
577 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
577 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
578 assert_response :success
578 assert_response :success
579 assert_template 'diff'
579 assert_template 'diff'
580 assert_select 'span.diff_out', :text => 'Line removed'
580 assert_select 'span.diff_out', :text => 'Line removed'
581 assert_select 'span.diff_in', :text => 'Line added'
581 assert_select 'span.diff_in', :text => 'Line added'
582 end
582 end
583
583
584 def test_diff_with_invalid_version_should_respond_with_404
584 def test_diff_with_invalid_version_should_respond_with_404
585 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
585 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
586 assert_response 404
586 assert_response 404
587 end
587 end
588
588
589 def test_diff_with_invalid_version_from_should_respond_with_404
589 def test_diff_with_invalid_version_from_should_respond_with_404
590 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98'
590 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98'
591 assert_response 404
591 assert_response 404
592 end
592 end
593
593
594 def test_annotate
594 def test_annotate
595 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
595 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
596 assert_response :success
596 assert_response :success
597 assert_template 'annotate'
597 assert_template 'annotate'
598
598
599 # Line 1
599 # Line 1
600 assert_tag :tag => 'tr', :child => {
600 assert_tag :tag => 'tr', :child => {
601 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
601 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
602 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
602 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
603 :tag => 'td', :content => /h1\. CookBook documentation/
603 :tag => 'td', :content => /h1\. CookBook documentation/
604 }
604 }
605 }
605 }
606 }
606 }
607
607
608 # Line 5
608 # Line 5
609 assert_tag :tag => 'tr', :child => {
609 assert_tag :tag => 'tr', :child => {
610 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
610 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
611 :tag => 'td', :attributes => {:class => 'author'}, :content => /Redmine Admin/, :sibling => {
611 :tag => 'td', :attributes => {:class => 'author'}, :content => /Redmine Admin/, :sibling => {
612 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
612 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
613 }
613 }
614 }
614 }
615 }
615 }
616 end
616 end
617
617
618 def test_annotate_with_invalid_version_should_respond_with_404
618 def test_annotate_with_invalid_version_should_respond_with_404
619 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
619 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99'
620 assert_response 404
620 assert_response 404
621 end
621 end
622
622
623 def test_get_rename
623 def test_get_rename
624 @request.session[:user_id] = 2
624 @request.session[:user_id] = 2
625 get :rename, :project_id => 1, :id => 'Another_page'
625 get :rename, :project_id => 1, :id => 'Another_page'
626 assert_response :success
626 assert_response :success
627 assert_template 'rename'
627 assert_template 'rename'
628 assert_tag 'option',
628 assert_tag 'option',
629 :attributes => {:value => ''},
629 :attributes => {:value => ''},
630 :content => '',
630 :content => '',
631 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
631 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
632 assert_no_tag 'option',
632 assert_no_tag 'option',
633 :attributes => {:selected => 'selected'},
633 :attributes => {:selected => 'selected'},
634 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
634 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
635 end
635 end
636
636
637 def test_get_rename_child_page
637 def test_get_rename_child_page
638 @request.session[:user_id] = 2
638 @request.session[:user_id] = 2
639 get :rename, :project_id => 1, :id => 'Child_1'
639 get :rename, :project_id => 1, :id => 'Child_1'
640 assert_response :success
640 assert_response :success
641 assert_template 'rename'
641 assert_template 'rename'
642 assert_tag 'option',
642 assert_tag 'option',
643 :attributes => {:value => ''},
643 :attributes => {:value => ''},
644 :content => '',
644 :content => '',
645 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
645 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
646 assert_tag 'option',
646 assert_tag 'option',
647 :attributes => {:value => '2', :selected => 'selected'},
647 :attributes => {:value => '2', :selected => 'selected'},
648 :content => /Another page/,
648 :content => /Another page/,
649 :parent => {
649 :parent => {
650 :tag => 'select',
650 :tag => 'select',
651 :attributes => {:name => 'wiki_page[parent_id]'}
651 :attributes => {:name => 'wiki_page[parent_id]'}
652 }
652 }
653 end
653 end
654
654
655 def test_rename_with_redirect
655 def test_rename_with_redirect
656 @request.session[:user_id] = 2
656 @request.session[:user_id] = 2
657 post :rename, :project_id => 1, :id => 'Another_page',
657 post :rename, :project_id => 1, :id => 'Another_page',
658 :wiki_page => { :title => 'Another renamed page',
658 :wiki_page => { :title => 'Another renamed page',
659 :redirect_existing_links => 1 }
659 :redirect_existing_links => 1 }
660 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
660 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
661 wiki = Project.find(1).wiki
661 wiki = Project.find(1).wiki
662 # Check redirects
662 # Check redirects
663 assert_not_nil wiki.find_page('Another page')
663 assert_not_nil wiki.find_page('Another page')
664 assert_nil wiki.find_page('Another page', :with_redirect => false)
664 assert_nil wiki.find_page('Another page', :with_redirect => false)
665 end
665 end
666
666
667 def test_rename_without_redirect
667 def test_rename_without_redirect
668 @request.session[:user_id] = 2
668 @request.session[:user_id] = 2
669 post :rename, :project_id => 1, :id => 'Another_page',
669 post :rename, :project_id => 1, :id => 'Another_page',
670 :wiki_page => { :title => 'Another renamed page',
670 :wiki_page => { :title => 'Another renamed page',
671 :redirect_existing_links => "0" }
671 :redirect_existing_links => "0" }
672 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
672 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
673 wiki = Project.find(1).wiki
673 wiki = Project.find(1).wiki
674 # Check that there's no redirects
674 # Check that there's no redirects
675 assert_nil wiki.find_page('Another page')
675 assert_nil wiki.find_page('Another page')
676 end
676 end
677
677
678 def test_rename_with_parent_assignment
678 def test_rename_with_parent_assignment
679 @request.session[:user_id] = 2
679 @request.session[:user_id] = 2
680 post :rename, :project_id => 1, :id => 'Another_page',
680 post :rename, :project_id => 1, :id => 'Another_page',
681 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
681 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
682 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
682 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
683 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
683 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
684 end
684 end
685
685
686 def test_rename_with_parent_unassignment
686 def test_rename_with_parent_unassignment
687 @request.session[:user_id] = 2
687 @request.session[:user_id] = 2
688 post :rename, :project_id => 1, :id => 'Child_1',
688 post :rename, :project_id => 1, :id => 'Child_1',
689 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
689 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
690 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
690 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
691 assert_nil WikiPage.find_by_title('Child_1').parent
691 assert_nil WikiPage.find_by_title('Child_1').parent
692 end
692 end
693
693
694 def test_destroy_a_page_without_children_should_not_ask_confirmation
694 def test_destroy_a_page_without_children_should_not_ask_confirmation
695 @request.session[:user_id] = 2
695 @request.session[:user_id] = 2
696 delete :destroy, :project_id => 1, :id => 'Child_2'
696 delete :destroy, :project_id => 1, :id => 'Child_2'
697 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
697 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
698 end
698 end
699
699
700 def test_destroy_parent_should_ask_confirmation
700 def test_destroy_parent_should_ask_confirmation
701 @request.session[:user_id] = 2
701 @request.session[:user_id] = 2
702 assert_no_difference('WikiPage.count') do
702 assert_no_difference('WikiPage.count') do
703 delete :destroy, :project_id => 1, :id => 'Another_page'
703 delete :destroy, :project_id => 1, :id => 'Another_page'
704 end
704 end
705 assert_response :success
705 assert_response :success
706 assert_template 'destroy'
706 assert_template 'destroy'
707 assert_select 'form' do
707 assert_select 'form' do
708 assert_select 'input[name=todo][value=nullify]'
708 assert_select 'input[name=todo][value=nullify]'
709 assert_select 'input[name=todo][value=destroy]'
709 assert_select 'input[name=todo][value=destroy]'
710 assert_select 'input[name=todo][value=reassign]'
710 assert_select 'input[name=todo][value=reassign]'
711 end
711 end
712 end
712 end
713
713
714 def test_destroy_parent_with_nullify_should_delete_parent_only
714 def test_destroy_parent_with_nullify_should_delete_parent_only
715 @request.session[:user_id] = 2
715 @request.session[:user_id] = 2
716 assert_difference('WikiPage.count', -1) do
716 assert_difference('WikiPage.count', -1) do
717 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
717 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
718 end
718 end
719 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
719 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
720 assert_nil WikiPage.find_by_id(2)
720 assert_nil WikiPage.find_by_id(2)
721 end
721 end
722
722
723 def test_destroy_parent_with_cascade_should_delete_descendants
723 def test_destroy_parent_with_cascade_should_delete_descendants
724 @request.session[:user_id] = 2
724 @request.session[:user_id] = 2
725 assert_difference('WikiPage.count', -4) do
725 assert_difference('WikiPage.count', -4) do
726 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
726 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
727 end
727 end
728 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
728 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
729 assert_nil WikiPage.find_by_id(2)
729 assert_nil WikiPage.find_by_id(2)
730 assert_nil WikiPage.find_by_id(5)
730 assert_nil WikiPage.find_by_id(5)
731 end
731 end
732
732
733 def test_destroy_parent_with_reassign
733 def test_destroy_parent_with_reassign
734 @request.session[:user_id] = 2
734 @request.session[:user_id] = 2
735 assert_difference('WikiPage.count', -1) do
735 assert_difference('WikiPage.count', -1) do
736 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
736 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
737 end
737 end
738 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
738 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
739 assert_nil WikiPage.find_by_id(2)
739 assert_nil WikiPage.find_by_id(2)
740 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
740 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
741 end
741 end
742
742
743 def test_destroy_version
743 def test_destroy_version
744 @request.session[:user_id] = 2
744 @request.session[:user_id] = 2
745 assert_difference 'WikiContent::Version.count', -1 do
745 assert_difference 'WikiContent::Version.count', -1 do
746 assert_no_difference 'WikiContent.count' do
746 assert_no_difference 'WikiContent.count' do
747 assert_no_difference 'WikiPage.count' do
747 assert_no_difference 'WikiPage.count' do
748 delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2
748 delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2
749 assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history'
749 assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history'
750 end
750 end
751 end
751 end
752 end
752 end
753 end
753 end
754
754
755 def test_index
755 def test_index
756 get :index, :project_id => 'ecookbook'
756 get :index, :project_id => 'ecookbook'
757 assert_response :success
757 assert_response :success
758 assert_template 'index'
758 assert_template 'index'
759 pages = assigns(:pages)
759 pages = assigns(:pages)
760 assert_not_nil pages
760 assert_not_nil pages
761 assert_equal Project.find(1).wiki.pages.size, pages.size
761 assert_equal Project.find(1).wiki.pages.size, pages.size
762 assert_equal pages.first.content.updated_on, pages.first.updated_on
762 assert_equal pages.first.content.updated_on, pages.first.updated_on
763
763
764 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
764 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
765 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
765 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
766 :content => 'CookBook documentation' },
766 :content => 'CookBook documentation' },
767 :child => { :tag => 'ul',
767 :child => { :tag => 'ul',
768 :child => { :tag => 'li',
768 :child => { :tag => 'li',
769 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
769 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
770 :content => 'Page with an inline image' } } } },
770 :content => 'Page with an inline image' } } } },
771 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
771 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
772 :content => 'Another page' } }
772 :content => 'Another page' } }
773 end
773 end
774
774
775 def test_index_should_include_atom_link
775 def test_index_should_include_atom_link
776 get :index, :project_id => 'ecookbook'
776 get :index, :project_id => 'ecookbook'
777 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
777 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
778 end
778 end
779
779
780 def test_export_to_html
780 def test_export_to_html
781 @request.session[:user_id] = 2
781 @request.session[:user_id] = 2
782 get :export, :project_id => 'ecookbook'
782 get :export, :project_id => 'ecookbook'
783
783
784 assert_response :success
784 assert_response :success
785 assert_not_nil assigns(:pages)
785 assert_not_nil assigns(:pages)
786 assert assigns(:pages).any?
786 assert assigns(:pages).any?
787 assert_equal "text/html", @response.content_type
787 assert_equal "text/html", @response.content_type
788
788
789 assert_select "a[name=?]", "CookBook_documentation"
789 assert_select "a[name=?]", "CookBook_documentation"
790 assert_select "a[name=?]", "Another_page"
790 assert_select "a[name=?]", "Another_page"
791 assert_select "a[name=?]", "Page_with_an_inline_image"
791 assert_select "a[name=?]", "Page_with_an_inline_image"
792 end
792 end
793
793
794 def test_export_to_pdf
794 def test_export_to_pdf
795 @request.session[:user_id] = 2
795 @request.session[:user_id] = 2
796 get :export, :project_id => 'ecookbook', :format => 'pdf'
796 get :export, :project_id => 'ecookbook', :format => 'pdf'
797
797
798 assert_response :success
798 assert_response :success
799 assert_not_nil assigns(:pages)
799 assert_not_nil assigns(:pages)
800 assert assigns(:pages).any?
800 assert assigns(:pages).any?
801 assert_equal 'application/pdf', @response.content_type
801 assert_equal 'application/pdf', @response.content_type
802 assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
802 assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
803 assert @response.body.starts_with?('%PDF')
803 assert @response.body.starts_with?('%PDF')
804 end
804 end
805
805
806 def test_export_without_permission_should_be_denied
806 def test_export_without_permission_should_be_denied
807 @request.session[:user_id] = 2
807 @request.session[:user_id] = 2
808 Role.find_by_name('Manager').remove_permission! :export_wiki_pages
808 Role.find_by_name('Manager').remove_permission! :export_wiki_pages
809 get :export, :project_id => 'ecookbook'
809 get :export, :project_id => 'ecookbook'
810
810
811 assert_response 403
811 assert_response 403
812 end
812 end
813
813
814 def test_date_index
814 def test_date_index
815 get :date_index, :project_id => 'ecookbook'
815 get :date_index, :project_id => 'ecookbook'
816
816
817 assert_response :success
817 assert_response :success
818 assert_template 'date_index'
818 assert_template 'date_index'
819 assert_not_nil assigns(:pages)
819 assert_not_nil assigns(:pages)
820 assert_not_nil assigns(:pages_by_date)
820 assert_not_nil assigns(:pages_by_date)
821
821
822 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
822 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
823 end
823 end
824
824
825 def test_not_found
825 def test_not_found
826 get :show, :project_id => 999
826 get :show, :project_id => 999
827 assert_response 404
827 assert_response 404
828 end
828 end
829
829
830 def test_protect_page
830 def test_protect_page
831 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
831 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
832 assert !page.protected?
832 assert !page.protected?
833 @request.session[:user_id] = 2
833 @request.session[:user_id] = 2
834 post :protect, :project_id => 1, :id => page.title, :protected => '1'
834 post :protect, :project_id => 1, :id => page.title, :protected => '1'
835 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
835 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
836 assert page.reload.protected?
836 assert page.reload.protected?
837 end
837 end
838
838
839 def test_unprotect_page
839 def test_unprotect_page
840 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
840 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
841 assert page.protected?
841 assert page.protected?
842 @request.session[:user_id] = 2
842 @request.session[:user_id] = 2
843 post :protect, :project_id => 1, :id => page.title, :protected => '0'
843 post :protect, :project_id => 1, :id => page.title, :protected => '0'
844 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
844 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
845 assert !page.reload.protected?
845 assert !page.reload.protected?
846 end
846 end
847
847
848 def test_show_page_with_edit_link
848 def test_show_page_with_edit_link
849 @request.session[:user_id] = 2
849 @request.session[:user_id] = 2
850 get :show, :project_id => 1
850 get :show, :project_id => 1
851 assert_response :success
851 assert_response :success
852 assert_template 'show'
852 assert_template 'show'
853 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
853 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
854 end
854 end
855
855
856 def test_show_page_without_edit_link
856 def test_show_page_without_edit_link
857 @request.session[:user_id] = 4
857 @request.session[:user_id] = 4
858 get :show, :project_id => 1
858 get :show, :project_id => 1
859 assert_response :success
859 assert_response :success
860 assert_template 'show'
860 assert_template 'show'
861 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
861 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
862 end
862 end
863
863
864 def test_show_pdf
864 def test_show_pdf
865 @request.session[:user_id] = 2
865 @request.session[:user_id] = 2
866 get :show, :project_id => 1, :format => 'pdf'
866 get :show, :project_id => 1, :format => 'pdf'
867 assert_response :success
867 assert_response :success
868 assert_not_nil assigns(:page)
868 assert_not_nil assigns(:page)
869 assert_equal 'application/pdf', @response.content_type
869 assert_equal 'application/pdf', @response.content_type
870 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
870 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
871 @response.headers['Content-Disposition']
871 @response.headers['Content-Disposition']
872 end
872 end
873
873
874 def test_show_html
874 def test_show_html
875 @request.session[:user_id] = 2
875 @request.session[:user_id] = 2
876 get :show, :project_id => 1, :format => 'html'
876 get :show, :project_id => 1, :format => 'html'
877 assert_response :success
877 assert_response :success
878 assert_not_nil assigns(:page)
878 assert_not_nil assigns(:page)
879 assert_equal 'text/html', @response.content_type
879 assert_equal 'text/html', @response.content_type
880 assert_equal 'attachment; filename="CookBook_documentation.html"',
880 assert_equal 'attachment; filename="CookBook_documentation.html"',
881 @response.headers['Content-Disposition']
881 @response.headers['Content-Disposition']
882 assert_tag 'h1', :content => 'CookBook documentation'
882 assert_tag 'h1', :content => 'CookBook documentation'
883 end
883 end
884
884
885 def test_show_versioned_html
885 def test_show_versioned_html
886 @request.session[:user_id] = 2
886 @request.session[:user_id] = 2
887 get :show, :project_id => 1, :format => 'html', :version => 2
887 get :show, :project_id => 1, :format => 'html', :version => 2
888 assert_response :success
888 assert_response :success
889 assert_not_nil assigns(:content)
889 assert_not_nil assigns(:content)
890 assert_equal 2, assigns(:content).version
890 assert_equal 2, assigns(:content).version
891 assert_equal 'text/html', @response.content_type
891 assert_equal 'text/html', @response.content_type
892 assert_equal 'attachment; filename="CookBook_documentation.html"',
892 assert_equal 'attachment; filename="CookBook_documentation.html"',
893 @response.headers['Content-Disposition']
893 @response.headers['Content-Disposition']
894 assert_tag 'h1', :content => 'CookBook documentation'
894 assert_tag 'h1', :content => 'CookBook documentation'
895 end
895 end
896
896
897 def test_show_txt
897 def test_show_txt
898 @request.session[:user_id] = 2
898 @request.session[:user_id] = 2
899 get :show, :project_id => 1, :format => 'txt'
899 get :show, :project_id => 1, :format => 'txt'
900 assert_response :success
900 assert_response :success
901 assert_not_nil assigns(:page)
901 assert_not_nil assigns(:page)
902 assert_equal 'text/plain', @response.content_type
902 assert_equal 'text/plain', @response.content_type
903 assert_equal 'attachment; filename="CookBook_documentation.txt"',
903 assert_equal 'attachment; filename="CookBook_documentation.txt"',
904 @response.headers['Content-Disposition']
904 @response.headers['Content-Disposition']
905 assert_include 'h1. CookBook documentation', @response.body
905 assert_include 'h1. CookBook documentation', @response.body
906 end
906 end
907
907
908 def test_show_versioned_txt
908 def test_show_versioned_txt
909 @request.session[:user_id] = 2
909 @request.session[:user_id] = 2
910 get :show, :project_id => 1, :format => 'txt', :version => 2
910 get :show, :project_id => 1, :format => 'txt', :version => 2
911 assert_response :success
911 assert_response :success
912 assert_not_nil assigns(:content)
912 assert_not_nil assigns(:content)
913 assert_equal 2, assigns(:content).version
913 assert_equal 2, assigns(:content).version
914 assert_equal 'text/plain', @response.content_type
914 assert_equal 'text/plain', @response.content_type
915 assert_equal 'attachment; filename="CookBook_documentation.txt"',
915 assert_equal 'attachment; filename="CookBook_documentation.txt"',
916 @response.headers['Content-Disposition']
916 @response.headers['Content-Disposition']
917 assert_include 'h1. CookBook documentation', @response.body
917 assert_include 'h1. CookBook documentation', @response.body
918 end
918 end
919
919
920 def test_edit_unprotected_page
920 def test_edit_unprotected_page
921 # Non members can edit unprotected wiki pages
921 # Non members can edit unprotected wiki pages
922 @request.session[:user_id] = 4
922 @request.session[:user_id] = 4
923 get :edit, :project_id => 1, :id => 'Another_page'
923 get :edit, :project_id => 1, :id => 'Another_page'
924 assert_response :success
924 assert_response :success
925 assert_template 'edit'
925 assert_template 'edit'
926 end
926 end
927
927
928 def test_edit_protected_page_by_nonmember
928 def test_edit_protected_page_by_nonmember
929 # Non members can't edit protected wiki pages
929 # Non members can't edit protected wiki pages
930 @request.session[:user_id] = 4
930 @request.session[:user_id] = 4
931 get :edit, :project_id => 1, :id => 'CookBook_documentation'
931 get :edit, :project_id => 1, :id => 'CookBook_documentation'
932 assert_response 403
932 assert_response 403
933 end
933 end
934
934
935 def test_edit_protected_page_by_member
935 def test_edit_protected_page_by_member
936 @request.session[:user_id] = 2
936 @request.session[:user_id] = 2
937 get :edit, :project_id => 1, :id => 'CookBook_documentation'
937 get :edit, :project_id => 1, :id => 'CookBook_documentation'
938 assert_response :success
938 assert_response :success
939 assert_template 'edit'
939 assert_template 'edit'
940 end
940 end
941
941
942 def test_history_of_non_existing_page_should_return_404
942 def test_history_of_non_existing_page_should_return_404
943 get :history, :project_id => 1, :id => 'Unknown_page'
943 get :history, :project_id => 1, :id => 'Unknown_page'
944 assert_response 404
944 assert_response 404
945 end
945 end
946
946
947 def test_add_attachment
947 def test_add_attachment
948 @request.session[:user_id] = 2
948 @request.session[:user_id] = 2
949 assert_difference 'Attachment.count' do
949 assert_difference 'Attachment.count' do
950 post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
950 post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
951 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
951 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
952 end
952 end
953 attachment = Attachment.first(:order => 'id DESC')
953 attachment = Attachment.first(:order => 'id DESC')
954 assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
954 assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
955 end
955 end
956 end
956 end
General Comments 0
You need to be logged in to leave comments. Login now