@@ -1,1269 +1,1274 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | require 'forwardable' |
|
21 | 21 | require 'cgi' |
|
22 | 22 | |
|
23 | 23 | module ApplicationHelper |
|
24 | 24 | include Redmine::WikiFormatting::Macros::Definitions |
|
25 | 25 | include Redmine::I18n |
|
26 | 26 | include GravatarHelper::PublicMethods |
|
27 | 27 | |
|
28 | 28 | extend Forwardable |
|
29 | 29 | def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
|
30 | 30 | |
|
31 | 31 | # Return true if user is authorized for controller/action, otherwise false |
|
32 | 32 | def authorize_for(controller, action) |
|
33 | 33 | User.current.allowed_to?({:controller => controller, :action => action}, @project) |
|
34 | 34 | end |
|
35 | 35 | |
|
36 | 36 | # Display a link if user is authorized |
|
37 | 37 | # |
|
38 | 38 | # @param [String] name Anchor text (passed to link_to) |
|
39 | 39 | # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized |
|
40 | 40 | # @param [optional, Hash] html_options Options passed to link_to |
|
41 | 41 | # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to |
|
42 | 42 | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) |
|
43 | 43 | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
|
44 | 44 | end |
|
45 | 45 | |
|
46 | 46 | # Displays a link to user's account page if active |
|
47 | 47 | def link_to_user(user, options={}) |
|
48 | 48 | if user.is_a?(User) |
|
49 | 49 | name = h(user.name(options[:format])) |
|
50 | 50 | if user.active? |
|
51 | 51 | link_to name, :controller => 'users', :action => 'show', :id => user |
|
52 | 52 | else |
|
53 | 53 | name |
|
54 | 54 | end |
|
55 | 55 | else |
|
56 | 56 | h(user.to_s) |
|
57 | 57 | end |
|
58 | 58 | end |
|
59 | 59 | |
|
60 | 60 | # Displays a link to +issue+ with its subject. |
|
61 | 61 | # Examples: |
|
62 | 62 | # |
|
63 | 63 | # link_to_issue(issue) # => Defect #6: This is the subject |
|
64 | 64 | # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... |
|
65 | 65 | # link_to_issue(issue, :subject => false) # => Defect #6 |
|
66 | 66 | # link_to_issue(issue, :project => true) # => Foo - Defect #6 |
|
67 | 67 | # |
|
68 | 68 | def link_to_issue(issue, options={}) |
|
69 | 69 | title = nil |
|
70 | 70 | subject = nil |
|
71 | 71 | if options[:subject] == false |
|
72 | 72 | title = truncate(issue.subject, :length => 60) |
|
73 | 73 | else |
|
74 | 74 | subject = issue.subject |
|
75 | 75 | if options[:truncate] |
|
76 | 76 | subject = truncate(subject, :length => options[:truncate]) |
|
77 | 77 | end |
|
78 | 78 | end |
|
79 | 79 | s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, |
|
80 | 80 | :class => issue.css_classes, |
|
81 | 81 | :title => title |
|
82 | 82 | s << h(": #{subject}") if subject |
|
83 | 83 | s = h("#{issue.project} - ") + s if options[:project] |
|
84 | 84 | s |
|
85 | 85 | end |
|
86 | 86 | |
|
87 | 87 | # Generates a link to an attachment. |
|
88 | 88 | # Options: |
|
89 | 89 | # * :text - Link text (default to attachment filename) |
|
90 | 90 | # * :download - Force download (default: false) |
|
91 | 91 | def link_to_attachment(attachment, options={}) |
|
92 | 92 | text = options.delete(:text) || attachment.filename |
|
93 | 93 | action = options.delete(:download) ? 'download' : 'show' |
|
94 | 94 | opt_only_path = {} |
|
95 | 95 | opt_only_path[:only_path] = (options[:only_path] == false ? false : true) |
|
96 | 96 | options.delete(:only_path) |
|
97 | 97 | link_to(h(text), |
|
98 | 98 | {:controller => 'attachments', :action => action, |
|
99 | 99 | :id => attachment, :filename => attachment.filename}.merge(opt_only_path), |
|
100 | 100 | options) |
|
101 | 101 | end |
|
102 | 102 | |
|
103 | 103 | # Generates a link to a SCM revision |
|
104 | 104 | # Options: |
|
105 | 105 | # * :text - Link text (default to the formatted revision) |
|
106 | 106 | def link_to_revision(revision, repository, options={}) |
|
107 | 107 | if repository.is_a?(Project) |
|
108 | 108 | repository = repository.repository |
|
109 | 109 | end |
|
110 | 110 | text = options.delete(:text) || format_revision(revision) |
|
111 | 111 | rev = revision.respond_to?(:identifier) ? revision.identifier : revision |
|
112 | 112 | link_to( |
|
113 | 113 | h(text), |
|
114 | 114 | {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, |
|
115 | 115 | :title => l(:label_revision_id, format_revision(revision)) |
|
116 | 116 | ) |
|
117 | 117 | end |
|
118 | 118 | |
|
119 | 119 | # Generates a link to a message |
|
120 | 120 | def link_to_message(message, options={}, html_options = nil) |
|
121 | 121 | link_to( |
|
122 | 122 | h(truncate(message.subject, :length => 60)), |
|
123 | 123 | { :controller => 'messages', :action => 'show', |
|
124 | 124 | :board_id => message.board_id, |
|
125 | 125 | :id => (message.parent_id || message.id), |
|
126 | 126 | :r => (message.parent_id && message.id), |
|
127 | 127 | :anchor => (message.parent_id ? "message-#{message.id}" : nil) |
|
128 | 128 | }.merge(options), |
|
129 | 129 | html_options |
|
130 | 130 | ) |
|
131 | 131 | end |
|
132 | 132 | |
|
133 | 133 | # Generates a link to a project if active |
|
134 | 134 | # Examples: |
|
135 | 135 | # |
|
136 | 136 | # link_to_project(project) # => link to the specified project overview |
|
137 | 137 | # link_to_project(project, :action=>'settings') # => link to project settings |
|
138 | 138 | # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options |
|
139 | 139 | # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) |
|
140 | 140 | # |
|
141 | 141 | def link_to_project(project, options={}, html_options = nil) |
|
142 | 142 | if project.archived? |
|
143 | 143 | h(project) |
|
144 | 144 | else |
|
145 | 145 | url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) |
|
146 | 146 | link_to(h(project), url, html_options) |
|
147 | 147 | end |
|
148 | 148 | end |
|
149 | 149 | |
|
150 | 150 | def thumbnail_tag(attachment) |
|
151 | 151 | link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), |
|
152 | 152 | {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, |
|
153 | 153 | :title => attachment.filename |
|
154 | 154 | end |
|
155 | 155 | |
|
156 | 156 | def toggle_link(name, id, options={}) |
|
157 | 157 | onclick = "$('##{id}').toggle(); " |
|
158 | 158 | onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") |
|
159 | 159 | onclick << "return false;" |
|
160 | 160 | link_to(name, "#", :onclick => onclick) |
|
161 | 161 | end |
|
162 | 162 | |
|
163 | 163 | def image_to_function(name, function, html_options = {}) |
|
164 | 164 | html_options.symbolize_keys! |
|
165 | 165 | tag(:input, html_options.merge({ |
|
166 | 166 | :type => "image", :src => image_path(name), |
|
167 | 167 | :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" |
|
168 | 168 | })) |
|
169 | 169 | end |
|
170 | 170 | |
|
171 | 171 | def format_activity_title(text) |
|
172 | 172 | h(truncate_single_line(text, :length => 100)) |
|
173 | 173 | end |
|
174 | 174 | |
|
175 | 175 | def format_activity_day(date) |
|
176 | 176 | date == User.current.today ? l(:label_today).titleize : format_date(date) |
|
177 | 177 | end |
|
178 | 178 | |
|
179 | 179 | def format_activity_description(text) |
|
180 | 180 | h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') |
|
181 | 181 | ).gsub(/[\r\n]+/, "<br />").html_safe |
|
182 | 182 | end |
|
183 | 183 | |
|
184 | 184 | def format_version_name(version) |
|
185 | 185 | if version.project == @project |
|
186 | 186 | h(version) |
|
187 | 187 | else |
|
188 | 188 | h("#{version.project} - #{version}") |
|
189 | 189 | end |
|
190 | 190 | end |
|
191 | 191 | |
|
192 | 192 | def due_date_distance_in_words(date) |
|
193 | 193 | if date |
|
194 | 194 | l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) |
|
195 | 195 | end |
|
196 | 196 | end |
|
197 | 197 | |
|
198 | 198 | # Renders a tree of projects as a nested set of unordered lists |
|
199 | 199 | # The given collection may be a subset of the whole project tree |
|
200 | 200 | # (eg. some intermediate nodes are private and can not be seen) |
|
201 | 201 | def render_project_nested_lists(projects) |
|
202 | 202 | s = '' |
|
203 | 203 | if projects.any? |
|
204 | 204 | ancestors = [] |
|
205 | 205 | original_project = @project |
|
206 | 206 | projects.sort_by(&:lft).each do |project| |
|
207 | 207 | # set the project environment to please macros. |
|
208 | 208 | @project = project |
|
209 | 209 | if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) |
|
210 | 210 | s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" |
|
211 | 211 | else |
|
212 | 212 | ancestors.pop |
|
213 | 213 | s << "</li>" |
|
214 | 214 | while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
|
215 | 215 | ancestors.pop |
|
216 | 216 | s << "</ul></li>\n" |
|
217 | 217 | end |
|
218 | 218 | end |
|
219 | 219 | classes = (ancestors.empty? ? 'root' : 'child') |
|
220 | 220 | s << "<li class='#{classes}'><div class='#{classes}'>" |
|
221 | 221 | s << h(block_given? ? yield(project) : project.name) |
|
222 | 222 | s << "</div>\n" |
|
223 | 223 | ancestors << project |
|
224 | 224 | end |
|
225 | 225 | s << ("</li></ul>\n" * ancestors.size) |
|
226 | 226 | @project = original_project |
|
227 | 227 | end |
|
228 | 228 | s.html_safe |
|
229 | 229 | end |
|
230 | 230 | |
|
231 | 231 | def render_page_hierarchy(pages, node=nil, options={}) |
|
232 | 232 | content = '' |
|
233 | 233 | if pages[node] |
|
234 | 234 | content << "<ul class=\"pages-hierarchy\">\n" |
|
235 | 235 | pages[node].each do |page| |
|
236 | 236 | content << "<li>" |
|
237 | 237 | content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}, |
|
238 | 238 | :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) |
|
239 | 239 | content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] |
|
240 | 240 | content << "</li>\n" |
|
241 | 241 | end |
|
242 | 242 | content << "</ul>\n" |
|
243 | 243 | end |
|
244 | 244 | content.html_safe |
|
245 | 245 | end |
|
246 | 246 | |
|
247 | 247 | # Renders flash messages |
|
248 | 248 | def render_flash_messages |
|
249 | 249 | s = '' |
|
250 | 250 | flash.each do |k,v| |
|
251 | 251 | s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") |
|
252 | 252 | end |
|
253 | 253 | s.html_safe |
|
254 | 254 | end |
|
255 | 255 | |
|
256 | 256 | # Renders tabs and their content |
|
257 | 257 | def render_tabs(tabs) |
|
258 | 258 | if tabs.any? |
|
259 | 259 | render :partial => 'common/tabs', :locals => {:tabs => tabs} |
|
260 | 260 | else |
|
261 | 261 | content_tag 'p', l(:label_no_data), :class => "nodata" |
|
262 | 262 | end |
|
263 | 263 | end |
|
264 | 264 | |
|
265 | 265 | # Renders the project quick-jump box |
|
266 | 266 | def render_project_jump_box |
|
267 | 267 | return unless User.current.logged? |
|
268 | 268 | projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq |
|
269 | 269 | if projects.any? |
|
270 | 270 | options = |
|
271 | 271 | ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" + |
|
272 | 272 | '<option value="" disabled="disabled">---</option>').html_safe |
|
273 | 273 | |
|
274 | 274 | options << project_tree_options_for_select(projects, :selected => @project) do |p| |
|
275 | 275 | { :value => project_path(:id => p, :jump => current_menu_item) } |
|
276 | 276 | end |
|
277 | 277 | |
|
278 | 278 | select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') |
|
279 | 279 | end |
|
280 | 280 | end |
|
281 | 281 | |
|
282 | 282 | def project_tree_options_for_select(projects, options = {}) |
|
283 | 283 | s = '' |
|
284 | 284 | project_tree(projects) do |project, level| |
|
285 | 285 | name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe |
|
286 | 286 | tag_options = {:value => project.id} |
|
287 | 287 | if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) |
|
288 | 288 | tag_options[:selected] = 'selected' |
|
289 | 289 | else |
|
290 | 290 | tag_options[:selected] = nil |
|
291 | 291 | end |
|
292 | 292 | tag_options.merge!(yield(project)) if block_given? |
|
293 | 293 | s << content_tag('option', name_prefix + h(project), tag_options) |
|
294 | 294 | end |
|
295 | 295 | s.html_safe |
|
296 | 296 | end |
|
297 | 297 | |
|
298 | 298 | # Yields the given block for each project with its level in the tree |
|
299 | 299 | # |
|
300 | 300 | # Wrapper for Project#project_tree |
|
301 | 301 | def project_tree(projects, &block) |
|
302 | 302 | Project.project_tree(projects, &block) |
|
303 | 303 | end |
|
304 | 304 | |
|
305 | 305 | def principals_check_box_tags(name, principals) |
|
306 | 306 | s = '' |
|
307 | 307 | principals.sort.each do |principal| |
|
308 | 308 | s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n" |
|
309 | 309 | end |
|
310 | 310 | s.html_safe |
|
311 | 311 | end |
|
312 | 312 | |
|
313 | 313 | # Returns a string for users/groups option tags |
|
314 | 314 | def principals_options_for_select(collection, selected=nil) |
|
315 | 315 | s = '' |
|
316 | 316 | if collection.include?(User.current) |
|
317 | 317 | s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) |
|
318 | 318 | end |
|
319 | 319 | groups = '' |
|
320 | 320 | collection.sort.each do |element| |
|
321 | 321 | selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) |
|
322 | 322 | (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>) |
|
323 | 323 | end |
|
324 | 324 | unless groups.empty? |
|
325 | 325 | s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>) |
|
326 | 326 | end |
|
327 | 327 | s.html_safe |
|
328 | 328 | end |
|
329 | 329 | |
|
330 | 330 | # Truncates and returns the string as a single line |
|
331 | 331 | def truncate_single_line(string, *args) |
|
332 | 332 | truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') |
|
333 | 333 | end |
|
334 | 334 | |
|
335 | 335 | # Truncates at line break after 250 characters or options[:length] |
|
336 | 336 | def truncate_lines(string, options={}) |
|
337 | 337 | length = options[:length] || 250 |
|
338 | 338 | if string.to_s =~ /\A(.{#{length}}.*?)$/m |
|
339 | 339 | "#{$1}..." |
|
340 | 340 | else |
|
341 | 341 | string |
|
342 | 342 | end |
|
343 | 343 | end |
|
344 | 344 | |
|
345 | 345 | def anchor(text) |
|
346 | 346 | text.to_s.gsub(' ', '_') |
|
347 | 347 | end |
|
348 | 348 | |
|
349 | 349 | def html_hours(text) |
|
350 | 350 | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe |
|
351 | 351 | end |
|
352 | 352 | |
|
353 | 353 | def authoring(created, author, options={}) |
|
354 | 354 | l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe |
|
355 | 355 | end |
|
356 | 356 | |
|
357 | 357 | def time_tag(time) |
|
358 | 358 | text = distance_of_time_in_words(Time.now, time) |
|
359 | 359 | if @project |
|
360 | 360 | link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) |
|
361 | 361 | else |
|
362 | 362 | content_tag('acronym', text, :title => format_time(time)) |
|
363 | 363 | end |
|
364 | 364 | end |
|
365 | 365 | |
|
366 | 366 | def syntax_highlight_lines(name, content) |
|
367 | 367 | lines = [] |
|
368 | 368 | syntax_highlight(name, content).each_line { |line| lines << line } |
|
369 | 369 | lines |
|
370 | 370 | end |
|
371 | 371 | |
|
372 | 372 | def syntax_highlight(name, content) |
|
373 | 373 | Redmine::SyntaxHighlighting.highlight_by_filename(content, name) |
|
374 | 374 | end |
|
375 | 375 | |
|
376 | 376 | def to_path_param(path) |
|
377 | 377 | str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") |
|
378 | 378 | str.blank? ? nil : str |
|
379 | 379 | end |
|
380 | 380 | |
|
381 | 381 | def pagination_links_full(paginator, count=nil, options={}) |
|
382 | 382 | page_param = options.delete(:page_param) || :page |
|
383 | 383 | per_page_links = options.delete(:per_page_links) |
|
384 | 384 | url_param = params.dup |
|
385 | 385 | |
|
386 | 386 | html = '' |
|
387 | 387 | if paginator.current.previous |
|
388 | 388 | # \xc2\xab(utf-8) = « |
|
389 | 389 | html << link_to_content_update( |
|
390 | 390 | "\xc2\xab " + l(:label_previous), |
|
391 | 391 | url_param.merge(page_param => paginator.current.previous)) + ' ' |
|
392 | 392 | end |
|
393 | 393 | |
|
394 | 394 | html << (pagination_links_each(paginator, options) do |n| |
|
395 | 395 | link_to_content_update(n.to_s, url_param.merge(page_param => n)) |
|
396 | 396 | end || '') |
|
397 | 397 | |
|
398 | 398 | if paginator.current.next |
|
399 | 399 | # \xc2\xbb(utf-8) = » |
|
400 | 400 | html << ' ' + link_to_content_update( |
|
401 | 401 | (l(:label_next) + " \xc2\xbb"), |
|
402 | 402 | url_param.merge(page_param => paginator.current.next)) |
|
403 | 403 | end |
|
404 | 404 | |
|
405 | 405 | unless count.nil? |
|
406 | 406 | html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" |
|
407 | 407 | if per_page_links != false && links = per_page_links(paginator.items_per_page, count) |
|
408 | 408 | html << " | #{links}" |
|
409 | 409 | end |
|
410 | 410 | end |
|
411 | 411 | |
|
412 | 412 | html.html_safe |
|
413 | 413 | end |
|
414 | 414 | |
|
415 | 415 | def per_page_links(selected=nil, item_count=nil) |
|
416 | 416 | values = Setting.per_page_options_array |
|
417 | 417 | if item_count && values.any? |
|
418 | 418 | if item_count > values.first |
|
419 | 419 | max = values.detect {|value| value >= item_count} || item_count |
|
420 | 420 | else |
|
421 | 421 | max = item_count |
|
422 | 422 | end |
|
423 | 423 | values = values.select {|value| value <= max || value == selected} |
|
424 | 424 | end |
|
425 | 425 | if values.empty? || (values.size == 1 && values.first == selected) |
|
426 | 426 | return nil |
|
427 | 427 | end |
|
428 | 428 | links = values.collect do |n| |
|
429 | 429 | n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) |
|
430 | 430 | end |
|
431 | 431 | l(:label_display_per_page, links.join(', ')) |
|
432 | 432 | end |
|
433 | 433 | |
|
434 | 434 | def reorder_links(name, url, method = :post) |
|
435 | 435 | link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), |
|
436 | 436 | url.merge({"#{name}[move_to]" => 'highest'}), |
|
437 | 437 | :method => method, :title => l(:label_sort_highest)) + |
|
438 | 438 | link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), |
|
439 | 439 | url.merge({"#{name}[move_to]" => 'higher'}), |
|
440 | 440 | :method => method, :title => l(:label_sort_higher)) + |
|
441 | 441 | link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), |
|
442 | 442 | url.merge({"#{name}[move_to]" => 'lower'}), |
|
443 | 443 | :method => method, :title => l(:label_sort_lower)) + |
|
444 | 444 | link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), |
|
445 | 445 | url.merge({"#{name}[move_to]" => 'lowest'}), |
|
446 | 446 | :method => method, :title => l(:label_sort_lowest)) |
|
447 | 447 | end |
|
448 | 448 | |
|
449 | 449 | def breadcrumb(*args) |
|
450 | 450 | elements = args.flatten |
|
451 | 451 | elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil |
|
452 | 452 | end |
|
453 | 453 | |
|
454 | 454 | def other_formats_links(&block) |
|
455 | 455 | concat('<p class="other-formats">'.html_safe + l(:label_export_to)) |
|
456 | 456 | yield Redmine::Views::OtherFormatsBuilder.new(self) |
|
457 | 457 | concat('</p>'.html_safe) |
|
458 | 458 | end |
|
459 | 459 | |
|
460 | 460 | def page_header_title |
|
461 | 461 | if @project.nil? || @project.new_record? |
|
462 | 462 | h(Setting.app_title) |
|
463 | 463 | else |
|
464 | 464 | b = [] |
|
465 | 465 | ancestors = (@project.root? ? [] : @project.ancestors.visible.all) |
|
466 | 466 | if ancestors.any? |
|
467 | 467 | root = ancestors.shift |
|
468 | 468 | b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') |
|
469 | 469 | if ancestors.size > 2 |
|
470 | 470 | b << "\xe2\x80\xa6" |
|
471 | 471 | ancestors = ancestors[-2, 2] |
|
472 | 472 | end |
|
473 | 473 | b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } |
|
474 | 474 | end |
|
475 | 475 | b << h(@project) |
|
476 | 476 | b.join(" \xc2\xbb ").html_safe |
|
477 | 477 | end |
|
478 | 478 | end |
|
479 | 479 | |
|
480 | 480 | def html_title(*args) |
|
481 | 481 | if args.empty? |
|
482 | 482 | title = @html_title || [] |
|
483 | 483 | title << @project.name if @project |
|
484 | 484 | title << Setting.app_title unless Setting.app_title == title.last |
|
485 | 485 | title.select {|t| !t.blank? }.join(' - ') |
|
486 | 486 | else |
|
487 | 487 | @html_title ||= [] |
|
488 | 488 | @html_title += args |
|
489 | 489 | end |
|
490 | 490 | end |
|
491 | 491 | |
|
492 | 492 | # Returns the theme, controller name, and action as css classes for the |
|
493 | 493 | # HTML body. |
|
494 | 494 | def body_css_classes |
|
495 | 495 | css = [] |
|
496 | 496 | if theme = Redmine::Themes.theme(Setting.ui_theme) |
|
497 | 497 | css << 'theme-' + theme.name |
|
498 | 498 | end |
|
499 | 499 | |
|
500 | 500 | css << 'controller-' + controller_name |
|
501 | 501 | css << 'action-' + action_name |
|
502 | 502 | css.join(' ') |
|
503 | 503 | end |
|
504 | 504 | |
|
505 | 505 | def accesskey(s) |
|
506 | 506 | Redmine::AccessKeys.key_for s |
|
507 | 507 | end |
|
508 | 508 | |
|
509 | 509 | # Formats text according to system settings. |
|
510 | 510 | # 2 ways to call this method: |
|
511 | 511 | # * with a String: textilizable(text, options) |
|
512 | 512 | # * with an object and one of its attribute: textilizable(issue, :description, options) |
|
513 | 513 | def textilizable(*args) |
|
514 | 514 | options = args.last.is_a?(Hash) ? args.pop : {} |
|
515 | 515 | case args.size |
|
516 | 516 | when 1 |
|
517 | 517 | obj = options[:object] |
|
518 | 518 | text = args.shift |
|
519 | 519 | when 2 |
|
520 | 520 | obj = args.shift |
|
521 | 521 | attr = args.shift |
|
522 | 522 | text = obj.send(attr).to_s |
|
523 | 523 | else |
|
524 | 524 | raise ArgumentError, 'invalid arguments to textilizable' |
|
525 | 525 | end |
|
526 | 526 | return '' if text.blank? |
|
527 | 527 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
|
528 | 528 | only_path = options.delete(:only_path) == false ? false : true |
|
529 | 529 | |
|
530 | 530 | text = text.dup |
|
531 | 531 | macros = catch_macros(text) |
|
532 | 532 | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) |
|
533 | 533 | |
|
534 | 534 | @parsed_headings = [] |
|
535 | 535 | @heading_anchors = {} |
|
536 | 536 | @current_section = 0 if options[:edit_section_links] |
|
537 | 537 | |
|
538 | 538 | parse_sections(text, project, obj, attr, only_path, options) |
|
539 | 539 | text = parse_non_pre_blocks(text, obj, macros) do |text| |
|
540 | 540 | [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| |
|
541 | 541 | send method_name, text, project, obj, attr, only_path, options |
|
542 | 542 | end |
|
543 | 543 | end |
|
544 | 544 | parse_headings(text, project, obj, attr, only_path, options) |
|
545 | 545 | |
|
546 | 546 | if @parsed_headings.any? |
|
547 | 547 | replace_toc(text, @parsed_headings) |
|
548 | 548 | end |
|
549 | 549 | |
|
550 | 550 | text.html_safe |
|
551 | 551 | end |
|
552 | 552 | |
|
553 | 553 | def parse_non_pre_blocks(text, obj, macros) |
|
554 | 554 | s = StringScanner.new(text) |
|
555 | 555 | tags = [] |
|
556 | 556 | parsed = '' |
|
557 | 557 | while !s.eos? |
|
558 | 558 | s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) |
|
559 | 559 | text, full_tag, closing, tag = s[1], s[2], s[3], s[4] |
|
560 | 560 | if tags.empty? |
|
561 | 561 | yield text |
|
562 | 562 | inject_macros(text, obj, macros) if macros.any? |
|
563 | 563 | else |
|
564 | 564 | inject_macros(text, obj, macros, false) if macros.any? |
|
565 | 565 | end |
|
566 | 566 | parsed << text |
|
567 | 567 | if tag |
|
568 | 568 | if closing |
|
569 | 569 | if tags.last == tag.downcase |
|
570 | 570 | tags.pop |
|
571 | 571 | end |
|
572 | 572 | else |
|
573 | 573 | tags << tag.downcase |
|
574 | 574 | end |
|
575 | 575 | parsed << full_tag |
|
576 | 576 | end |
|
577 | 577 | end |
|
578 | 578 | # Close any non closing tags |
|
579 | 579 | while tag = tags.pop |
|
580 | 580 | parsed << "</#{tag}>" |
|
581 | 581 | end |
|
582 | 582 | parsed |
|
583 | 583 | end |
|
584 | 584 | |
|
585 | 585 | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
|
586 | 586 | # when using an image link, try to use an attachment, if possible |
|
587 | 587 | if options[:attachments] || (obj && obj.respond_to?(:attachments)) |
|
588 | 588 | attachments = options[:attachments] || obj.attachments |
|
589 | 589 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
|
590 | 590 | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
|
591 | 591 | # search for the picture in attachments |
|
592 | 592 | if found = Attachment.latest_attach(attachments, filename) |
|
593 | 593 | image_url = url_for :only_path => only_path, :controller => 'attachments', |
|
594 | 594 | :action => 'download', :id => found |
|
595 | 595 | desc = found.description.to_s.gsub('"', '') |
|
596 | 596 | if !desc.blank? && alttext.blank? |
|
597 | 597 | alt = " title=\"#{desc}\" alt=\"#{desc}\"" |
|
598 | 598 | end |
|
599 | 599 | "src=\"#{image_url}\"#{alt}" |
|
600 | 600 | else |
|
601 | 601 | m |
|
602 | 602 | end |
|
603 | 603 | end |
|
604 | 604 | end |
|
605 | 605 | end |
|
606 | 606 | |
|
607 | 607 | # Wiki links |
|
608 | 608 | # |
|
609 | 609 | # Examples: |
|
610 | 610 | # [[mypage]] |
|
611 | 611 | # [[mypage|mytext]] |
|
612 | 612 | # wiki links can refer other project wikis, using project name or identifier: |
|
613 | 613 | # [[project:]] -> wiki starting page |
|
614 | 614 | # [[project:|mytext]] |
|
615 | 615 | # [[project:mypage]] |
|
616 | 616 | # [[project:mypage|mytext]] |
|
617 | 617 | def parse_wiki_links(text, project, obj, attr, only_path, options) |
|
618 | 618 | text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
|
619 | 619 | link_project = project |
|
620 | 620 | esc, all, page, title = $1, $2, $3, $5 |
|
621 | 621 | if esc.nil? |
|
622 | 622 | if page =~ /^([^\:]+)\:(.*)$/ |
|
623 | 623 | link_project = Project.find_by_identifier($1) || Project.find_by_name($1) |
|
624 | 624 | page = $2 |
|
625 | 625 | title ||= $1 if page.blank? |
|
626 | 626 | end |
|
627 | 627 | |
|
628 | 628 | if link_project && link_project.wiki |
|
629 | 629 | # extract anchor |
|
630 | 630 | anchor = nil |
|
631 | 631 | if page =~ /^(.+?)\#(.+)$/ |
|
632 | 632 | page, anchor = $1, $2 |
|
633 | 633 | end |
|
634 | 634 | anchor = sanitize_anchor_name(anchor) if anchor.present? |
|
635 | 635 | # check if page exists |
|
636 | 636 | wiki_page = link_project.wiki.find_page(page) |
|
637 | 637 | url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page |
|
638 | 638 | "##{anchor}" |
|
639 | 639 | else |
|
640 | 640 | case options[:wiki_links] |
|
641 | 641 | when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') |
|
642 | 642 | when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export |
|
643 | 643 | else |
|
644 | 644 | wiki_page_id = page.present? ? Wiki.titleize(page) : nil |
|
645 | 645 | parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil |
|
646 | 646 | url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, |
|
647 | 647 | :id => wiki_page_id, :anchor => anchor, :parent => parent) |
|
648 | 648 | end |
|
649 | 649 | end |
|
650 | 650 | link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) |
|
651 | 651 | else |
|
652 | 652 | # project or wiki doesn't exist |
|
653 | 653 | all |
|
654 | 654 | end |
|
655 | 655 | else |
|
656 | 656 | all |
|
657 | 657 | end |
|
658 | 658 | end |
|
659 | 659 | end |
|
660 | 660 | |
|
661 | 661 | # Redmine links |
|
662 | 662 | # |
|
663 | 663 | # Examples: |
|
664 | 664 | # Issues: |
|
665 | 665 | # #52 -> Link to issue #52 |
|
666 | 666 | # Changesets: |
|
667 | 667 | # r52 -> Link to revision 52 |
|
668 | 668 | # commit:a85130f -> Link to scmid starting with a85130f |
|
669 | 669 | # Documents: |
|
670 | 670 | # document#17 -> Link to document with id 17 |
|
671 | 671 | # document:Greetings -> Link to the document with title "Greetings" |
|
672 | 672 | # document:"Some document" -> Link to the document with title "Some document" |
|
673 | 673 | # Versions: |
|
674 | 674 | # version#3 -> Link to version with id 3 |
|
675 | 675 | # version:1.0.0 -> Link to version named "1.0.0" |
|
676 | 676 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
|
677 | 677 | # Attachments: |
|
678 | 678 | # attachment:file.zip -> Link to the attachment of the current object named file.zip |
|
679 | 679 | # Source files: |
|
680 | 680 | # source:some/file -> Link to the file located at /some/file in the project's repository |
|
681 | 681 | # source:some/file@52 -> Link to the file's revision 52 |
|
682 | 682 | # source:some/file#L120 -> Link to line 120 of the file |
|
683 | 683 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
|
684 | 684 | # export:some/file -> Force the download of the file |
|
685 | 685 | # Forum messages: |
|
686 | 686 | # message#1218 -> Link to message with id 1218 |
|
687 | 687 | # |
|
688 | 688 | # Links can refer other objects from other projects, using project identifier: |
|
689 | 689 | # identifier:r52 |
|
690 | 690 | # identifier:document:"Some document" |
|
691 | 691 | # identifier:version:1.0.0 |
|
692 | 692 | # identifier:source:some/file |
|
693 | 693 | def parse_redmine_links(text, project, obj, attr, only_path, options) |
|
694 | 694 | 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| |
|
695 | 695 | 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 |
|
696 | 696 | link = nil |
|
697 | 697 | if project_identifier |
|
698 | 698 | project = Project.visible.find_by_identifier(project_identifier) |
|
699 | 699 | end |
|
700 | 700 | if esc.nil? |
|
701 | 701 | if prefix.nil? && sep == 'r' |
|
702 | 702 | if project |
|
703 | 703 | repository = nil |
|
704 | 704 | if repo_identifier |
|
705 | 705 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
706 | 706 | else |
|
707 | 707 | repository = project.repository |
|
708 | 708 | end |
|
709 | 709 | # project.changesets.visible raises an SQL error because of a double join on repositories |
|
710 | 710 | if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) |
|
711 | 711 | link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision}, |
|
712 | 712 | :class => 'changeset', |
|
713 | 713 | :title => truncate_single_line(changeset.comments, :length => 100)) |
|
714 | 714 | end |
|
715 | 715 | end |
|
716 | 716 | elsif sep == '#' |
|
717 | 717 | oid = identifier.to_i |
|
718 | 718 | case prefix |
|
719 | 719 | when nil |
|
720 | 720 | if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) |
|
721 | 721 | anchor = comment_id ? "note-#{comment_id}" : nil |
|
722 | 722 | link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, |
|
723 | 723 | :class => issue.css_classes, |
|
724 | 724 | :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") |
|
725 | 725 | end |
|
726 | 726 | when 'document' |
|
727 | 727 | if document = Document.visible.find_by_id(oid) |
|
728 | 728 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
729 | 729 | :class => 'document' |
|
730 | 730 | end |
|
731 | 731 | when 'version' |
|
732 | 732 | if version = Version.visible.find_by_id(oid) |
|
733 | 733 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
734 | 734 | :class => 'version' |
|
735 | 735 | end |
|
736 | 736 | when 'message' |
|
737 | 737 | if message = Message.visible.find_by_id(oid, :include => :parent) |
|
738 | 738 | link = link_to_message(message, {:only_path => only_path}, :class => 'message') |
|
739 | 739 | end |
|
740 | 740 | when 'forum' |
|
741 | 741 | if board = Board.visible.find_by_id(oid) |
|
742 | 742 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, |
|
743 | 743 | :class => 'board' |
|
744 | 744 | end |
|
745 | 745 | when 'news' |
|
746 | 746 | if news = News.visible.find_by_id(oid) |
|
747 | 747 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, |
|
748 | 748 | :class => 'news' |
|
749 | 749 | end |
|
750 | 750 | when 'project' |
|
751 | 751 | if p = Project.visible.find_by_id(oid) |
|
752 | 752 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
753 | 753 | end |
|
754 | 754 | end |
|
755 | 755 | elsif sep == ':' |
|
756 | 756 | # removes the double quotes if any |
|
757 | 757 | name = identifier.gsub(%r{^"(.*)"$}, "\\1") |
|
758 | 758 | case prefix |
|
759 | 759 | when 'document' |
|
760 | 760 | if project && document = project.documents.visible.find_by_title(name) |
|
761 | 761 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
762 | 762 | :class => 'document' |
|
763 | 763 | end |
|
764 | 764 | when 'version' |
|
765 | 765 | if project && version = project.versions.visible.find_by_name(name) |
|
766 | 766 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
767 | 767 | :class => 'version' |
|
768 | 768 | end |
|
769 | 769 | when 'forum' |
|
770 | 770 | if project && board = project.boards.visible.find_by_name(name) |
|
771 | 771 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, |
|
772 | 772 | :class => 'board' |
|
773 | 773 | end |
|
774 | 774 | when 'news' |
|
775 | 775 | if project && news = project.news.visible.find_by_title(name) |
|
776 | 776 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, |
|
777 | 777 | :class => 'news' |
|
778 | 778 | end |
|
779 | 779 | when 'commit', 'source', 'export' |
|
780 | 780 | if project |
|
781 | 781 | repository = nil |
|
782 | 782 | if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$} |
|
783 | 783 | repo_prefix, repo_identifier, name = $1, $2, $3 |
|
784 | 784 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
785 | 785 | else |
|
786 | 786 | repository = project.repository |
|
787 | 787 | end |
|
788 | 788 | if prefix == 'commit' |
|
789 | 789 | if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"])) |
|
790 | 790 | link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, |
|
791 | 791 | :class => 'changeset', |
|
792 | 792 | :title => truncate_single_line(h(changeset.comments), :length => 100) |
|
793 | 793 | end |
|
794 | 794 | else |
|
795 | 795 | if repository && User.current.allowed_to?(:browse_repository, project) |
|
796 | 796 | name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} |
|
797 | 797 | path, rev, anchor = $1, $3, $5 |
|
798 | 798 | link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param, |
|
799 | 799 | :path => to_path_param(path), |
|
800 | 800 | :rev => rev, |
|
801 | 801 | :anchor => anchor, |
|
802 | 802 | :format => (prefix == 'export' ? 'raw' : nil)}, |
|
803 | 803 | :class => (prefix == 'export' ? 'source download' : 'source') |
|
804 | 804 | end |
|
805 | 805 | end |
|
806 | 806 | repo_prefix = nil |
|
807 | 807 | end |
|
808 | 808 | when 'attachment' |
|
809 | 809 | attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
|
810 | 810 | if attachments && attachment = attachments.detect {|a| a.filename == name } |
|
811 | 811 | link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, |
|
812 | 812 | :class => 'attachment' |
|
813 | 813 | end |
|
814 | 814 | when 'project' |
|
815 | 815 | if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) |
|
816 | 816 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
817 | 817 | end |
|
818 | 818 | end |
|
819 | 819 | end |
|
820 | 820 | end |
|
821 | 821 | (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) |
|
822 | 822 | end |
|
823 | 823 | end |
|
824 | 824 | |
|
825 | 825 | HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) |
|
826 | 826 | |
|
827 | 827 | def parse_sections(text, project, obj, attr, only_path, options) |
|
828 | 828 | return unless options[:edit_section_links] |
|
829 | 829 | text.gsub!(HEADING_RE) do |
|
830 | 830 | heading = $1 |
|
831 | 831 | @current_section += 1 |
|
832 | 832 | if @current_section > 1 |
|
833 | 833 | content_tag('div', |
|
834 | 834 | link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), |
|
835 | 835 | :class => 'contextual', |
|
836 | 836 | :title => l(:button_edit_section)) + heading.html_safe |
|
837 | 837 | else |
|
838 | 838 | heading |
|
839 | 839 | end |
|
840 | 840 | end |
|
841 | 841 | end |
|
842 | 842 | |
|
843 | 843 | # Headings and TOC |
|
844 | 844 | # Adds ids and links to headings unless options[:headings] is set to false |
|
845 | 845 | def parse_headings(text, project, obj, attr, only_path, options) |
|
846 | 846 | return if options[:headings] == false |
|
847 | 847 | |
|
848 | 848 | text.gsub!(HEADING_RE) do |
|
849 | 849 | level, attrs, content = $2.to_i, $3, $4 |
|
850 | 850 | item = strip_tags(content).strip |
|
851 | 851 | anchor = sanitize_anchor_name(item) |
|
852 | 852 | # used for single-file wiki export |
|
853 | 853 | anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) |
|
854 | 854 | @heading_anchors[anchor] ||= 0 |
|
855 | 855 | idx = (@heading_anchors[anchor] += 1) |
|
856 | 856 | if idx > 1 |
|
857 | 857 | anchor = "#{anchor}-#{idx}" |
|
858 | 858 | end |
|
859 | 859 | @parsed_headings << [level, anchor, item] |
|
860 | 860 | "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>" |
|
861 | 861 | end |
|
862 | 862 | end |
|
863 | 863 | |
|
864 | 864 | MACROS_RE = /( |
|
865 | 865 | (!)? # escaping |
|
866 | 866 | ( |
|
867 | 867 | \{\{ # opening tag |
|
868 | 868 | ([\w]+) # macro name |
|
869 | 869 | (\(([^\n\r]*?)\))? # optional arguments |
|
870 | 870 | ([\n\r].*?[\n\r])? # optional block of text |
|
871 | 871 | \}\} # closing tag |
|
872 | 872 | ) |
|
873 | 873 | )/mx unless const_defined?(:MACROS_RE) |
|
874 | 874 | |
|
875 | 875 | MACRO_SUB_RE = /( |
|
876 | 876 | \{\{ |
|
877 | 877 | macro\((\d+)\) |
|
878 | 878 | \}\} |
|
879 | 879 | )/x unless const_defined?(:MACRO_SUB_RE) |
|
880 | 880 | |
|
881 | 881 | # Extracts macros from text |
|
882 | 882 | def catch_macros(text) |
|
883 | 883 | macros = {} |
|
884 | 884 | text.gsub!(MACROS_RE) do |
|
885 | 885 | all, macro = $1, $4.downcase |
|
886 | 886 | if macro_exists?(macro) || all =~ MACRO_SUB_RE |
|
887 | 887 | index = macros.size |
|
888 | 888 | macros[index] = all |
|
889 | 889 | "{{macro(#{index})}}" |
|
890 | 890 | else |
|
891 | 891 | all |
|
892 | 892 | end |
|
893 | 893 | end |
|
894 | 894 | macros |
|
895 | 895 | end |
|
896 | 896 | |
|
897 | 897 | # Executes and replaces macros in text |
|
898 | 898 | def inject_macros(text, obj, macros, execute=true) |
|
899 | 899 | text.gsub!(MACRO_SUB_RE) do |
|
900 | 900 | all, index = $1, $2.to_i |
|
901 | 901 | orig = macros.delete(index) |
|
902 | 902 | if execute && orig && orig =~ MACROS_RE |
|
903 | 903 | esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) |
|
904 | 904 | if esc.nil? |
|
905 | 905 | h(exec_macro(macro, obj, args, block) || all) |
|
906 | 906 | else |
|
907 | 907 | h(all) |
|
908 | 908 | end |
|
909 | 909 | elsif orig |
|
910 | 910 | h(orig) |
|
911 | 911 | else |
|
912 | 912 | h(all) |
|
913 | 913 | end |
|
914 | 914 | end |
|
915 | 915 | end |
|
916 | 916 | |
|
917 | 917 | TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) |
|
918 | 918 | |
|
919 | 919 | # Renders the TOC with given headings |
|
920 | 920 | def replace_toc(text, headings) |
|
921 | 921 | text.gsub!(TOC_RE) do |
|
922 | 922 | # Keep only the 4 first levels |
|
923 | 923 | headings = headings.select{|level, anchor, item| level <= 4} |
|
924 | 924 | if headings.empty? |
|
925 | 925 | '' |
|
926 | 926 | else |
|
927 | 927 | div_class = 'toc' |
|
928 | 928 | div_class << ' right' if $1 == '>' |
|
929 | 929 | div_class << ' left' if $1 == '<' |
|
930 | 930 | out = "<ul class=\"#{div_class}\"><li>" |
|
931 | 931 | root = headings.map(&:first).min |
|
932 | 932 | current = root |
|
933 | 933 | started = false |
|
934 | 934 | headings.each do |level, anchor, item| |
|
935 | 935 | if level > current |
|
936 | 936 | out << '<ul><li>' * (level - current) |
|
937 | 937 | elsif level < current |
|
938 | 938 | out << "</li></ul>\n" * (current - level) + "</li><li>" |
|
939 | 939 | elsif started |
|
940 | 940 | out << '</li><li>' |
|
941 | 941 | end |
|
942 | 942 | out << "<a href=\"##{anchor}\">#{item}</a>" |
|
943 | 943 | current = level |
|
944 | 944 | started = true |
|
945 | 945 | end |
|
946 | 946 | out << '</li></ul>' * (current - root) |
|
947 | 947 | out << '</li></ul>' |
|
948 | 948 | end |
|
949 | 949 | end |
|
950 | 950 | end |
|
951 | 951 | |
|
952 | 952 | # Same as Rails' simple_format helper without using paragraphs |
|
953 | 953 | def simple_format_without_paragraph(text) |
|
954 | 954 | text.to_s. |
|
955 | 955 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
|
956 | 956 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
|
957 | 957 | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br |
|
958 | 958 | html_safe |
|
959 | 959 | end |
|
960 | 960 | |
|
961 | 961 | def lang_options_for_select(blank=true) |
|
962 | 962 | (blank ? [["(auto)", ""]] : []) + |
|
963 | 963 | valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } |
|
964 | 964 | end |
|
965 | 965 | |
|
966 | 966 | def label_tag_for(name, option_tags = nil, options = {}) |
|
967 | 967 | label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") |
|
968 | 968 | content_tag("label", label_text) |
|
969 | 969 | end |
|
970 | 970 | |
|
971 | 971 | def labelled_form_for(*args, &proc) |
|
972 | 972 | args << {} unless args.last.is_a?(Hash) |
|
973 | 973 | options = args.last |
|
974 | 974 | if args.first.is_a?(Symbol) |
|
975 | 975 | options.merge!(:as => args.shift) |
|
976 | 976 | end |
|
977 | 977 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
978 | 978 | form_for(*args, &proc) |
|
979 | 979 | end |
|
980 | 980 | |
|
981 | 981 | def labelled_fields_for(*args, &proc) |
|
982 | 982 | args << {} unless args.last.is_a?(Hash) |
|
983 | 983 | options = args.last |
|
984 | 984 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
985 | 985 | fields_for(*args, &proc) |
|
986 | 986 | end |
|
987 | 987 | |
|
988 | 988 | def labelled_remote_form_for(*args, &proc) |
|
989 | 989 | ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." |
|
990 | 990 | args << {} unless args.last.is_a?(Hash) |
|
991 | 991 | options = args.last |
|
992 | 992 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) |
|
993 | 993 | form_for(*args, &proc) |
|
994 | 994 | end |
|
995 | 995 | |
|
996 | 996 | def error_messages_for(*objects) |
|
997 | 997 | html = "" |
|
998 | 998 | objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact |
|
999 | 999 | errors = objects.map {|o| o.errors.full_messages}.flatten |
|
1000 | 1000 | if errors.any? |
|
1001 | 1001 | html << "<div id='errorExplanation'><ul>\n" |
|
1002 | 1002 | errors.each do |error| |
|
1003 | 1003 | html << "<li>#{h error}</li>\n" |
|
1004 | 1004 | end |
|
1005 | 1005 | html << "</ul></div>\n" |
|
1006 | 1006 | end |
|
1007 | 1007 | html.html_safe |
|
1008 | 1008 | end |
|
1009 | 1009 | |
|
1010 | 1010 | def delete_link(url, options={}) |
|
1011 | 1011 | options = { |
|
1012 | 1012 | :method => :delete, |
|
1013 | 1013 | :data => {:confirm => l(:text_are_you_sure)}, |
|
1014 | 1014 | :class => 'icon icon-del' |
|
1015 | 1015 | }.merge(options) |
|
1016 | 1016 | |
|
1017 | 1017 | link_to l(:button_delete), url, options |
|
1018 | 1018 | end |
|
1019 | 1019 | |
|
1020 | 1020 | def preview_link(url, form, target='preview', options={}) |
|
1021 | 1021 | content_tag 'a', l(:label_preview), { |
|
1022 | 1022 | :href => "#", |
|
1023 | 1023 | :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, |
|
1024 | 1024 | :accesskey => accesskey(:preview) |
|
1025 | 1025 | }.merge(options) |
|
1026 | 1026 | end |
|
1027 | 1027 | |
|
1028 | 1028 | def link_to_function(name, function, html_options={}) |
|
1029 | 1029 | content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) |
|
1030 | 1030 | end |
|
1031 | 1031 | |
|
1032 | # Helper to render JSON in views | |
|
1033 | def raw_json(arg) | |
|
1034 | arg.to_json.to_s.gsub('/', '\/').html_safe | |
|
1035 | end | |
|
1036 | ||
|
1032 | 1037 | def back_url |
|
1033 | 1038 | url = params[:back_url] |
|
1034 | 1039 | if url.nil? && referer = request.env['HTTP_REFERER'] |
|
1035 | 1040 | url = CGI.unescape(referer.to_s) |
|
1036 | 1041 | end |
|
1037 | 1042 | url |
|
1038 | 1043 | end |
|
1039 | 1044 | |
|
1040 | 1045 | def back_url_hidden_field_tag |
|
1041 | 1046 | url = back_url |
|
1042 | 1047 | hidden_field_tag('back_url', url, :id => nil) unless url.blank? |
|
1043 | 1048 | end |
|
1044 | 1049 | |
|
1045 | 1050 | def check_all_links(form_name) |
|
1046 | 1051 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + |
|
1047 | 1052 | " | ".html_safe + |
|
1048 | 1053 | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") |
|
1049 | 1054 | end |
|
1050 | 1055 | |
|
1051 | 1056 | def progress_bar(pcts, options={}) |
|
1052 | 1057 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
|
1053 | 1058 | pcts = pcts.collect(&:round) |
|
1054 | 1059 | pcts[1] = pcts[1] - pcts[0] |
|
1055 | 1060 | pcts << (100 - pcts[1] - pcts[0]) |
|
1056 | 1061 | width = options[:width] || '100px;' |
|
1057 | 1062 | legend = options[:legend] || '' |
|
1058 | 1063 | content_tag('table', |
|
1059 | 1064 | content_tag('tr', |
|
1060 | 1065 | (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + |
|
1061 | 1066 | (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + |
|
1062 | 1067 | (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) |
|
1063 | 1068 | ), :class => 'progress', :style => "width: #{width};").html_safe + |
|
1064 | 1069 | content_tag('p', legend, :class => 'pourcent').html_safe |
|
1065 | 1070 | end |
|
1066 | 1071 | |
|
1067 | 1072 | def checked_image(checked=true) |
|
1068 | 1073 | if checked |
|
1069 | 1074 | image_tag 'toggle_check.png' |
|
1070 | 1075 | end |
|
1071 | 1076 | end |
|
1072 | 1077 | |
|
1073 | 1078 | def context_menu(url) |
|
1074 | 1079 | unless @context_menu_included |
|
1075 | 1080 | content_for :header_tags do |
|
1076 | 1081 | javascript_include_tag('context_menu') + |
|
1077 | 1082 | stylesheet_link_tag('context_menu') |
|
1078 | 1083 | end |
|
1079 | 1084 | if l(:direction) == 'rtl' |
|
1080 | 1085 | content_for :header_tags do |
|
1081 | 1086 | stylesheet_link_tag('context_menu_rtl') |
|
1082 | 1087 | end |
|
1083 | 1088 | end |
|
1084 | 1089 | @context_menu_included = true |
|
1085 | 1090 | end |
|
1086 | 1091 | javascript_tag "contextMenuInit('#{ url_for(url) }')" |
|
1087 | 1092 | end |
|
1088 | 1093 | |
|
1089 | 1094 | def calendar_for(field_id) |
|
1090 | 1095 | include_calendar_headers_tags |
|
1091 | 1096 | javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") |
|
1092 | 1097 | end |
|
1093 | 1098 | |
|
1094 | 1099 | def include_calendar_headers_tags |
|
1095 | 1100 | unless @calendar_headers_tags_included |
|
1096 | 1101 | @calendar_headers_tags_included = true |
|
1097 | 1102 | content_for :header_tags do |
|
1098 | 1103 | start_of_week = Setting.start_of_week |
|
1099 | 1104 | start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? |
|
1100 | 1105 | # Redmine uses 1..7 (monday..sunday) in settings and locales |
|
1101 | 1106 | # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 |
|
1102 | 1107 | start_of_week = start_of_week.to_i % 7 |
|
1103 | 1108 | |
|
1104 | 1109 | tags = javascript_tag( |
|
1105 | 1110 | "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + |
|
1106 | 1111 | "showOn: 'button', buttonImageOnly: true, buttonImage: '" + |
|
1107 | 1112 | path_to_image('/images/calendar.png') + |
|
1108 | 1113 | "', showButtonPanel: true};") |
|
1109 | 1114 | jquery_locale = l('jquery.locale', :default => current_language.to_s) |
|
1110 | 1115 | unless jquery_locale == 'en' |
|
1111 | 1116 | tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") |
|
1112 | 1117 | end |
|
1113 | 1118 | tags |
|
1114 | 1119 | end |
|
1115 | 1120 | end |
|
1116 | 1121 | end |
|
1117 | 1122 | |
|
1118 | 1123 | # Overrides Rails' stylesheet_link_tag with themes and plugins support. |
|
1119 | 1124 | # Examples: |
|
1120 | 1125 | # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults |
|
1121 | 1126 | # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets |
|
1122 | 1127 | # |
|
1123 | 1128 | def stylesheet_link_tag(*sources) |
|
1124 | 1129 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1125 | 1130 | plugin = options.delete(:plugin) |
|
1126 | 1131 | sources = sources.map do |source| |
|
1127 | 1132 | if plugin |
|
1128 | 1133 | "/plugin_assets/#{plugin}/stylesheets/#{source}" |
|
1129 | 1134 | elsif current_theme && current_theme.stylesheets.include?(source) |
|
1130 | 1135 | current_theme.stylesheet_path(source) |
|
1131 | 1136 | else |
|
1132 | 1137 | source |
|
1133 | 1138 | end |
|
1134 | 1139 | end |
|
1135 | 1140 | super sources, options |
|
1136 | 1141 | end |
|
1137 | 1142 | |
|
1138 | 1143 | # Overrides Rails' image_tag with themes and plugins support. |
|
1139 | 1144 | # Examples: |
|
1140 | 1145 | # image_tag('image.png') # => picks image.png from the current theme or defaults |
|
1141 | 1146 | # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets |
|
1142 | 1147 | # |
|
1143 | 1148 | def image_tag(source, options={}) |
|
1144 | 1149 | if plugin = options.delete(:plugin) |
|
1145 | 1150 | source = "/plugin_assets/#{plugin}/images/#{source}" |
|
1146 | 1151 | elsif current_theme && current_theme.images.include?(source) |
|
1147 | 1152 | source = current_theme.image_path(source) |
|
1148 | 1153 | end |
|
1149 | 1154 | super source, options |
|
1150 | 1155 | end |
|
1151 | 1156 | |
|
1152 | 1157 | # Overrides Rails' javascript_include_tag with plugins support |
|
1153 | 1158 | # Examples: |
|
1154 | 1159 | # javascript_include_tag('scripts') # => picks scripts.js from defaults |
|
1155 | 1160 | # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets |
|
1156 | 1161 | # |
|
1157 | 1162 | def javascript_include_tag(*sources) |
|
1158 | 1163 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1159 | 1164 | if plugin = options.delete(:plugin) |
|
1160 | 1165 | sources = sources.map do |source| |
|
1161 | 1166 | if plugin |
|
1162 | 1167 | "/plugin_assets/#{plugin}/javascripts/#{source}" |
|
1163 | 1168 | else |
|
1164 | 1169 | source |
|
1165 | 1170 | end |
|
1166 | 1171 | end |
|
1167 | 1172 | end |
|
1168 | 1173 | super sources, options |
|
1169 | 1174 | end |
|
1170 | 1175 | |
|
1171 | 1176 | def content_for(name, content = nil, &block) |
|
1172 | 1177 | @has_content ||= {} |
|
1173 | 1178 | @has_content[name] = true |
|
1174 | 1179 | super(name, content, &block) |
|
1175 | 1180 | end |
|
1176 | 1181 | |
|
1177 | 1182 | def has_content?(name) |
|
1178 | 1183 | (@has_content && @has_content[name]) || false |
|
1179 | 1184 | end |
|
1180 | 1185 | |
|
1181 | 1186 | def sidebar_content? |
|
1182 | 1187 | has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? |
|
1183 | 1188 | end |
|
1184 | 1189 | |
|
1185 | 1190 | def view_layouts_base_sidebar_hook_response |
|
1186 | 1191 | @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) |
|
1187 | 1192 | end |
|
1188 | 1193 | |
|
1189 | 1194 | def email_delivery_enabled? |
|
1190 | 1195 | !!ActionMailer::Base.perform_deliveries |
|
1191 | 1196 | end |
|
1192 | 1197 | |
|
1193 | 1198 | # Returns the avatar image tag for the given +user+ if avatars are enabled |
|
1194 | 1199 | # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') |
|
1195 | 1200 | def avatar(user, options = { }) |
|
1196 | 1201 | if Setting.gravatar_enabled? |
|
1197 | 1202 | options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) |
|
1198 | 1203 | email = nil |
|
1199 | 1204 | if user.respond_to?(:mail) |
|
1200 | 1205 | email = user.mail |
|
1201 | 1206 | elsif user.to_s =~ %r{<(.+?)>} |
|
1202 | 1207 | email = $1 |
|
1203 | 1208 | end |
|
1204 | 1209 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
|
1205 | 1210 | else |
|
1206 | 1211 | '' |
|
1207 | 1212 | end |
|
1208 | 1213 | end |
|
1209 | 1214 | |
|
1210 | 1215 | def sanitize_anchor_name(anchor) |
|
1211 | 1216 | if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' |
|
1212 | 1217 | anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1213 | 1218 | else |
|
1214 | 1219 | # TODO: remove when ruby1.8 is no longer supported |
|
1215 | 1220 | anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1216 | 1221 | end |
|
1217 | 1222 | end |
|
1218 | 1223 | |
|
1219 | 1224 | # Returns the javascript tags that are included in the html layout head |
|
1220 | 1225 | def javascript_heads |
|
1221 | 1226 | tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') |
|
1222 | 1227 | unless User.current.pref.warn_on_leaving_unsaved == '0' |
|
1223 | 1228 | tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") |
|
1224 | 1229 | end |
|
1225 | 1230 | tags |
|
1226 | 1231 | end |
|
1227 | 1232 | |
|
1228 | 1233 | def favicon |
|
1229 | 1234 | "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe |
|
1230 | 1235 | end |
|
1231 | 1236 | |
|
1232 | 1237 | def robot_exclusion_tag |
|
1233 | 1238 | '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe |
|
1234 | 1239 | end |
|
1235 | 1240 | |
|
1236 | 1241 | # Returns true if arg is expected in the API response |
|
1237 | 1242 | def include_in_api_response?(arg) |
|
1238 | 1243 | unless @included_in_api_response |
|
1239 | 1244 | param = params[:include] |
|
1240 | 1245 | @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') |
|
1241 | 1246 | @included_in_api_response.collect!(&:strip) |
|
1242 | 1247 | end |
|
1243 | 1248 | @included_in_api_response.include?(arg.to_s) |
|
1244 | 1249 | end |
|
1245 | 1250 | |
|
1246 | 1251 | # Returns options or nil if nometa param or X-Redmine-Nometa header |
|
1247 | 1252 | # was set in the request |
|
1248 | 1253 | def api_meta(options) |
|
1249 | 1254 | if params[:nometa].present? || request.headers['X-Redmine-Nometa'] |
|
1250 | 1255 | # compatibility mode for activeresource clients that raise |
|
1251 | 1256 | # an error when unserializing an array with attributes |
|
1252 | 1257 | nil |
|
1253 | 1258 | else |
|
1254 | 1259 | options |
|
1255 | 1260 | end |
|
1256 | 1261 | end |
|
1257 | 1262 | |
|
1258 | 1263 | private |
|
1259 | 1264 | |
|
1260 | 1265 | def wiki_helper |
|
1261 | 1266 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
1262 | 1267 | extend helper |
|
1263 | 1268 | return self |
|
1264 | 1269 | end |
|
1265 | 1270 | |
|
1266 | 1271 | def link_to_content_update(text, url_params = {}, html_options = {}) |
|
1267 | 1272 | link_to(text, url_params, html_options) |
|
1268 | 1273 | end |
|
1269 | 1274 | end |
@@ -1,27 +1,27 | |||
|
1 | 1 | <%= javascript_tag do %> |
|
2 |
var operatorLabels = <%= raw Query.operators_labels |
|
|
3 |
var operatorByType = <%= raw Query.operators_by_filter_type |
|
|
4 |
var availableFilters = <%= raw query.available_filters_as |
|
|
5 |
var labelDayPlural = |
|
|
2 | var operatorLabels = <%= raw_json Query.operators_labels %>; | |
|
3 | var operatorByType = <%= raw_json Query.operators_by_filter_type %>; | |
|
4 | var availableFilters = <%= raw_json query.available_filters_as_json %>; | |
|
5 | var labelDayPlural = <%= raw_json l(:label_day_plural) %>; | |
|
6 | 6 | $(document).ready(function(){ |
|
7 | 7 | initFilters(); |
|
8 | 8 | <% query.filters.each do |field, options| %> |
|
9 |
addFilter("<%= field %>", <%= raw query.operator_for(field) |
|
|
9 | addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>); | |
|
10 | 10 | <% end %> |
|
11 | 11 | }); |
|
12 | 12 | <% end %> |
|
13 | 13 | |
|
14 | 14 | <table style="width:100%"> |
|
15 | 15 | <tr> |
|
16 | 16 | <td> |
|
17 | 17 | <table id="filters-table"> |
|
18 | 18 | </table> |
|
19 | 19 | </td> |
|
20 | 20 | <td class="add-filter"> |
|
21 | 21 | <%= label_tag('add_filter_select', l(:label_filter_add)) %> |
|
22 | 22 | <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %> |
|
23 | 23 | </td> |
|
24 | 24 | </tr> |
|
25 | 25 | </table> |
|
26 | 26 | <%= hidden_field_tag 'f[]', '' %> |
|
27 | 27 | <% include_calendar_headers_tags %> |
@@ -1,582 +1,582 | |||
|
1 | 1 | /* Redmine - project management software |
|
2 | 2 | Copyright (C) 2006-2012 Jean-Philippe Lang */ |
|
3 | 3 | |
|
4 | 4 | function checkAll(id, checked) { |
|
5 | 5 | if (checked) { |
|
6 | 6 | $('#'+id).find('input[type=checkbox]').attr('checked', true); |
|
7 | 7 | } else { |
|
8 | 8 | $('#'+id).find('input[type=checkbox]').removeAttr('checked'); |
|
9 | 9 | } |
|
10 | 10 | } |
|
11 | 11 | |
|
12 | 12 | function toggleCheckboxesBySelector(selector) { |
|
13 | 13 | var all_checked = true; |
|
14 | 14 | $(selector).each(function(index) { |
|
15 | 15 | if (!$(this).is(':checked')) { all_checked = false; } |
|
16 | 16 | }); |
|
17 | 17 | $(selector).attr('checked', !all_checked) |
|
18 | 18 | } |
|
19 | 19 | |
|
20 | 20 | function showAndScrollTo(id, focus) { |
|
21 | 21 | $('#'+id).show(); |
|
22 | 22 | if (focus!=null) { |
|
23 | 23 | $('#'+focus).focus(); |
|
24 | 24 | } |
|
25 | 25 | $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100); |
|
26 | 26 | } |
|
27 | 27 | |
|
28 | 28 | function toggleRowGroup(el) { |
|
29 | 29 | var tr = $(el).parents('tr').first(); |
|
30 | 30 | var n = tr.next(); |
|
31 | 31 | tr.toggleClass('open'); |
|
32 | 32 | while (n.length && !n.hasClass('group')) { |
|
33 | 33 | n.toggle(); |
|
34 | 34 | n = n.next('tr'); |
|
35 | 35 | } |
|
36 | 36 | } |
|
37 | 37 | |
|
38 | 38 | function collapseAllRowGroups(el) { |
|
39 | 39 | var tbody = $(el).parents('tbody').first(); |
|
40 | 40 | tbody.children('tr').each(function(index) { |
|
41 | 41 | if ($(this).hasClass('group')) { |
|
42 | 42 | $(this).removeClass('open'); |
|
43 | 43 | } else { |
|
44 | 44 | $(this).hide(); |
|
45 | 45 | } |
|
46 | 46 | }); |
|
47 | 47 | } |
|
48 | 48 | |
|
49 | 49 | function expandAllRowGroups(el) { |
|
50 | 50 | var tbody = $(el).parents('tbody').first(); |
|
51 | 51 | tbody.children('tr').each(function(index) { |
|
52 | 52 | if ($(this).hasClass('group')) { |
|
53 | 53 | $(this).addClass('open'); |
|
54 | 54 | } else { |
|
55 | 55 | $(this).show(); |
|
56 | 56 | } |
|
57 | 57 | }); |
|
58 | 58 | } |
|
59 | 59 | |
|
60 | 60 | function toggleAllRowGroups(el) { |
|
61 | 61 | var tr = $(el).parents('tr').first(); |
|
62 | 62 | if (tr.hasClass('open')) { |
|
63 | 63 | collapseAllRowGroups(el); |
|
64 | 64 | } else { |
|
65 | 65 | expandAllRowGroups(el); |
|
66 | 66 | } |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | function toggleFieldset(el) { |
|
70 | 70 | var fieldset = $(el).parents('fieldset').first(); |
|
71 | 71 | fieldset.toggleClass('collapsed'); |
|
72 | 72 | fieldset.children('div').toggle(); |
|
73 | 73 | } |
|
74 | 74 | |
|
75 | 75 | function hideFieldset(el) { |
|
76 | 76 | var fieldset = $(el).parents('fieldset').first(); |
|
77 | 77 | fieldset.toggleClass('collapsed'); |
|
78 | 78 | fieldset.children('div').hide(); |
|
79 | 79 | } |
|
80 | 80 | |
|
81 | 81 | function initFilters(){ |
|
82 | 82 | $('#add_filter_select').change(function(){ |
|
83 | 83 | addFilter($(this).val(), '', []); |
|
84 | 84 | }); |
|
85 | 85 | $('#filters-table td.field input[type=checkbox]').each(function(){ |
|
86 | 86 | toggleFilter($(this).val()); |
|
87 | 87 | }); |
|
88 | 88 | $('#filters-table td.field input[type=checkbox]').live('click',function(){ |
|
89 | 89 | toggleFilter($(this).val()); |
|
90 | 90 | }); |
|
91 | 91 | $('#filters-table .toggle-multiselect').live('click',function(){ |
|
92 | 92 | toggleMultiSelect($(this).siblings('select')); |
|
93 | 93 | }); |
|
94 | 94 | $('#filters-table input[type=text]').live('keypress', function(e){ |
|
95 | 95 | if (e.keyCode == 13) submit_query_form("query_form"); |
|
96 | 96 | }); |
|
97 | 97 | } |
|
98 | 98 | |
|
99 | 99 | function addFilter(field, operator, values) { |
|
100 | 100 | var fieldId = field.replace('.', '_'); |
|
101 | 101 | var tr = $('#tr_'+fieldId); |
|
102 | 102 | if (tr.length > 0) { |
|
103 | 103 | tr.show(); |
|
104 | 104 | } else { |
|
105 | 105 | buildFilterRow(field, operator, values); |
|
106 | 106 | } |
|
107 | 107 | $('#cb_'+fieldId).attr('checked', true); |
|
108 | 108 | toggleFilter(field); |
|
109 | 109 | $('#add_filter_select').val('').children('option').each(function(){ |
|
110 | 110 | if ($(this).attr('value') == field) { |
|
111 | 111 | $(this).attr('disabled', true); |
|
112 | 112 | } |
|
113 | 113 | }); |
|
114 | 114 | } |
|
115 | 115 | |
|
116 | 116 | function buildFilterRow(field, operator, values) { |
|
117 | 117 | var fieldId = field.replace('.', '_'); |
|
118 | 118 | var filterTable = $("#filters-table"); |
|
119 | 119 | var filterOptions = availableFilters[field]; |
|
120 | 120 | var operators = operatorByType[filterOptions['type']]; |
|
121 | 121 | var filterValues = filterOptions['values']; |
|
122 | 122 | var i, select; |
|
123 | 123 | |
|
124 | 124 | var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html( |
|
125 | 125 | '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' + |
|
126 | 126 | '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' + |
|
127 | 127 | '<td class="values"></td>' |
|
128 | 128 | ); |
|
129 | 129 | filterTable.append(tr); |
|
130 | 130 | |
|
131 | 131 | select = tr.find('td.operator select'); |
|
132 | 132 | for (i=0;i<operators.length;i++){ |
|
133 | 133 | var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]); |
|
134 | 134 | if (operators[i] == operator) {option.attr('selected', true)}; |
|
135 | 135 | select.append(option); |
|
136 | 136 | } |
|
137 | 137 | select.change(function(){toggleOperator(field)}); |
|
138 | 138 | |
|
139 | 139 | switch (filterOptions['type']){ |
|
140 | 140 | case "list": |
|
141 | 141 | case "list_optional": |
|
142 | 142 | case "list_status": |
|
143 | 143 | case "list_subprojects": |
|
144 | 144 | tr.find('td.values').append( |
|
145 | 145 | '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' + |
|
146 | 146 | ' <span class="toggle-multiselect"> </span></span>' |
|
147 | 147 | ); |
|
148 | 148 | select = tr.find('td.values select'); |
|
149 | 149 | if (values.length > 1) {select.attr('multiple', true)}; |
|
150 | 150 | for (i=0;i<filterValues.length;i++){ |
|
151 | 151 | var filterValue = filterValues[i]; |
|
152 | 152 | var option = $('<option>'); |
|
153 | 153 | if ($.isArray(filterValue)) { |
|
154 | 154 | option.val(filterValue[1]).text(filterValue[0]); |
|
155 | 155 | if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);} |
|
156 | 156 | } else { |
|
157 | 157 | option.val(filterValue).text(filterValue); |
|
158 | 158 | if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);} |
|
159 | 159 | } |
|
160 | 160 | select.append(option); |
|
161 | 161 | } |
|
162 | 162 | break; |
|
163 | 163 | case "date": |
|
164 | 164 | case "date_past": |
|
165 | 165 | tr.find('td.values').append( |
|
166 |
'<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" |
|
|
167 |
' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" |
|
|
168 |
' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" |
|
|
166 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' + | |
|
167 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' + | |
|
168 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>' | |
|
169 | 169 | ); |
|
170 | 170 | $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions); |
|
171 | 171 | $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions); |
|
172 | 172 | $('#values_'+fieldId).val(values[0]); |
|
173 | 173 | break; |
|
174 | 174 | case "string": |
|
175 | 175 | case "text": |
|
176 | 176 | tr.find('td.values').append( |
|
177 |
'<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" |
|
|
177 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>' | |
|
178 | 178 | ); |
|
179 | 179 | $('#values_'+fieldId).val(values[0]); |
|
180 | 180 | break; |
|
181 | 181 | case "integer": |
|
182 | 182 | case "float": |
|
183 | 183 | tr.find('td.values').append( |
|
184 |
'<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" |
|
|
185 |
' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" |
|
|
184 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' + | |
|
185 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>' | |
|
186 | 186 | ); |
|
187 | 187 | $('#values_'+fieldId+'_1').val(values[0]); |
|
188 | 188 | $('#values_'+fieldId+'_2').val(values[1]); |
|
189 | 189 | break; |
|
190 | 190 | } |
|
191 | 191 | } |
|
192 | 192 | |
|
193 | 193 | function toggleFilter(field) { |
|
194 | 194 | var fieldId = field.replace('.', '_'); |
|
195 | 195 | if ($('#cb_' + fieldId).is(':checked')) { |
|
196 | 196 | $("#operators_" + fieldId).show().removeAttr('disabled'); |
|
197 | 197 | toggleOperator(field); |
|
198 | 198 | } else { |
|
199 | 199 | $("#operators_" + fieldId).hide().attr('disabled', true); |
|
200 | 200 | enableValues(field, []); |
|
201 | 201 | } |
|
202 | 202 | } |
|
203 | 203 | |
|
204 | 204 | function enableValues(field, indexes) { |
|
205 | 205 | var fieldId = field.replace('.', '_'); |
|
206 | 206 | $('#tr_'+fieldId+' td.values .value').each(function(index) { |
|
207 | 207 | if ($.inArray(index, indexes) >= 0) { |
|
208 | 208 | $(this).removeAttr('disabled'); |
|
209 | 209 | $(this).parents('span').first().show(); |
|
210 | 210 | } else { |
|
211 | 211 | $(this).val(''); |
|
212 | 212 | $(this).attr('disabled', true); |
|
213 | 213 | $(this).parents('span').first().hide(); |
|
214 | 214 | } |
|
215 | 215 | |
|
216 | 216 | if ($(this).hasClass('group')) { |
|
217 | 217 | $(this).addClass('open'); |
|
218 | 218 | } else { |
|
219 | 219 | $(this).show(); |
|
220 | 220 | } |
|
221 | 221 | }); |
|
222 | 222 | } |
|
223 | 223 | |
|
224 | 224 | function toggleOperator(field) { |
|
225 | 225 | var fieldId = field.replace('.', '_'); |
|
226 | 226 | var operator = $("#operators_" + fieldId); |
|
227 | 227 | switch (operator.val()) { |
|
228 | 228 | case "!*": |
|
229 | 229 | case "*": |
|
230 | 230 | case "t": |
|
231 | 231 | case "w": |
|
232 | 232 | case "o": |
|
233 | 233 | case "c": |
|
234 | 234 | enableValues(field, []); |
|
235 | 235 | break; |
|
236 | 236 | case "><": |
|
237 | 237 | enableValues(field, [0,1]); |
|
238 | 238 | break; |
|
239 | 239 | case "<t+": |
|
240 | 240 | case ">t+": |
|
241 | 241 | case "t+": |
|
242 | 242 | case ">t-": |
|
243 | 243 | case "<t-": |
|
244 | 244 | case "t-": |
|
245 | 245 | enableValues(field, [2]); |
|
246 | 246 | break; |
|
247 | 247 | default: |
|
248 | 248 | enableValues(field, [0]); |
|
249 | 249 | break; |
|
250 | 250 | } |
|
251 | 251 | } |
|
252 | 252 | |
|
253 | 253 | function toggleMultiSelect(el) { |
|
254 | 254 | if (el.attr('multiple')) { |
|
255 | 255 | el.removeAttr('multiple'); |
|
256 | 256 | } else { |
|
257 | 257 | el.attr('multiple', true); |
|
258 | 258 | } |
|
259 | 259 | } |
|
260 | 260 | |
|
261 | 261 | function submit_query_form(id) { |
|
262 | 262 | selectAllOptions("selected_columns"); |
|
263 | 263 | $('#'+id).submit(); |
|
264 | 264 | } |
|
265 | 265 | |
|
266 | 266 | var fileFieldCount = 1; |
|
267 | 267 | function addFileField() { |
|
268 | 268 | var fields = $('#attachments_fields'); |
|
269 | 269 | if (fields.children().length >= 10) return false; |
|
270 | 270 | fileFieldCount++; |
|
271 | 271 | var s = fields.children('span').first().clone(); |
|
272 | 272 | s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val(''); |
|
273 | 273 | s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val(''); |
|
274 | 274 | fields.append(s); |
|
275 | 275 | } |
|
276 | 276 | |
|
277 | 277 | function removeFileField(el) { |
|
278 | 278 | var fields = $('#attachments_fields'); |
|
279 | 279 | var s = $(el).parents('span').first(); |
|
280 | 280 | if (fields.children().length > 1) { |
|
281 | 281 | s.remove(); |
|
282 | 282 | } else { |
|
283 | 283 | s.children('input.file').val(''); |
|
284 | 284 | s.children('input.description').val(''); |
|
285 | 285 | } |
|
286 | 286 | } |
|
287 | 287 | |
|
288 | 288 | function checkFileSize(el, maxSize, message) { |
|
289 | 289 | var files = el.files; |
|
290 | 290 | if (files) { |
|
291 | 291 | for (var i=0; i<files.length; i++) { |
|
292 | 292 | if (files[i].size > maxSize) { |
|
293 | 293 | alert(message); |
|
294 | 294 | el.value = ""; |
|
295 | 295 | } |
|
296 | 296 | } |
|
297 | 297 | } |
|
298 | 298 | } |
|
299 | 299 | |
|
300 | 300 | function showTab(name) { |
|
301 | 301 | $('div#content .tab-content').hide(); |
|
302 | 302 | $('div.tabs a').removeClass('selected'); |
|
303 | 303 | $('#tab-content-' + name).show(); |
|
304 | 304 | $('#tab-' + name).addClass('selected'); |
|
305 | 305 | return false; |
|
306 | 306 | } |
|
307 | 307 | |
|
308 | 308 | function moveTabRight(el) { |
|
309 | 309 | var lis = $(el).parents('div.tabs').first().find('ul').children(); |
|
310 | 310 | var tabsWidth = 0; |
|
311 | 311 | var i = 0; |
|
312 | 312 | lis.each(function(){ |
|
313 | 313 | if ($(this).is(':visible')) { |
|
314 | 314 | tabsWidth += $(this).width() + 6; |
|
315 | 315 | } |
|
316 | 316 | }); |
|
317 | 317 | if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; } |
|
318 | 318 | while (i<lis.length && !lis.eq(i).is(':visible')) { i++; } |
|
319 | 319 | lis.eq(i).hide(); |
|
320 | 320 | } |
|
321 | 321 | |
|
322 | 322 | function moveTabLeft(el) { |
|
323 | 323 | var lis = $(el).parents('div.tabs').first().find('ul').children(); |
|
324 | 324 | var i = 0; |
|
325 | 325 | while (i<lis.length && !lis.eq(i).is(':visible')) { i++; } |
|
326 | 326 | if (i>0) { |
|
327 | 327 | lis.eq(i-1).show(); |
|
328 | 328 | } |
|
329 | 329 | } |
|
330 | 330 | |
|
331 | 331 | function displayTabsButtons() { |
|
332 | 332 | var lis; |
|
333 | 333 | var tabsWidth = 0; |
|
334 | 334 | var el; |
|
335 | 335 | $('div.tabs').each(function() { |
|
336 | 336 | el = $(this); |
|
337 | 337 | lis = el.find('ul').children(); |
|
338 | 338 | lis.each(function(){ |
|
339 | 339 | if ($(this).is(':visible')) { |
|
340 | 340 | tabsWidth += $(this).width() + 6; |
|
341 | 341 | } |
|
342 | 342 | }); |
|
343 | 343 | if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) { |
|
344 | 344 | el.find('div.tabs-buttons').hide(); |
|
345 | 345 | } else { |
|
346 | 346 | el.find('div.tabs-buttons').show(); |
|
347 | 347 | } |
|
348 | 348 | }); |
|
349 | 349 | } |
|
350 | 350 | |
|
351 | 351 | function setPredecessorFieldsVisibility() { |
|
352 | 352 | var relationType = $('#relation_relation_type'); |
|
353 | 353 | if (relationType.val() == "precedes" || relationType.val() == "follows") { |
|
354 | 354 | $('#predecessor_fields').show(); |
|
355 | 355 | } else { |
|
356 | 356 | $('#predecessor_fields').hide(); |
|
357 | 357 | } |
|
358 | 358 | } |
|
359 | 359 | |
|
360 | 360 | function showModal(id, width) { |
|
361 | 361 | var el = $('#'+id).first(); |
|
362 | 362 | if (el.length == 0 || el.is(':visible')) {return;} |
|
363 | 363 | var title = el.find('h3.title').text(); |
|
364 | 364 | el.dialog({ |
|
365 | 365 | width: width, |
|
366 | 366 | modal: true, |
|
367 | 367 | resizable: false, |
|
368 | 368 | dialogClass: 'modal', |
|
369 | 369 | title: title |
|
370 | 370 | }); |
|
371 | 371 | el.find("input[type=text], input[type=submit]").first().focus(); |
|
372 | 372 | } |
|
373 | 373 | |
|
374 | 374 | function hideModal(el) { |
|
375 | 375 | var modal; |
|
376 | 376 | if (el) { |
|
377 | 377 | modal = $(el).parents('.ui-dialog-content'); |
|
378 | 378 | } else { |
|
379 | 379 | modal = $('#ajax-modal'); |
|
380 | 380 | } |
|
381 | 381 | modal.dialog("close"); |
|
382 | 382 | } |
|
383 | 383 | |
|
384 | 384 | function submitPreview(url, form, target) { |
|
385 | 385 | $.ajax({ |
|
386 | 386 | url: url, |
|
387 | 387 | type: 'post', |
|
388 | 388 | data: $('#'+form).serialize(), |
|
389 | 389 | success: function(data){ |
|
390 | 390 | $('#'+target).html(data); |
|
391 | 391 | } |
|
392 | 392 | }); |
|
393 | 393 | } |
|
394 | 394 | |
|
395 | 395 | function collapseScmEntry(id) { |
|
396 | 396 | $('.'+id).each(function() { |
|
397 | 397 | if ($(this).hasClass('open')) { |
|
398 | 398 | collapseScmEntry($(this).attr('id')); |
|
399 | 399 | } |
|
400 | 400 | $(this).hide(); |
|
401 | 401 | }); |
|
402 | 402 | $('#'+id).removeClass('open'); |
|
403 | 403 | } |
|
404 | 404 | |
|
405 | 405 | function expandScmEntry(id) { |
|
406 | 406 | $('.'+id).each(function() { |
|
407 | 407 | $(this).show(); |
|
408 | 408 | if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) { |
|
409 | 409 | expandScmEntry($(this).attr('id')); |
|
410 | 410 | } |
|
411 | 411 | }); |
|
412 | 412 | $('#'+id).addClass('open'); |
|
413 | 413 | } |
|
414 | 414 | |
|
415 | 415 | function scmEntryClick(id, url) { |
|
416 | 416 | el = $('#'+id); |
|
417 | 417 | if (el.hasClass('open')) { |
|
418 | 418 | collapseScmEntry(id); |
|
419 | 419 | el.addClass('collapsed'); |
|
420 | 420 | return false; |
|
421 | 421 | } else if (el.hasClass('loaded')) { |
|
422 | 422 | expandScmEntry(id); |
|
423 | 423 | el.removeClass('collapsed'); |
|
424 | 424 | return false; |
|
425 | 425 | } |
|
426 | 426 | if (el.hasClass('loading')) { |
|
427 | 427 | return false; |
|
428 | 428 | } |
|
429 | 429 | el.addClass('loading'); |
|
430 | 430 | $.ajax({ |
|
431 | 431 | url: url, |
|
432 | 432 | success: function(data){ |
|
433 | 433 | el.after(data); |
|
434 | 434 | el.addClass('open').addClass('loaded').removeClass('loading'); |
|
435 | 435 | } |
|
436 | 436 | }); |
|
437 | 437 | return true; |
|
438 | 438 | } |
|
439 | 439 | |
|
440 | 440 | function randomKey(size) { |
|
441 | 441 | var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); |
|
442 | 442 | var key = ''; |
|
443 | 443 | for (i = 0; i < size; i++) { |
|
444 | 444 | key += chars[Math.floor(Math.random() * chars.length)]; |
|
445 | 445 | } |
|
446 | 446 | return key; |
|
447 | 447 | } |
|
448 | 448 | |
|
449 | 449 | // Can't use Rails' remote select because we need the form data |
|
450 | 450 | function updateIssueFrom(url) { |
|
451 | 451 | $.ajax({ |
|
452 | 452 | url: url, |
|
453 | 453 | type: 'post', |
|
454 | 454 | data: $('#issue-form').serialize() |
|
455 | 455 | }); |
|
456 | 456 | } |
|
457 | 457 | |
|
458 | 458 | function updateBulkEditFrom(url) { |
|
459 | 459 | $.ajax({ |
|
460 | 460 | url: url, |
|
461 | 461 | type: 'post', |
|
462 | 462 | data: $('#bulk_edit_form').serialize() |
|
463 | 463 | }); |
|
464 | 464 | } |
|
465 | 465 | |
|
466 | 466 | function observeAutocompleteField(fieldId, url) { |
|
467 | 467 | $('#'+fieldId).autocomplete({ |
|
468 | 468 | source: url, |
|
469 | 469 | minLength: 2, |
|
470 | 470 | }); |
|
471 | 471 | } |
|
472 | 472 | |
|
473 | 473 | function observeSearchfield(fieldId, targetId, url) { |
|
474 | 474 | $('#'+fieldId).each(function() { |
|
475 | 475 | var $this = $(this); |
|
476 | 476 | $this.attr('data-value-was', $this.val()); |
|
477 | 477 | var check = function() { |
|
478 | 478 | var val = $this.val(); |
|
479 | 479 | if ($this.attr('data-value-was') != val){ |
|
480 | 480 | $this.attr('data-value-was', val); |
|
481 | 481 | $.ajax({ |
|
482 | 482 | url: url, |
|
483 | 483 | type: 'get', |
|
484 | 484 | data: {q: $this.val()}, |
|
485 | 485 | success: function(data){ $('#'+targetId).html(data); }, |
|
486 | 486 | beforeSend: function(){ $this.addClass('ajax-loading'); }, |
|
487 | 487 | complete: function(){ $this.removeClass('ajax-loading'); } |
|
488 | 488 | }); |
|
489 | 489 | } |
|
490 | 490 | }; |
|
491 | 491 | var reset = function() { |
|
492 | 492 | if (timer) { |
|
493 | 493 | clearInterval(timer); |
|
494 | 494 | timer = setInterval(check, 300); |
|
495 | 495 | } |
|
496 | 496 | }; |
|
497 | 497 | var timer = setInterval(check, 300); |
|
498 | 498 | $this.bind('keyup click mousemove', reset); |
|
499 | 499 | }); |
|
500 | 500 | } |
|
501 | 501 | |
|
502 | 502 | function observeProjectModules() { |
|
503 | 503 | var f = function() { |
|
504 | 504 | /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */ |
|
505 | 505 | if ($('#project_enabled_module_names_issue_tracking').attr('checked')) { |
|
506 | 506 | $('#project_trackers').show(); |
|
507 | 507 | }else{ |
|
508 | 508 | $('#project_trackers').hide(); |
|
509 | 509 | } |
|
510 | 510 | }; |
|
511 | 511 | |
|
512 | 512 | $(window).load(f); |
|
513 | 513 | $('#project_enabled_module_names_issue_tracking').change(f); |
|
514 | 514 | } |
|
515 | 515 | |
|
516 | 516 | function initMyPageSortable(list, url) { |
|
517 | 517 | $('#list-'+list).sortable({ |
|
518 | 518 | connectWith: '.block-receiver', |
|
519 | 519 | tolerance: 'pointer', |
|
520 | 520 | update: function(){ |
|
521 | 521 | $.ajax({ |
|
522 | 522 | url: url, |
|
523 | 523 | type: 'post', |
|
524 | 524 | data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})} |
|
525 | 525 | }); |
|
526 | 526 | } |
|
527 | 527 | }); |
|
528 | 528 | $("#list-top, #list-left, #list-right").disableSelection(); |
|
529 | 529 | } |
|
530 | 530 | |
|
531 | 531 | var warnLeavingUnsavedMessage; |
|
532 | 532 | function warnLeavingUnsaved(message) { |
|
533 | 533 | warnLeavingUnsavedMessage = message; |
|
534 | 534 | |
|
535 | 535 | $('form').submit(function(){ |
|
536 | 536 | $('textarea').removeData('changed'); |
|
537 | 537 | }); |
|
538 | 538 | $('textarea').change(function(){ |
|
539 | 539 | $(this).data('changed', 'changed'); |
|
540 | 540 | }); |
|
541 | 541 | window.onbeforeunload = function(){ |
|
542 | 542 | var warn = false; |
|
543 | 543 | $('textarea').blur().each(function(){ |
|
544 | 544 | if ($(this).data('changed')) { |
|
545 | 545 | warn = true; |
|
546 | 546 | } |
|
547 | 547 | }); |
|
548 | 548 | if (warn) {return warnLeavingUnsavedMessage;} |
|
549 | 549 | }; |
|
550 | 550 | }; |
|
551 | 551 | |
|
552 | 552 | $(document).ready(function(){ |
|
553 | 553 | $('#ajax-indicator').bind('ajaxSend', function(){ |
|
554 | 554 | if ($('.ajax-loading').length == 0) { |
|
555 | 555 | $('#ajax-indicator').show(); |
|
556 | 556 | } |
|
557 | 557 | }); |
|
558 | 558 | $('#ajax-indicator').bind('ajaxStop', function(){ |
|
559 | 559 | $('#ajax-indicator').hide(); |
|
560 | 560 | }); |
|
561 | 561 | }); |
|
562 | 562 | |
|
563 | 563 | function hideOnLoad() { |
|
564 | 564 | $('.hol').hide(); |
|
565 | 565 | } |
|
566 | 566 | |
|
567 | 567 | function addFormObserversForDoubleSubmit() { |
|
568 | 568 | $('form[method=post]').each(function() { |
|
569 | 569 | if (!$(this).hasClass('multiple-submit')) { |
|
570 | 570 | $(this).submit(function(form_submission) { |
|
571 | 571 | if ($(form_submission.target).attr('data-submitted')) { |
|
572 | 572 | form_submission.preventDefault(); |
|
573 | 573 | } else { |
|
574 | 574 | $(form_submission.target).attr('data-submitted', true); |
|
575 | 575 | } |
|
576 | 576 | }); |
|
577 | 577 | } |
|
578 | 578 | }); |
|
579 | 579 | } |
|
580 | 580 | |
|
581 | 581 | $(document).ready(hideOnLoad); |
|
582 | 582 | $(document).ready(addFormObserversForDoubleSubmit); |
@@ -1,276 +1,284 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | |
|
20 | 20 | class QueriesControllerTest < ActionController::TestCase |
|
21 | 21 | fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries |
|
22 | 22 | |
|
23 | 23 | def setup |
|
24 | 24 | User.current = nil |
|
25 | 25 | end |
|
26 | 26 | |
|
27 | 27 | def test_new_project_query |
|
28 | 28 | @request.session[:user_id] = 2 |
|
29 | 29 | get :new, :project_id => 1 |
|
30 | 30 | assert_response :success |
|
31 | 31 | assert_template 'new' |
|
32 | 32 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
33 | 33 | :name => 'query[is_public]', |
|
34 | 34 | :checked => nil } |
|
35 | 35 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
36 | 36 | :name => 'query_is_for_all', |
|
37 | 37 | :checked => nil, |
|
38 | 38 | :disabled => nil } |
|
39 | 39 | assert_select 'select[name=?]', 'c[]' do |
|
40 | 40 | assert_select 'option[value=tracker]' |
|
41 | 41 | assert_select 'option[value=subject]' |
|
42 | 42 | end |
|
43 | 43 | end |
|
44 | 44 | |
|
45 | 45 | def test_new_global_query |
|
46 | 46 | @request.session[:user_id] = 2 |
|
47 | 47 | get :new |
|
48 | 48 | assert_response :success |
|
49 | 49 | assert_template 'new' |
|
50 | 50 | assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
51 | 51 | :name => 'query[is_public]' } |
|
52 | 52 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
53 | 53 | :name => 'query_is_for_all', |
|
54 | 54 | :checked => 'checked', |
|
55 | 55 | :disabled => nil } |
|
56 | 56 | end |
|
57 | 57 | |
|
58 | 58 | def test_new_on_invalid_project |
|
59 | 59 | @request.session[:user_id] = 2 |
|
60 | 60 | get :new, :project_id => 'invalid' |
|
61 | 61 | assert_response 404 |
|
62 | 62 | end |
|
63 | 63 | |
|
64 | 64 | def test_create_project_public_query |
|
65 | 65 | @request.session[:user_id] = 2 |
|
66 | 66 | post :create, |
|
67 | 67 | :project_id => 'ecookbook', |
|
68 | 68 | :default_columns => '1', |
|
69 | 69 | :f => ["status_id", "assigned_to_id"], |
|
70 | 70 | :op => {"assigned_to_id" => "=", "status_id" => "o"}, |
|
71 | 71 | :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, |
|
72 | 72 | :query => {"name" => "test_new_project_public_query", "is_public" => "1"} |
|
73 | 73 | |
|
74 | 74 | q = Query.find_by_name('test_new_project_public_query') |
|
75 | 75 | assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q |
|
76 | 76 | assert q.is_public? |
|
77 | 77 | assert q.has_default_columns? |
|
78 | 78 | assert q.valid? |
|
79 | 79 | end |
|
80 | 80 | |
|
81 | 81 | def test_create_project_private_query |
|
82 | 82 | @request.session[:user_id] = 3 |
|
83 | 83 | post :create, |
|
84 | 84 | :project_id => 'ecookbook', |
|
85 | 85 | :default_columns => '1', |
|
86 | 86 | :fields => ["status_id", "assigned_to_id"], |
|
87 | 87 | :operators => {"assigned_to_id" => "=", "status_id" => "o"}, |
|
88 | 88 | :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, |
|
89 | 89 | :query => {"name" => "test_new_project_private_query", "is_public" => "1"} |
|
90 | 90 | |
|
91 | 91 | q = Query.find_by_name('test_new_project_private_query') |
|
92 | 92 | assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q |
|
93 | 93 | assert !q.is_public? |
|
94 | 94 | assert q.has_default_columns? |
|
95 | 95 | assert q.valid? |
|
96 | 96 | end |
|
97 | 97 | |
|
98 | 98 | def test_create_global_private_query_with_custom_columns |
|
99 | 99 | @request.session[:user_id] = 3 |
|
100 | 100 | post :create, |
|
101 | 101 | :fields => ["status_id", "assigned_to_id"], |
|
102 | 102 | :operators => {"assigned_to_id" => "=", "status_id" => "o"}, |
|
103 | 103 | :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, |
|
104 | 104 | :query => {"name" => "test_new_global_private_query", "is_public" => "1"}, |
|
105 | 105 | :c => ["", "tracker", "subject", "priority", "category"] |
|
106 | 106 | |
|
107 | 107 | q = Query.find_by_name('test_new_global_private_query') |
|
108 | 108 | assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q |
|
109 | 109 | assert !q.is_public? |
|
110 | 110 | assert !q.has_default_columns? |
|
111 | 111 | assert_equal [:tracker, :subject, :priority, :category], q.columns.collect {|c| c.name} |
|
112 | 112 | assert q.valid? |
|
113 | 113 | end |
|
114 | 114 | |
|
115 | 115 | def test_create_global_query_with_custom_filters |
|
116 | 116 | @request.session[:user_id] = 3 |
|
117 | 117 | post :create, |
|
118 | 118 | :fields => ["assigned_to_id"], |
|
119 | 119 | :operators => {"assigned_to_id" => "="}, |
|
120 | 120 | :values => { "assigned_to_id" => ["me"]}, |
|
121 | 121 | :query => {"name" => "test_new_global_query"} |
|
122 | 122 | |
|
123 | 123 | q = Query.find_by_name('test_new_global_query') |
|
124 | 124 | assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q |
|
125 | 125 | assert !q.has_filter?(:status_id) |
|
126 | 126 | assert_equal ['assigned_to_id'], q.filters.keys |
|
127 | 127 | assert q.valid? |
|
128 | 128 | end |
|
129 | 129 | |
|
130 | 130 | def test_create_with_sort |
|
131 | 131 | @request.session[:user_id] = 1 |
|
132 | 132 | post :create, |
|
133 | 133 | :default_columns => '1', |
|
134 | 134 | :operators => {"status_id" => "o"}, |
|
135 | 135 | :values => {"status_id" => ["1"]}, |
|
136 | 136 | :query => {:name => "test_new_with_sort", |
|
137 | 137 | :is_public => "1", |
|
138 | 138 | :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}} |
|
139 | 139 | |
|
140 | 140 | query = Query.find_by_name("test_new_with_sort") |
|
141 | 141 | assert_not_nil query |
|
142 | 142 | assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria |
|
143 | 143 | end |
|
144 | 144 | |
|
145 | 145 | def test_create_with_failure |
|
146 | 146 | @request.session[:user_id] = 2 |
|
147 | 147 | assert_no_difference '::Query.count' do |
|
148 | 148 | post :create, :project_id => 'ecookbook', :query => {:name => ''} |
|
149 | 149 | end |
|
150 | 150 | assert_response :success |
|
151 | 151 | assert_template 'new' |
|
152 | 152 | assert_select 'input[name=?]', 'query[name]' |
|
153 | 153 | end |
|
154 | 154 | |
|
155 | 155 | def test_edit_global_public_query |
|
156 | 156 | @request.session[:user_id] = 1 |
|
157 | 157 | get :edit, :id => 4 |
|
158 | 158 | assert_response :success |
|
159 | 159 | assert_template 'edit' |
|
160 | 160 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
161 | 161 | :name => 'query[is_public]', |
|
162 | 162 | :checked => 'checked' } |
|
163 | 163 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
164 | 164 | :name => 'query_is_for_all', |
|
165 | 165 | :checked => 'checked', |
|
166 | 166 | :disabled => 'disabled' } |
|
167 | 167 | end |
|
168 | 168 | |
|
169 | 169 | def test_edit_global_private_query |
|
170 | 170 | @request.session[:user_id] = 3 |
|
171 | 171 | get :edit, :id => 3 |
|
172 | 172 | assert_response :success |
|
173 | 173 | assert_template 'edit' |
|
174 | 174 | assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
175 | 175 | :name => 'query[is_public]' } |
|
176 | 176 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
177 | 177 | :name => 'query_is_for_all', |
|
178 | 178 | :checked => 'checked', |
|
179 | 179 | :disabled => 'disabled' } |
|
180 | 180 | end |
|
181 | 181 | |
|
182 | 182 | def test_edit_project_private_query |
|
183 | 183 | @request.session[:user_id] = 3 |
|
184 | 184 | get :edit, :id => 2 |
|
185 | 185 | assert_response :success |
|
186 | 186 | assert_template 'edit' |
|
187 | 187 | assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
188 | 188 | :name => 'query[is_public]' } |
|
189 | 189 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
190 | 190 | :name => 'query_is_for_all', |
|
191 | 191 | :checked => nil, |
|
192 | 192 | :disabled => nil } |
|
193 | 193 | end |
|
194 | 194 | |
|
195 | 195 | def test_edit_project_public_query |
|
196 | 196 | @request.session[:user_id] = 2 |
|
197 | 197 | get :edit, :id => 1 |
|
198 | 198 | assert_response :success |
|
199 | 199 | assert_template 'edit' |
|
200 | 200 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
201 | 201 | :name => 'query[is_public]', |
|
202 | 202 | :checked => 'checked' |
|
203 | 203 | } |
|
204 | 204 | assert_tag :tag => 'input', :attributes => { :type => 'checkbox', |
|
205 | 205 | :name => 'query_is_for_all', |
|
206 | 206 | :checked => nil, |
|
207 | 207 | :disabled => 'disabled' } |
|
208 | 208 | end |
|
209 | 209 | |
|
210 | 210 | def test_edit_sort_criteria |
|
211 | 211 | @request.session[:user_id] = 1 |
|
212 | 212 | get :edit, :id => 5 |
|
213 | 213 | assert_response :success |
|
214 | 214 | assert_template 'edit' |
|
215 | 215 | assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' }, |
|
216 | 216 | :child => { :tag => 'option', :attributes => { :value => 'priority', |
|
217 | 217 | :selected => 'selected' } } |
|
218 | 218 | assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' }, |
|
219 | 219 | :child => { :tag => 'option', :attributes => { :value => 'desc', |
|
220 | 220 | :selected => 'selected' } } |
|
221 | 221 | end |
|
222 | 222 | |
|
223 | 223 | def test_edit_invalid_query |
|
224 | 224 | @request.session[:user_id] = 2 |
|
225 | 225 | get :edit, :id => 99 |
|
226 | 226 | assert_response 404 |
|
227 | 227 | end |
|
228 | 228 | |
|
229 | 229 | def test_udpate_global_private_query |
|
230 | 230 | @request.session[:user_id] = 3 |
|
231 | 231 | put :update, |
|
232 | 232 | :id => 3, |
|
233 | 233 | :default_columns => '1', |
|
234 | 234 | :fields => ["status_id", "assigned_to_id"], |
|
235 | 235 | :operators => {"assigned_to_id" => "=", "status_id" => "o"}, |
|
236 | 236 | :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, |
|
237 | 237 | :query => {"name" => "test_edit_global_private_query", "is_public" => "1"} |
|
238 | 238 | |
|
239 | 239 | assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3 |
|
240 | 240 | q = Query.find_by_name('test_edit_global_private_query') |
|
241 | 241 | assert !q.is_public? |
|
242 | 242 | assert q.has_default_columns? |
|
243 | 243 | assert q.valid? |
|
244 | 244 | end |
|
245 | 245 | |
|
246 | 246 | def test_update_global_public_query |
|
247 | 247 | @request.session[:user_id] = 1 |
|
248 | 248 | put :update, |
|
249 | 249 | :id => 4, |
|
250 | 250 | :default_columns => '1', |
|
251 | 251 | :fields => ["status_id", "assigned_to_id"], |
|
252 | 252 | :operators => {"assigned_to_id" => "=", "status_id" => "o"}, |
|
253 | 253 | :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, |
|
254 | 254 | :query => {"name" => "test_edit_global_public_query", "is_public" => "1"} |
|
255 | 255 | |
|
256 | 256 | assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4 |
|
257 | 257 | q = Query.find_by_name('test_edit_global_public_query') |
|
258 | 258 | assert q.is_public? |
|
259 | 259 | assert q.has_default_columns? |
|
260 | 260 | assert q.valid? |
|
261 | 261 | end |
|
262 | 262 | |
|
263 | 263 | def test_update_with_failure |
|
264 | 264 | @request.session[:user_id] = 1 |
|
265 | 265 | put :update, :id => 4, :query => {:name => ''} |
|
266 | 266 | assert_response :success |
|
267 | 267 | assert_template 'edit' |
|
268 | 268 | end |
|
269 | 269 | |
|
270 | 270 | def test_destroy |
|
271 | 271 | @request.session[:user_id] = 2 |
|
272 | 272 | delete :destroy, :id => 1 |
|
273 | 273 | assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil |
|
274 | 274 | assert_nil Query.find_by_id(1) |
|
275 | 275 | end |
|
276 | ||
|
277 | def test_backslash_should_be_escaped_in_filters | |
|
278 | @request.session[:user_id] = 2 | |
|
279 | get :new, :subject => 'foo/bar' | |
|
280 | assert_response :success | |
|
281 | assert_template 'new' | |
|
282 | assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body | |
|
283 | end | |
|
276 | 284 | end |
General Comments 0
You need to be logged in to leave comments.
Login now