@@ -1,525 +1,525 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 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 'coderay' |
|
19 | 19 | require 'coderay/helpers/file_type' |
|
20 | 20 | |
|
21 | 21 | module ApplicationHelper |
|
22 | 22 | include Redmine::WikiFormatting::Macros::Definitions |
|
23 | 23 | |
|
24 | 24 | def current_role |
|
25 | 25 | @current_role ||= User.current.role_for_project(@project) |
|
26 | 26 | end |
|
27 | 27 | |
|
28 | 28 | # Return true if user is authorized for controller/action, otherwise false |
|
29 | 29 | def authorize_for(controller, action) |
|
30 | 30 | User.current.allowed_to?({:controller => controller, :action => action}, @project) |
|
31 | 31 | end |
|
32 | 32 | |
|
33 | 33 | # Display a link if user is authorized |
|
34 | 34 | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) |
|
35 | 35 | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
|
36 | 36 | end |
|
37 | 37 | |
|
38 | 38 | # Display a link to user's account page |
|
39 | 39 | def link_to_user(user) |
|
40 | 40 | user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous' |
|
41 | 41 | end |
|
42 | 42 | |
|
43 | 43 | def link_to_issue(issue, options={}) |
|
44 | 44 | link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options |
|
45 | 45 | end |
|
46 | 46 | |
|
47 | 47 | def toggle_link(name, id, options={}) |
|
48 | 48 | onclick = "Element.toggle('#{id}'); " |
|
49 | 49 | onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") |
|
50 | 50 | onclick << "return false;" |
|
51 | 51 | link_to(name, "#", :onclick => onclick) |
|
52 | 52 | end |
|
53 | 53 | |
|
54 |
def show_and_goto_ |
|
|
54 | def show_and_goto_js(id, options={}) | |
|
55 | 55 | onclick = "Element.show('#{id}'); " |
|
56 | 56 | onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") |
|
57 | 57 | onclick << "Element.scrollTo('#{id}'); " |
|
58 | 58 | onclick << "return false;" |
|
59 | 59 | link_to(name, "#", options.merge(:onclick => onclick)) |
|
60 | 60 | end |
|
61 | 61 | |
|
62 | 62 | def image_to_function(name, function, html_options = {}) |
|
63 | 63 | html_options.symbolize_keys! |
|
64 | 64 | tag(:input, html_options.merge({ |
|
65 | 65 | :type => "image", :src => image_path(name), |
|
66 | 66 | :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" |
|
67 | 67 | })) |
|
68 | 68 | end |
|
69 | 69 | |
|
70 | 70 | def prompt_to_remote(name, text, param, url, html_options = {}) |
|
71 | 71 | html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" |
|
72 | 72 | link_to name, {}, html_options |
|
73 | 73 | end |
|
74 | 74 | |
|
75 | 75 | def format_date(date) |
|
76 | 76 | return nil unless date |
|
77 | 77 | # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed) |
|
78 | 78 | @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) |
|
79 | 79 | date.strftime(@date_format) |
|
80 | 80 | end |
|
81 | 81 | |
|
82 | 82 | def format_time(time, include_date = true) |
|
83 | 83 | return nil unless time |
|
84 | 84 | time = time.to_time if time.is_a?(String) |
|
85 | 85 | zone = User.current.time_zone |
|
86 | 86 | if time.utc? |
|
87 | 87 | local = zone ? zone.adjust(time) : time.getlocal |
|
88 | 88 | else |
|
89 | 89 | local = zone ? zone.adjust(time.getutc) : time |
|
90 | 90 | end |
|
91 | 91 | @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) |
|
92 | 92 | @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) |
|
93 | 93 | include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) |
|
94 | 94 | end |
|
95 | 95 | |
|
96 | 96 | # Truncates and returns the string as a single line |
|
97 | 97 | def truncate_single_line(string, *args) |
|
98 | 98 | truncate(string, *args).gsub(%r{[\r\n]+}m, ' ') |
|
99 | 99 | end |
|
100 | 100 | |
|
101 | 101 | def html_hours(text) |
|
102 | 102 | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') |
|
103 | 103 | end |
|
104 | 104 | |
|
105 | 105 | def authoring(created, author) |
|
106 | 106 | time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) |
|
107 | 107 | l(:label_added_time_by, author || 'Anonymous', time_tag) |
|
108 | 108 | end |
|
109 | 109 | |
|
110 | 110 | def l_or_humanize(s) |
|
111 | 111 | l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize |
|
112 | 112 | end |
|
113 | 113 | |
|
114 | 114 | def day_name(day) |
|
115 | 115 | l(:general_day_names).split(',')[day-1] |
|
116 | 116 | end |
|
117 | 117 | |
|
118 | 118 | def month_name(month) |
|
119 | 119 | l(:actionview_datehelper_select_month_names).split(',')[month-1] |
|
120 | 120 | end |
|
121 | 121 | |
|
122 | 122 | def syntax_highlight(name, content) |
|
123 | 123 | type = CodeRay::FileType[name] |
|
124 | 124 | type ? CodeRay.scan(content, type).html : h(content) |
|
125 | 125 | end |
|
126 | 126 | |
|
127 | 127 | def pagination_links_full(paginator, count=nil, options={}) |
|
128 | 128 | page_param = options.delete(:page_param) || :page |
|
129 | 129 | url_param = params.dup |
|
130 | 130 | # don't reuse params if filters are present |
|
131 | 131 | url_param.clear if url_param.has_key?(:set_filter) |
|
132 | 132 | |
|
133 | 133 | html = '' |
|
134 | 134 | html << link_to_remote(('« ' + l(:label_previous)), |
|
135 | 135 | {:update => 'content', |
|
136 | 136 | :url => url_param.merge(page_param => paginator.current.previous), |
|
137 | 137 | :complete => 'window.scrollTo(0,0)'}, |
|
138 | 138 | {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous |
|
139 | 139 | |
|
140 | 140 | html << (pagination_links_each(paginator, options) do |n| |
|
141 | 141 | link_to_remote(n.to_s, |
|
142 | 142 | {:url => {:params => url_param.merge(page_param => n)}, |
|
143 | 143 | :update => 'content', |
|
144 | 144 | :complete => 'window.scrollTo(0,0)'}, |
|
145 | 145 | {:href => url_for(:params => url_param.merge(page_param => n))}) |
|
146 | 146 | end || '') |
|
147 | 147 | |
|
148 | 148 | html << ' ' + link_to_remote((l(:label_next) + ' »'), |
|
149 | 149 | {:update => 'content', |
|
150 | 150 | :url => url_param.merge(page_param => paginator.current.next), |
|
151 | 151 | :complete => 'window.scrollTo(0,0)'}, |
|
152 | 152 | {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next |
|
153 | 153 | |
|
154 | 154 | unless count.nil? |
|
155 | 155 | html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ') |
|
156 | 156 | end |
|
157 | 157 | |
|
158 | 158 | html |
|
159 | 159 | end |
|
160 | 160 | |
|
161 | 161 | def per_page_links(selected=nil) |
|
162 | 162 | url_param = params.dup |
|
163 | 163 | url_param.clear if url_param.has_key?(:set_filter) |
|
164 | 164 | |
|
165 | 165 | links = Setting.per_page_options_array.collect do |n| |
|
166 | 166 | n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)}, |
|
167 | 167 | {:href => url_for(url_param.merge(:per_page => n))}) |
|
168 | 168 | end |
|
169 | 169 | links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil |
|
170 | 170 | end |
|
171 | 171 | |
|
172 | 172 | def breadcrumb(*args) |
|
173 | 173 | content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') |
|
174 | 174 | end |
|
175 | 175 | |
|
176 | 176 | def html_title(*args) |
|
177 | 177 | if args.empty? |
|
178 | 178 | title = [] |
|
179 | 179 | title << @project.name if @project |
|
180 | 180 | title += @html_title if @html_title |
|
181 | 181 | title << Setting.app_title |
|
182 | 182 | title.compact.join(' - ') |
|
183 | 183 | else |
|
184 | 184 | @html_title ||= [] |
|
185 | 185 | @html_title += args |
|
186 | 186 | end |
|
187 | 187 | end |
|
188 | 188 | |
|
189 | 189 | def accesskey(s) |
|
190 | 190 | Redmine::AccessKeys.key_for s |
|
191 | 191 | end |
|
192 | 192 | |
|
193 | 193 | # Formats text according to system settings. |
|
194 | 194 | # 2 ways to call this method: |
|
195 | 195 | # * with a String: textilizable(text, options) |
|
196 | 196 | # * with an object and one of its attribute: textilizable(issue, :description, options) |
|
197 | 197 | def textilizable(*args) |
|
198 | 198 | options = args.last.is_a?(Hash) ? args.pop : {} |
|
199 | 199 | case args.size |
|
200 | 200 | when 1 |
|
201 | 201 | obj = nil |
|
202 | 202 | text = args.shift |
|
203 | 203 | when 2 |
|
204 | 204 | obj = args.shift |
|
205 | 205 | text = obj.send(args.shift).to_s |
|
206 | 206 | else |
|
207 | 207 | raise ArgumentError, 'invalid arguments to textilizable' |
|
208 | 208 | end |
|
209 | 209 | return '' if text.blank? |
|
210 | 210 | |
|
211 | 211 | only_path = options.delete(:only_path) == false ? false : true |
|
212 | 212 | |
|
213 | 213 | # when using an image link, try to use an attachment, if possible |
|
214 | 214 | attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
|
215 | 215 | |
|
216 | 216 | if attachments |
|
217 | 217 | text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m| |
|
218 | 218 | style = $1 |
|
219 | 219 | filename = $6 |
|
220 | 220 | rf = Regexp.new(filename, Regexp::IGNORECASE) |
|
221 | 221 | # search for the picture in attachments |
|
222 | 222 | if found = attachments.detect { |att| att.filename =~ rf } |
|
223 | 223 | image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found |
|
224 | 224 | desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") |
|
225 | 225 | alt = desc.blank? ? nil : "(#{desc})" |
|
226 | 226 | "!#{style}#{image_url}#{alt}!" |
|
227 | 227 | else |
|
228 | 228 | "!#{style}#{filename}!" |
|
229 | 229 | end |
|
230 | 230 | end |
|
231 | 231 | end |
|
232 | 232 | |
|
233 | 233 | text = (Setting.text_formatting == 'textile') ? |
|
234 | 234 | Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } : |
|
235 | 235 | simple_format(auto_link(h(text))) |
|
236 | 236 | |
|
237 | 237 | # different methods for formatting wiki links |
|
238 | 238 | case options[:wiki_links] |
|
239 | 239 | when :local |
|
240 | 240 | # used for local links to html files |
|
241 | 241 | format_wiki_link = Proc.new {|project, title| "#{title}.html" } |
|
242 | 242 | when :anchor |
|
243 | 243 | # used for single-file wiki export |
|
244 | 244 | format_wiki_link = Proc.new {|project, title| "##{title}" } |
|
245 | 245 | else |
|
246 | 246 | format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) } |
|
247 | 247 | end |
|
248 | 248 | |
|
249 | 249 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
|
250 | 250 | |
|
251 | 251 | # Wiki links |
|
252 | 252 | # |
|
253 | 253 | # Examples: |
|
254 | 254 | # [[mypage]] |
|
255 | 255 | # [[mypage|mytext]] |
|
256 | 256 | # wiki links can refer other project wikis, using project name or identifier: |
|
257 | 257 | # [[project:]] -> wiki starting page |
|
258 | 258 | # [[project:|mytext]] |
|
259 | 259 | # [[project:mypage]] |
|
260 | 260 | # [[project:mypage|mytext]] |
|
261 | 261 | text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
|
262 | 262 | link_project = project |
|
263 | 263 | esc, all, page, title = $1, $2, $3, $5 |
|
264 | 264 | if esc.nil? |
|
265 | 265 | if page =~ /^([^\:]+)\:(.*)$/ |
|
266 | 266 | link_project = Project.find_by_name($1) || Project.find_by_identifier($1) |
|
267 | 267 | page = $2 |
|
268 | 268 | title ||= $1 if page.blank? |
|
269 | 269 | end |
|
270 | 270 | |
|
271 | 271 | if link_project && link_project.wiki |
|
272 | 272 | # check if page exists |
|
273 | 273 | wiki_page = link_project.wiki.find_page(page) |
|
274 | 274 | link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)), |
|
275 | 275 | :class => ('wiki-page' + (wiki_page ? '' : ' new'))) |
|
276 | 276 | else |
|
277 | 277 | # project or wiki doesn't exist |
|
278 | 278 | title || page |
|
279 | 279 | end |
|
280 | 280 | else |
|
281 | 281 | all |
|
282 | 282 | end |
|
283 | 283 | end |
|
284 | 284 | |
|
285 | 285 | # Redmine links |
|
286 | 286 | # |
|
287 | 287 | # Examples: |
|
288 | 288 | # Issues: |
|
289 | 289 | # #52 -> Link to issue #52 |
|
290 | 290 | # Changesets: |
|
291 | 291 | # r52 -> Link to revision 52 |
|
292 | 292 | # commit:a85130f -> Link to scmid starting with a85130f |
|
293 | 293 | # Documents: |
|
294 | 294 | # document#17 -> Link to document with id 17 |
|
295 | 295 | # document:Greetings -> Link to the document with title "Greetings" |
|
296 | 296 | # document:"Some document" -> Link to the document with title "Some document" |
|
297 | 297 | # Versions: |
|
298 | 298 | # version#3 -> Link to version with id 3 |
|
299 | 299 | # version:1.0.0 -> Link to version named "1.0.0" |
|
300 | 300 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
|
301 | 301 | # Attachments: |
|
302 | 302 | # attachment:file.zip -> Link to the attachment of the current object named file.zip |
|
303 | 303 | # Source files: |
|
304 | 304 | # source:some/file -> Link to the file located at /some/file in the project's repository |
|
305 | 305 | # source:some/file@52 -> Link to the file's revision 52 |
|
306 | 306 | # source:some/file#L120 -> Link to line 120 of the file |
|
307 | 307 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
|
308 | 308 | # export:some/file -> Force the download of the file |
|
309 | 309 | text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m| |
|
310 | 310 | leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8 |
|
311 | 311 | link = nil |
|
312 | 312 | if esc.nil? |
|
313 | 313 | if prefix.nil? && sep == 'r' |
|
314 | 314 | if project && (changeset = project.changesets.find_by_revision(oid)) |
|
315 | 315 | link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid}, |
|
316 | 316 | :class => 'changeset', |
|
317 | 317 | :title => truncate_single_line(changeset.comments, 100)) |
|
318 | 318 | end |
|
319 | 319 | elsif sep == '#' |
|
320 | 320 | oid = oid.to_i |
|
321 | 321 | case prefix |
|
322 | 322 | when nil |
|
323 | 323 | if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current)) |
|
324 | 324 | link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, |
|
325 | 325 | :class => (issue.closed? ? 'issue closed' : 'issue'), |
|
326 | 326 | :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})") |
|
327 | 327 | link = content_tag('del', link) if issue.closed? |
|
328 | 328 | end |
|
329 | 329 | when 'document' |
|
330 | 330 | if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) |
|
331 | 331 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
332 | 332 | :class => 'document' |
|
333 | 333 | end |
|
334 | 334 | when 'version' |
|
335 | 335 | if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current)) |
|
336 | 336 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
337 | 337 | :class => 'version' |
|
338 | 338 | end |
|
339 | 339 | end |
|
340 | 340 | elsif sep == ':' |
|
341 | 341 | # removes the double quotes if any |
|
342 | 342 | name = oid.gsub(%r{^"(.*)"$}, "\\1") |
|
343 | 343 | case prefix |
|
344 | 344 | when 'document' |
|
345 | 345 | if project && document = project.documents.find_by_title(name) |
|
346 | 346 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
347 | 347 | :class => 'document' |
|
348 | 348 | end |
|
349 | 349 | when 'version' |
|
350 | 350 | if project && version = project.versions.find_by_name(name) |
|
351 | 351 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
352 | 352 | :class => 'version' |
|
353 | 353 | end |
|
354 | 354 | when 'commit' |
|
355 | 355 | if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])) |
|
356 | 356 | link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, |
|
357 | 357 | :class => 'changeset', |
|
358 | 358 | :title => truncate_single_line(changeset.comments, 100) |
|
359 | 359 | end |
|
360 | 360 | when 'source', 'export' |
|
361 | 361 | if project && project.repository |
|
362 | 362 | name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} |
|
363 | 363 | path, rev, anchor = $1, $3, $5 |
|
364 | 364 | link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :path => path, |
|
365 | 365 | :rev => rev, |
|
366 | 366 | :anchor => anchor, |
|
367 | 367 | :format => (prefix == 'export' ? 'raw' : nil)}, |
|
368 | 368 | :class => (prefix == 'export' ? 'source download' : 'source') |
|
369 | 369 | end |
|
370 | 370 | when 'attachment' |
|
371 | 371 | if attachments && attachment = attachments.detect {|a| a.filename == name } |
|
372 | 372 | link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, |
|
373 | 373 | :class => 'attachment' |
|
374 | 374 | end |
|
375 | 375 | end |
|
376 | 376 | end |
|
377 | 377 | end |
|
378 | 378 | leading + (link || "#{prefix}#{sep}#{oid}") |
|
379 | 379 | end |
|
380 | 380 | |
|
381 | 381 | text |
|
382 | 382 | end |
|
383 | 383 | |
|
384 | 384 | # Same as Rails' simple_format helper without using paragraphs |
|
385 | 385 | def simple_format_without_paragraph(text) |
|
386 | 386 | text.to_s. |
|
387 | 387 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
|
388 | 388 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
|
389 | 389 | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br |
|
390 | 390 | end |
|
391 | 391 | |
|
392 | 392 | def error_messages_for(object_name, options = {}) |
|
393 | 393 | options = options.symbolize_keys |
|
394 | 394 | object = instance_variable_get("@#{object_name}") |
|
395 | 395 | if object && !object.errors.empty? |
|
396 | 396 | # build full_messages here with controller current language |
|
397 | 397 | full_messages = [] |
|
398 | 398 | object.errors.each do |attr, msg| |
|
399 | 399 | next if msg.nil? |
|
400 | 400 | msg = msg.first if msg.is_a? Array |
|
401 | 401 | if attr == "base" |
|
402 | 402 | full_messages << l(msg) |
|
403 | 403 | else |
|
404 | 404 | full_messages << "« " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " » " + l(msg) unless attr == "custom_values" |
|
405 | 405 | end |
|
406 | 406 | end |
|
407 | 407 | # retrieve custom values error messages |
|
408 | 408 | if object.errors[:custom_values] |
|
409 | 409 | object.custom_values.each do |v| |
|
410 | 410 | v.errors.each do |attr, msg| |
|
411 | 411 | next if msg.nil? |
|
412 | 412 | msg = msg.first if msg.is_a? Array |
|
413 | 413 | full_messages << "« " + v.custom_field.name + " » " + l(msg) |
|
414 | 414 | end |
|
415 | 415 | end |
|
416 | 416 | end |
|
417 | 417 | content_tag("div", |
|
418 | 418 | content_tag( |
|
419 | 419 | options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":" |
|
420 | 420 | ) + |
|
421 | 421 | content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }), |
|
422 | 422 | "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" |
|
423 | 423 | ) |
|
424 | 424 | else |
|
425 | 425 | "" |
|
426 | 426 | end |
|
427 | 427 | end |
|
428 | 428 | |
|
429 | 429 | def lang_options_for_select(blank=true) |
|
430 | 430 | (blank ? [["(auto)", ""]] : []) + |
|
431 | 431 | GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } |
|
432 | 432 | end |
|
433 | 433 | |
|
434 | 434 | def label_tag_for(name, option_tags = nil, options = {}) |
|
435 | 435 | label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") |
|
436 | 436 | content_tag("label", label_text) |
|
437 | 437 | end |
|
438 | 438 | |
|
439 | 439 | def labelled_tabular_form_for(name, object, options, &proc) |
|
440 | 440 | options[:html] ||= {} |
|
441 | 441 | options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) |
|
442 | 442 | form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) |
|
443 | 443 | end |
|
444 | 444 | |
|
445 | 445 | def back_url_hidden_field_tag |
|
446 | 446 | hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER']) |
|
447 | 447 | end |
|
448 | 448 | |
|
449 | 449 | def check_all_links(form_name) |
|
450 | 450 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + |
|
451 | 451 | " | " + |
|
452 | 452 | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") |
|
453 | 453 | end |
|
454 | 454 | |
|
455 | 455 | def progress_bar(pcts, options={}) |
|
456 | 456 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
|
457 | 457 | pcts[1] = pcts[1] - pcts[0] |
|
458 | 458 | pcts << (100 - pcts[1] - pcts[0]) |
|
459 | 459 | width = options[:width] || '100px;' |
|
460 | 460 | legend = options[:legend] || '' |
|
461 | 461 | content_tag('table', |
|
462 | 462 | content_tag('tr', |
|
463 | 463 | (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') + |
|
464 | 464 | (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') + |
|
465 | 465 | (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '') |
|
466 | 466 | ), :class => 'progress', :style => "width: #{width};") + |
|
467 | 467 | content_tag('p', legend, :class => 'pourcent') |
|
468 | 468 | end |
|
469 | 469 | |
|
470 | 470 | def context_menu_link(name, url, options={}) |
|
471 | 471 | options[:class] ||= '' |
|
472 | 472 | if options.delete(:selected) |
|
473 | 473 | options[:class] << ' icon-checked disabled' |
|
474 | 474 | options[:disabled] = true |
|
475 | 475 | end |
|
476 | 476 | if options.delete(:disabled) |
|
477 | 477 | options.delete(:method) |
|
478 | 478 | options.delete(:confirm) |
|
479 | 479 | options.delete(:onclick) |
|
480 | 480 | options[:class] << ' disabled' |
|
481 | 481 | url = '#' |
|
482 | 482 | end |
|
483 | 483 | link_to name, url, options |
|
484 | 484 | end |
|
485 | 485 | |
|
486 | 486 | def calendar_for(field_id) |
|
487 | 487 | include_calendar_headers_tags |
|
488 | 488 | image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + |
|
489 | 489 | javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") |
|
490 | 490 | end |
|
491 | 491 | |
|
492 | 492 | def include_calendar_headers_tags |
|
493 | 493 | unless @calendar_headers_tags_included |
|
494 | 494 | @calendar_headers_tags_included = true |
|
495 | 495 | content_for :header_tags do |
|
496 | 496 | javascript_include_tag('calendar/calendar') + |
|
497 | 497 | javascript_include_tag("calendar/lang/calendar-#{current_language}.js") + |
|
498 | 498 | javascript_include_tag('calendar/calendar-setup') + |
|
499 | 499 | stylesheet_link_tag('calendar') |
|
500 | 500 | end |
|
501 | 501 | end |
|
502 | 502 | end |
|
503 | 503 | |
|
504 | 504 | def wikitoolbar_for(field_id) |
|
505 | 505 | return '' unless Setting.text_formatting == 'textile' |
|
506 | 506 | |
|
507 | 507 | help_link = l(:setting_text_formatting) + ': ' + |
|
508 | 508 | link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'), |
|
509 | 509 | :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;") |
|
510 | 510 | |
|
511 | 511 | javascript_include_tag('jstoolbar/jstoolbar') + |
|
512 | 512 | javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") + |
|
513 | 513 | javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();") |
|
514 | 514 | end |
|
515 | 515 | |
|
516 | 516 | def content_for(name, content = nil, &block) |
|
517 | 517 | @has_content ||= {} |
|
518 | 518 | @has_content[name] = true |
|
519 | 519 | super(name, content, &block) |
|
520 | 520 | end |
|
521 | 521 | |
|
522 | 522 | def has_content?(name) |
|
523 | 523 | (@has_content && @has_content[name]) || false |
|
524 | 524 | end |
|
525 | 525 | end |
@@ -1,120 +1,120 | |||
|
1 | 1 | <div class="contextual"> |
|
2 | <%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %> | |
|
2 | <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> | |
|
3 | 3 | <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> |
|
4 | 4 | <%= watcher_tag(@issue, User.current) %> |
|
5 | 5 | <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> |
|
6 | 6 | <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %> |
|
7 | 7 | <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> |
|
8 | 8 | </div> |
|
9 | 9 | |
|
10 | 10 | <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2> |
|
11 | 11 | |
|
12 | 12 | <div class="issue <%= "status-#{@issue.status.position} priority-#{@issue.priority.position}" %>"> |
|
13 | 13 | <h3><%=h @issue.subject %></h3> |
|
14 | 14 | <p class="author"> |
|
15 | 15 | <%= authoring @issue.created_on, @issue.author %>. |
|
16 | 16 | <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %> |
|
17 | 17 | </p> |
|
18 | 18 | |
|
19 | 19 | <table width="100%"> |
|
20 | 20 | <tr> |
|
21 | 21 | <td style="width:15%"><b><%=l(:field_status)%>:</b></td><td style="width:35%"><%= @issue.status.name %></td> |
|
22 | 22 | <td style="width:15%"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td> |
|
23 | 23 | </tr> |
|
24 | 24 | <tr> |
|
25 | 25 | <td><b><%=l(:field_priority)%>:</b></td><td><%= @issue.priority.name %></td> |
|
26 | 26 | <td><b><%=l(:field_due_date)%>:</b></td><td><%= format_date(@issue.due_date) %></td> |
|
27 | 27 | </tr> |
|
28 | 28 | <tr> |
|
29 | 29 | <td><b><%=l(:field_assigned_to)%>:</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td> |
|
30 | 30 | <td><b><%=l(:field_done_ratio)%>:</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td> |
|
31 | 31 | </tr> |
|
32 | 32 | <tr> |
|
33 | 33 | <td><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td> |
|
34 | 34 | <% if User.current.allowed_to?(:view_time_entries, @project) %> |
|
35 | 35 | <td><b><%=l(:label_spent_time)%>:</b></td> |
|
36 | 36 | <td><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td> |
|
37 | 37 | <% end %> |
|
38 | 38 | </tr> |
|
39 | 39 | <tr> |
|
40 | 40 | <td><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td> |
|
41 | 41 | <% if @issue.estimated_hours %> |
|
42 | 42 | <td><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td> |
|
43 | 43 | <% end %> |
|
44 | 44 | </tr> |
|
45 | 45 | <tr> |
|
46 | 46 | <% n = 0 -%> |
|
47 | 47 | <% @issue.custom_values.each do |value| -%> |
|
48 | 48 | <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td> |
|
49 | 49 | <% n = n + 1 |
|
50 | 50 | if (n > 1) |
|
51 | 51 | n = 0 %> |
|
52 | 52 | </tr><tr> |
|
53 | 53 | <%end |
|
54 | 54 | end %> |
|
55 | 55 | </tr> |
|
56 | 56 | </table> |
|
57 | 57 | <hr /> |
|
58 | 58 | |
|
59 | 59 | <div class="contextual"> |
|
60 | 60 | <%= link_to_remote(image_tag('comment.png'), |
|
61 | 61 | { :url => {:controller => 'issues', :action => 'reply', :id => @issue} }, |
|
62 | 62 | :title => l(:button_reply)) if authorize_for('issues', 'edit') %> |
|
63 | 63 | </div> |
|
64 | 64 | |
|
65 | 65 | <p><strong><%=l(:field_description)%></strong></p> |
|
66 | 66 | <div class="wiki"> |
|
67 | 67 | <%= textilizable @issue, :description, :attachments => @issue.attachments %> |
|
68 | 68 | </div> |
|
69 | 69 | |
|
70 | 70 | <% if @issue.attachments.any? %> |
|
71 | 71 | <%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %> |
|
72 | 72 | <% end %> |
|
73 | 73 | |
|
74 | 74 | <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %> |
|
75 | 75 | <hr /> |
|
76 | 76 | <div id="relations"> |
|
77 | 77 | <%= render :partial => 'relations' %> |
|
78 | 78 | </div> |
|
79 | 79 | <% end %> |
|
80 | 80 | |
|
81 | 81 | </div> |
|
82 | 82 | |
|
83 | 83 | <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %> |
|
84 | 84 | <div id="issue-changesets"> |
|
85 | 85 | <h3><%=l(:label_associated_revisions)%></h3> |
|
86 | 86 | <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %> |
|
87 | 87 | </div> |
|
88 | 88 | <% end %> |
|
89 | 89 | |
|
90 | 90 | <% if @journals.any? %> |
|
91 | 91 | <div id="history"> |
|
92 | 92 | <h3><%=l(:label_history)%></h3> |
|
93 | 93 | <%= render :partial => 'history', :locals => { :journals => @journals } %> |
|
94 | 94 | </div> |
|
95 | 95 | <% end %> |
|
96 | 96 | <div style="clear: both;"></div> |
|
97 | 97 | |
|
98 | 98 | <% if authorize_for('issues', 'edit') %> |
|
99 | 99 | <div id="update" style="display:none;"> |
|
100 | 100 | <h3><%= l(:button_update) %></h3> |
|
101 | 101 | <%= render :partial => 'edit' %> |
|
102 | 102 | </div> |
|
103 | 103 | <% end %> |
|
104 | 104 | |
|
105 | 105 | <p class="other-formats"> |
|
106 | 106 | <%= l(:label_export_to) %> |
|
107 | 107 | <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> |
|
108 | 108 | <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span> |
|
109 | 109 | </p> |
|
110 | 110 | |
|
111 | 111 | <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> |
|
112 | 112 | |
|
113 | 113 | <% content_for :sidebar do %> |
|
114 | 114 | <%= render :partial => 'issues/sidebar' %> |
|
115 | 115 | <% end %> |
|
116 | 116 | |
|
117 | 117 | <% content_for :header_tags do %> |
|
118 | 118 | <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %> |
|
119 | 119 | <%= stylesheet_link_tag 'scm' %> |
|
120 | 120 | <% end %> |
@@ -1,131 +1,137 | |||
|
1 | 1 | /* redMine - project management software |
|
2 | 2 | Copyright (C) 2006-2008 Jean-Philippe Lang */ |
|
3 | 3 | |
|
4 | 4 | function checkAll (id, checked) { |
|
5 | 5 | var els = Element.descendants(id); |
|
6 | 6 | for (var i = 0; i < els.length; i++) { |
|
7 | 7 | if (els[i].disabled==false) { |
|
8 | 8 | els[i].checked = checked; |
|
9 | 9 | } |
|
10 | 10 | } |
|
11 | 11 | } |
|
12 | 12 | |
|
13 | function showAndScrollTo(id, focus) { | |
|
14 | Element.show(id); | |
|
15 | if (focus!=null) { Form.Element.focus(focus); } | |
|
16 | Element.scrollTo(id); | |
|
17 | } | |
|
18 | ||
|
13 | 19 | var fileFieldCount = 1; |
|
14 | 20 | |
|
15 | 21 | function addFileField() { |
|
16 | 22 | if (fileFieldCount >= 10) return false |
|
17 | 23 | fileFieldCount++; |
|
18 | 24 | var f = document.createElement("input"); |
|
19 | 25 | f.type = "file"; |
|
20 | 26 | f.name = "attachments[" + fileFieldCount + "][file]"; |
|
21 | 27 | f.size = 30; |
|
22 | 28 | var d = document.createElement("input"); |
|
23 | 29 | d.type = "text"; |
|
24 | 30 | d.name = "attachments[" + fileFieldCount + "][description]"; |
|
25 | 31 | d.size = 60; |
|
26 | 32 | |
|
27 | 33 | p = document.getElementById("attachments_fields"); |
|
28 | 34 | p.appendChild(document.createElement("br")); |
|
29 | 35 | p.appendChild(f); |
|
30 | 36 | p.appendChild(d); |
|
31 | 37 | } |
|
32 | 38 | |
|
33 | 39 | function showTab(name) { |
|
34 | 40 | var f = $$('div#content .tab-content'); |
|
35 | 41 | for(var i=0; i<f.length; i++){ |
|
36 | 42 | Element.hide(f[i]); |
|
37 | 43 | } |
|
38 | 44 | var f = $$('div.tabs a'); |
|
39 | 45 | for(var i=0; i<f.length; i++){ |
|
40 | 46 | Element.removeClassName(f[i], "selected"); |
|
41 | 47 | } |
|
42 | 48 | Element.show('tab-content-' + name); |
|
43 | 49 | Element.addClassName('tab-' + name, "selected"); |
|
44 | 50 | return false; |
|
45 | 51 | } |
|
46 | 52 | |
|
47 | 53 | function setPredecessorFieldsVisibility() { |
|
48 | 54 | relationType = $('relation_relation_type'); |
|
49 | 55 | if (relationType && relationType.value == "precedes") { |
|
50 | 56 | Element.show('predecessor_fields'); |
|
51 | 57 | } else { |
|
52 | 58 | Element.hide('predecessor_fields'); |
|
53 | 59 | } |
|
54 | 60 | } |
|
55 | 61 | |
|
56 | 62 | function promptToRemote(text, param, url) { |
|
57 | 63 | value = prompt(text + ':'); |
|
58 | 64 | if (value) { |
|
59 | 65 | new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true}); |
|
60 | 66 | return false; |
|
61 | 67 | } |
|
62 | 68 | } |
|
63 | 69 | |
|
64 | 70 | function collapseScmEntry(id) { |
|
65 | 71 | var els = document.getElementsByClassName(id, 'browser'); |
|
66 | 72 | for (var i = 0; i < els.length; i++) { |
|
67 | 73 | if (els[i].hasClassName('open')) { |
|
68 | 74 | collapseScmEntry(els[i].id); |
|
69 | 75 | } |
|
70 | 76 | Element.hide(els[i]); |
|
71 | 77 | } |
|
72 | 78 | $(id).removeClassName('open'); |
|
73 | 79 | } |
|
74 | 80 | |
|
75 | 81 | function expandScmEntry(id) { |
|
76 | 82 | var els = document.getElementsByClassName(id, 'browser'); |
|
77 | 83 | for (var i = 0; i < els.length; i++) { |
|
78 | 84 | Element.show(els[i]); |
|
79 | 85 | if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) { |
|
80 | 86 | expandScmEntry(els[i].id); |
|
81 | 87 | } |
|
82 | 88 | } |
|
83 | 89 | $(id).addClassName('open'); |
|
84 | 90 | } |
|
85 | 91 | |
|
86 | 92 | function scmEntryClick(id) { |
|
87 | 93 | el = $(id); |
|
88 | 94 | if (el.hasClassName('open')) { |
|
89 | 95 | collapseScmEntry(id); |
|
90 | 96 | el.addClassName('collapsed'); |
|
91 | 97 | return false; |
|
92 | 98 | } else if (el.hasClassName('loaded')) { |
|
93 | 99 | expandScmEntry(id); |
|
94 | 100 | el.removeClassName('collapsed'); |
|
95 | 101 | return false; |
|
96 | 102 | } |
|
97 | 103 | if (el.hasClassName('loading')) { |
|
98 | 104 | return false; |
|
99 | 105 | } |
|
100 | 106 | el.addClassName('loading'); |
|
101 | 107 | return true; |
|
102 | 108 | } |
|
103 | 109 | |
|
104 | 110 | function scmEntryLoaded(id) { |
|
105 | 111 | Element.addClassName(id, 'open'); |
|
106 | 112 | Element.addClassName(id, 'loaded'); |
|
107 | 113 | Element.removeClassName(id, 'loading'); |
|
108 | 114 | } |
|
109 | 115 | |
|
110 | 116 | function randomKey(size) { |
|
111 | 117 | 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'); |
|
112 | 118 | var key = ''; |
|
113 | 119 | for (i = 0; i < size; i++) { |
|
114 | 120 | key += chars[Math.floor(Math.random() * chars.length)]; |
|
115 | 121 | } |
|
116 | 122 | return key; |
|
117 | 123 | } |
|
118 | 124 | |
|
119 | 125 | /* shows and hides ajax indicator */ |
|
120 | 126 | Ajax.Responders.register({ |
|
121 | 127 | onCreate: function(){ |
|
122 | 128 | if ($('ajax-indicator') && Ajax.activeRequestCount > 0) { |
|
123 | 129 | Element.show('ajax-indicator'); |
|
124 | 130 | } |
|
125 | 131 | }, |
|
126 | 132 | onComplete: function(){ |
|
127 | 133 | if ($('ajax-indicator') && Ajax.activeRequestCount == 0) { |
|
128 | 134 | Element.hide('ajax-indicator'); |
|
129 | 135 | } |
|
130 | 136 | } |
|
131 | 137 | }); |
General Comments 0
You need to be logged in to leave comments.
Login now