##// END OF EJS Templates
Fixed that sidebar with hook content only should not be hidden....
Jean-Philippe Lang -
r9415:f54ecfc55f78
parent child
Show More
@@ -0,0 +1,67
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../test_helper', __FILE__)
19
20 class MenuManagerTest < ActionController::IntegrationTest
21
22 fixtures :users, :roles, :projects, :members, :member_roles
23
24 # Hooks that are manually registered later
25 class ProjectBasedTemplate < Redmine::Hook::ViewListener
26 def view_layouts_base_html_head(context)
27 # Adds a project stylesheet
28 stylesheet_link_tag(context[:project].identifier) if context[:project]
29 end
30 end
31
32 class SidebarContent < Redmine::Hook::ViewListener
33 def view_layouts_base_sidebar(context)
34 content_tag('p', 'Sidebar hook')
35 end
36 end
37
38 def setup
39 Redmine::Hook.clear_listeners
40 end
41
42 def teardown
43 Redmine::Hook.clear_listeners
44 end
45
46 def test_html_head_hook_response
47 Redmine::Hook.add_listener(ProjectBasedTemplate)
48
49 get '/projects/ecookbook'
50 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
51 :parent => {:tag => 'head'}
52 end
53
54 def test_empty_sidebar_should_be_hidden
55 get '/'
56 assert_select 'div#main.nosidebar'
57 end
58
59 def test_sidebar_with_hook_content_should_not_be_hidden
60 Redmine::Hook.add_listener(SidebarContent)
61
62 get '/'
63 assert_select 'div#sidebar p', :text => 'Sidebar hook'
64 assert_select 'div#main'
65 assert_select 'div#main.nosidebar', 0
66 end
67 end
@@ -1,1186 +1,1194
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :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 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 opt_only_path = {}
100 opt_only_path = {}
101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
101 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
102 options.delete(:only_path)
102 options.delete(:only_path)
103 link_to(h(text),
103 link_to(h(text),
104 {:controller => 'attachments', :action => action,
104 {:controller => 'attachments', :action => action,
105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
105 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
106 options)
106 options)
107 end
107 end
108
108
109 # Generates a link to a SCM revision
109 # Generates a link to a SCM revision
110 # Options:
110 # Options:
111 # * :text - Link text (default to the formatted revision)
111 # * :text - Link text (default to the formatted revision)
112 def link_to_revision(revision, repository, options={})
112 def link_to_revision(revision, repository, options={})
113 if repository.is_a?(Project)
113 if repository.is_a?(Project)
114 repository = repository.repository
114 repository = repository.repository
115 end
115 end
116 text = options.delete(:text) || format_revision(revision)
116 text = options.delete(:text) || format_revision(revision)
117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
117 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
118 link_to(
118 link_to(
119 h(text),
119 h(text),
120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
120 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
121 :title => l(:label_revision_id, format_revision(revision))
121 :title => l(:label_revision_id, format_revision(revision))
122 )
122 )
123 end
123 end
124
124
125 # Generates a link to a message
125 # Generates a link to a message
126 def link_to_message(message, options={}, html_options = nil)
126 def link_to_message(message, options={}, html_options = nil)
127 link_to(
127 link_to(
128 h(truncate(message.subject, :length => 60)),
128 h(truncate(message.subject, :length => 60)),
129 { :controller => 'messages', :action => 'show',
129 { :controller => 'messages', :action => 'show',
130 :board_id => message.board_id,
130 :board_id => message.board_id,
131 :id => message.root,
131 :id => message.root,
132 :r => (message.parent_id && message.id),
132 :r => (message.parent_id && message.id),
133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
133 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
134 }.merge(options),
134 }.merge(options),
135 html_options
135 html_options
136 )
136 )
137 end
137 end
138
138
139 # Generates a link to a project if active
139 # Generates a link to a project if active
140 # Examples:
140 # Examples:
141 #
141 #
142 # link_to_project(project) # => link to the specified project overview
142 # link_to_project(project) # => link to the specified project overview
143 # link_to_project(project, :action=>'settings') # => link to project settings
143 # link_to_project(project, :action=>'settings') # => link to project settings
144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
144 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
145 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
146 #
146 #
147 def link_to_project(project, options={}, html_options = nil)
147 def link_to_project(project, options={}, html_options = nil)
148 if project.active?
148 if project.active?
149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
149 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
150 link_to(h(project), url, html_options)
150 link_to(h(project), url, html_options)
151 else
151 else
152 h(project)
152 h(project)
153 end
153 end
154 end
154 end
155
155
156 def toggle_link(name, id, options={})
156 def toggle_link(name, id, options={})
157 onclick = "Element.toggle('#{id}'); "
157 onclick = "Element.toggle('#{id}'); "
158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
158 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
159 onclick << "return false;"
159 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
160 link_to(name, "#", :onclick => onclick)
161 end
161 end
162
162
163 def image_to_function(name, function, html_options = {})
163 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
164 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
165 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
166 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
168 }))
169 end
169 end
170
170
171 def prompt_to_remote(name, text, param, url, html_options = {})
171 def prompt_to_remote(name, text, param, url, html_options = {})
172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
172 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
173 link_to name, {}, html_options
173 link_to name, {}, html_options
174 end
174 end
175
175
176 def format_activity_title(text)
176 def format_activity_title(text)
177 h(truncate_single_line(text, :length => 100))
177 h(truncate_single_line(text, :length => 100))
178 end
178 end
179
179
180 def format_activity_day(date)
180 def format_activity_day(date)
181 date == Date.today ? l(:label_today).titleize : format_date(date)
181 date == Date.today ? l(:label_today).titleize : format_date(date)
182 end
182 end
183
183
184 def format_activity_description(text)
184 def format_activity_description(text)
185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
185 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
186 ).gsub(/[\r\n]+/, "<br />").html_safe
186 ).gsub(/[\r\n]+/, "<br />").html_safe
187 end
187 end
188
188
189 def format_version_name(version)
189 def format_version_name(version)
190 if version.project == @project
190 if version.project == @project
191 h(version)
191 h(version)
192 else
192 else
193 h("#{version.project} - #{version}")
193 h("#{version.project} - #{version}")
194 end
194 end
195 end
195 end
196
196
197 def due_date_distance_in_words(date)
197 def due_date_distance_in_words(date)
198 if date
198 if date
199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
199 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
200 end
200 end
201 end
201 end
202
202
203 def render_page_hierarchy(pages, node=nil, options={})
203 def render_page_hierarchy(pages, node=nil, options={})
204 content = ''
204 content = ''
205 if pages[node]
205 if pages[node]
206 content << "<ul class=\"pages-hierarchy\">\n"
206 content << "<ul class=\"pages-hierarchy\">\n"
207 pages[node].each do |page|
207 pages[node].each do |page|
208 content << "<li>"
208 content << "<li>"
209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
209 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
210 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
211 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
212 content << "</li>\n"
212 content << "</li>\n"
213 end
213 end
214 content << "</ul>\n"
214 content << "</ul>\n"
215 end
215 end
216 content.html_safe
216 content.html_safe
217 end
217 end
218
218
219 # Renders flash messages
219 # Renders flash messages
220 def render_flash_messages
220 def render_flash_messages
221 s = ''
221 s = ''
222 flash.each do |k,v|
222 flash.each do |k,v|
223 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
223 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
224 end
224 end
225 s.html_safe
225 s.html_safe
226 end
226 end
227
227
228 # Renders tabs and their content
228 # Renders tabs and their content
229 def render_tabs(tabs)
229 def render_tabs(tabs)
230 if tabs.any?
230 if tabs.any?
231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
231 render :partial => 'common/tabs', :locals => {:tabs => tabs}
232 else
232 else
233 content_tag 'p', l(:label_no_data), :class => "nodata"
233 content_tag 'p', l(:label_no_data), :class => "nodata"
234 end
234 end
235 end
235 end
236
236
237 # Renders the project quick-jump box
237 # Renders the project quick-jump box
238 def render_project_jump_box
238 def render_project_jump_box
239 return unless User.current.logged?
239 return unless User.current.logged?
240 projects = User.current.memberships.collect(&:project).compact.uniq
240 projects = User.current.memberships.collect(&:project).compact.uniq
241 if projects.any?
241 if projects.any?
242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
242 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
243 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
244 '<option value="" disabled="disabled">---</option>'
244 '<option value="" disabled="disabled">---</option>'
245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
245 s << project_tree_options_for_select(projects, :selected => @project) do |p|
246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
246 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
247 end
247 end
248 s << '</select>'
248 s << '</select>'
249 s.html_safe
249 s.html_safe
250 end
250 end
251 end
251 end
252
252
253 def project_tree_options_for_select(projects, options = {})
253 def project_tree_options_for_select(projects, options = {})
254 s = ''
254 s = ''
255 project_tree(projects) do |project, level|
255 project_tree(projects) do |project, level|
256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
256 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
257 tag_options = {:value => project.id}
257 tag_options = {:value => project.id}
258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
258 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
259 tag_options[:selected] = 'selected'
259 tag_options[:selected] = 'selected'
260 else
260 else
261 tag_options[:selected] = nil
261 tag_options[:selected] = nil
262 end
262 end
263 tag_options.merge!(yield(project)) if block_given?
263 tag_options.merge!(yield(project)) if block_given?
264 s << content_tag('option', name_prefix + h(project), tag_options)
264 s << content_tag('option', name_prefix + h(project), tag_options)
265 end
265 end
266 s.html_safe
266 s.html_safe
267 end
267 end
268
268
269 # Yields the given block for each project with its level in the tree
269 # Yields the given block for each project with its level in the tree
270 #
270 #
271 # Wrapper for Project#project_tree
271 # Wrapper for Project#project_tree
272 def project_tree(projects, &block)
272 def project_tree(projects, &block)
273 Project.project_tree(projects, &block)
273 Project.project_tree(projects, &block)
274 end
274 end
275
275
276 def project_nested_ul(projects, &block)
276 def project_nested_ul(projects, &block)
277 s = ''
277 s = ''
278 if projects.any?
278 if projects.any?
279 ancestors = []
279 ancestors = []
280 projects.sort_by(&:lft).each do |project|
280 projects.sort_by(&:lft).each do |project|
281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
281 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
282 s << "<ul>\n"
282 s << "<ul>\n"
283 else
283 else
284 ancestors.pop
284 ancestors.pop
285 s << "</li>"
285 s << "</li>"
286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
286 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
287 ancestors.pop
287 ancestors.pop
288 s << "</ul></li>\n"
288 s << "</ul></li>\n"
289 end
289 end
290 end
290 end
291 s << "<li>"
291 s << "<li>"
292 s << yield(project).to_s
292 s << yield(project).to_s
293 ancestors << project
293 ancestors << project
294 end
294 end
295 s << ("</li></ul>\n" * ancestors.size)
295 s << ("</li></ul>\n" * ancestors.size)
296 end
296 end
297 s.html_safe
297 s.html_safe
298 end
298 end
299
299
300 def principals_check_box_tags(name, principals)
300 def principals_check_box_tags(name, principals)
301 s = ''
301 s = ''
302 principals.sort.each do |principal|
302 principals.sort.each do |principal|
303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
303 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
304 end
304 end
305 s.html_safe
305 s.html_safe
306 end
306 end
307
307
308 # Returns a string for users/groups option tags
308 # Returns a string for users/groups option tags
309 def principals_options_for_select(collection, selected=nil)
309 def principals_options_for_select(collection, selected=nil)
310 s = ''
310 s = ''
311 if collection.include?(User.current)
311 if collection.include?(User.current)
312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
312 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
313 end
313 end
314 groups = ''
314 groups = ''
315 collection.sort.each do |element|
315 collection.sort.each do |element|
316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
316 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
317 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
318 end
318 end
319 unless groups.empty?
319 unless groups.empty?
320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
320 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
321 end
321 end
322 s.html_safe
322 s.html_safe
323 end
323 end
324
324
325 # Truncates and returns the string as a single line
325 # Truncates and returns the string as a single line
326 def truncate_single_line(string, *args)
326 def truncate_single_line(string, *args)
327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
327 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
328 end
328 end
329
329
330 # Truncates at line break after 250 characters or options[:length]
330 # Truncates at line break after 250 characters or options[:length]
331 def truncate_lines(string, options={})
331 def truncate_lines(string, options={})
332 length = options[:length] || 250
332 length = options[:length] || 250
333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
333 if string.to_s =~ /\A(.{#{length}}.*?)$/m
334 "#{$1}..."
334 "#{$1}..."
335 else
335 else
336 string
336 string
337 end
337 end
338 end
338 end
339
339
340 def anchor(text)
340 def anchor(text)
341 text.to_s.gsub(' ', '_')
341 text.to_s.gsub(' ', '_')
342 end
342 end
343
343
344 def html_hours(text)
344 def html_hours(text)
345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
345 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
346 end
346 end
347
347
348 def authoring(created, author, options={})
348 def authoring(created, author, options={})
349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
349 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
350 end
350 end
351
351
352 def time_tag(time)
352 def time_tag(time)
353 text = distance_of_time_in_words(Time.now, time)
353 text = distance_of_time_in_words(Time.now, time)
354 if @project
354 if @project
355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
355 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
356 else
356 else
357 content_tag('acronym', text, :title => format_time(time))
357 content_tag('acronym', text, :title => format_time(time))
358 end
358 end
359 end
359 end
360
360
361 def syntax_highlight_lines(name, content)
361 def syntax_highlight_lines(name, content)
362 lines = []
362 lines = []
363 syntax_highlight(name, content).each_line { |line| lines << line }
363 syntax_highlight(name, content).each_line { |line| lines << line }
364 lines
364 lines
365 end
365 end
366
366
367 def syntax_highlight(name, content)
367 def syntax_highlight(name, content)
368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
368 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
369 end
369 end
370
370
371 def to_path_param(path)
371 def to_path_param(path)
372 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
372 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
373 end
373 end
374
374
375 def pagination_links_full(paginator, count=nil, options={})
375 def pagination_links_full(paginator, count=nil, options={})
376 page_param = options.delete(:page_param) || :page
376 page_param = options.delete(:page_param) || :page
377 per_page_links = options.delete(:per_page_links)
377 per_page_links = options.delete(:per_page_links)
378 url_param = params.dup
378 url_param = params.dup
379
379
380 html = ''
380 html = ''
381 if paginator.current.previous
381 if paginator.current.previous
382 # \xc2\xab(utf-8) = &#171;
382 # \xc2\xab(utf-8) = &#171;
383 html << link_to_content_update(
383 html << link_to_content_update(
384 "\xc2\xab " + l(:label_previous),
384 "\xc2\xab " + l(:label_previous),
385 url_param.merge(page_param => paginator.current.previous)) + ' '
385 url_param.merge(page_param => paginator.current.previous)) + ' '
386 end
386 end
387
387
388 html << (pagination_links_each(paginator, options) do |n|
388 html << (pagination_links_each(paginator, options) do |n|
389 link_to_content_update(n.to_s, url_param.merge(page_param => n))
389 link_to_content_update(n.to_s, url_param.merge(page_param => n))
390 end || '')
390 end || '')
391
391
392 if paginator.current.next
392 if paginator.current.next
393 # \xc2\xbb(utf-8) = &#187;
393 # \xc2\xbb(utf-8) = &#187;
394 html << ' ' + link_to_content_update(
394 html << ' ' + link_to_content_update(
395 (l(:label_next) + " \xc2\xbb"),
395 (l(:label_next) + " \xc2\xbb"),
396 url_param.merge(page_param => paginator.current.next))
396 url_param.merge(page_param => paginator.current.next))
397 end
397 end
398
398
399 unless count.nil?
399 unless count.nil?
400 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
400 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
401 if per_page_links != false && links = per_page_links(paginator.items_per_page)
401 if per_page_links != false && links = per_page_links(paginator.items_per_page)
402 html << " | #{links}"
402 html << " | #{links}"
403 end
403 end
404 end
404 end
405
405
406 html.html_safe
406 html.html_safe
407 end
407 end
408
408
409 def per_page_links(selected=nil)
409 def per_page_links(selected=nil)
410 links = Setting.per_page_options_array.collect do |n|
410 links = Setting.per_page_options_array.collect do |n|
411 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
411 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
412 end
412 end
413 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
413 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
414 end
414 end
415
415
416 def reorder_links(name, url, method = :post)
416 def reorder_links(name, url, method = :post)
417 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
417 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
418 url.merge({"#{name}[move_to]" => 'highest'}),
418 url.merge({"#{name}[move_to]" => 'highest'}),
419 :method => method, :title => l(:label_sort_highest)) +
419 :method => method, :title => l(:label_sort_highest)) +
420 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
420 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
421 url.merge({"#{name}[move_to]" => 'higher'}),
421 url.merge({"#{name}[move_to]" => 'higher'}),
422 :method => method, :title => l(:label_sort_higher)) +
422 :method => method, :title => l(:label_sort_higher)) +
423 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
423 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
424 url.merge({"#{name}[move_to]" => 'lower'}),
424 url.merge({"#{name}[move_to]" => 'lower'}),
425 :method => method, :title => l(:label_sort_lower)) +
425 :method => method, :title => l(:label_sort_lower)) +
426 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
426 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
427 url.merge({"#{name}[move_to]" => 'lowest'}),
427 url.merge({"#{name}[move_to]" => 'lowest'}),
428 :method => method, :title => l(:label_sort_lowest))
428 :method => method, :title => l(:label_sort_lowest))
429 end
429 end
430
430
431 def breadcrumb(*args)
431 def breadcrumb(*args)
432 elements = args.flatten
432 elements = args.flatten
433 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
433 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
434 end
434 end
435
435
436 def other_formats_links(&block)
436 def other_formats_links(&block)
437 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
437 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
438 yield Redmine::Views::OtherFormatsBuilder.new(self)
438 yield Redmine::Views::OtherFormatsBuilder.new(self)
439 concat('</p>'.html_safe)
439 concat('</p>'.html_safe)
440 end
440 end
441
441
442 def page_header_title
442 def page_header_title
443 if @project.nil? || @project.new_record?
443 if @project.nil? || @project.new_record?
444 h(Setting.app_title)
444 h(Setting.app_title)
445 else
445 else
446 b = []
446 b = []
447 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
447 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
448 if ancestors.any?
448 if ancestors.any?
449 root = ancestors.shift
449 root = ancestors.shift
450 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
450 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
451 if ancestors.size > 2
451 if ancestors.size > 2
452 b << "\xe2\x80\xa6"
452 b << "\xe2\x80\xa6"
453 ancestors = ancestors[-2, 2]
453 ancestors = ancestors[-2, 2]
454 end
454 end
455 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
455 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
456 end
456 end
457 b << h(@project)
457 b << h(@project)
458 b.join(" \xc2\xbb ").html_safe
458 b.join(" \xc2\xbb ").html_safe
459 end
459 end
460 end
460 end
461
461
462 def html_title(*args)
462 def html_title(*args)
463 if args.empty?
463 if args.empty?
464 title = @html_title || []
464 title = @html_title || []
465 title << @project.name if @project
465 title << @project.name if @project
466 title << Setting.app_title unless Setting.app_title == title.last
466 title << Setting.app_title unless Setting.app_title == title.last
467 title.select {|t| !t.blank? }.join(' - ')
467 title.select {|t| !t.blank? }.join(' - ')
468 else
468 else
469 @html_title ||= []
469 @html_title ||= []
470 @html_title += args
470 @html_title += args
471 end
471 end
472 end
472 end
473
473
474 # Returns the theme, controller name, and action as css classes for the
474 # Returns the theme, controller name, and action as css classes for the
475 # HTML body.
475 # HTML body.
476 def body_css_classes
476 def body_css_classes
477 css = []
477 css = []
478 if theme = Redmine::Themes.theme(Setting.ui_theme)
478 if theme = Redmine::Themes.theme(Setting.ui_theme)
479 css << 'theme-' + theme.name
479 css << 'theme-' + theme.name
480 end
480 end
481
481
482 css << 'controller-' + controller_name
482 css << 'controller-' + controller_name
483 css << 'action-' + action_name
483 css << 'action-' + action_name
484 css.join(' ')
484 css.join(' ')
485 end
485 end
486
486
487 def accesskey(s)
487 def accesskey(s)
488 Redmine::AccessKeys.key_for s
488 Redmine::AccessKeys.key_for s
489 end
489 end
490
490
491 # Formats text according to system settings.
491 # Formats text according to system settings.
492 # 2 ways to call this method:
492 # 2 ways to call this method:
493 # * with a String: textilizable(text, options)
493 # * with a String: textilizable(text, options)
494 # * with an object and one of its attribute: textilizable(issue, :description, options)
494 # * with an object and one of its attribute: textilizable(issue, :description, options)
495 def textilizable(*args)
495 def textilizable(*args)
496 options = args.last.is_a?(Hash) ? args.pop : {}
496 options = args.last.is_a?(Hash) ? args.pop : {}
497 case args.size
497 case args.size
498 when 1
498 when 1
499 obj = options[:object]
499 obj = options[:object]
500 text = args.shift
500 text = args.shift
501 when 2
501 when 2
502 obj = args.shift
502 obj = args.shift
503 attr = args.shift
503 attr = args.shift
504 text = obj.send(attr).to_s
504 text = obj.send(attr).to_s
505 else
505 else
506 raise ArgumentError, 'invalid arguments to textilizable'
506 raise ArgumentError, 'invalid arguments to textilizable'
507 end
507 end
508 return '' if text.blank?
508 return '' if text.blank?
509 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
509 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
510 only_path = options.delete(:only_path) == false ? false : true
510 only_path = options.delete(:only_path) == false ? false : true
511
511
512 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
512 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
513
513
514 @parsed_headings = []
514 @parsed_headings = []
515 @heading_anchors = {}
515 @heading_anchors = {}
516 @current_section = 0 if options[:edit_section_links]
516 @current_section = 0 if options[:edit_section_links]
517
517
518 parse_sections(text, project, obj, attr, only_path, options)
518 parse_sections(text, project, obj, attr, only_path, options)
519 text = parse_non_pre_blocks(text) do |text|
519 text = parse_non_pre_blocks(text) do |text|
520 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
520 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
521 send method_name, text, project, obj, attr, only_path, options
521 send method_name, text, project, obj, attr, only_path, options
522 end
522 end
523 end
523 end
524 parse_headings(text, project, obj, attr, only_path, options)
524 parse_headings(text, project, obj, attr, only_path, options)
525
525
526 if @parsed_headings.any?
526 if @parsed_headings.any?
527 replace_toc(text, @parsed_headings)
527 replace_toc(text, @parsed_headings)
528 end
528 end
529
529
530 text.html_safe
530 text.html_safe
531 end
531 end
532
532
533 def parse_non_pre_blocks(text)
533 def parse_non_pre_blocks(text)
534 s = StringScanner.new(text)
534 s = StringScanner.new(text)
535 tags = []
535 tags = []
536 parsed = ''
536 parsed = ''
537 while !s.eos?
537 while !s.eos?
538 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
538 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
539 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
539 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
540 if tags.empty?
540 if tags.empty?
541 yield text
541 yield text
542 end
542 end
543 parsed << text
543 parsed << text
544 if tag
544 if tag
545 if closing
545 if closing
546 if tags.last == tag.downcase
546 if tags.last == tag.downcase
547 tags.pop
547 tags.pop
548 end
548 end
549 else
549 else
550 tags << tag.downcase
550 tags << tag.downcase
551 end
551 end
552 parsed << full_tag
552 parsed << full_tag
553 end
553 end
554 end
554 end
555 # Close any non closing tags
555 # Close any non closing tags
556 while tag = tags.pop
556 while tag = tags.pop
557 parsed << "</#{tag}>"
557 parsed << "</#{tag}>"
558 end
558 end
559 parsed
559 parsed
560 end
560 end
561
561
562 def parse_inline_attachments(text, project, obj, attr, only_path, options)
562 def parse_inline_attachments(text, project, obj, attr, only_path, options)
563 # when using an image link, try to use an attachment, if possible
563 # when using an image link, try to use an attachment, if possible
564 if options[:attachments] || (obj && obj.respond_to?(:attachments))
564 if options[:attachments] || (obj && obj.respond_to?(:attachments))
565 attachments = options[:attachments] || obj.attachments
565 attachments = options[:attachments] || obj.attachments
566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
566 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
567 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
568 # search for the picture in attachments
568 # search for the picture in attachments
569 if found = Attachment.latest_attach(attachments, filename)
569 if found = Attachment.latest_attach(attachments, filename)
570 image_url = url_for :only_path => only_path, :controller => 'attachments',
570 image_url = url_for :only_path => only_path, :controller => 'attachments',
571 :action => 'download', :id => found
571 :action => 'download', :id => found
572 desc = found.description.to_s.gsub('"', '')
572 desc = found.description.to_s.gsub('"', '')
573 if !desc.blank? && alttext.blank?
573 if !desc.blank? && alttext.blank?
574 alt = " title=\"#{desc}\" alt=\"#{desc}\""
574 alt = " title=\"#{desc}\" alt=\"#{desc}\""
575 end
575 end
576 "src=\"#{image_url}\"#{alt}"
576 "src=\"#{image_url}\"#{alt}"
577 else
577 else
578 m
578 m
579 end
579 end
580 end
580 end
581 end
581 end
582 end
582 end
583
583
584 # Wiki links
584 # Wiki links
585 #
585 #
586 # Examples:
586 # Examples:
587 # [[mypage]]
587 # [[mypage]]
588 # [[mypage|mytext]]
588 # [[mypage|mytext]]
589 # wiki links can refer other project wikis, using project name or identifier:
589 # wiki links can refer other project wikis, using project name or identifier:
590 # [[project:]] -> wiki starting page
590 # [[project:]] -> wiki starting page
591 # [[project:|mytext]]
591 # [[project:|mytext]]
592 # [[project:mypage]]
592 # [[project:mypage]]
593 # [[project:mypage|mytext]]
593 # [[project:mypage|mytext]]
594 def parse_wiki_links(text, project, obj, attr, only_path, options)
594 def parse_wiki_links(text, project, obj, attr, only_path, options)
595 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
595 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
596 link_project = project
596 link_project = project
597 esc, all, page, title = $1, $2, $3, $5
597 esc, all, page, title = $1, $2, $3, $5
598 if esc.nil?
598 if esc.nil?
599 if page =~ /^([^\:]+)\:(.*)$/
599 if page =~ /^([^\:]+)\:(.*)$/
600 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
600 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
601 page = $2
601 page = $2
602 title ||= $1 if page.blank?
602 title ||= $1 if page.blank?
603 end
603 end
604
604
605 if link_project && link_project.wiki
605 if link_project && link_project.wiki
606 # extract anchor
606 # extract anchor
607 anchor = nil
607 anchor = nil
608 if page =~ /^(.+?)\#(.+)$/
608 if page =~ /^(.+?)\#(.+)$/
609 page, anchor = $1, $2
609 page, anchor = $1, $2
610 end
610 end
611 anchor = sanitize_anchor_name(anchor) if anchor.present?
611 anchor = sanitize_anchor_name(anchor) if anchor.present?
612 # check if page exists
612 # check if page exists
613 wiki_page = link_project.wiki.find_page(page)
613 wiki_page = link_project.wiki.find_page(page)
614 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
614 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
615 "##{anchor}"
615 "##{anchor}"
616 else
616 else
617 case options[:wiki_links]
617 case options[:wiki_links]
618 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
618 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
619 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
619 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
620 else
620 else
621 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
621 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
622 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
622 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
623 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
623 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
624 :id => wiki_page_id, :anchor => anchor, :parent => parent)
624 :id => wiki_page_id, :anchor => anchor, :parent => parent)
625 end
625 end
626 end
626 end
627 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
627 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
628 else
628 else
629 # project or wiki doesn't exist
629 # project or wiki doesn't exist
630 all
630 all
631 end
631 end
632 else
632 else
633 all
633 all
634 end
634 end
635 end
635 end
636 end
636 end
637
637
638 # Redmine links
638 # Redmine links
639 #
639 #
640 # Examples:
640 # Examples:
641 # Issues:
641 # Issues:
642 # #52 -> Link to issue #52
642 # #52 -> Link to issue #52
643 # Changesets:
643 # Changesets:
644 # r52 -> Link to revision 52
644 # r52 -> Link to revision 52
645 # commit:a85130f -> Link to scmid starting with a85130f
645 # commit:a85130f -> Link to scmid starting with a85130f
646 # Documents:
646 # Documents:
647 # document#17 -> Link to document with id 17
647 # document#17 -> Link to document with id 17
648 # document:Greetings -> Link to the document with title "Greetings"
648 # document:Greetings -> Link to the document with title "Greetings"
649 # document:"Some document" -> Link to the document with title "Some document"
649 # document:"Some document" -> Link to the document with title "Some document"
650 # Versions:
650 # Versions:
651 # version#3 -> Link to version with id 3
651 # version#3 -> Link to version with id 3
652 # version:1.0.0 -> Link to version named "1.0.0"
652 # version:1.0.0 -> Link to version named "1.0.0"
653 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
653 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
654 # Attachments:
654 # Attachments:
655 # attachment:file.zip -> Link to the attachment of the current object named file.zip
655 # attachment:file.zip -> Link to the attachment of the current object named file.zip
656 # Source files:
656 # Source files:
657 # source:some/file -> Link to the file located at /some/file in the project's repository
657 # source:some/file -> Link to the file located at /some/file in the project's repository
658 # source:some/file@52 -> Link to the file's revision 52
658 # source:some/file@52 -> Link to the file's revision 52
659 # source:some/file#L120 -> Link to line 120 of the file
659 # source:some/file#L120 -> Link to line 120 of the file
660 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
660 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
661 # export:some/file -> Force the download of the file
661 # export:some/file -> Force the download of the file
662 # Forum messages:
662 # Forum messages:
663 # message#1218 -> Link to message with id 1218
663 # message#1218 -> Link to message with id 1218
664 #
664 #
665 # Links can refer other objects from other projects, using project identifier:
665 # Links can refer other objects from other projects, using project identifier:
666 # identifier:r52
666 # identifier:r52
667 # identifier:document:"Some document"
667 # identifier:document:"Some document"
668 # identifier:version:1.0.0
668 # identifier:version:1.0.0
669 # identifier:source:some/file
669 # identifier:source:some/file
670 def parse_redmine_links(text, project, obj, attr, only_path, options)
670 def parse_redmine_links(text, project, obj, attr, only_path, options)
671 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
671 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
672 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
672 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
673 link = nil
673 link = nil
674 if project_identifier
674 if project_identifier
675 project = Project.visible.find_by_identifier(project_identifier)
675 project = Project.visible.find_by_identifier(project_identifier)
676 end
676 end
677 if esc.nil?
677 if esc.nil?
678 if prefix.nil? && sep == 'r'
678 if prefix.nil? && sep == 'r'
679 if project
679 if project
680 repository = nil
680 repository = nil
681 if repo_identifier
681 if repo_identifier
682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
682 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
683 else
683 else
684 repository = project.repository
684 repository = project.repository
685 end
685 end
686 # project.changesets.visible raises an SQL error because of a double join on repositories
686 # project.changesets.visible raises an SQL error because of a double join on repositories
687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
687 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
688 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
688 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
689 :class => 'changeset',
689 :class => 'changeset',
690 :title => truncate_single_line(changeset.comments, :length => 100))
690 :title => truncate_single_line(changeset.comments, :length => 100))
691 end
691 end
692 end
692 end
693 elsif sep == '#'
693 elsif sep == '#'
694 oid = identifier.to_i
694 oid = identifier.to_i
695 case prefix
695 case prefix
696 when nil
696 when nil
697 if issue = Issue.visible.find_by_id(oid, :include => :status)
697 if issue = Issue.visible.find_by_id(oid, :include => :status)
698 anchor = comment_id ? "note-#{comment_id}" : nil
698 anchor = comment_id ? "note-#{comment_id}" : nil
699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
699 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
700 :class => issue.css_classes,
700 :class => issue.css_classes,
701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
701 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
702 end
702 end
703 when 'document'
703 when 'document'
704 if document = Document.visible.find_by_id(oid)
704 if document = Document.visible.find_by_id(oid)
705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
705 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
706 :class => 'document'
706 :class => 'document'
707 end
707 end
708 when 'version'
708 when 'version'
709 if version = Version.visible.find_by_id(oid)
709 if version = Version.visible.find_by_id(oid)
710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
710 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
711 :class => 'version'
711 :class => 'version'
712 end
712 end
713 when 'message'
713 when 'message'
714 if message = Message.visible.find_by_id(oid, :include => :parent)
714 if message = Message.visible.find_by_id(oid, :include => :parent)
715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
715 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
716 end
716 end
717 when 'forum'
717 when 'forum'
718 if board = Board.visible.find_by_id(oid)
718 if board = Board.visible.find_by_id(oid)
719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
719 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
720 :class => 'board'
720 :class => 'board'
721 end
721 end
722 when 'news'
722 when 'news'
723 if news = News.visible.find_by_id(oid)
723 if news = News.visible.find_by_id(oid)
724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
724 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
725 :class => 'news'
725 :class => 'news'
726 end
726 end
727 when 'project'
727 when 'project'
728 if p = Project.visible.find_by_id(oid)
728 if p = Project.visible.find_by_id(oid)
729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
729 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
730 end
730 end
731 end
731 end
732 elsif sep == ':'
732 elsif sep == ':'
733 # removes the double quotes if any
733 # removes the double quotes if any
734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
734 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
735 case prefix
735 case prefix
736 when 'document'
736 when 'document'
737 if project && document = project.documents.visible.find_by_title(name)
737 if project && document = project.documents.visible.find_by_title(name)
738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
738 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
739 :class => 'document'
739 :class => 'document'
740 end
740 end
741 when 'version'
741 when 'version'
742 if project && version = project.versions.visible.find_by_name(name)
742 if project && version = project.versions.visible.find_by_name(name)
743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
743 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
744 :class => 'version'
744 :class => 'version'
745 end
745 end
746 when 'forum'
746 when 'forum'
747 if project && board = project.boards.visible.find_by_name(name)
747 if project && board = project.boards.visible.find_by_name(name)
748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
748 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
749 :class => 'board'
749 :class => 'board'
750 end
750 end
751 when 'news'
751 when 'news'
752 if project && news = project.news.visible.find_by_title(name)
752 if project && news = project.news.visible.find_by_title(name)
753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
753 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
754 :class => 'news'
754 :class => 'news'
755 end
755 end
756 when 'commit', 'source', 'export'
756 when 'commit', 'source', 'export'
757 if project
757 if project
758 repository = nil
758 repository = nil
759 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
759 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
760 repo_prefix, repo_identifier, name = $1, $2, $3
760 repo_prefix, repo_identifier, name = $1, $2, $3
761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
762 else
762 else
763 repository = project.repository
763 repository = project.repository
764 end
764 end
765 if prefix == 'commit'
765 if prefix == 'commit'
766 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
766 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
767 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},
767 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},
768 :class => 'changeset',
768 :class => 'changeset',
769 :title => truncate_single_line(h(changeset.comments), :length => 100)
769 :title => truncate_single_line(h(changeset.comments), :length => 100)
770 end
770 end
771 else
771 else
772 if repository && User.current.allowed_to?(:browse_repository, project)
772 if repository && User.current.allowed_to?(:browse_repository, project)
773 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
773 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
774 path, rev, anchor = $1, $3, $5
774 path, rev, anchor = $1, $3, $5
775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
775 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
776 :path => to_path_param(path),
776 :path => to_path_param(path),
777 :rev => rev,
777 :rev => rev,
778 :anchor => anchor,
778 :anchor => anchor,
779 :format => (prefix == 'export' ? 'raw' : nil)},
779 :format => (prefix == 'export' ? 'raw' : nil)},
780 :class => (prefix == 'export' ? 'source download' : 'source')
780 :class => (prefix == 'export' ? 'source download' : 'source')
781 end
781 end
782 end
782 end
783 repo_prefix = nil
783 repo_prefix = nil
784 end
784 end
785 when 'attachment'
785 when 'attachment'
786 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
786 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
787 if attachments && attachment = attachments.detect {|a| a.filename == name }
787 if attachments && attachment = attachments.detect {|a| a.filename == name }
788 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
788 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
789 :class => 'attachment'
789 :class => 'attachment'
790 end
790 end
791 when 'project'
791 when 'project'
792 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
792 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
793 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
793 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
794 end
794 end
795 end
795 end
796 end
796 end
797 end
797 end
798 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
798 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
799 end
799 end
800 end
800 end
801
801
802 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
802 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
803
803
804 def parse_sections(text, project, obj, attr, only_path, options)
804 def parse_sections(text, project, obj, attr, only_path, options)
805 return unless options[:edit_section_links]
805 return unless options[:edit_section_links]
806 text.gsub!(HEADING_RE) do
806 text.gsub!(HEADING_RE) do
807 heading = $1
807 heading = $1
808 @current_section += 1
808 @current_section += 1
809 if @current_section > 1
809 if @current_section > 1
810 content_tag('div',
810 content_tag('div',
811 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
811 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
812 :class => 'contextual',
812 :class => 'contextual',
813 :title => l(:button_edit_section)) + heading.html_safe
813 :title => l(:button_edit_section)) + heading.html_safe
814 else
814 else
815 heading
815 heading
816 end
816 end
817 end
817 end
818 end
818 end
819
819
820 # Headings and TOC
820 # Headings and TOC
821 # Adds ids and links to headings unless options[:headings] is set to false
821 # Adds ids and links to headings unless options[:headings] is set to false
822 def parse_headings(text, project, obj, attr, only_path, options)
822 def parse_headings(text, project, obj, attr, only_path, options)
823 return if options[:headings] == false
823 return if options[:headings] == false
824
824
825 text.gsub!(HEADING_RE) do
825 text.gsub!(HEADING_RE) do
826 level, attrs, content = $2.to_i, $3, $4
826 level, attrs, content = $2.to_i, $3, $4
827 item = strip_tags(content).strip
827 item = strip_tags(content).strip
828 anchor = sanitize_anchor_name(item)
828 anchor = sanitize_anchor_name(item)
829 # used for single-file wiki export
829 # used for single-file wiki export
830 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
830 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
831 @heading_anchors[anchor] ||= 0
831 @heading_anchors[anchor] ||= 0
832 idx = (@heading_anchors[anchor] += 1)
832 idx = (@heading_anchors[anchor] += 1)
833 if idx > 1
833 if idx > 1
834 anchor = "#{anchor}-#{idx}"
834 anchor = "#{anchor}-#{idx}"
835 end
835 end
836 @parsed_headings << [level, anchor, item]
836 @parsed_headings << [level, anchor, item]
837 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
837 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
838 end
838 end
839 end
839 end
840
840
841 MACROS_RE = /
841 MACROS_RE = /
842 (!)? # escaping
842 (!)? # escaping
843 (
843 (
844 \{\{ # opening tag
844 \{\{ # opening tag
845 ([\w]+) # macro name
845 ([\w]+) # macro name
846 (\(([^\}]*)\))? # optional arguments
846 (\(([^\}]*)\))? # optional arguments
847 \}\} # closing tag
847 \}\} # closing tag
848 )
848 )
849 /x unless const_defined?(:MACROS_RE)
849 /x unless const_defined?(:MACROS_RE)
850
850
851 # Macros substitution
851 # Macros substitution
852 def parse_macros(text, project, obj, attr, only_path, options)
852 def parse_macros(text, project, obj, attr, only_path, options)
853 text.gsub!(MACROS_RE) do
853 text.gsub!(MACROS_RE) do
854 esc, all, macro = $1, $2, $3.downcase
854 esc, all, macro = $1, $2, $3.downcase
855 args = ($5 || '').split(',').each(&:strip)
855 args = ($5 || '').split(',').each(&:strip)
856 if esc.nil?
856 if esc.nil?
857 begin
857 begin
858 exec_macro(macro, obj, args)
858 exec_macro(macro, obj, args)
859 rescue => e
859 rescue => e
860 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
860 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
861 end || all
861 end || all
862 else
862 else
863 all
863 all
864 end
864 end
865 end
865 end
866 end
866 end
867
867
868 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
868 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
869
869
870 # Renders the TOC with given headings
870 # Renders the TOC with given headings
871 def replace_toc(text, headings)
871 def replace_toc(text, headings)
872 text.gsub!(TOC_RE) do
872 text.gsub!(TOC_RE) do
873 if headings.empty?
873 if headings.empty?
874 ''
874 ''
875 else
875 else
876 div_class = 'toc'
876 div_class = 'toc'
877 div_class << ' right' if $1 == '>'
877 div_class << ' right' if $1 == '>'
878 div_class << ' left' if $1 == '<'
878 div_class << ' left' if $1 == '<'
879 out = "<ul class=\"#{div_class}\"><li>"
879 out = "<ul class=\"#{div_class}\"><li>"
880 root = headings.map(&:first).min
880 root = headings.map(&:first).min
881 current = root
881 current = root
882 started = false
882 started = false
883 headings.each do |level, anchor, item|
883 headings.each do |level, anchor, item|
884 if level > current
884 if level > current
885 out << '<ul><li>' * (level - current)
885 out << '<ul><li>' * (level - current)
886 elsif level < current
886 elsif level < current
887 out << "</li></ul>\n" * (current - level) + "</li><li>"
887 out << "</li></ul>\n" * (current - level) + "</li><li>"
888 elsif started
888 elsif started
889 out << '</li><li>'
889 out << '</li><li>'
890 end
890 end
891 out << "<a href=\"##{anchor}\">#{item}</a>"
891 out << "<a href=\"##{anchor}\">#{item}</a>"
892 current = level
892 current = level
893 started = true
893 started = true
894 end
894 end
895 out << '</li></ul>' * (current - root)
895 out << '</li></ul>' * (current - root)
896 out << '</li></ul>'
896 out << '</li></ul>'
897 end
897 end
898 end
898 end
899 end
899 end
900
900
901 # Same as Rails' simple_format helper without using paragraphs
901 # Same as Rails' simple_format helper without using paragraphs
902 def simple_format_without_paragraph(text)
902 def simple_format_without_paragraph(text)
903 text.to_s.
903 text.to_s.
904 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
904 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
905 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
905 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
906 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
906 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
907 html_safe
907 html_safe
908 end
908 end
909
909
910 def lang_options_for_select(blank=true)
910 def lang_options_for_select(blank=true)
911 (blank ? [["(auto)", ""]] : []) +
911 (blank ? [["(auto)", ""]] : []) +
912 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
912 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
913 end
913 end
914
914
915 def label_tag_for(name, option_tags = nil, options = {})
915 def label_tag_for(name, option_tags = nil, options = {})
916 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
916 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
917 content_tag("label", label_text)
917 content_tag("label", label_text)
918 end
918 end
919
919
920 def labelled_tabular_form_for(*args, &proc)
920 def labelled_tabular_form_for(*args, &proc)
921 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
921 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
922 args << {} unless args.last.is_a?(Hash)
922 args << {} unless args.last.is_a?(Hash)
923 options = args.last
923 options = args.last
924 options[:html] ||= {}
924 options[:html] ||= {}
925 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
925 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
926 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
926 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
927 form_for(*args, &proc)
927 form_for(*args, &proc)
928 end
928 end
929
929
930 def labelled_form_for(*args, &proc)
930 def labelled_form_for(*args, &proc)
931 args << {} unless args.last.is_a?(Hash)
931 args << {} unless args.last.is_a?(Hash)
932 options = args.last
932 options = args.last
933 if args.first.is_a?(Symbol)
933 if args.first.is_a?(Symbol)
934 options.merge!(:as => args.shift)
934 options.merge!(:as => args.shift)
935 end
935 end
936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
937 form_for(*args, &proc)
937 form_for(*args, &proc)
938 end
938 end
939
939
940 def labelled_fields_for(*args, &proc)
940 def labelled_fields_for(*args, &proc)
941 args << {} unless args.last.is_a?(Hash)
941 args << {} unless args.last.is_a?(Hash)
942 options = args.last
942 options = args.last
943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
943 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
944 fields_for(*args, &proc)
944 fields_for(*args, &proc)
945 end
945 end
946
946
947 def labelled_remote_form_for(*args, &proc)
947 def labelled_remote_form_for(*args, &proc)
948 args << {} unless args.last.is_a?(Hash)
948 args << {} unless args.last.is_a?(Hash)
949 options = args.last
949 options = args.last
950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
950 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
951 form_for(*args, &proc)
951 form_for(*args, &proc)
952 end
952 end
953
953
954 def error_messages_for(*objects)
954 def error_messages_for(*objects)
955 html = ""
955 html = ""
956 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
956 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
957 errors = objects.map {|o| o.errors.full_messages}.flatten
957 errors = objects.map {|o| o.errors.full_messages}.flatten
958 if errors.any?
958 if errors.any?
959 html << "<div id='errorExplanation'><ul>\n"
959 html << "<div id='errorExplanation'><ul>\n"
960 errors.each do |error|
960 errors.each do |error|
961 html << "<li>#{h error}</li>\n"
961 html << "<li>#{h error}</li>\n"
962 end
962 end
963 html << "</ul></div>\n"
963 html << "</ul></div>\n"
964 end
964 end
965 html.html_safe
965 html.html_safe
966 end
966 end
967
967
968 def back_url_hidden_field_tag
968 def back_url_hidden_field_tag
969 back_url = params[:back_url] || request.env['HTTP_REFERER']
969 back_url = params[:back_url] || request.env['HTTP_REFERER']
970 back_url = CGI.unescape(back_url.to_s)
970 back_url = CGI.unescape(back_url.to_s)
971 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
971 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
972 end
972 end
973
973
974 def check_all_links(form_name)
974 def check_all_links(form_name)
975 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
975 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
976 " | ".html_safe +
976 " | ".html_safe +
977 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
977 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
978 end
978 end
979
979
980 def progress_bar(pcts, options={})
980 def progress_bar(pcts, options={})
981 pcts = [pcts, pcts] unless pcts.is_a?(Array)
981 pcts = [pcts, pcts] unless pcts.is_a?(Array)
982 pcts = pcts.collect(&:round)
982 pcts = pcts.collect(&:round)
983 pcts[1] = pcts[1] - pcts[0]
983 pcts[1] = pcts[1] - pcts[0]
984 pcts << (100 - pcts[1] - pcts[0])
984 pcts << (100 - pcts[1] - pcts[0])
985 width = options[:width] || '100px;'
985 width = options[:width] || '100px;'
986 legend = options[:legend] || ''
986 legend = options[:legend] || ''
987 content_tag('table',
987 content_tag('table',
988 content_tag('tr',
988 content_tag('tr',
989 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
989 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
990 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
990 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
991 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
991 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
992 ), :class => 'progress', :style => "width: #{width};").html_safe +
992 ), :class => 'progress', :style => "width: #{width};").html_safe +
993 content_tag('p', legend, :class => 'pourcent').html_safe
993 content_tag('p', legend, :class => 'pourcent').html_safe
994 end
994 end
995
995
996 def checked_image(checked=true)
996 def checked_image(checked=true)
997 if checked
997 if checked
998 image_tag 'toggle_check.png'
998 image_tag 'toggle_check.png'
999 end
999 end
1000 end
1000 end
1001
1001
1002 def context_menu(url)
1002 def context_menu(url)
1003 unless @context_menu_included
1003 unless @context_menu_included
1004 content_for :header_tags do
1004 content_for :header_tags do
1005 javascript_include_tag('context_menu') +
1005 javascript_include_tag('context_menu') +
1006 stylesheet_link_tag('context_menu')
1006 stylesheet_link_tag('context_menu')
1007 end
1007 end
1008 if l(:direction) == 'rtl'
1008 if l(:direction) == 'rtl'
1009 content_for :header_tags do
1009 content_for :header_tags do
1010 stylesheet_link_tag('context_menu_rtl')
1010 stylesheet_link_tag('context_menu_rtl')
1011 end
1011 end
1012 end
1012 end
1013 @context_menu_included = true
1013 @context_menu_included = true
1014 end
1014 end
1015 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1015 javascript_tag "new ContextMenu('#{ url_for(url) }')"
1016 end
1016 end
1017
1017
1018 def calendar_for(field_id)
1018 def calendar_for(field_id)
1019 include_calendar_headers_tags
1019 include_calendar_headers_tags
1020 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1020 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
1021 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1021 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
1022 end
1022 end
1023
1023
1024 def include_calendar_headers_tags
1024 def include_calendar_headers_tags
1025 unless @calendar_headers_tags_included
1025 unless @calendar_headers_tags_included
1026 @calendar_headers_tags_included = true
1026 @calendar_headers_tags_included = true
1027 content_for :header_tags do
1027 content_for :header_tags do
1028 start_of_week = case Setting.start_of_week.to_i
1028 start_of_week = case Setting.start_of_week.to_i
1029 when 1
1029 when 1
1030 'Calendar._FD = 1;' # Monday
1030 'Calendar._FD = 1;' # Monday
1031 when 7
1031 when 7
1032 'Calendar._FD = 0;' # Sunday
1032 'Calendar._FD = 0;' # Sunday
1033 when 6
1033 when 6
1034 'Calendar._FD = 6;' # Saturday
1034 'Calendar._FD = 6;' # Saturday
1035 else
1035 else
1036 '' # use language
1036 '' # use language
1037 end
1037 end
1038
1038
1039 javascript_include_tag('calendar/calendar') +
1039 javascript_include_tag('calendar/calendar') +
1040 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1040 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1041 javascript_tag(start_of_week) +
1041 javascript_tag(start_of_week) +
1042 javascript_include_tag('calendar/calendar-setup') +
1042 javascript_include_tag('calendar/calendar-setup') +
1043 stylesheet_link_tag('calendar')
1043 stylesheet_link_tag('calendar')
1044 end
1044 end
1045 end
1045 end
1046 end
1046 end
1047
1047
1048 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1048 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1049 # Examples:
1049 # Examples:
1050 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1050 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1051 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1051 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1052 #
1052 #
1053 def stylesheet_link_tag(*sources)
1053 def stylesheet_link_tag(*sources)
1054 options = sources.last.is_a?(Hash) ? sources.pop : {}
1054 options = sources.last.is_a?(Hash) ? sources.pop : {}
1055 plugin = options.delete(:plugin)
1055 plugin = options.delete(:plugin)
1056 sources = sources.map do |source|
1056 sources = sources.map do |source|
1057 if plugin
1057 if plugin
1058 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1058 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1059 elsif current_theme && current_theme.stylesheets.include?(source)
1059 elsif current_theme && current_theme.stylesheets.include?(source)
1060 current_theme.stylesheet_path(source)
1060 current_theme.stylesheet_path(source)
1061 else
1061 else
1062 source
1062 source
1063 end
1063 end
1064 end
1064 end
1065 super sources, options
1065 super sources, options
1066 end
1066 end
1067
1067
1068 # Overrides Rails' image_tag with themes and plugins support.
1068 # Overrides Rails' image_tag with themes and plugins support.
1069 # Examples:
1069 # Examples:
1070 # image_tag('image.png') # => picks image.png from the current theme or defaults
1070 # image_tag('image.png') # => picks image.png from the current theme or defaults
1071 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1071 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1072 #
1072 #
1073 def image_tag(source, options={})
1073 def image_tag(source, options={})
1074 if plugin = options.delete(:plugin)
1074 if plugin = options.delete(:plugin)
1075 source = "/plugin_assets/#{plugin}/images/#{source}"
1075 source = "/plugin_assets/#{plugin}/images/#{source}"
1076 elsif current_theme && current_theme.images.include?(source)
1076 elsif current_theme && current_theme.images.include?(source)
1077 source = current_theme.image_path(source)
1077 source = current_theme.image_path(source)
1078 end
1078 end
1079 super source, options
1079 super source, options
1080 end
1080 end
1081
1081
1082 # Overrides Rails' javascript_include_tag with plugins support
1082 # Overrides Rails' javascript_include_tag with plugins support
1083 # Examples:
1083 # Examples:
1084 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1084 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1085 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1085 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1086 #
1086 #
1087 def javascript_include_tag(*sources)
1087 def javascript_include_tag(*sources)
1088 options = sources.last.is_a?(Hash) ? sources.pop : {}
1088 options = sources.last.is_a?(Hash) ? sources.pop : {}
1089 if plugin = options.delete(:plugin)
1089 if plugin = options.delete(:plugin)
1090 sources = sources.map do |source|
1090 sources = sources.map do |source|
1091 if plugin
1091 if plugin
1092 "/plugin_assets/#{plugin}/javascripts/#{source}"
1092 "/plugin_assets/#{plugin}/javascripts/#{source}"
1093 else
1093 else
1094 source
1094 source
1095 end
1095 end
1096 end
1096 end
1097 end
1097 end
1098 super sources, options
1098 super sources, options
1099 end
1099 end
1100
1100
1101 def content_for(name, content = nil, &block)
1101 def content_for(name, content = nil, &block)
1102 @has_content ||= {}
1102 @has_content ||= {}
1103 @has_content[name] = true
1103 @has_content[name] = true
1104 super(name, content, &block)
1104 super(name, content, &block)
1105 end
1105 end
1106
1106
1107 def has_content?(name)
1107 def has_content?(name)
1108 (@has_content && @has_content[name]) || false
1108 (@has_content && @has_content[name]) || false
1109 end
1109 end
1110
1110
1111 def sidebar_content?
1112 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1113 end
1114
1115 def view_layouts_base_sidebar_hook_response
1116 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1117 end
1118
1111 def email_delivery_enabled?
1119 def email_delivery_enabled?
1112 !!ActionMailer::Base.perform_deliveries
1120 !!ActionMailer::Base.perform_deliveries
1113 end
1121 end
1114
1122
1115 # Returns the avatar image tag for the given +user+ if avatars are enabled
1123 # Returns the avatar image tag for the given +user+ if avatars are enabled
1116 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1124 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1117 def avatar(user, options = { })
1125 def avatar(user, options = { })
1118 if Setting.gravatar_enabled?
1126 if Setting.gravatar_enabled?
1119 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1127 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1120 email = nil
1128 email = nil
1121 if user.respond_to?(:mail)
1129 if user.respond_to?(:mail)
1122 email = user.mail
1130 email = user.mail
1123 elsif user.to_s =~ %r{<(.+?)>}
1131 elsif user.to_s =~ %r{<(.+?)>}
1124 email = $1
1132 email = $1
1125 end
1133 end
1126 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1134 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1127 else
1135 else
1128 ''
1136 ''
1129 end
1137 end
1130 end
1138 end
1131
1139
1132 def sanitize_anchor_name(anchor)
1140 def sanitize_anchor_name(anchor)
1133 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1141 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1134 end
1142 end
1135
1143
1136 # Returns the javascript tags that are included in the html layout head
1144 # Returns the javascript tags that are included in the html layout head
1137 def javascript_heads
1145 def javascript_heads
1138 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1146 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1139 unless User.current.pref.warn_on_leaving_unsaved == '0'
1147 unless User.current.pref.warn_on_leaving_unsaved == '0'
1140 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1148 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1141 end
1149 end
1142 tags
1150 tags
1143 end
1151 end
1144
1152
1145 def favicon
1153 def favicon
1146 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1154 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1147 end
1155 end
1148
1156
1149 def robot_exclusion_tag
1157 def robot_exclusion_tag
1150 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1158 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1151 end
1159 end
1152
1160
1153 # Returns true if arg is expected in the API response
1161 # Returns true if arg is expected in the API response
1154 def include_in_api_response?(arg)
1162 def include_in_api_response?(arg)
1155 unless @included_in_api_response
1163 unless @included_in_api_response
1156 param = params[:include]
1164 param = params[:include]
1157 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1165 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1158 @included_in_api_response.collect!(&:strip)
1166 @included_in_api_response.collect!(&:strip)
1159 end
1167 end
1160 @included_in_api_response.include?(arg.to_s)
1168 @included_in_api_response.include?(arg.to_s)
1161 end
1169 end
1162
1170
1163 # Returns options or nil if nometa param or X-Redmine-Nometa header
1171 # Returns options or nil if nometa param or X-Redmine-Nometa header
1164 # was set in the request
1172 # was set in the request
1165 def api_meta(options)
1173 def api_meta(options)
1166 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1174 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1167 # compatibility mode for activeresource clients that raise
1175 # compatibility mode for activeresource clients that raise
1168 # an error when unserializing an array with attributes
1176 # an error when unserializing an array with attributes
1169 nil
1177 nil
1170 else
1178 else
1171 options
1179 options
1172 end
1180 end
1173 end
1181 end
1174
1182
1175 private
1183 private
1176
1184
1177 def wiki_helper
1185 def wiki_helper
1178 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1186 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1179 extend helper
1187 extend helper
1180 return self
1188 return self
1181 end
1189 end
1182
1190
1183 def link_to_content_update(text, url_params = {}, html_options = {})
1191 def link_to_content_update(text, url_params = {}, html_options = {})
1184 link_to(text, url_params, html_options)
1192 link_to(text, url_params, html_options)
1185 end
1193 end
1186 end
1194 end
@@ -1,85 +1,85
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <title><%=h html_title %></title>
5 <title><%=h html_title %></title>
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 <meta name="keywords" content="issue,bug,tracker" />
7 <meta name="keywords" content="issue,bug,tracker" />
8 <meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
8 <meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
9 <%= csrf_meta_tag %>
9 <%= csrf_meta_tag %>
10 <%= favicon %>
10 <%= favicon %>
11 <%= stylesheet_link_tag 'application', :media => 'all' %>
11 <%= stylesheet_link_tag 'application', :media => 'all' %>
12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
12 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 <%= javascript_heads %>
13 <%= javascript_heads %>
14 <%= heads_for_theme %>
14 <%= heads_for_theme %>
15 <!--[if IE 6]>
15 <!--[if IE 6]>
16 <style type="text/css">
16 <style type="text/css">
17 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
17 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
18 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
18 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
19 </style>
19 </style>
20 <![endif]-->
20 <![endif]-->
21 <%= call_hook :view_layouts_base_html_head %>
21 <%= call_hook :view_layouts_base_html_head %>
22 <!-- page specific tags -->
22 <!-- page specific tags -->
23 <%= yield :header_tags -%>
23 <%= yield :header_tags -%>
24 </head>
24 </head>
25 <body class="<%=h body_css_classes %>">
25 <body class="<%=h body_css_classes %>">
26 <div id="wrapper">
26 <div id="wrapper">
27 <div id="wrapper2">
27 <div id="wrapper2">
28 <div id="top-menu">
28 <div id="top-menu">
29 <div id="account">
29 <div id="account">
30 <%= render_menu :account_menu -%>
30 <%= render_menu :account_menu -%>
31 </div>
31 </div>
32 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
32 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
33 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
33 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
34 </div>
34 </div>
35
35
36 <div id="header">
36 <div id="header">
37 <% if User.current.logged? || !Setting.login_required? %>
37 <% if User.current.logged? || !Setting.login_required? %>
38 <div id="quick-search">
38 <div id="quick-search">
39 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
39 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
40 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
40 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
41 <label for='q'>
41 <label for='q'>
42 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
42 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
43 </label>
43 </label>
44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
45 <% end %>
45 <% end %>
46 <%= render_project_jump_box %>
46 <%= render_project_jump_box %>
47 </div>
47 </div>
48 <% end %>
48 <% end %>
49
49
50 <h1><%= page_header_title %></h1>
50 <h1><%= page_header_title %></h1>
51
51
52 <% if display_main_menu?(@project) %>
52 <% if display_main_menu?(@project) %>
53 <div id="main-menu">
53 <div id="main-menu">
54 <%= render_main_menu(@project) %>
54 <%= render_main_menu(@project) %>
55 </div>
55 </div>
56 <% end %>
56 <% end %>
57 </div>
57 </div>
58
58
59 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
59 <%= tag('div', {:id => 'main', :class => (sidebar_content? ? '' : 'nosidebar')}, true) %>
60 <div id="sidebar">
60 <div id="sidebar">
61 <%= yield :sidebar %>
61 <%= yield :sidebar %>
62 <%= call_hook :view_layouts_base_sidebar %>
62 <%= view_layouts_base_sidebar_hook_response %>
63 </div>
63 </div>
64
64
65 <div id="content">
65 <div id="content">
66 <%= render_flash_messages %>
66 <%= render_flash_messages %>
67 <%= yield %>
67 <%= yield %>
68 <%= call_hook :view_layouts_base_content %>
68 <%= call_hook :view_layouts_base_content %>
69 <div style="clear:both;"></div>
69 <div style="clear:both;"></div>
70 </div>
70 </div>
71 </div>
71 </div>
72
72
73 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
73 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
74 <div id="ajax-modal" style="display:none;"></div>
74 <div id="ajax-modal" style="display:none;"></div>
75
75
76 <div id="footer">
76 <div id="footer">
77 <div class="bgl"><div class="bgr">
77 <div class="bgl"><div class="bgr">
78 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2012 Jean-Philippe Lang
78 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2012 Jean-Philippe Lang
79 </div></div>
79 </div></div>
80 </div>
80 </div>
81 </div>
81 </div>
82 </div>
82 </div>
83 <%= call_hook :view_layouts_base_body_bottom %>
83 <%= call_hook :view_layouts_base_body_bottom %>
84 </body>
84 </body>
85 </html>
85 </html>
@@ -1,542 +1,523
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < ActionController::TestCase
24 class ProjectsControllerTest < ActionController::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments, :custom_fields, :custom_values, :time_entries
27 :attachments, :custom_fields, :custom_values, :time_entries
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:projects)
41 assert_not_nil assigns(:projects)
42
42
43 assert_tag :ul, :child => {:tag => 'li',
43 assert_tag :ul, :child => {:tag => 'li',
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 :child => { :tag => 'ul',
45 :child => { :tag => 'ul',
46 :descendant => { :tag => 'a',
46 :descendant => { :tag => 'a',
47 :content => 'Child of private child'
47 :content => 'Child of private child'
48 }
48 }
49 }
49 }
50 }
50 }
51
51
52 assert_no_tag :a, :content => /Private child of eCookbook/
52 assert_no_tag :a, :content => /Private child of eCookbook/
53 end
53 end
54
54
55 def test_index_atom
55 def test_index_atom
56 get :index, :format => 'atom'
56 get :index, :format => 'atom'
57 assert_response :success
57 assert_response :success
58 assert_template 'common/feed'
58 assert_template 'common/feed'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_condition(User.current))
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_condition(User.current))
61 end
61 end
62
62
63 context "#index" do
63 context "#index" do
64 context "by non-admin user with view_time_entries permission" do
64 context "by non-admin user with view_time_entries permission" do
65 setup do
65 setup do
66 @request.session[:user_id] = 3
66 @request.session[:user_id] = 3
67 end
67 end
68 should "show overall spent time link" do
68 should "show overall spent time link" do
69 get :index
69 get :index
70 assert_template 'index'
70 assert_template 'index'
71 assert_tag :a, :attributes => {:href => '/time_entries'}
71 assert_tag :a, :attributes => {:href => '/time_entries'}
72 end
72 end
73 end
73 end
74
74
75 context "by non-admin user without view_time_entries permission" do
75 context "by non-admin user without view_time_entries permission" do
76 setup do
76 setup do
77 Role.find(2).remove_permission! :view_time_entries
77 Role.find(2).remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
78 Role.non_member.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
79 Role.anonymous.remove_permission! :view_time_entries
80 @request.session[:user_id] = 3
80 @request.session[:user_id] = 3
81 end
81 end
82 should "not show overall spent time link" do
82 should "not show overall spent time link" do
83 get :index
83 get :index
84 assert_template 'index'
84 assert_template 'index'
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
85 assert_no_tag :a, :attributes => {:href => '/time_entries'}
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 context "#new" do
90 context "#new" do
91 context "by admin user" do
91 context "by admin user" do
92 setup do
92 setup do
93 @request.session[:user_id] = 1
93 @request.session[:user_id] = 1
94 end
94 end
95
95
96 should "accept get" do
96 should "accept get" do
97 get :new
97 get :new
98 assert_response :success
98 assert_response :success
99 assert_template 'new'
99 assert_template 'new'
100 end
100 end
101
101
102 end
102 end
103
103
104 context "by non-admin user with add_project permission" do
104 context "by non-admin user with add_project permission" do
105 setup do
105 setup do
106 Role.non_member.add_permission! :add_project
106 Role.non_member.add_permission! :add_project
107 @request.session[:user_id] = 9
107 @request.session[:user_id] = 9
108 end
108 end
109
109
110 should "accept get" do
110 should "accept get" do
111 get :new
111 get :new
112 assert_response :success
112 assert_response :success
113 assert_template 'new'
113 assert_template 'new'
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
114 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
115 end
115 end
116 end
116 end
117
117
118 context "by non-admin user with add_subprojects permission" do
118 context "by non-admin user with add_subprojects permission" do
119 setup do
119 setup do
120 Role.find(1).remove_permission! :add_project
120 Role.find(1).remove_permission! :add_project
121 Role.find(1).add_permission! :add_subprojects
121 Role.find(1).add_permission! :add_subprojects
122 @request.session[:user_id] = 2
122 @request.session[:user_id] = 2
123 end
123 end
124
124
125 should "accept get" do
125 should "accept get" do
126 get :new, :parent_id => 'ecookbook'
126 get :new, :parent_id => 'ecookbook'
127 assert_response :success
127 assert_response :success
128 assert_template 'new'
128 assert_template 'new'
129 # parent project selected
129 # parent project selected
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
130 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
131 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
132 # no empty value
132 # no empty value
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
133 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
134 :child => {:tag => 'option', :attributes => {:value => ''}}
134 :child => {:tag => 'option', :attributes => {:value => ''}}
135 end
135 end
136 end
136 end
137
137
138 end
138 end
139
139
140 context "POST :create" do
140 context "POST :create" do
141 context "by admin user" do
141 context "by admin user" do
142 setup do
142 setup do
143 @request.session[:user_id] = 1
143 @request.session[:user_id] = 1
144 end
144 end
145
145
146 should "create a new project" do
146 should "create a new project" do
147 post :create,
147 post :create,
148 :project => {
148 :project => {
149 :name => "blog",
149 :name => "blog",
150 :description => "weblog",
150 :description => "weblog",
151 :homepage => 'http://weblog',
151 :homepage => 'http://weblog',
152 :identifier => "blog",
152 :identifier => "blog",
153 :is_public => 1,
153 :is_public => 1,
154 :custom_field_values => { '3' => 'Beta' },
154 :custom_field_values => { '3' => 'Beta' },
155 :tracker_ids => ['1', '3'],
155 :tracker_ids => ['1', '3'],
156 # an issue custom field that is not for all project
156 # an issue custom field that is not for all project
157 :issue_custom_field_ids => ['9'],
157 :issue_custom_field_ids => ['9'],
158 :enabled_module_names => ['issue_tracking', 'news', 'repository']
158 :enabled_module_names => ['issue_tracking', 'news', 'repository']
159 }
159 }
160 assert_redirected_to '/projects/blog/settings'
160 assert_redirected_to '/projects/blog/settings'
161
161
162 project = Project.find_by_name('blog')
162 project = Project.find_by_name('blog')
163 assert_kind_of Project, project
163 assert_kind_of Project, project
164 assert project.active?
164 assert project.active?
165 assert_equal 'weblog', project.description
165 assert_equal 'weblog', project.description
166 assert_equal 'http://weblog', project.homepage
166 assert_equal 'http://weblog', project.homepage
167 assert_equal true, project.is_public?
167 assert_equal true, project.is_public?
168 assert_nil project.parent
168 assert_nil project.parent
169 assert_equal 'Beta', project.custom_value_for(3).value
169 assert_equal 'Beta', project.custom_value_for(3).value
170 assert_equal [1, 3], project.trackers.map(&:id).sort
170 assert_equal [1, 3], project.trackers.map(&:id).sort
171 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
171 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
172 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
172 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
173 end
173 end
174
174
175 should "create a new subproject" do
175 should "create a new subproject" do
176 post :create, :project => { :name => "blog",
176 post :create, :project => { :name => "blog",
177 :description => "weblog",
177 :description => "weblog",
178 :identifier => "blog",
178 :identifier => "blog",
179 :is_public => 1,
179 :is_public => 1,
180 :custom_field_values => { '3' => 'Beta' },
180 :custom_field_values => { '3' => 'Beta' },
181 :parent_id => 1
181 :parent_id => 1
182 }
182 }
183 assert_redirected_to '/projects/blog/settings'
183 assert_redirected_to '/projects/blog/settings'
184
184
185 project = Project.find_by_name('blog')
185 project = Project.find_by_name('blog')
186 assert_kind_of Project, project
186 assert_kind_of Project, project
187 assert_equal Project.find(1), project.parent
187 assert_equal Project.find(1), project.parent
188 end
188 end
189
189
190 should "continue" do
190 should "continue" do
191 assert_difference 'Project.count' do
191 assert_difference 'Project.count' do
192 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
192 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
193 end
193 end
194 assert_redirected_to '/projects/new?'
194 assert_redirected_to '/projects/new?'
195 end
195 end
196 end
196 end
197
197
198 context "by non-admin user with add_project permission" do
198 context "by non-admin user with add_project permission" do
199 setup do
199 setup do
200 Role.non_member.add_permission! :add_project
200 Role.non_member.add_permission! :add_project
201 @request.session[:user_id] = 9
201 @request.session[:user_id] = 9
202 end
202 end
203
203
204 should "accept create a Project" do
204 should "accept create a Project" do
205 post :create, :project => { :name => "blog",
205 post :create, :project => { :name => "blog",
206 :description => "weblog",
206 :description => "weblog",
207 :identifier => "blog",
207 :identifier => "blog",
208 :is_public => 1,
208 :is_public => 1,
209 :custom_field_values => { '3' => 'Beta' },
209 :custom_field_values => { '3' => 'Beta' },
210 :tracker_ids => ['1', '3'],
210 :tracker_ids => ['1', '3'],
211 :enabled_module_names => ['issue_tracking', 'news', 'repository']
211 :enabled_module_names => ['issue_tracking', 'news', 'repository']
212 }
212 }
213
213
214 assert_redirected_to '/projects/blog/settings'
214 assert_redirected_to '/projects/blog/settings'
215
215
216 project = Project.find_by_name('blog')
216 project = Project.find_by_name('blog')
217 assert_kind_of Project, project
217 assert_kind_of Project, project
218 assert_equal 'weblog', project.description
218 assert_equal 'weblog', project.description
219 assert_equal true, project.is_public?
219 assert_equal true, project.is_public?
220 assert_equal [1, 3], project.trackers.map(&:id).sort
220 assert_equal [1, 3], project.trackers.map(&:id).sort
221 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
221 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
222
222
223 # User should be added as a project member
223 # User should be added as a project member
224 assert User.find(9).member_of?(project)
224 assert User.find(9).member_of?(project)
225 assert_equal 1, project.members.size
225 assert_equal 1, project.members.size
226 end
226 end
227
227
228 should "fail with parent_id" do
228 should "fail with parent_id" do
229 assert_no_difference 'Project.count' do
229 assert_no_difference 'Project.count' do
230 post :create, :project => { :name => "blog",
230 post :create, :project => { :name => "blog",
231 :description => "weblog",
231 :description => "weblog",
232 :identifier => "blog",
232 :identifier => "blog",
233 :is_public => 1,
233 :is_public => 1,
234 :custom_field_values => { '3' => 'Beta' },
234 :custom_field_values => { '3' => 'Beta' },
235 :parent_id => 1
235 :parent_id => 1
236 }
236 }
237 end
237 end
238 assert_response :success
238 assert_response :success
239 project = assigns(:project)
239 project = assigns(:project)
240 assert_kind_of Project, project
240 assert_kind_of Project, project
241 assert_not_nil project.errors[:parent_id]
241 assert_not_nil project.errors[:parent_id]
242 end
242 end
243 end
243 end
244
244
245 context "by non-admin user with add_subprojects permission" do
245 context "by non-admin user with add_subprojects permission" do
246 setup do
246 setup do
247 Role.find(1).remove_permission! :add_project
247 Role.find(1).remove_permission! :add_project
248 Role.find(1).add_permission! :add_subprojects
248 Role.find(1).add_permission! :add_subprojects
249 @request.session[:user_id] = 2
249 @request.session[:user_id] = 2
250 end
250 end
251
251
252 should "create a project with a parent_id" do
252 should "create a project with a parent_id" do
253 post :create, :project => { :name => "blog",
253 post :create, :project => { :name => "blog",
254 :description => "weblog",
254 :description => "weblog",
255 :identifier => "blog",
255 :identifier => "blog",
256 :is_public => 1,
256 :is_public => 1,
257 :custom_field_values => { '3' => 'Beta' },
257 :custom_field_values => { '3' => 'Beta' },
258 :parent_id => 1
258 :parent_id => 1
259 }
259 }
260 assert_redirected_to '/projects/blog/settings'
260 assert_redirected_to '/projects/blog/settings'
261 project = Project.find_by_name('blog')
261 project = Project.find_by_name('blog')
262 end
262 end
263
263
264 should "fail without parent_id" do
264 should "fail without parent_id" do
265 assert_no_difference 'Project.count' do
265 assert_no_difference 'Project.count' do
266 post :create, :project => { :name => "blog",
266 post :create, :project => { :name => "blog",
267 :description => "weblog",
267 :description => "weblog",
268 :identifier => "blog",
268 :identifier => "blog",
269 :is_public => 1,
269 :is_public => 1,
270 :custom_field_values => { '3' => 'Beta' }
270 :custom_field_values => { '3' => 'Beta' }
271 }
271 }
272 end
272 end
273 assert_response :success
273 assert_response :success
274 project = assigns(:project)
274 project = assigns(:project)
275 assert_kind_of Project, project
275 assert_kind_of Project, project
276 assert_not_nil project.errors[:parent_id]
276 assert_not_nil project.errors[:parent_id]
277 end
277 end
278
278
279 should "fail with unauthorized parent_id" do
279 should "fail with unauthorized parent_id" do
280 assert !User.find(2).member_of?(Project.find(6))
280 assert !User.find(2).member_of?(Project.find(6))
281 assert_no_difference 'Project.count' do
281 assert_no_difference 'Project.count' do
282 post :create, :project => { :name => "blog",
282 post :create, :project => { :name => "blog",
283 :description => "weblog",
283 :description => "weblog",
284 :identifier => "blog",
284 :identifier => "blog",
285 :is_public => 1,
285 :is_public => 1,
286 :custom_field_values => { '3' => 'Beta' },
286 :custom_field_values => { '3' => 'Beta' },
287 :parent_id => 6
287 :parent_id => 6
288 }
288 }
289 end
289 end
290 assert_response :success
290 assert_response :success
291 project = assigns(:project)
291 project = assigns(:project)
292 assert_kind_of Project, project
292 assert_kind_of Project, project
293 assert_not_nil project.errors[:parent_id]
293 assert_not_nil project.errors[:parent_id]
294 end
294 end
295 end
295 end
296 end
296 end
297
297
298 def test_create_should_preserve_modules_on_validation_failure
298 def test_create_should_preserve_modules_on_validation_failure
299 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
299 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
300 @request.session[:user_id] = 1
300 @request.session[:user_id] = 1
301 assert_no_difference 'Project.count' do
301 assert_no_difference 'Project.count' do
302 post :create, :project => {
302 post :create, :project => {
303 :name => "blog",
303 :name => "blog",
304 :identifier => "",
304 :identifier => "",
305 :enabled_module_names => %w(issue_tracking news)
305 :enabled_module_names => %w(issue_tracking news)
306 }
306 }
307 end
307 end
308 assert_response :success
308 assert_response :success
309 project = assigns(:project)
309 project = assigns(:project)
310 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
310 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
311 end
311 end
312 end
312 end
313
313
314 def test_show_by_id
314 def test_show_by_id
315 get :show, :id => 1
315 get :show, :id => 1
316 assert_response :success
316 assert_response :success
317 assert_template 'show'
317 assert_template 'show'
318 assert_not_nil assigns(:project)
318 assert_not_nil assigns(:project)
319 end
319 end
320
320
321 def test_show_by_identifier
321 def test_show_by_identifier
322 get :show, :id => 'ecookbook'
322 get :show, :id => 'ecookbook'
323 assert_response :success
323 assert_response :success
324 assert_template 'show'
324 assert_template 'show'
325 assert_not_nil assigns(:project)
325 assert_not_nil assigns(:project)
326 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
326 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
327
327
328 assert_tag 'li', :content => /Development status/
328 assert_tag 'li', :content => /Development status/
329 end
329 end
330
330
331 def test_show_should_not_display_hidden_custom_fields
331 def test_show_should_not_display_hidden_custom_fields
332 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
332 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
333 get :show, :id => 'ecookbook'
333 get :show, :id => 'ecookbook'
334 assert_response :success
334 assert_response :success
335 assert_template 'show'
335 assert_template 'show'
336 assert_not_nil assigns(:project)
336 assert_not_nil assigns(:project)
337
337
338 assert_no_tag 'li', :content => /Development status/
338 assert_no_tag 'li', :content => /Development status/
339 end
339 end
340
340
341 def test_show_should_not_fail_when_custom_values_are_nil
341 def test_show_should_not_fail_when_custom_values_are_nil
342 project = Project.find_by_identifier('ecookbook')
342 project = Project.find_by_identifier('ecookbook')
343 project.custom_values.first.update_attribute(:value, nil)
343 project.custom_values.first.update_attribute(:value, nil)
344 get :show, :id => 'ecookbook'
344 get :show, :id => 'ecookbook'
345 assert_response :success
345 assert_response :success
346 assert_template 'show'
346 assert_template 'show'
347 assert_not_nil assigns(:project)
347 assert_not_nil assigns(:project)
348 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
348 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
349 end
349 end
350
350
351 def show_archived_project_should_be_denied
351 def show_archived_project_should_be_denied
352 project = Project.find_by_identifier('ecookbook')
352 project = Project.find_by_identifier('ecookbook')
353 project.archive!
353 project.archive!
354
354
355 get :show, :id => 'ecookbook'
355 get :show, :id => 'ecookbook'
356 assert_response 403
356 assert_response 403
357 assert_nil assigns(:project)
357 assert_nil assigns(:project)
358 assert_tag :tag => 'p', :content => /archived/
358 assert_tag :tag => 'p', :content => /archived/
359 end
359 end
360
360
361 def test_private_subprojects_hidden
361 def test_private_subprojects_hidden
362 get :show, :id => 'ecookbook'
362 get :show, :id => 'ecookbook'
363 assert_response :success
363 assert_response :success
364 assert_template 'show'
364 assert_template 'show'
365 assert_no_tag :tag => 'a', :content => /Private child/
365 assert_no_tag :tag => 'a', :content => /Private child/
366 end
366 end
367
367
368 def test_private_subprojects_visible
368 def test_private_subprojects_visible
369 @request.session[:user_id] = 2 # manager who is a member of the private subproject
369 @request.session[:user_id] = 2 # manager who is a member of the private subproject
370 get :show, :id => 'ecookbook'
370 get :show, :id => 'ecookbook'
371 assert_response :success
371 assert_response :success
372 assert_template 'show'
372 assert_template 'show'
373 assert_tag :tag => 'a', :content => /Private child/
373 assert_tag :tag => 'a', :content => /Private child/
374 end
374 end
375
375
376 def test_settings
376 def test_settings
377 @request.session[:user_id] = 2 # manager
377 @request.session[:user_id] = 2 # manager
378 get :settings, :id => 1
378 get :settings, :id => 1
379 assert_response :success
379 assert_response :success
380 assert_template 'settings'
380 assert_template 'settings'
381 end
381 end
382
382
383 def test_update
383 def test_update
384 @request.session[:user_id] = 2 # manager
384 @request.session[:user_id] = 2 # manager
385 post :update, :id => 1, :project => {:name => 'Test changed name',
385 post :update, :id => 1, :project => {:name => 'Test changed name',
386 :issue_custom_field_ids => ['']}
386 :issue_custom_field_ids => ['']}
387 assert_redirected_to '/projects/ecookbook/settings'
387 assert_redirected_to '/projects/ecookbook/settings'
388 project = Project.find(1)
388 project = Project.find(1)
389 assert_equal 'Test changed name', project.name
389 assert_equal 'Test changed name', project.name
390 end
390 end
391
391
392 def test_update_with_failure
392 def test_update_with_failure
393 @request.session[:user_id] = 2 # manager
393 @request.session[:user_id] = 2 # manager
394 post :update, :id => 1, :project => {:name => ''}
394 post :update, :id => 1, :project => {:name => ''}
395 assert_response :success
395 assert_response :success
396 assert_template 'settings'
396 assert_template 'settings'
397 assert_error_tag :content => /name can't be blank/i
397 assert_error_tag :content => /name can't be blank/i
398 end
398 end
399
399
400 def test_modules
400 def test_modules
401 @request.session[:user_id] = 2
401 @request.session[:user_id] = 2
402 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
402 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
403
403
404 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
404 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
405 assert_redirected_to '/projects/ecookbook/settings/modules'
405 assert_redirected_to '/projects/ecookbook/settings/modules'
406 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
406 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
407 end
407 end
408
408
409 def test_destroy_without_confirmation
409 def test_destroy_without_confirmation
410 @request.session[:user_id] = 1 # admin
410 @request.session[:user_id] = 1 # admin
411 delete :destroy, :id => 1
411 delete :destroy, :id => 1
412 assert_response :success
412 assert_response :success
413 assert_template 'destroy'
413 assert_template 'destroy'
414 assert_not_nil Project.find_by_id(1)
414 assert_not_nil Project.find_by_id(1)
415 end
415 end
416
416
417 def test_destroy
417 def test_destroy
418 @request.session[:user_id] = 1 # admin
418 @request.session[:user_id] = 1 # admin
419 delete :destroy, :id => 1, :confirm => 1
419 delete :destroy, :id => 1, :confirm => 1
420 assert_redirected_to '/admin/projects'
420 assert_redirected_to '/admin/projects'
421 assert_nil Project.find_by_id(1)
421 assert_nil Project.find_by_id(1)
422 end
422 end
423
423
424 def test_archive
424 def test_archive
425 @request.session[:user_id] = 1 # admin
425 @request.session[:user_id] = 1 # admin
426 post :archive, :id => 1
426 post :archive, :id => 1
427 assert_redirected_to '/admin/projects'
427 assert_redirected_to '/admin/projects'
428 assert !Project.find(1).active?
428 assert !Project.find(1).active?
429 end
429 end
430
430
431 def test_archive_with_failure
431 def test_archive_with_failure
432 @request.session[:user_id] = 1
432 @request.session[:user_id] = 1
433 Project.any_instance.stubs(:archive).returns(false)
433 Project.any_instance.stubs(:archive).returns(false)
434 post :archive, :id => 1
434 post :archive, :id => 1
435 assert_redirected_to '/admin/projects'
435 assert_redirected_to '/admin/projects'
436 assert_match /project cannot be archived/i, flash[:error]
436 assert_match /project cannot be archived/i, flash[:error]
437 end
437 end
438
438
439 def test_unarchive
439 def test_unarchive
440 @request.session[:user_id] = 1 # admin
440 @request.session[:user_id] = 1 # admin
441 Project.find(1).archive
441 Project.find(1).archive
442 post :unarchive, :id => 1
442 post :unarchive, :id => 1
443 assert_redirected_to '/admin/projects'
443 assert_redirected_to '/admin/projects'
444 assert Project.find(1).active?
444 assert Project.find(1).active?
445 end
445 end
446
446
447 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
447 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
448 CustomField.delete_all
448 CustomField.delete_all
449 parent = nil
449 parent = nil
450 6.times do |i|
450 6.times do |i|
451 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
451 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
452 p.set_parent!(parent)
452 p.set_parent!(parent)
453 get :show, :id => p
453 get :show, :id => p
454 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
454 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
455 :children => { :count => [i, 3].min,
455 :children => { :count => [i, 3].min,
456 :only => { :tag => 'a' } }
456 :only => { :tag => 'a' } }
457
457
458 parent = p
458 parent = p
459 end
459 end
460 end
460 end
461
461
462 def test_get_copy
462 def test_get_copy
463 @request.session[:user_id] = 1 # admin
463 @request.session[:user_id] = 1 # admin
464 get :copy, :id => 1
464 get :copy, :id => 1
465 assert_response :success
465 assert_response :success
466 assert_template 'copy'
466 assert_template 'copy'
467 assert assigns(:project)
467 assert assigns(:project)
468 assert_equal Project.find(1).description, assigns(:project).description
468 assert_equal Project.find(1).description, assigns(:project).description
469 assert_nil assigns(:project).id
469 assert_nil assigns(:project).id
470
470
471 assert_tag :tag => 'input',
471 assert_tag :tag => 'input',
472 :attributes => {:name => 'project[enabled_module_names][]', :value => 'issue_tracking'}
472 :attributes => {:name => 'project[enabled_module_names][]', :value => 'issue_tracking'}
473 end
473 end
474
474
475 def test_post_copy_should_copy_requested_items
475 def test_post_copy_should_copy_requested_items
476 @request.session[:user_id] = 1 # admin
476 @request.session[:user_id] = 1 # admin
477 CustomField.delete_all
477 CustomField.delete_all
478
478
479 assert_difference 'Project.count' do
479 assert_difference 'Project.count' do
480 post :copy, :id => 1,
480 post :copy, :id => 1,
481 :project => {
481 :project => {
482 :name => 'Copy',
482 :name => 'Copy',
483 :identifier => 'unique-copy',
483 :identifier => 'unique-copy',
484 :tracker_ids => ['1', '2', '3', ''],
484 :tracker_ids => ['1', '2', '3', ''],
485 :enabled_module_names => %w(issue_tracking time_tracking)
485 :enabled_module_names => %w(issue_tracking time_tracking)
486 },
486 },
487 :only => %w(issues versions)
487 :only => %w(issues versions)
488 end
488 end
489 project = Project.find('unique-copy')
489 project = Project.find('unique-copy')
490 source = Project.find(1)
490 source = Project.find(1)
491 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
491 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
492
492
493 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
493 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
494 # issues assigned to a closed version won't be copied
494 # issues assigned to a closed version won't be copied
495 assert_equal source.issues.select {|i| i.fixed_version.nil? || i.fixed_version.open?}.size,
495 assert_equal source.issues.select {|i| i.fixed_version.nil? || i.fixed_version.open?}.size,
496 project.issues.count, "All issues were not copied"
496 project.issues.count, "All issues were not copied"
497 assert_equal 0, project.members.count
497 assert_equal 0, project.members.count
498 end
498 end
499
499
500 def test_post_copy_should_redirect_to_settings_when_successful
500 def test_post_copy_should_redirect_to_settings_when_successful
501 @request.session[:user_id] = 1 # admin
501 @request.session[:user_id] = 1 # admin
502 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
502 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
503 assert_response :redirect
503 assert_response :redirect
504 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
504 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
505 end
505 end
506
506
507 def test_jump_should_redirect_to_active_tab
507 def test_jump_should_redirect_to_active_tab
508 get :show, :id => 1, :jump => 'issues'
508 get :show, :id => 1, :jump => 'issues'
509 assert_redirected_to '/projects/ecookbook/issues'
509 assert_redirected_to '/projects/ecookbook/issues'
510 end
510 end
511
511
512 def test_jump_should_not_redirect_to_inactive_tab
512 def test_jump_should_not_redirect_to_inactive_tab
513 get :show, :id => 3, :jump => 'documents'
513 get :show, :id => 3, :jump => 'documents'
514 assert_response :success
514 assert_response :success
515 assert_template 'show'
515 assert_template 'show'
516 end
516 end
517
517
518 def test_jump_should_not_redirect_to_unknown_tab
518 def test_jump_should_not_redirect_to_unknown_tab
519 get :show, :id => 3, :jump => 'foobar'
519 get :show, :id => 3, :jump => 'foobar'
520 assert_response :success
520 assert_response :success
521 assert_template 'show'
521 assert_template 'show'
522 end
522 end
523
524 # A hook that is manually registered later
525 class ProjectBasedTemplate < Redmine::Hook::ViewListener
526 def view_layouts_base_html_head(context)
527 # Adds a project stylesheet
528 stylesheet_link_tag(context[:project].identifier) if context[:project]
529 end
530 end
531 # Don't use this hook now
532 Redmine::Hook.clear_listeners
533
534 def test_hook_response
535 Redmine::Hook.add_listener(ProjectBasedTemplate)
536 get :show, :id => 1
537 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
538 :parent => {:tag => 'head'}
539
540 Redmine::Hook.clear_listeners
541 end
542 end
523 end
General Comments 0
You need to be logged in to leave comments. Login now