##// END OF EJS Templates
Responsive layout for mobile devices (#19097)....
Jean-Philippe Lang -
r14435:e680ae1aa31b
parent child
Show More
@@ -0,0 +1,83
1 // generic layout specific responsive stuff goes here
2
3 function openFlyout() {
4 $('html').addClass('flyout-is-active');
5 $('#wrapper2').on('click', function(e){
6 e.preventDefault();
7 e.stopPropagation();
8 closeFlyout();
9 });
10 }
11
12 function closeFlyout() {
13 $('html').removeClass('flyout-is-active');
14 $('#wrapper2').off('click');
15 }
16
17
18 function isMobile() {
19 return $('.js-flyout-menu-toggle-button').is(":visible");
20 }
21
22 function setupFlyout() {
23 var mobileInit = false,
24 desktopInit = false;
25
26 /* click handler for mobile menu toggle */
27 $('.js-flyout-menu-toggle-button').on('click', function(e) {
28 e.preventDefault();
29 e.stopPropagation();
30 if($('html').hasClass('flyout-is-active')) {
31 closeFlyout();
32 } else {
33 openFlyout();
34 }
35 });
36
37 /* bind resize handler */
38 $(window).resize(function() {
39 initMenu();
40 })
41
42 /* menu init function for dom detaching and appending on mobile / desktop view */
43 function initMenu() {
44
45 var _initMobileMenu = function() {
46 /* only init mobile menu, if it hasn't been done yet */
47 if(!mobileInit) {
48
49 $('#main-menu > ul').detach().appendTo('.js-project-menu');
50 $('#top-menu > ul').detach().appendTo('.js-general-menu');
51 $('#sidebar > *').detach().appendTo('.js-sidebar');
52 $('#account ul').detach().appendTo('.js-profile-menu');
53
54 mobileInit = true;
55 desktopInit = false;
56 }
57 }
58
59 var _initDesktopMenu = function() {
60 if(!desktopInit) {
61
62 $('.js-project-menu > ul').detach().appendTo('#main-menu');
63 $('.js-general-menu ul').detach().appendTo('#top-menu');
64 $('.js-sidebar > *').detach().appendTo('#sidebar');
65 $('.js-profile-menu ul').detach().appendTo('#account');
66
67 desktopInit = true;
68 mobileInit = false;
69 }
70 }
71
72 if(isMobile()) {
73 _initMobileMenu();
74 } else {
75 _initDesktopMenu();
76 }
77 }
78
79 // init menu on page load
80 initMenu();
81 }
82
83 $(document).ready(setupFlyout);
This diff has been collapsed as it changes many lines, (595 lines changed) Show them Hide them
@@ -0,0 +1,595
1 /*----------------------------------------*\
2 RESPONSIVE CSS
3 \*----------------------------------------*/
4
5
6 /*
7
8 CONTENTS
9
10 A) BASIC MOBILE RESETS
11 B) HEADER & TOP MENUS
12 C) MAIN CONTENT & SIDEBAR
13 D) TOGGLE BUTTON & FLYOUT MENU
14
15 */
16
17
18 /* Hide new elements (toggle button and flyout menu) above 900px */
19 .mobile-toggle-button,
20 .flyout-menu
21 {
22 display: none;
23 }
24
25 /*
26 redmine's body is set to min-width: 900px
27 add first breakpoint here and start adding responsiveness
28 */
29
30 @media all and (max-width: 899px)
31 {
32 /*----------------------------------------*\
33 A) BASIC MOBILE RESETS
34 \*----------------------------------------*/
35
36 /*
37 apply natural border box, see: http://www.paulirish.com/2012/box-sizing-border-box-ftw/
38 this helps us to better deal with percentages and padding / margin
39 */
40 *,
41 *:before,
42 *:after
43 {
44 -webkit-box-sizing: border-box;
45 -moz-box-sizing: border-box;
46 box-sizing: border-box;
47 }
48
49 body,
50 html
51 {
52 height: 100%;
53 margin: 0;
54 padding: 0;
55 }
56
57 html
58 {
59 overflow-y: auto; /* avoid 2nd scrollbar on desktop */
60 }
61
62 body
63 {
64 overflow-x: hidden; /* hide horizontal overflow */
65
66 min-width: 0; /* reset the min-width of 900px */
67 }
68
69
70 body,
71 input,
72 select,
73 textarea,
74 button
75 {
76 font-size: 14px; /* Set font-size for standard elements to 14px */
77 }
78
79
80 select
81 {
82 max-width: 100%; /* prevent long names within select menues from breaking content */
83 }
84
85
86 #wrapper
87 {
88 position: relative;
89
90 max-width: 100%;
91 }
92
93 #wrapper,
94 #wrapper2
95 {
96 margin: 0;
97 }
98
99 /*----------------------------------------*\
100 B) HEADER & TOP MENUS
101 \*----------------------------------------*/
102
103 #header
104 {
105 width: 100%;
106 height: 64px; /* the height of our header on mobile */
107 min-height: 0;
108 margin: 0;
109 padding: 0;
110
111 border: none;
112 background-color: #628db6;
113 }
114
115 /* Hide project name on mobile (project name is still visible in select menu) */
116 #header h1
117 {
118 display: none !important;
119 }
120
121 /* reset #header a color for mobile toggle button */
122 #header a.mobile-toggle-button
123 {
124 color: #f8f8f8;
125 }
126
127
128 /* Hide top-menu and main-menu on mobile, because it's placed in our flyout menu */
129 #top-menu,
130 #header #main-menu
131 {
132 display: none;
133 }
134
135 /* the quick search within header holding search form and #project_quick_jump_box box*/
136 #header #quick-search
137 {
138 float: none;
139 clear: none; /* there are themes which set clear property, this resets it */
140
141 max-width: 100%; /* reset max-width */
142 margin: 0;
143
144 background: inherit;
145 }
146
147 /* this represents the dropdown arrow to left of the mobile project menu */
148 #header .jump-box-arrow:before
149 {
150 /* set a font-size in order to achive same result in different themes */
151 font-family: Verdana, sans-serif;
152 font-size: 2em;
153 line-height: 64px;
154
155 position: absolute;
156 left: 0;
157
158 width: 2em;
159 padding: 0 .5em;
160 /* achieve dropdwon arrow by scaling a caret character */
161
162 content: '^';
163 -webkit-transform: scale(1,-.8);
164 -ms-transform: scale(1,-.8);
165 transform: scale(1,-.8);
166 text-align: right;
167 pointer-events: none;
168
169 opacity: .6;
170 }
171
172 /* styles for combobox within quick-search (#project_quick_jump_box) */
173 #header #quick-search select
174 {
175 font-size: 1.5em;
176 font-weight: bold;
177 line-height: 1.2;
178
179 position: absolute;
180 top: 15px;
181 left: 0;
182
183 float: left;
184
185 width: 100%;
186 max-width: 100%;
187 height: 2em;
188 height: 35px;
189 padding: 5px;
190 padding-right: 72px;
191 padding-left: 50px;
192
193 text-indent: .01px;
194
195 color: inherit;
196 border: 0;
197 -webkit-border-radius: 0;
198 border-radius: 0;
199 background: none;
200 -webkit-box-shadow: none;
201 box-shadow: none;
202 /* hide default browser arrow */
203
204 -webkit-appearance: none;
205 -moz-appearance: none;
206 }
207
208 #header #quick-search form
209 {
210 display: none;
211 }
212
213 /*----------------------------------------*\
214 C) MAIN CONTENT & SIDEBAR
215 \*----------------------------------------*/
216
217 #main
218 {
219 padding: 0;
220 }
221
222 #main.nosidebar #content,
223 div#content
224 {
225 width: 100%;
226 min-height: 0; /* reset min-height of #content */
227 margin: 0;
228 }
229
230
231 /* hide sidebar and sidebar switch panel, since it's placed in mobile flyout menu */
232 #sidebar,
233 #sidebar-switch-panel
234 {
235 display: none;
236 }
237
238 .splitcontentleft
239 {
240 width: 100%; /* use full width */
241 }
242
243 .splitcontentright
244 {
245 width: 100%; /* use full width */
246 }
247
248 /*----------------------------------------*\
249 D) TOGGLE BUTTON & FLYOUT MENU
250 \*----------------------------------------*/
251
252 /* Mobile toggle button */
253
254 .mobile-toggle-button
255 {
256 font-size: 42px;
257 line-height: 64px;
258
259 position: relative;
260 z-index: 10;
261
262 display: block; /* remove display: none; of non-mobile version */
263 float: right;
264
265 width: 60px;
266 height: 64px;
267 margin-top: 0;
268
269 text-align: center;
270
271 border-left: 1px solid #ddd;
272 }
273
274 .mobile-toggle-button:hover,
275 .mobile-toggle-button:active
276 {
277 text-decoration: none;
278 }
279
280 .mobile-toggle-button:after
281 {
282 font-family: Verdana, sans-serif;
283
284 display: block;
285
286 margin-top: -3px;
287
288 content: '\2261';
289 }
290
291 /* search magnifier icon */
292 .search-magnifier
293 {
294 font-family: Verdana;
295
296 cursor: pointer;
297 -webkit-transform: rotate(-45deg);
298 -moz-transform: rotate(45deg);
299 -o-transform: rotate(45deg);
300
301 color: #bbb;
302 }
303
304 .search-magnifier--flyout
305 {
306 font-size: 25px;
307 line-height: 54px;
308
309 position: absolute;
310 z-index: 1;
311 left: 12px;
312 }
313
314
315 /* Flyout Menu */
316
317 .flyout-menu
318 {
319 position: absolute;
320 right: -250px;
321
322 display: block; /* remove display: none; of non-mobile version */
323 overflow-x: hidden;
324
325 width: 250px;
326 height: 100%;
327 margin: 0; /* reset margin for themes that define it */
328 padding: 0; /* reset padding for themes that define it */
329
330 color: white;
331 background-color: #3e5b76;
332 }
333
334
335 /* avoid zoom on search input focus for ios devices */
336 .flyout-menu input[type='text']
337 {
338 font-size: 16px;
339 }
340
341 .flyout-menu h3
342 {
343 font-size: 11px;
344 line-height: 19px;
345
346 height: 20px;
347 margin: 0;
348 padding: 0;
349
350 letter-spacing: .1em;
351 text-transform: uppercase;
352
353 color: white;
354 border-top: 1px solid #506a83;
355 border-bottom: 1px solid #506a83;
356 background-color: #628db6;
357 }
358
359 .flyout-menu h4
360 {
361 color: white;
362 }
363
364 .flyout-menu h3,
365 .flyout-menu h4,
366 .flyout-menu > p,
367 .flyout-menu > a,
368 .flyout-menu ul li a,
369 .flyout-menu__search,
370 .flyout-menu__sidebar > div,
371 .flyout-menu__sidebar > p,
372 .flyout-menu__sidebar > a,
373 .flyout-menu__sidebar > form,
374 .flyout-menu > div,
375 .flyout-menu > form
376 {
377 padding-left: 8px;
378 }
379
380 .flyout-menu .flyout-menu__avatar
381 {
382 margin-top: -1px; /* move avatar up 1px */
383 padding-left: 0;
384 }
385
386 .flyout-menu__sidebar > form
387 {
388 display: block;
389 }
390
391 .flyout-menu__sidebar > form h3
392 {
393 margin-left: -8px;
394 }
395
396 .flyout-menu__sidebar > form label
397 {
398 display: inline-block;
399
400 margin: 8px 0;
401 }
402
403 .flyout-menu__sidebar > form br br
404 {
405 display: none;
406 }
407
408 .flyout-menu ul
409 {
410 margin: 0;
411 padding: 0;
412
413 list-style: none;
414 }
415
416 .flyout-menu ul li a
417 {
418 line-height: 40px;
419
420 display: block;
421 overflow: hidden;
422
423 height: 40px;
424
425 white-space: nowrap;
426 text-overflow: ellipsis;
427
428 border-top: 1px solid rgba(255,255,255,.1);
429 }
430
431 .flyout-menu ul li:first-child a
432 {
433 line-height: 39px;
434
435 height: 39px;
436
437 border-top: none;
438 }
439
440 .flyout-menu a
441 {
442 color: white;
443 }
444
445 .flyout-menu ul li a:hover
446 {
447 text-decoration: none;
448 }
449
450 .flyout-menu ul li a.new-object,
451 .new-object ~ .menu-children
452 {
453 display: none;
454 }
455
456 /* Left flyout search container */
457 .flyout-menu__search
458 {
459 line-height: 54px;
460
461 height: 64px;
462 padding-top: 3px;
463 padding-right: 8px;
464 }
465
466 .flyout-menu__search input[type='text']
467 {
468 line-height: 2;
469
470 width: 100%;
471 height: 38px;
472 padding-left: 27px;
473
474 vertical-align: middle;
475
476 border: none;
477 -webkit-border-radius: 3px;
478 border-radius: 3px;
479 background-color: #fff;
480 }
481
482 .flyout-menu__avatar
483 {
484 display: -webkit-box;
485 display: -webkit-flex;
486 display: -ms-flexbox;
487 display: flex;
488
489 width: 100%;
490
491 border-top: 1px solid rgba(255,255,255,.1);
492 }
493
494
495 .flyout-menu__avatar img.gravatar
496 {
497 width: 40px;
498 height: 40px;
499 padding: 0;
500
501 vertical-align: top;
502
503 border-width: 0;
504 }
505
506 .flyout-menu__avatar a
507 {
508 line-height: 40px;
509
510 height: auto;
511 height: 40px;
512
513 text-decoration: none;
514
515 color: white;
516 }
517
518 /* avatar */
519 .flyout-menu__avatar a:first-child
520 {
521 line-height: 0;
522
523 width: 40px;
524 padding: 0;
525 }
526
527 .flyout-menu__avatar .user
528 {
529 padding-left: 15px;
530 }
531
532 /* user link when no avatar is present */
533 .flyout-menu__avatar--no-avatar a.user
534 {
535 line-height: 40px;
536
537 padding-left: 8px;
538 }
539
540
541 .flyout-is-active body
542 {
543 overflow: hidden; /* for body not to have scrollbars when left flyout menu is active */
544 }
545
546 html.flyout-is-active
547 {
548 overflow: hidden;
549 }
550
551
552 .flyout-is-active #wrapper
553 {
554 right: 250px; /* when left flyout is active, move body to the right (same amount like flyout-menu's width) */
555
556 height: 100%;
557 }
558
559 .flyout-is-active .mobile-toggle-button:after
560 {
561 content: '\00D7'; /* close glyph */
562 }
563
564 .flyout-is-active #wrapper2
565 {
566
567 /*
568 * only relevant for devices with cursor when flyout it active, in order to show,
569 * that whole wrapper content is clickable and closes flyout menu
570 */
571 cursor: pointer;
572 }
573
574
575 #admin-menu
576 {
577 padding-left: 0;
578 }
579
580 #admin-menu li
581 {
582 padding-bottom: 0;
583 }
584
585 #admin-menu a,
586 #admin-menu a.selected
587 {
588 line-height: 40px;
589
590 padding: 0;
591 padding-left: 32px !important;
592
593 background-position: 8px 50%;
594 }
595 }
@@ -1,1337 +1,1338
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31
31
32 extend Forwardable
32 extend Forwardable
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34
34
35 # Return true if user is authorized for controller/action, otherwise false
35 # Return true if user is authorized for controller/action, otherwise false
36 def authorize_for(controller, action)
36 def authorize_for(controller, action)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 end
38 end
39
39
40 # Display a link if user is authorized
40 # Display a link if user is authorized
41 #
41 #
42 # @param [String] name Anchor text (passed to link_to)
42 # @param [String] name Anchor text (passed to link_to)
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [optional, Hash] html_options Options passed to link_to
44 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active? || (User.current.admin? && user.logged?)
54 if user.active? || (User.current.admin? && user.logged?)
55 link_to name, user_path(user), :class => user.css_classes
55 link_to name, user_path(user), :class => user.css_classes
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 #
72 #
73 def link_to_issue(issue, options={})
73 def link_to_issue(issue, options={})
74 title = nil
74 title = nil
75 subject = nil
75 subject = nil
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 if options[:subject] == false
77 if options[:subject] == false
78 title = issue.subject.truncate(60)
78 title = issue.subject.truncate(60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if truncate_length = options[:truncate]
81 if truncate_length = options[:truncate]
82 subject = subject.truncate(truncate_length)
82 subject = subject.truncate(truncate_length)
83 end
83 end
84 end
84 end
85 only_path = options[:only_path].nil? ? true : options[:only_path]
85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 s = link_to(text, issue_url(issue, :only_path => only_path),
86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 :class => issue.css_classes, :title => title)
87 :class => issue.css_classes, :title => title)
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 html_options = options.slice!(:only_path)
100 html_options = options.slice!(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
102 url = send(route_method, attachment, attachment.filename, options)
102 url = send(route_method, attachment, attachment.filename, options)
103 link_to text, url, html_options
103 link_to text, url, html_options
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, repository, options={})
109 def link_to_revision(revision, repository, options={})
110 if repository.is_a?(Project)
110 if repository.is_a?(Project)
111 repository = repository.repository
111 repository = repository.repository
112 end
112 end
113 text = options.delete(:text) || format_revision(revision)
113 text = options.delete(:text) || format_revision(revision)
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 link_to(
115 link_to(
116 h(text),
116 h(text),
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 :title => l(:label_revision_id, format_revision(revision)),
118 :title => l(:label_revision_id, format_revision(revision)),
119 :accesskey => options[:accesskey]
119 :accesskey => options[:accesskey]
120 )
120 )
121 end
121 end
122
122
123 # Generates a link to a message
123 # Generates a link to a message
124 def link_to_message(message, options={}, html_options = nil)
124 def link_to_message(message, options={}, html_options = nil)
125 link_to(
125 link_to(
126 message.subject.truncate(60),
126 message.subject.truncate(60),
127 board_message_url(message.board_id, message.parent_id || message.id, {
127 board_message_url(message.board_id, message.parent_id || message.id, {
128 :r => (message.parent_id && message.id),
128 :r => (message.parent_id && message.id),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :only_path => true
130 :only_path => true
131 }.merge(options)),
131 }.merge(options)),
132 html_options
132 html_options
133 )
133 )
134 end
134 end
135
135
136 # Generates a link to a project if active
136 # Generates a link to a project if active
137 # Examples:
137 # Examples:
138 #
138 #
139 # link_to_project(project) # => link to the specified project overview
139 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 #
142 #
143 def link_to_project(project, options={}, html_options = nil)
143 def link_to_project(project, options={}, html_options = nil)
144 if project.archived?
144 if project.archived?
145 h(project.name)
145 h(project.name)
146 else
146 else
147 link_to project.name,
147 link_to project.name,
148 project_url(project, {:only_path => true}.merge(options)),
148 project_url(project, {:only_path => true}.merge(options)),
149 html_options
149 html_options
150 end
150 end
151 end
151 end
152
152
153 # Generates a link to a project settings if active
153 # Generates a link to a project settings if active
154 def link_to_project_settings(project, options={}, html_options=nil)
154 def link_to_project_settings(project, options={}, html_options=nil)
155 if project.active?
155 if project.active?
156 link_to project.name, settings_project_path(project, options), html_options
156 link_to project.name, settings_project_path(project, options), html_options
157 elsif project.archived?
157 elsif project.archived?
158 h(project.name)
158 h(project.name)
159 else
159 else
160 link_to project.name, project_path(project, options), html_options
160 link_to project.name, project_path(project, options), html_options
161 end
161 end
162 end
162 end
163
163
164 # Generates a link to a version
164 # Generates a link to a version
165 def link_to_version(version, options = {})
165 def link_to_version(version, options = {})
166 return '' unless version && version.is_a?(Version)
166 return '' unless version && version.is_a?(Version)
167 options = {:title => format_date(version.effective_date)}.merge(options)
167 options = {:title => format_date(version.effective_date)}.merge(options)
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 end
169 end
170
170
171 # Helper that formats object for html or text rendering
171 # Helper that formats object for html or text rendering
172 def format_object(object, html=true, &block)
172 def format_object(object, html=true, &block)
173 if block_given?
173 if block_given?
174 object = yield object
174 object = yield object
175 end
175 end
176 case object.class.name
176 case object.class.name
177 when 'Array'
177 when 'Array'
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 when 'Time'
179 when 'Time'
180 format_time(object)
180 format_time(object)
181 when 'Date'
181 when 'Date'
182 format_date(object)
182 format_date(object)
183 when 'Fixnum'
183 when 'Fixnum'
184 object.to_s
184 object.to_s
185 when 'Float'
185 when 'Float'
186 sprintf "%.2f", object
186 sprintf "%.2f", object
187 when 'User'
187 when 'User'
188 html ? link_to_user(object) : object.to_s
188 html ? link_to_user(object) : object.to_s
189 when 'Project'
189 when 'Project'
190 html ? link_to_project(object) : object.to_s
190 html ? link_to_project(object) : object.to_s
191 when 'Version'
191 when 'Version'
192 html ? link_to_version(object) : object.to_s
192 html ? link_to_version(object) : object.to_s
193 when 'TrueClass'
193 when 'TrueClass'
194 l(:general_text_Yes)
194 l(:general_text_Yes)
195 when 'FalseClass'
195 when 'FalseClass'
196 l(:general_text_No)
196 l(:general_text_No)
197 when 'Issue'
197 when 'Issue'
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 when 'CustomValue', 'CustomFieldValue'
199 when 'CustomValue', 'CustomFieldValue'
200 if object.custom_field
200 if object.custom_field
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 if f.nil? || f.is_a?(String)
202 if f.nil? || f.is_a?(String)
203 f
203 f
204 else
204 else
205 format_object(f, html, &block)
205 format_object(f, html, &block)
206 end
206 end
207 else
207 else
208 object.value.to_s
208 object.value.to_s
209 end
209 end
210 else
210 else
211 html ? h(object) : object.to_s
211 html ? h(object) : object.to_s
212 end
212 end
213 end
213 end
214
214
215 def wiki_page_path(page, options={})
215 def wiki_page_path(page, options={})
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 end
217 end
218
218
219 def thumbnail_tag(attachment)
219 def thumbnail_tag(attachment)
220 link_to image_tag(thumbnail_path(attachment)),
220 link_to image_tag(thumbnail_path(attachment)),
221 named_attachment_path(attachment, attachment.filename),
221 named_attachment_path(attachment, attachment.filename),
222 :title => attachment.filename
222 :title => attachment.filename
223 end
223 end
224
224
225 def toggle_link(name, id, options={})
225 def toggle_link(name, id, options={})
226 onclick = "$('##{id}').toggle(); "
226 onclick = "$('##{id}').toggle(); "
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << "return false;"
228 onclick << "return false;"
229 link_to(name, "#", :onclick => onclick)
229 link_to(name, "#", :onclick => onclick)
230 end
230 end
231
231
232 def format_activity_title(text)
232 def format_activity_title(text)
233 h(truncate_single_line_raw(text, 100))
233 h(truncate_single_line_raw(text, 100))
234 end
234 end
235
235
236 def format_activity_day(date)
236 def format_activity_day(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 end
238 end
239
239
240 def format_activity_description(text)
240 def format_activity_description(text)
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 ).gsub(/[\r\n]+/, "<br />").html_safe
242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 end
243 end
244
244
245 def format_version_name(version)
245 def format_version_name(version)
246 if version.project == @project
246 if version.project == @project
247 h(version)
247 h(version)
248 else
248 else
249 h("#{version.project} - #{version}")
249 h("#{version.project} - #{version}")
250 end
250 end
251 end
251 end
252
252
253 def due_date_distance_in_words(date)
253 def due_date_distance_in_words(date)
254 if date
254 if date
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 end
256 end
257 end
257 end
258
258
259 # Renders a tree of projects as a nested set of unordered lists
259 # Renders a tree of projects as a nested set of unordered lists
260 # The given collection may be a subset of the whole project tree
260 # The given collection may be a subset of the whole project tree
261 # (eg. some intermediate nodes are private and can not be seen)
261 # (eg. some intermediate nodes are private and can not be seen)
262 def render_project_nested_lists(projects, &block)
262 def render_project_nested_lists(projects, &block)
263 s = ''
263 s = ''
264 if projects.any?
264 if projects.any?
265 ancestors = []
265 ancestors = []
266 original_project = @project
266 original_project = @project
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 # set the project environment to please macros.
268 # set the project environment to please macros.
269 @project = project
269 @project = project
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 else
272 else
273 ancestors.pop
273 ancestors.pop
274 s << "</li>"
274 s << "</li>"
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 ancestors.pop
276 ancestors.pop
277 s << "</ul></li>\n"
277 s << "</ul></li>\n"
278 end
278 end
279 end
279 end
280 classes = (ancestors.empty? ? 'root' : 'child')
280 classes = (ancestors.empty? ? 'root' : 'child')
281 s << "<li class='#{classes}'><div class='#{classes}'>"
281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << h(block_given? ? capture(project, &block) : project.name)
282 s << h(block_given? ? capture(project, &block) : project.name)
283 s << "</div>\n"
283 s << "</div>\n"
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 @project = original_project
287 @project = original_project
288 end
288 end
289 s.html_safe
289 s.html_safe
290 end
290 end
291
291
292 def render_page_hierarchy(pages, node=nil, options={})
292 def render_page_hierarchy(pages, node=nil, options={})
293 content = ''
293 content = ''
294 if pages[node]
294 if pages[node]
295 content << "<ul class=\"pages-hierarchy\">\n"
295 content << "<ul class=\"pages-hierarchy\">\n"
296 pages[node].each do |page|
296 pages[node].each do |page|
297 content << "<li>"
297 content << "<li>"
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "</li>\n"
301 content << "</li>\n"
302 end
302 end
303 content << "</ul>\n"
303 content << "</ul>\n"
304 end
304 end
305 content.html_safe
305 content.html_safe
306 end
306 end
307
307
308 # Renders flash messages
308 # Renders flash messages
309 def render_flash_messages
309 def render_flash_messages
310 s = ''
310 s = ''
311 flash.each do |k,v|
311 flash.each do |k,v|
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 end
313 end
314 s.html_safe
314 s.html_safe
315 end
315 end
316
316
317 # Renders tabs and their content
317 # Renders tabs and their content
318 def render_tabs(tabs, selected=params[:tab])
318 def render_tabs(tabs, selected=params[:tab])
319 if tabs.any?
319 if tabs.any?
320 unless tabs.detect {|tab| tab[:name] == selected}
320 unless tabs.detect {|tab| tab[:name] == selected}
321 selected = nil
321 selected = nil
322 end
322 end
323 selected ||= tabs.first[:name]
323 selected ||= tabs.first[:name]
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 else
325 else
326 content_tag 'p', l(:label_no_data), :class => "nodata"
326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 end
327 end
328 end
328 end
329
329
330 # Renders the project quick-jump box
330 # Renders the project quick-jump box
331 def render_project_jump_box
331 def render_project_jump_box
332 return unless User.current.logged?
332 return unless User.current.logged?
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 if projects.any?
334 if projects.any?
335 options =
335 options =
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 '<option value="" disabled="disabled">---</option>').html_safe
337 '<option value="" disabled="disabled">---</option>').html_safe
338
338
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 { :value => project_path(:id => p, :jump => current_menu_item) }
340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 end
341 end
342
342
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
343 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 end
345 end
345 end
346 end
346
347
347 def project_tree_options_for_select(projects, options = {})
348 def project_tree_options_for_select(projects, options = {})
348 s = ''.html_safe
349 s = ''.html_safe
349 if blank_text = options[:include_blank]
350 if blank_text = options[:include_blank]
350 if blank_text == true
351 if blank_text == true
351 blank_text = '&nbsp;'.html_safe
352 blank_text = '&nbsp;'.html_safe
352 end
353 end
353 s << content_tag('option', blank_text, :value => '')
354 s << content_tag('option', blank_text, :value => '')
354 end
355 end
355 project_tree(projects) do |project, level|
356 project_tree(projects) do |project, level|
356 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 tag_options = {:value => project.id}
358 tag_options = {:value => project.id}
358 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 tag_options[:selected] = 'selected'
360 tag_options[:selected] = 'selected'
360 else
361 else
361 tag_options[:selected] = nil
362 tag_options[:selected] = nil
362 end
363 end
363 tag_options.merge!(yield(project)) if block_given?
364 tag_options.merge!(yield(project)) if block_given?
364 s << content_tag('option', name_prefix + h(project), tag_options)
365 s << content_tag('option', name_prefix + h(project), tag_options)
365 end
366 end
366 s.html_safe
367 s.html_safe
367 end
368 end
368
369
369 # Yields the given block for each project with its level in the tree
370 # Yields the given block for each project with its level in the tree
370 #
371 #
371 # Wrapper for Project#project_tree
372 # Wrapper for Project#project_tree
372 def project_tree(projects, &block)
373 def project_tree(projects, &block)
373 Project.project_tree(projects, &block)
374 Project.project_tree(projects, &block)
374 end
375 end
375
376
376 def principals_check_box_tags(name, principals)
377 def principals_check_box_tags(name, principals)
377 s = ''
378 s = ''
378 principals.each do |principal|
379 principals.each do |principal|
379 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 end
381 end
381 s.html_safe
382 s.html_safe
382 end
383 end
383
384
384 # Returns a string for users/groups option tags
385 # Returns a string for users/groups option tags
385 def principals_options_for_select(collection, selected=nil)
386 def principals_options_for_select(collection, selected=nil)
386 s = ''
387 s = ''
387 if collection.include?(User.current)
388 if collection.include?(User.current)
388 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 end
390 end
390 groups = ''
391 groups = ''
391 collection.sort.each do |element|
392 collection.sort.each do |element|
392 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 end
395 end
395 unless groups.empty?
396 unless groups.empty?
396 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 end
398 end
398 s.html_safe
399 s.html_safe
399 end
400 end
400
401
401 def option_tag(name, text, value, selected=nil, options={})
402 def option_tag(name, text, value, selected=nil, options={})
402 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 end
404 end
404
405
405 def truncate_single_line_raw(string, length)
406 def truncate_single_line_raw(string, length)
406 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 end
408 end
408
409
409 # Truncates at line break after 250 characters or options[:length]
410 # Truncates at line break after 250 characters or options[:length]
410 def truncate_lines(string, options={})
411 def truncate_lines(string, options={})
411 length = options[:length] || 250
412 length = options[:length] || 250
412 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 "#{$1}..."
414 "#{$1}..."
414 else
415 else
415 string
416 string
416 end
417 end
417 end
418 end
418
419
419 def anchor(text)
420 def anchor(text)
420 text.to_s.gsub(' ', '_')
421 text.to_s.gsub(' ', '_')
421 end
422 end
422
423
423 def html_hours(text)
424 def html_hours(text)
424 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 end
426 end
426
427
427 def authoring(created, author, options={})
428 def authoring(created, author, options={})
428 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 end
430 end
430
431
431 def time_tag(time)
432 def time_tag(time)
432 text = distance_of_time_in_words(Time.now, time)
433 text = distance_of_time_in_words(Time.now, time)
433 if @project
434 if @project
434 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 else
436 else
436 content_tag('abbr', text, :title => format_time(time))
437 content_tag('abbr', text, :title => format_time(time))
437 end
438 end
438 end
439 end
439
440
440 def syntax_highlight_lines(name, content)
441 def syntax_highlight_lines(name, content)
441 lines = []
442 lines = []
442 syntax_highlight(name, content).each_line { |line| lines << line }
443 syntax_highlight(name, content).each_line { |line| lines << line }
443 lines
444 lines
444 end
445 end
445
446
446 def syntax_highlight(name, content)
447 def syntax_highlight(name, content)
447 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 end
449 end
449
450
450 def to_path_param(path)
451 def to_path_param(path)
451 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 str.blank? ? nil : str
453 str.blank? ? nil : str
453 end
454 end
454
455
455 def reorder_links(name, url, method = :post)
456 def reorder_links(name, url, method = :post)
456 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
457 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
457 url.merge({"#{name}[move_to]" => 'highest'}),
458 url.merge({"#{name}[move_to]" => 'highest'}),
458 :method => method, :title => l(:label_sort_highest)) +
459 :method => method, :title => l(:label_sort_highest)) +
459 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
460 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
460 url.merge({"#{name}[move_to]" => 'higher'}),
461 url.merge({"#{name}[move_to]" => 'higher'}),
461 :method => method, :title => l(:label_sort_higher)) +
462 :method => method, :title => l(:label_sort_higher)) +
462 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
463 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
463 url.merge({"#{name}[move_to]" => 'lower'}),
464 url.merge({"#{name}[move_to]" => 'lower'}),
464 :method => method, :title => l(:label_sort_lower)) +
465 :method => method, :title => l(:label_sort_lower)) +
465 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
466 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
466 url.merge({"#{name}[move_to]" => 'lowest'}),
467 url.merge({"#{name}[move_to]" => 'lowest'}),
467 :method => method, :title => l(:label_sort_lowest))
468 :method => method, :title => l(:label_sort_lowest))
468 end
469 end
469
470
470 def breadcrumb(*args)
471 def breadcrumb(*args)
471 elements = args.flatten
472 elements = args.flatten
472 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
473 end
474 end
474
475
475 def other_formats_links(&block)
476 def other_formats_links(&block)
476 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
477 yield Redmine::Views::OtherFormatsBuilder.new(self)
478 yield Redmine::Views::OtherFormatsBuilder.new(self)
478 concat('</p>'.html_safe)
479 concat('</p>'.html_safe)
479 end
480 end
480
481
481 def page_header_title
482 def page_header_title
482 if @project.nil? || @project.new_record?
483 if @project.nil? || @project.new_record?
483 h(Setting.app_title)
484 h(Setting.app_title)
484 else
485 else
485 b = []
486 b = []
486 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
487 if ancestors.any?
488 if ancestors.any?
488 root = ancestors.shift
489 root = ancestors.shift
489 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
490 if ancestors.size > 2
491 if ancestors.size > 2
491 b << "\xe2\x80\xa6"
492 b << "\xe2\x80\xa6"
492 ancestors = ancestors[-2, 2]
493 ancestors = ancestors[-2, 2]
493 end
494 end
494 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
495 end
496 end
496 b << h(@project)
497 b << h(@project)
497 b.join(" \xc2\xbb ").html_safe
498 b.join(" \xc2\xbb ").html_safe
498 end
499 end
499 end
500 end
500
501
501 # Returns a h2 tag and sets the html title with the given arguments
502 # Returns a h2 tag and sets the html title with the given arguments
502 def title(*args)
503 def title(*args)
503 strings = args.map do |arg|
504 strings = args.map do |arg|
504 if arg.is_a?(Array) && arg.size >= 2
505 if arg.is_a?(Array) && arg.size >= 2
505 link_to(*arg)
506 link_to(*arg)
506 else
507 else
507 h(arg.to_s)
508 h(arg.to_s)
508 end
509 end
509 end
510 end
510 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
511 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
511 content_tag('h2', strings.join(' &#187; ').html_safe)
512 content_tag('h2', strings.join(' &#187; ').html_safe)
512 end
513 end
513
514
514 # Sets the html title
515 # Sets the html title
515 # Returns the html title when called without arguments
516 # Returns the html title when called without arguments
516 # Current project name and app_title and automatically appended
517 # Current project name and app_title and automatically appended
517 # Exemples:
518 # Exemples:
518 # html_title 'Foo', 'Bar'
519 # html_title 'Foo', 'Bar'
519 # html_title # => 'Foo - Bar - My Project - Redmine'
520 # html_title # => 'Foo - Bar - My Project - Redmine'
520 def html_title(*args)
521 def html_title(*args)
521 if args.empty?
522 if args.empty?
522 title = @html_title || []
523 title = @html_title || []
523 title << @project.name if @project
524 title << @project.name if @project
524 title << Setting.app_title unless Setting.app_title == title.last
525 title << Setting.app_title unless Setting.app_title == title.last
525 title.reject(&:blank?).join(' - ')
526 title.reject(&:blank?).join(' - ')
526 else
527 else
527 @html_title ||= []
528 @html_title ||= []
528 @html_title += args
529 @html_title += args
529 end
530 end
530 end
531 end
531
532
532 # Returns the theme, controller name, and action as css classes for the
533 # Returns the theme, controller name, and action as css classes for the
533 # HTML body.
534 # HTML body.
534 def body_css_classes
535 def body_css_classes
535 css = []
536 css = []
536 if theme = Redmine::Themes.theme(Setting.ui_theme)
537 if theme = Redmine::Themes.theme(Setting.ui_theme)
537 css << 'theme-' + theme.name
538 css << 'theme-' + theme.name
538 end
539 end
539
540
540 css << 'project-' + @project.identifier if @project && @project.identifier.present?
541 css << 'project-' + @project.identifier if @project && @project.identifier.present?
541 css << 'controller-' + controller_name
542 css << 'controller-' + controller_name
542 css << 'action-' + action_name
543 css << 'action-' + action_name
543 css.join(' ')
544 css.join(' ')
544 end
545 end
545
546
546 def accesskey(s)
547 def accesskey(s)
547 @used_accesskeys ||= []
548 @used_accesskeys ||= []
548 key = Redmine::AccessKeys.key_for(s)
549 key = Redmine::AccessKeys.key_for(s)
549 return nil if @used_accesskeys.include?(key)
550 return nil if @used_accesskeys.include?(key)
550 @used_accesskeys << key
551 @used_accesskeys << key
551 key
552 key
552 end
553 end
553
554
554 # Formats text according to system settings.
555 # Formats text according to system settings.
555 # 2 ways to call this method:
556 # 2 ways to call this method:
556 # * with a String: textilizable(text, options)
557 # * with a String: textilizable(text, options)
557 # * with an object and one of its attribute: textilizable(issue, :description, options)
558 # * with an object and one of its attribute: textilizable(issue, :description, options)
558 def textilizable(*args)
559 def textilizable(*args)
559 options = args.last.is_a?(Hash) ? args.pop : {}
560 options = args.last.is_a?(Hash) ? args.pop : {}
560 case args.size
561 case args.size
561 when 1
562 when 1
562 obj = options[:object]
563 obj = options[:object]
563 text = args.shift
564 text = args.shift
564 when 2
565 when 2
565 obj = args.shift
566 obj = args.shift
566 attr = args.shift
567 attr = args.shift
567 text = obj.send(attr).to_s
568 text = obj.send(attr).to_s
568 else
569 else
569 raise ArgumentError, 'invalid arguments to textilizable'
570 raise ArgumentError, 'invalid arguments to textilizable'
570 end
571 end
571 return '' if text.blank?
572 return '' if text.blank?
572 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
573 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
573 @only_path = only_path = options.delete(:only_path) == false ? false : true
574 @only_path = only_path = options.delete(:only_path) == false ? false : true
574
575
575 text = text.dup
576 text = text.dup
576 macros = catch_macros(text)
577 macros = catch_macros(text)
577 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
578 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
578
579
579 @parsed_headings = []
580 @parsed_headings = []
580 @heading_anchors = {}
581 @heading_anchors = {}
581 @current_section = 0 if options[:edit_section_links]
582 @current_section = 0 if options[:edit_section_links]
582
583
583 parse_sections(text, project, obj, attr, only_path, options)
584 parse_sections(text, project, obj, attr, only_path, options)
584 text = parse_non_pre_blocks(text, obj, macros) do |text|
585 text = parse_non_pre_blocks(text, obj, macros) do |text|
585 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
586 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
586 send method_name, text, project, obj, attr, only_path, options
587 send method_name, text, project, obj, attr, only_path, options
587 end
588 end
588 end
589 end
589 parse_headings(text, project, obj, attr, only_path, options)
590 parse_headings(text, project, obj, attr, only_path, options)
590
591
591 if @parsed_headings.any?
592 if @parsed_headings.any?
592 replace_toc(text, @parsed_headings)
593 replace_toc(text, @parsed_headings)
593 end
594 end
594
595
595 text.html_safe
596 text.html_safe
596 end
597 end
597
598
598 def parse_non_pre_blocks(text, obj, macros)
599 def parse_non_pre_blocks(text, obj, macros)
599 s = StringScanner.new(text)
600 s = StringScanner.new(text)
600 tags = []
601 tags = []
601 parsed = ''
602 parsed = ''
602 while !s.eos?
603 while !s.eos?
603 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
604 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
604 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
605 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
605 if tags.empty?
606 if tags.empty?
606 yield text
607 yield text
607 inject_macros(text, obj, macros) if macros.any?
608 inject_macros(text, obj, macros) if macros.any?
608 else
609 else
609 inject_macros(text, obj, macros, false) if macros.any?
610 inject_macros(text, obj, macros, false) if macros.any?
610 end
611 end
611 parsed << text
612 parsed << text
612 if tag
613 if tag
613 if closing
614 if closing
614 if tags.last && tags.last.casecmp(tag) == 0
615 if tags.last && tags.last.casecmp(tag) == 0
615 tags.pop
616 tags.pop
616 end
617 end
617 else
618 else
618 tags << tag.downcase
619 tags << tag.downcase
619 end
620 end
620 parsed << full_tag
621 parsed << full_tag
621 end
622 end
622 end
623 end
623 # Close any non closing tags
624 # Close any non closing tags
624 while tag = tags.pop
625 while tag = tags.pop
625 parsed << "</#{tag}>"
626 parsed << "</#{tag}>"
626 end
627 end
627 parsed
628 parsed
628 end
629 end
629
630
630 def parse_inline_attachments(text, project, obj, attr, only_path, options)
631 def parse_inline_attachments(text, project, obj, attr, only_path, options)
631 return if options[:inline_attachments] == false
632 return if options[:inline_attachments] == false
632
633
633 # when using an image link, try to use an attachment, if possible
634 # when using an image link, try to use an attachment, if possible
634 attachments = options[:attachments] || []
635 attachments = options[:attachments] || []
635 attachments += obj.attachments if obj.respond_to?(:attachments)
636 attachments += obj.attachments if obj.respond_to?(:attachments)
636 if attachments.present?
637 if attachments.present?
637 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
638 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
638 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
639 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
639 # search for the picture in attachments
640 # search for the picture in attachments
640 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
641 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
641 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
642 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
642 desc = found.description.to_s.gsub('"', '')
643 desc = found.description.to_s.gsub('"', '')
643 if !desc.blank? && alttext.blank?
644 if !desc.blank? && alttext.blank?
644 alt = " title=\"#{desc}\" alt=\"#{desc}\""
645 alt = " title=\"#{desc}\" alt=\"#{desc}\""
645 end
646 end
646 "src=\"#{image_url}\"#{alt}"
647 "src=\"#{image_url}\"#{alt}"
647 else
648 else
648 m
649 m
649 end
650 end
650 end
651 end
651 end
652 end
652 end
653 end
653
654
654 # Wiki links
655 # Wiki links
655 #
656 #
656 # Examples:
657 # Examples:
657 # [[mypage]]
658 # [[mypage]]
658 # [[mypage|mytext]]
659 # [[mypage|mytext]]
659 # wiki links can refer other project wikis, using project name or identifier:
660 # wiki links can refer other project wikis, using project name or identifier:
660 # [[project:]] -> wiki starting page
661 # [[project:]] -> wiki starting page
661 # [[project:|mytext]]
662 # [[project:|mytext]]
662 # [[project:mypage]]
663 # [[project:mypage]]
663 # [[project:mypage|mytext]]
664 # [[project:mypage|mytext]]
664 def parse_wiki_links(text, project, obj, attr, only_path, options)
665 def parse_wiki_links(text, project, obj, attr, only_path, options)
665 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
666 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
666 link_project = project
667 link_project = project
667 esc, all, page, title = $1, $2, $3, $5
668 esc, all, page, title = $1, $2, $3, $5
668 if esc.nil?
669 if esc.nil?
669 if page =~ /^([^\:]+)\:(.*)$/
670 if page =~ /^([^\:]+)\:(.*)$/
670 identifier, page = $1, $2
671 identifier, page = $1, $2
671 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
672 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
672 title ||= identifier if page.blank?
673 title ||= identifier if page.blank?
673 end
674 end
674
675
675 if link_project && link_project.wiki
676 if link_project && link_project.wiki
676 # extract anchor
677 # extract anchor
677 anchor = nil
678 anchor = nil
678 if page =~ /^(.+?)\#(.+)$/
679 if page =~ /^(.+?)\#(.+)$/
679 page, anchor = $1, $2
680 page, anchor = $1, $2
680 end
681 end
681 anchor = sanitize_anchor_name(anchor) if anchor.present?
682 anchor = sanitize_anchor_name(anchor) if anchor.present?
682 # check if page exists
683 # check if page exists
683 wiki_page = link_project.wiki.find_page(page)
684 wiki_page = link_project.wiki.find_page(page)
684 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
685 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
685 "##{anchor}"
686 "##{anchor}"
686 else
687 else
687 case options[:wiki_links]
688 case options[:wiki_links]
688 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
689 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
689 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
690 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
690 else
691 else
691 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
692 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
692 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
693 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
693 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
694 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
694 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
695 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
695 end
696 end
696 end
697 end
697 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
698 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
698 else
699 else
699 # project or wiki doesn't exist
700 # project or wiki doesn't exist
700 all
701 all
701 end
702 end
702 else
703 else
703 all
704 all
704 end
705 end
705 end
706 end
706 end
707 end
707
708
708 # Redmine links
709 # Redmine links
709 #
710 #
710 # Examples:
711 # Examples:
711 # Issues:
712 # Issues:
712 # #52 -> Link to issue #52
713 # #52 -> Link to issue #52
713 # Changesets:
714 # Changesets:
714 # r52 -> Link to revision 52
715 # r52 -> Link to revision 52
715 # commit:a85130f -> Link to scmid starting with a85130f
716 # commit:a85130f -> Link to scmid starting with a85130f
716 # Documents:
717 # Documents:
717 # document#17 -> Link to document with id 17
718 # document#17 -> Link to document with id 17
718 # document:Greetings -> Link to the document with title "Greetings"
719 # document:Greetings -> Link to the document with title "Greetings"
719 # document:"Some document" -> Link to the document with title "Some document"
720 # document:"Some document" -> Link to the document with title "Some document"
720 # Versions:
721 # Versions:
721 # version#3 -> Link to version with id 3
722 # version#3 -> Link to version with id 3
722 # version:1.0.0 -> Link to version named "1.0.0"
723 # version:1.0.0 -> Link to version named "1.0.0"
723 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
724 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
724 # Attachments:
725 # Attachments:
725 # attachment:file.zip -> Link to the attachment of the current object named file.zip
726 # attachment:file.zip -> Link to the attachment of the current object named file.zip
726 # Source files:
727 # Source files:
727 # source:some/file -> Link to the file located at /some/file in the project's repository
728 # source:some/file -> Link to the file located at /some/file in the project's repository
728 # source:some/file@52 -> Link to the file's revision 52
729 # source:some/file@52 -> Link to the file's revision 52
729 # source:some/file#L120 -> Link to line 120 of the file
730 # source:some/file#L120 -> Link to line 120 of the file
730 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
731 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
731 # export:some/file -> Force the download of the file
732 # export:some/file -> Force the download of the file
732 # Forum messages:
733 # Forum messages:
733 # message#1218 -> Link to message with id 1218
734 # message#1218 -> Link to message with id 1218
734 # Projects:
735 # Projects:
735 # project:someproject -> Link to project named "someproject"
736 # project:someproject -> Link to project named "someproject"
736 # project#3 -> Link to project with id 3
737 # project#3 -> Link to project with id 3
737 #
738 #
738 # Links can refer other objects from other projects, using project identifier:
739 # Links can refer other objects from other projects, using project identifier:
739 # identifier:r52
740 # identifier:r52
740 # identifier:document:"Some document"
741 # identifier:document:"Some document"
741 # identifier:version:1.0.0
742 # identifier:version:1.0.0
742 # identifier:source:some/file
743 # identifier:source:some/file
743 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
744 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
744 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
745 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
745 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
746 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
746 if tag_content
747 if tag_content
747 $&
748 $&
748 else
749 else
749 link = nil
750 link = nil
750 project = default_project
751 project = default_project
751 if project_identifier
752 if project_identifier
752 project = Project.visible.find_by_identifier(project_identifier)
753 project = Project.visible.find_by_identifier(project_identifier)
753 end
754 end
754 if esc.nil?
755 if esc.nil?
755 if prefix.nil? && sep == 'r'
756 if prefix.nil? && sep == 'r'
756 if project
757 if project
757 repository = nil
758 repository = nil
758 if repo_identifier
759 if repo_identifier
759 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
760 else
761 else
761 repository = project.repository
762 repository = project.repository
762 end
763 end
763 # project.changesets.visible raises an SQL error because of a double join on repositories
764 # project.changesets.visible raises an SQL error because of a double join on repositories
764 if repository &&
765 if repository &&
765 (changeset = Changeset.visible.
766 (changeset = Changeset.visible.
766 find_by_repository_id_and_revision(repository.id, identifier))
767 find_by_repository_id_and_revision(repository.id, identifier))
767 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
768 {:only_path => only_path, :controller => 'repositories',
769 {:only_path => only_path, :controller => 'repositories',
769 :action => 'revision', :id => project,
770 :action => 'revision', :id => project,
770 :repository_id => repository.identifier_param,
771 :repository_id => repository.identifier_param,
771 :rev => changeset.revision},
772 :rev => changeset.revision},
772 :class => 'changeset',
773 :class => 'changeset',
773 :title => truncate_single_line_raw(changeset.comments, 100))
774 :title => truncate_single_line_raw(changeset.comments, 100))
774 end
775 end
775 end
776 end
776 elsif sep == '#'
777 elsif sep == '#'
777 oid = identifier.to_i
778 oid = identifier.to_i
778 case prefix
779 case prefix
779 when nil
780 when nil
780 if oid.to_s == identifier &&
781 if oid.to_s == identifier &&
781 issue = Issue.visible.find_by_id(oid)
782 issue = Issue.visible.find_by_id(oid)
782 anchor = comment_id ? "note-#{comment_id}" : nil
783 anchor = comment_id ? "note-#{comment_id}" : nil
783 link = link_to("##{oid}#{comment_suffix}",
784 link = link_to("##{oid}#{comment_suffix}",
784 issue_url(issue, :only_path => only_path, :anchor => anchor),
785 issue_url(issue, :only_path => only_path, :anchor => anchor),
785 :class => issue.css_classes,
786 :class => issue.css_classes,
786 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
787 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
787 end
788 end
788 when 'document'
789 when 'document'
789 if document = Document.visible.find_by_id(oid)
790 if document = Document.visible.find_by_id(oid)
790 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
791 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
791 end
792 end
792 when 'version'
793 when 'version'
793 if version = Version.visible.find_by_id(oid)
794 if version = Version.visible.find_by_id(oid)
794 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
795 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
795 end
796 end
796 when 'message'
797 when 'message'
797 if message = Message.visible.find_by_id(oid)
798 if message = Message.visible.find_by_id(oid)
798 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
799 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
799 end
800 end
800 when 'forum'
801 when 'forum'
801 if board = Board.visible.find_by_id(oid)
802 if board = Board.visible.find_by_id(oid)
802 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
803 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
803 end
804 end
804 when 'news'
805 when 'news'
805 if news = News.visible.find_by_id(oid)
806 if news = News.visible.find_by_id(oid)
806 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
807 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
807 end
808 end
808 when 'project'
809 when 'project'
809 if p = Project.visible.find_by_id(oid)
810 if p = Project.visible.find_by_id(oid)
810 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
811 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
811 end
812 end
812 end
813 end
813 elsif sep == ':'
814 elsif sep == ':'
814 # removes the double quotes if any
815 # removes the double quotes if any
815 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
816 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
816 name = CGI.unescapeHTML(name)
817 name = CGI.unescapeHTML(name)
817 case prefix
818 case prefix
818 when 'document'
819 when 'document'
819 if project && document = project.documents.visible.find_by_title(name)
820 if project && document = project.documents.visible.find_by_title(name)
820 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
821 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
821 end
822 end
822 when 'version'
823 when 'version'
823 if project && version = project.versions.visible.find_by_name(name)
824 if project && version = project.versions.visible.find_by_name(name)
824 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
825 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
825 end
826 end
826 when 'forum'
827 when 'forum'
827 if project && board = project.boards.visible.find_by_name(name)
828 if project && board = project.boards.visible.find_by_name(name)
828 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 end
830 end
830 when 'news'
831 when 'news'
831 if project && news = project.news.visible.find_by_title(name)
832 if project && news = project.news.visible.find_by_title(name)
832 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 end
834 end
834 when 'commit', 'source', 'export'
835 when 'commit', 'source', 'export'
835 if project
836 if project
836 repository = nil
837 repository = nil
837 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
838 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
838 repo_prefix, repo_identifier, name = $1, $2, $3
839 repo_prefix, repo_identifier, name = $1, $2, $3
839 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
840 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
840 else
841 else
841 repository = project.repository
842 repository = project.repository
842 end
843 end
843 if prefix == 'commit'
844 if prefix == 'commit'
844 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
845 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
845 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},
846 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},
846 :class => 'changeset',
847 :class => 'changeset',
847 :title => truncate_single_line_raw(changeset.comments, 100)
848 :title => truncate_single_line_raw(changeset.comments, 100)
848 end
849 end
849 else
850 else
850 if repository && User.current.allowed_to?(:browse_repository, project)
851 if repository && User.current.allowed_to?(:browse_repository, project)
851 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
852 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
852 path, rev, anchor = $1, $3, $5
853 path, rev, anchor = $1, $3, $5
853 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
854 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
854 :path => to_path_param(path),
855 :path => to_path_param(path),
855 :rev => rev,
856 :rev => rev,
856 :anchor => anchor},
857 :anchor => anchor},
857 :class => (prefix == 'export' ? 'source download' : 'source')
858 :class => (prefix == 'export' ? 'source download' : 'source')
858 end
859 end
859 end
860 end
860 repo_prefix = nil
861 repo_prefix = nil
861 end
862 end
862 when 'attachment'
863 when 'attachment'
863 attachments = options[:attachments] || []
864 attachments = options[:attachments] || []
864 attachments += obj.attachments if obj.respond_to?(:attachments)
865 attachments += obj.attachments if obj.respond_to?(:attachments)
865 if attachments && attachment = Attachment.latest_attach(attachments, name)
866 if attachments && attachment = Attachment.latest_attach(attachments, name)
866 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
867 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
867 end
868 end
868 when 'project'
869 when 'project'
869 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
870 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
870 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
871 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
871 end
872 end
872 end
873 end
873 end
874 end
874 end
875 end
875 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
876 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
876 end
877 end
877 end
878 end
878 end
879 end
879
880
880 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
881 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
881
882
882 def parse_sections(text, project, obj, attr, only_path, options)
883 def parse_sections(text, project, obj, attr, only_path, options)
883 return unless options[:edit_section_links]
884 return unless options[:edit_section_links]
884 text.gsub!(HEADING_RE) do
885 text.gsub!(HEADING_RE) do
885 heading = $1
886 heading = $1
886 @current_section += 1
887 @current_section += 1
887 if @current_section > 1
888 if @current_section > 1
888 content_tag('div',
889 content_tag('div',
889 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
890 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
890 :class => 'contextual',
891 :class => 'contextual',
891 :title => l(:button_edit_section),
892 :title => l(:button_edit_section),
892 :id => "section-#{@current_section}") + heading.html_safe
893 :id => "section-#{@current_section}") + heading.html_safe
893 else
894 else
894 heading
895 heading
895 end
896 end
896 end
897 end
897 end
898 end
898
899
899 # Headings and TOC
900 # Headings and TOC
900 # Adds ids and links to headings unless options[:headings] is set to false
901 # Adds ids and links to headings unless options[:headings] is set to false
901 def parse_headings(text, project, obj, attr, only_path, options)
902 def parse_headings(text, project, obj, attr, only_path, options)
902 return if options[:headings] == false
903 return if options[:headings] == false
903
904
904 text.gsub!(HEADING_RE) do
905 text.gsub!(HEADING_RE) do
905 level, attrs, content = $2.to_i, $3, $4
906 level, attrs, content = $2.to_i, $3, $4
906 item = strip_tags(content).strip
907 item = strip_tags(content).strip
907 anchor = sanitize_anchor_name(item)
908 anchor = sanitize_anchor_name(item)
908 # used for single-file wiki export
909 # used for single-file wiki export
909 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
910 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
910 @heading_anchors[anchor] ||= 0
911 @heading_anchors[anchor] ||= 0
911 idx = (@heading_anchors[anchor] += 1)
912 idx = (@heading_anchors[anchor] += 1)
912 if idx > 1
913 if idx > 1
913 anchor = "#{anchor}-#{idx}"
914 anchor = "#{anchor}-#{idx}"
914 end
915 end
915 @parsed_headings << [level, anchor, item]
916 @parsed_headings << [level, anchor, item]
916 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
917 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
917 end
918 end
918 end
919 end
919
920
920 MACROS_RE = /(
921 MACROS_RE = /(
921 (!)? # escaping
922 (!)? # escaping
922 (
923 (
923 \{\{ # opening tag
924 \{\{ # opening tag
924 ([\w]+) # macro name
925 ([\w]+) # macro name
925 (\(([^\n\r]*?)\))? # optional arguments
926 (\(([^\n\r]*?)\))? # optional arguments
926 ([\n\r].*?[\n\r])? # optional block of text
927 ([\n\r].*?[\n\r])? # optional block of text
927 \}\} # closing tag
928 \}\} # closing tag
928 )
929 )
929 )/mx unless const_defined?(:MACROS_RE)
930 )/mx unless const_defined?(:MACROS_RE)
930
931
931 MACRO_SUB_RE = /(
932 MACRO_SUB_RE = /(
932 \{\{
933 \{\{
933 macro\((\d+)\)
934 macro\((\d+)\)
934 \}\}
935 \}\}
935 )/x unless const_defined?(:MACRO_SUB_RE)
936 )/x unless const_defined?(:MACRO_SUB_RE)
936
937
937 # Extracts macros from text
938 # Extracts macros from text
938 def catch_macros(text)
939 def catch_macros(text)
939 macros = {}
940 macros = {}
940 text.gsub!(MACROS_RE) do
941 text.gsub!(MACROS_RE) do
941 all, macro = $1, $4.downcase
942 all, macro = $1, $4.downcase
942 if macro_exists?(macro) || all =~ MACRO_SUB_RE
943 if macro_exists?(macro) || all =~ MACRO_SUB_RE
943 index = macros.size
944 index = macros.size
944 macros[index] = all
945 macros[index] = all
945 "{{macro(#{index})}}"
946 "{{macro(#{index})}}"
946 else
947 else
947 all
948 all
948 end
949 end
949 end
950 end
950 macros
951 macros
951 end
952 end
952
953
953 # Executes and replaces macros in text
954 # Executes and replaces macros in text
954 def inject_macros(text, obj, macros, execute=true)
955 def inject_macros(text, obj, macros, execute=true)
955 text.gsub!(MACRO_SUB_RE) do
956 text.gsub!(MACRO_SUB_RE) do
956 all, index = $1, $2.to_i
957 all, index = $1, $2.to_i
957 orig = macros.delete(index)
958 orig = macros.delete(index)
958 if execute && orig && orig =~ MACROS_RE
959 if execute && orig && orig =~ MACROS_RE
959 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
960 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
960 if esc.nil?
961 if esc.nil?
961 h(exec_macro(macro, obj, args, block) || all)
962 h(exec_macro(macro, obj, args, block) || all)
962 else
963 else
963 h(all)
964 h(all)
964 end
965 end
965 elsif orig
966 elsif orig
966 h(orig)
967 h(orig)
967 else
968 else
968 h(all)
969 h(all)
969 end
970 end
970 end
971 end
971 end
972 end
972
973
973 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
974 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
974
975
975 # Renders the TOC with given headings
976 # Renders the TOC with given headings
976 def replace_toc(text, headings)
977 def replace_toc(text, headings)
977 text.gsub!(TOC_RE) do
978 text.gsub!(TOC_RE) do
978 left_align, right_align = $2, $3
979 left_align, right_align = $2, $3
979 # Keep only the 4 first levels
980 # Keep only the 4 first levels
980 headings = headings.select{|level, anchor, item| level <= 4}
981 headings = headings.select{|level, anchor, item| level <= 4}
981 if headings.empty?
982 if headings.empty?
982 ''
983 ''
983 else
984 else
984 div_class = 'toc'
985 div_class = 'toc'
985 div_class << ' right' if right_align
986 div_class << ' right' if right_align
986 div_class << ' left' if left_align
987 div_class << ' left' if left_align
987 out = "<ul class=\"#{div_class}\"><li>"
988 out = "<ul class=\"#{div_class}\"><li>"
988 root = headings.map(&:first).min
989 root = headings.map(&:first).min
989 current = root
990 current = root
990 started = false
991 started = false
991 headings.each do |level, anchor, item|
992 headings.each do |level, anchor, item|
992 if level > current
993 if level > current
993 out << '<ul><li>' * (level - current)
994 out << '<ul><li>' * (level - current)
994 elsif level < current
995 elsif level < current
995 out << "</li></ul>\n" * (current - level) + "</li><li>"
996 out << "</li></ul>\n" * (current - level) + "</li><li>"
996 elsif started
997 elsif started
997 out << '</li><li>'
998 out << '</li><li>'
998 end
999 end
999 out << "<a href=\"##{anchor}\">#{item}</a>"
1000 out << "<a href=\"##{anchor}\">#{item}</a>"
1000 current = level
1001 current = level
1001 started = true
1002 started = true
1002 end
1003 end
1003 out << '</li></ul>' * (current - root)
1004 out << '</li></ul>' * (current - root)
1004 out << '</li></ul>'
1005 out << '</li></ul>'
1005 end
1006 end
1006 end
1007 end
1007 end
1008 end
1008
1009
1009 # Same as Rails' simple_format helper without using paragraphs
1010 # Same as Rails' simple_format helper without using paragraphs
1010 def simple_format_without_paragraph(text)
1011 def simple_format_without_paragraph(text)
1011 text.to_s.
1012 text.to_s.
1012 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1013 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1013 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1014 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1014 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1015 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1015 html_safe
1016 html_safe
1016 end
1017 end
1017
1018
1018 def lang_options_for_select(blank=true)
1019 def lang_options_for_select(blank=true)
1019 (blank ? [["(auto)", ""]] : []) + languages_options
1020 (blank ? [["(auto)", ""]] : []) + languages_options
1020 end
1021 end
1021
1022
1022 def labelled_form_for(*args, &proc)
1023 def labelled_form_for(*args, &proc)
1023 args << {} unless args.last.is_a?(Hash)
1024 args << {} unless args.last.is_a?(Hash)
1024 options = args.last
1025 options = args.last
1025 if args.first.is_a?(Symbol)
1026 if args.first.is_a?(Symbol)
1026 options.merge!(:as => args.shift)
1027 options.merge!(:as => args.shift)
1027 end
1028 end
1028 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1029 form_for(*args, &proc)
1030 form_for(*args, &proc)
1030 end
1031 end
1031
1032
1032 def labelled_fields_for(*args, &proc)
1033 def labelled_fields_for(*args, &proc)
1033 args << {} unless args.last.is_a?(Hash)
1034 args << {} unless args.last.is_a?(Hash)
1034 options = args.last
1035 options = args.last
1035 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1036 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1036 fields_for(*args, &proc)
1037 fields_for(*args, &proc)
1037 end
1038 end
1038
1039
1039 def error_messages_for(*objects)
1040 def error_messages_for(*objects)
1040 html = ""
1041 html = ""
1041 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1042 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1042 errors = objects.map {|o| o.errors.full_messages}.flatten
1043 errors = objects.map {|o| o.errors.full_messages}.flatten
1043 if errors.any?
1044 if errors.any?
1044 html << "<div id='errorExplanation'><ul>\n"
1045 html << "<div id='errorExplanation'><ul>\n"
1045 errors.each do |error|
1046 errors.each do |error|
1046 html << "<li>#{h error}</li>\n"
1047 html << "<li>#{h error}</li>\n"
1047 end
1048 end
1048 html << "</ul></div>\n"
1049 html << "</ul></div>\n"
1049 end
1050 end
1050 html.html_safe
1051 html.html_safe
1051 end
1052 end
1052
1053
1053 def delete_link(url, options={})
1054 def delete_link(url, options={})
1054 options = {
1055 options = {
1055 :method => :delete,
1056 :method => :delete,
1056 :data => {:confirm => l(:text_are_you_sure)},
1057 :data => {:confirm => l(:text_are_you_sure)},
1057 :class => 'icon icon-del'
1058 :class => 'icon icon-del'
1058 }.merge(options)
1059 }.merge(options)
1059
1060
1060 link_to l(:button_delete), url, options
1061 link_to l(:button_delete), url, options
1061 end
1062 end
1062
1063
1063 def preview_link(url, form, target='preview', options={})
1064 def preview_link(url, form, target='preview', options={})
1064 content_tag 'a', l(:label_preview), {
1065 content_tag 'a', l(:label_preview), {
1065 :href => "#",
1066 :href => "#",
1066 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1067 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1067 :accesskey => accesskey(:preview)
1068 :accesskey => accesskey(:preview)
1068 }.merge(options)
1069 }.merge(options)
1069 end
1070 end
1070
1071
1071 def link_to_function(name, function, html_options={})
1072 def link_to_function(name, function, html_options={})
1072 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1073 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1073 end
1074 end
1074
1075
1075 # Helper to render JSON in views
1076 # Helper to render JSON in views
1076 def raw_json(arg)
1077 def raw_json(arg)
1077 arg.to_json.to_s.gsub('/', '\/').html_safe
1078 arg.to_json.to_s.gsub('/', '\/').html_safe
1078 end
1079 end
1079
1080
1080 def back_url
1081 def back_url
1081 url = params[:back_url]
1082 url = params[:back_url]
1082 if url.nil? && referer = request.env['HTTP_REFERER']
1083 if url.nil? && referer = request.env['HTTP_REFERER']
1083 url = CGI.unescape(referer.to_s)
1084 url = CGI.unescape(referer.to_s)
1084 end
1085 end
1085 url
1086 url
1086 end
1087 end
1087
1088
1088 def back_url_hidden_field_tag
1089 def back_url_hidden_field_tag
1089 url = back_url
1090 url = back_url
1090 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1091 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1091 end
1092 end
1092
1093
1093 def check_all_links(form_name)
1094 def check_all_links(form_name)
1094 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1095 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1095 " | ".html_safe +
1096 " | ".html_safe +
1096 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1097 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1097 end
1098 end
1098
1099
1099 def toggle_checkboxes_link(selector)
1100 def toggle_checkboxes_link(selector)
1100 link_to_function image_tag('toggle_check.png'),
1101 link_to_function image_tag('toggle_check.png'),
1101 "toggleCheckboxesBySelector('#{selector}')",
1102 "toggleCheckboxesBySelector('#{selector}')",
1102 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1103 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1103 end
1104 end
1104
1105
1105 def progress_bar(pcts, options={})
1106 def progress_bar(pcts, options={})
1106 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1107 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1107 pcts = pcts.collect(&:round)
1108 pcts = pcts.collect(&:round)
1108 pcts[1] = pcts[1] - pcts[0]
1109 pcts[1] = pcts[1] - pcts[0]
1109 pcts << (100 - pcts[1] - pcts[0])
1110 pcts << (100 - pcts[1] - pcts[0])
1110 width = options[:width] || '100px;'
1111 width = options[:width] || '100px;'
1111 legend = options[:legend] || ''
1112 legend = options[:legend] || ''
1112 content_tag('table',
1113 content_tag('table',
1113 content_tag('tr',
1114 content_tag('tr',
1114 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1115 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1115 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1116 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1116 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1117 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1117 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1118 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1118 content_tag('p', legend, :class => 'percent').html_safe
1119 content_tag('p', legend, :class => 'percent').html_safe
1119 end
1120 end
1120
1121
1121 def checked_image(checked=true)
1122 def checked_image(checked=true)
1122 if checked
1123 if checked
1123 @checked_image_tag ||= image_tag('toggle_check.png')
1124 @checked_image_tag ||= image_tag('toggle_check.png')
1124 end
1125 end
1125 end
1126 end
1126
1127
1127 def context_menu(url)
1128 def context_menu(url)
1128 unless @context_menu_included
1129 unless @context_menu_included
1129 content_for :header_tags do
1130 content_for :header_tags do
1130 javascript_include_tag('context_menu') +
1131 javascript_include_tag('context_menu') +
1131 stylesheet_link_tag('context_menu')
1132 stylesheet_link_tag('context_menu')
1132 end
1133 end
1133 if l(:direction) == 'rtl'
1134 if l(:direction) == 'rtl'
1134 content_for :header_tags do
1135 content_for :header_tags do
1135 stylesheet_link_tag('context_menu_rtl')
1136 stylesheet_link_tag('context_menu_rtl')
1136 end
1137 end
1137 end
1138 end
1138 @context_menu_included = true
1139 @context_menu_included = true
1139 end
1140 end
1140 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1141 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1141 end
1142 end
1142
1143
1143 def calendar_for(field_id)
1144 def calendar_for(field_id)
1144 include_calendar_headers_tags
1145 include_calendar_headers_tags
1145 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1146 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1146 end
1147 end
1147
1148
1148 def include_calendar_headers_tags
1149 def include_calendar_headers_tags
1149 unless @calendar_headers_tags_included
1150 unless @calendar_headers_tags_included
1150 tags = ''.html_safe
1151 tags = ''.html_safe
1151 @calendar_headers_tags_included = true
1152 @calendar_headers_tags_included = true
1152 content_for :header_tags do
1153 content_for :header_tags do
1153 start_of_week = Setting.start_of_week
1154 start_of_week = Setting.start_of_week
1154 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1155 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1155 # Redmine uses 1..7 (monday..sunday) in settings and locales
1156 # Redmine uses 1..7 (monday..sunday) in settings and locales
1156 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1157 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1157 start_of_week = start_of_week.to_i % 7
1158 start_of_week = start_of_week.to_i % 7
1158 tags << javascript_tag(
1159 tags << javascript_tag(
1159 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1160 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1160 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1161 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1161 path_to_image('/images/calendar.png') +
1162 path_to_image('/images/calendar.png') +
1162 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1163 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1163 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1164 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1164 "beforeShow: beforeShowDatePicker};")
1165 "beforeShow: beforeShowDatePicker};")
1165 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1166 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1166 unless jquery_locale == 'en'
1167 unless jquery_locale == 'en'
1167 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1168 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1168 end
1169 end
1169 tags
1170 tags
1170 end
1171 end
1171 end
1172 end
1172 end
1173 end
1173
1174
1174 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1175 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1175 # Examples:
1176 # Examples:
1176 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1177 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1177 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1178 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1178 #
1179 #
1179 def stylesheet_link_tag(*sources)
1180 def stylesheet_link_tag(*sources)
1180 options = sources.last.is_a?(Hash) ? sources.pop : {}
1181 options = sources.last.is_a?(Hash) ? sources.pop : {}
1181 plugin = options.delete(:plugin)
1182 plugin = options.delete(:plugin)
1182 sources = sources.map do |source|
1183 sources = sources.map do |source|
1183 if plugin
1184 if plugin
1184 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1185 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1185 elsif current_theme && current_theme.stylesheets.include?(source)
1186 elsif current_theme && current_theme.stylesheets.include?(source)
1186 current_theme.stylesheet_path(source)
1187 current_theme.stylesheet_path(source)
1187 else
1188 else
1188 source
1189 source
1189 end
1190 end
1190 end
1191 end
1191 super *sources, options
1192 super *sources, options
1192 end
1193 end
1193
1194
1194 # Overrides Rails' image_tag with themes and plugins support.
1195 # Overrides Rails' image_tag with themes and plugins support.
1195 # Examples:
1196 # Examples:
1196 # image_tag('image.png') # => picks image.png from the current theme or defaults
1197 # image_tag('image.png') # => picks image.png from the current theme or defaults
1197 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1198 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1198 #
1199 #
1199 def image_tag(source, options={})
1200 def image_tag(source, options={})
1200 if plugin = options.delete(:plugin)
1201 if plugin = options.delete(:plugin)
1201 source = "/plugin_assets/#{plugin}/images/#{source}"
1202 source = "/plugin_assets/#{plugin}/images/#{source}"
1202 elsif current_theme && current_theme.images.include?(source)
1203 elsif current_theme && current_theme.images.include?(source)
1203 source = current_theme.image_path(source)
1204 source = current_theme.image_path(source)
1204 end
1205 end
1205 super source, options
1206 super source, options
1206 end
1207 end
1207
1208
1208 # Overrides Rails' javascript_include_tag with plugins support
1209 # Overrides Rails' javascript_include_tag with plugins support
1209 # Examples:
1210 # Examples:
1210 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1211 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1211 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1212 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1212 #
1213 #
1213 def javascript_include_tag(*sources)
1214 def javascript_include_tag(*sources)
1214 options = sources.last.is_a?(Hash) ? sources.pop : {}
1215 options = sources.last.is_a?(Hash) ? sources.pop : {}
1215 if plugin = options.delete(:plugin)
1216 if plugin = options.delete(:plugin)
1216 sources = sources.map do |source|
1217 sources = sources.map do |source|
1217 if plugin
1218 if plugin
1218 "/plugin_assets/#{plugin}/javascripts/#{source}"
1219 "/plugin_assets/#{plugin}/javascripts/#{source}"
1219 else
1220 else
1220 source
1221 source
1221 end
1222 end
1222 end
1223 end
1223 end
1224 end
1224 super *sources, options
1225 super *sources, options
1225 end
1226 end
1226
1227
1227 def sidebar_content?
1228 def sidebar_content?
1228 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1229 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1229 end
1230 end
1230
1231
1231 def view_layouts_base_sidebar_hook_response
1232 def view_layouts_base_sidebar_hook_response
1232 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1233 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1233 end
1234 end
1234
1235
1235 def email_delivery_enabled?
1236 def email_delivery_enabled?
1236 !!ActionMailer::Base.perform_deliveries
1237 !!ActionMailer::Base.perform_deliveries
1237 end
1238 end
1238
1239
1239 # Returns the avatar image tag for the given +user+ if avatars are enabled
1240 # Returns the avatar image tag for the given +user+ if avatars are enabled
1240 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1241 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1241 def avatar(user, options = { })
1242 def avatar(user, options = { })
1242 if Setting.gravatar_enabled?
1243 if Setting.gravatar_enabled?
1243 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1244 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1244 email = nil
1245 email = nil
1245 if user.respond_to?(:mail)
1246 if user.respond_to?(:mail)
1246 email = user.mail
1247 email = user.mail
1247 elsif user.to_s =~ %r{<(.+?)>}
1248 elsif user.to_s =~ %r{<(.+?)>}
1248 email = $1
1249 email = $1
1249 end
1250 end
1250 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1251 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1251 else
1252 else
1252 ''
1253 ''
1253 end
1254 end
1254 end
1255 end
1255
1256
1256 # Returns a link to edit user's avatar if avatars are enabled
1257 # Returns a link to edit user's avatar if avatars are enabled
1257 def avatar_edit_link(user, options={})
1258 def avatar_edit_link(user, options={})
1258 if Setting.gravatar_enabled?
1259 if Setting.gravatar_enabled?
1259 url = "https://gravatar.com"
1260 url = "https://gravatar.com"
1260 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1261 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1261 end
1262 end
1262 end
1263 end
1263
1264
1264 def sanitize_anchor_name(anchor)
1265 def sanitize_anchor_name(anchor)
1265 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1266 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1266 end
1267 end
1267
1268
1268 # Returns the javascript tags that are included in the html layout head
1269 # Returns the javascript tags that are included in the html layout head
1269 def javascript_heads
1270 def javascript_heads
1270 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application')
1271 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1271 unless User.current.pref.warn_on_leaving_unsaved == '0'
1272 unless User.current.pref.warn_on_leaving_unsaved == '0'
1272 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1273 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1273 end
1274 end
1274 tags
1275 tags
1275 end
1276 end
1276
1277
1277 def favicon
1278 def favicon
1278 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1279 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1279 end
1280 end
1280
1281
1281 # Returns the path to the favicon
1282 # Returns the path to the favicon
1282 def favicon_path
1283 def favicon_path
1283 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1284 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1284 image_path(icon)
1285 image_path(icon)
1285 end
1286 end
1286
1287
1287 # Returns the full URL to the favicon
1288 # Returns the full URL to the favicon
1288 def favicon_url
1289 def favicon_url
1289 # TODO: use #image_url introduced in Rails4
1290 # TODO: use #image_url introduced in Rails4
1290 path = favicon_path
1291 path = favicon_path
1291 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1292 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1292 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1293 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1293 end
1294 end
1294
1295
1295 def robot_exclusion_tag
1296 def robot_exclusion_tag
1296 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1297 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1297 end
1298 end
1298
1299
1299 # Returns true if arg is expected in the API response
1300 # Returns true if arg is expected in the API response
1300 def include_in_api_response?(arg)
1301 def include_in_api_response?(arg)
1301 unless @included_in_api_response
1302 unless @included_in_api_response
1302 param = params[:include]
1303 param = params[:include]
1303 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1304 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1304 @included_in_api_response.collect!(&:strip)
1305 @included_in_api_response.collect!(&:strip)
1305 end
1306 end
1306 @included_in_api_response.include?(arg.to_s)
1307 @included_in_api_response.include?(arg.to_s)
1307 end
1308 end
1308
1309
1309 # Returns options or nil if nometa param or X-Redmine-Nometa header
1310 # Returns options or nil if nometa param or X-Redmine-Nometa header
1310 # was set in the request
1311 # was set in the request
1311 def api_meta(options)
1312 def api_meta(options)
1312 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1313 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1313 # compatibility mode for activeresource clients that raise
1314 # compatibility mode for activeresource clients that raise
1314 # an error when deserializing an array with attributes
1315 # an error when deserializing an array with attributes
1315 nil
1316 nil
1316 else
1317 else
1317 options
1318 options
1318 end
1319 end
1319 end
1320 end
1320
1321
1321 def generate_csv(&block)
1322 def generate_csv(&block)
1322 decimal_separator = l(:general_csv_decimal_separator)
1323 decimal_separator = l(:general_csv_decimal_separator)
1323 encoding = l(:general_csv_encoding)
1324 encoding = l(:general_csv_encoding)
1324 end
1325 end
1325
1326
1326 private
1327 private
1327
1328
1328 def wiki_helper
1329 def wiki_helper
1329 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1330 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1330 extend helper
1331 extend helper
1331 return self
1332 return self
1332 end
1333 end
1333
1334
1334 def link_to_content_update(text, url_params = {}, html_options = {})
1335 def link_to_content_update(text, url_params = {}, html_options = {})
1335 link_to(text, url_params, html_options)
1336 link_to(text, url_params, html_options)
1336 end
1337 end
1337 end
1338 end
@@ -1,81 +1,123
1 <!DOCTYPE html>
1 <!DOCTYPE html>
2 <html lang="<%= current_language %>">
2 <html lang="<%= current_language %>">
3 <head>
3 <head>
4 <meta charset="utf-8" />
4 <meta charset="utf-8" />
5 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
5 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
6 <title><%= html_title %></title>
6 <title><%= html_title %></title>
7 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <meta name="description" content="<%= Redmine::Info.app_name %>" />
8 <meta name="description" content="<%= Redmine::Info.app_name %>" />
8 <meta name="keywords" content="issue,bug,tracker" />
9 <meta name="keywords" content="issue,bug,tracker" />
9 <%= csrf_meta_tag %>
10 <%= csrf_meta_tag %>
10 <%= favicon %>
11 <%= favicon %>
11 <%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', :media => 'all' %>
12 <%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', 'responsive', :media => 'all' %>
12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 <%= javascript_heads %>
14 <%= javascript_heads %>
14 <%= heads_for_theme %>
15 <%= heads_for_theme %>
15 <%= call_hook :view_layouts_base_html_head %>
16 <%= call_hook :view_layouts_base_html_head %>
16 <!-- page specific tags -->
17 <!-- page specific tags -->
17 <%= yield :header_tags -%>
18 <%= yield :header_tags -%>
18 </head>
19 </head>
19 <body class="<%= body_css_classes %>">
20 <body class="<%= body_css_classes %>">
20 <div id="wrapper">
21 <div id="wrapper">
22
23 <div class="flyout-menu js-flyout-menu">
24
25
26 <% if User.current.logged? || !Setting.login_required? %>
27 <div class="flyout-menu__search">
28 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
29 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
30 <%= label_tag 'flyout-search', '&#9906;'.html_safe, :class => 'search-magnifier search-magnifier--flyout' %>
31 <%= text_field_tag 'q', @question, :id => 'flyout-search', :class => 'small js-search-input', :placeholder => l(:label_search) %>
32 <% end %>
33 </div>
34 <% end %>
35
36 <% if User.current.logged? %>
37 <div class="flyout-menu__avatar <% if !Setting.gravatar_enabled? %>flyout-menu__avatar--no-avatar<% end %>">
38 <% if Setting.gravatar_enabled? %>
39 <%= link_to(avatar(User.current, :size => "80"), user_path(User.current)) %>
40 <% end %>
41 <%= link_to_user(User.current, :format => :username) %>
42 </div>
43 <% end %>
44
45 <% if display_main_menu?(@project) %>
46 <h3><%= l(:label_project) %></h3>
47 <span class="js-project-menu"></span>
48 <% end %>
49
50 <h3><%= l(:label_general) %></h3>
51 <span class="js-general-menu"></span>
52
53 <span class="js-sidebar flyout-menu__sidebar"></span>
54
55 <h3><%= l(:label_profile) %></h3>
56 <span class="js-profile-menu"></span>
57
58 </div>
59
21 <div id="wrapper2">
60 <div id="wrapper2">
22 <div id="wrapper3">
61 <div id="wrapper3">
23 <div id="top-menu">
62 <div id="top-menu">
24 <div id="account">
63 <div id="account">
25 <%= render_menu :account_menu -%>
64 <%= render_menu :account_menu -%>
26 </div>
65 </div>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
66 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
28 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
67 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
29 </div>
68 </div>
30
69
31 <div id="header">
70 <div id="header">
71
72 <a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a>
73
32 <% if User.current.logged? || !Setting.login_required? %>
74 <% if User.current.logged? || !Setting.login_required? %>
33 <div id="quick-search">
75 <div id="quick-search">
34 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
76 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
35 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
77 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
36 <label for='q'>
78 <label for='q'>
37 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
79 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
38 </label>
80 </label>
39 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
81 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
40 <% end %>
82 <% end %>
41 <%= render_project_jump_box %>
83 <%= render_project_jump_box %>
42 </div>
84 </div>
43 <% end %>
85 <% end %>
44
86
45 <h1><%= page_header_title %></h1>
87 <h1><%= page_header_title %></h1>
46
88
47 <% if display_main_menu?(@project) %>
89 <% if display_main_menu?(@project) %>
48 <div id="main-menu">
90 <div id="main-menu">
49 <%= render_main_menu(@project) %>
91 <%= render_main_menu(@project) %>
50 </div>
92 </div>
51 <% end %>
93 <% end %>
52 </div>
94 </div>
53
95
54 <div id="main" class="<%= sidebar_content? ? '' : 'nosidebar' %>">
96 <div id="main" class="<%= sidebar_content? ? '' : 'nosidebar' %>">
55 <div id="sidebar">
97 <div id="sidebar">
56 <%= yield :sidebar %>
98 <%= yield :sidebar %>
57 <%= view_layouts_base_sidebar_hook_response %>
99 <%= view_layouts_base_sidebar_hook_response %>
58 </div>
100 </div>
59
101
60 <div id="content">
102 <div id="content">
61 <%= render_flash_messages %>
103 <%= render_flash_messages %>
62 <%= yield %>
104 <%= yield %>
63 <%= call_hook :view_layouts_base_content %>
105 <%= call_hook :view_layouts_base_content %>
64 <div style="clear:both;"></div>
106 <div style="clear:both;"></div>
65 </div>
107 </div>
66 </div>
108 </div>
67 </div>
109 </div>
68
110
69 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
111 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
70 <div id="ajax-modal" style="display:none;"></div>
112 <div id="ajax-modal" style="display:none;"></div>
71
113
72 <div id="footer">
114 <div id="footer">
73 <div class="bgl"><div class="bgr">
115 <div class="bgl"><div class="bgr">
74 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2015 Jean-Philippe Lang
116 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2015 Jean-Philippe Lang
75 </div></div>
117 </div></div>
76 </div>
118 </div>
77 </div>
119 </div>
78 </div>
120 </div>
79 <%= call_hook :view_layouts_base_body_bottom %>
121 <%= call_hook :view_layouts_base_body_bottom %>
80 </body>
122 </body>
81 </html>
123 </html>
General Comments 0
You need to be logged in to leave comments. Login now