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