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