You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

application_helper.rb 58KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601
  1. # encoding: utf-8
  2. #
  3. # Redmine - project management software
  4. # Copyright (C) 2006-2017 Jean-Philippe Lang
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  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
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. require 'forwardable'
  20. require 'cgi'
  21. module ApplicationHelper
  22. include Redmine::WikiFormatting::Macros::Definitions
  23. include Redmine::I18n
  24. include GravatarHelper::PublicMethods
  25. include Redmine::Pagination::Helper
  26. include Redmine::SudoMode::Helper
  27. include Redmine::Themes::Helper
  28. include Redmine::Hook::Helper
  29. include Redmine::Helpers::URL
  30. extend Forwardable
  31. def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
  32. # Return true if user is authorized for controller/action, otherwise false
  33. def authorize_for(controller, action)
  34. User.current.allowed_to?({:controller => controller, :action => action}, @project)
  35. end
  36. # Display a link if user is authorized
  37. #
  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
  40. # @param [optional, Hash] html_options Options passed to 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)
  43. link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
  44. end
  45. # Displays a link to user's account page if active
  46. def link_to_user(user, options={})
  47. if user.is_a?(User)
  48. name = h(user.name(options[:format]))
  49. if user.active? || (User.current.admin? && user.logged?)
  50. only_path = options[:only_path].nil? ? true : options[:only_path]
  51. link_to name, user_url(user, :only_path => only_path), :class => user.css_classes
  52. else
  53. name
  54. end
  55. else
  56. h(user.to_s)
  57. end
  58. end
  59. # Displays a link to +issue+ with its subject.
  60. # Examples:
  61. #
  62. # link_to_issue(issue) # => Defect #6: This is the subject
  63. # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
  64. # link_to_issue(issue, :subject => false) # => Defect #6
  65. # link_to_issue(issue, :project => true) # => Foo - Defect #6
  66. # link_to_issue(issue, :subject => false, :tracker => false) # => #6
  67. #
  68. def link_to_issue(issue, options={})
  69. title = nil
  70. subject = nil
  71. text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
  72. if options[:subject] == false
  73. title = issue.subject.truncate(60)
  74. else
  75. subject = issue.subject
  76. if truncate_length = options[:truncate]
  77. subject = subject.truncate(truncate_length)
  78. end
  79. end
  80. only_path = options[:only_path].nil? ? true : options[:only_path]
  81. s = link_to(text, issue_url(issue, :only_path => only_path),
  82. :class => issue.css_classes, :title => title)
  83. s << h(": #{subject}") if subject
  84. s = h("#{issue.project} - ") + s if options[:project]
  85. s
  86. end
  87. # Generates a link to an attachment.
  88. # Options:
  89. # * :text - Link text (default to attachment filename)
  90. # * :download - Force download (default: false)
  91. def link_to_attachment(attachment, options={})
  92. text = options.delete(:text) || attachment.filename
  93. if options.delete(:download)
  94. route_method = :download_named_attachment_url
  95. options[:filename] = attachment.filename
  96. else
  97. route_method = :attachment_url
  98. # make sure we don't have an extraneous :filename in the options
  99. options.delete(:filename)
  100. end
  101. html_options = options.slice!(:only_path, :filename)
  102. options[:only_path] = true unless options.key?(:only_path)
  103. url = send(route_method, attachment, options)
  104. link_to text, url, html_options
  105. end
  106. # Generates a link to a SCM revision
  107. # Options:
  108. # * :text - Link text (default to the formatted revision)
  109. def link_to_revision(revision, repository, options={})
  110. if repository.is_a?(Project)
  111. repository = repository.repository
  112. end
  113. text = options.delete(:text) || format_revision(revision)
  114. rev = revision.respond_to?(:identifier) ? revision.identifier : revision
  115. link_to(
  116. h(text),
  117. {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
  118. :title => l(:label_revision_id, format_revision(revision)),
  119. :accesskey => options[:accesskey]
  120. )
  121. end
  122. # Generates a link to a message
  123. def link_to_message(message, options={}, html_options = nil)
  124. link_to(
  125. message.subject.truncate(60),
  126. board_message_url(message.board_id, message.parent_id || message.id, {
  127. :r => (message.parent_id && message.id),
  128. :anchor => (message.parent_id ? "message-#{message.id}" : nil),
  129. :only_path => true
  130. }.merge(options)),
  131. html_options
  132. )
  133. end
  134. # Generates a link to a project if active
  135. # Examples:
  136. #
  137. # link_to_project(project) # => link to the specified project overview
  138. # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
  139. # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
  140. #
  141. def link_to_project(project, options={}, html_options = nil)
  142. if project.archived?
  143. h(project.name)
  144. else
  145. link_to project.name,
  146. project_url(project, {:only_path => true}.merge(options)),
  147. html_options
  148. end
  149. end
  150. # Generates a link to a project settings if active
  151. def link_to_project_settings(project, options={}, html_options=nil)
  152. if project.active?
  153. link_to project.name, settings_project_path(project, options), html_options
  154. elsif project.archived?
  155. h(project.name)
  156. else
  157. link_to project.name, project_path(project, options), html_options
  158. end
  159. end
  160. # Generates a link to a version
  161. def link_to_version(version, options = {})
  162. return '' unless version && version.is_a?(Version)
  163. options = {:title => format_date(version.effective_date)}.merge(options)
  164. link_to_if version.visible?, format_version_name(version), version_path(version), options
  165. end
  166. RECORD_LINK = {
  167. 'CustomValue' => -> (custom_value) { link_to_record(custom_value.customized) },
  168. 'Document' => -> (document) { link_to(document.title, document_path(document)) },
  169. 'Group' => -> (group) { link_to(group.name, group_path(group)) },
  170. 'Issue' => -> (issue) { link_to_issue(issue, :subject => false) },
  171. 'Message' => -> (message) { link_to_message(message) },
  172. 'News' => -> (news) { link_to(news.title, news_path(news)) },
  173. 'Project' => -> (project) { link_to_project(project) },
  174. 'User' => -> (user) { link_to_user(user) },
  175. 'Version' => -> (version) { link_to_version(version) },
  176. 'WikiPage' => -> (wiki_page) { link_to(wiki_page.pretty_title, project_wiki_page_path(wiki_page.project, wiki_page.title)) }
  177. }
  178. def link_to_record(record)
  179. if link = RECORD_LINK[record.class.name]
  180. self.instance_exec(record, &link)
  181. end
  182. end
  183. ATTACHMENT_CONTAINER_LINK = {
  184. # Custom list, since project/version attachments are listed in the files
  185. # view and not in the project/milestone view
  186. 'Project' => -> (project) { link_to(l(:project_module_files), project_files_path(project)) },
  187. 'Version' => -> (version) { link_to(l(:project_module_files), project_files_path(version.project)) },
  188. }
  189. def link_to_attachment_container(attachment_container)
  190. if link = ATTACHMENT_CONTAINER_LINK[attachment_container.class.name] ||
  191. RECORD_LINK[attachment_container.class.name]
  192. self.instance_exec(attachment_container, &link)
  193. end
  194. end
  195. # Helper that formats object for html or text rendering
  196. def format_object(object, html=true, &block)
  197. if block_given?
  198. object = yield object
  199. end
  200. case object.class.name
  201. when 'Array'
  202. formatted_objects = object.map {|o| format_object(o, html)}
  203. html ? safe_join(formatted_objects, ', ') : formatted_objects.join(', ')
  204. when 'Time'
  205. format_time(object)
  206. when 'Date'
  207. format_date(object)
  208. when 'Fixnum'
  209. object.to_s
  210. when 'Float'
  211. sprintf "%.2f", object
  212. when 'User'
  213. html ? link_to_user(object) : object.to_s
  214. when 'Project'
  215. html ? link_to_project(object) : object.to_s
  216. when 'Version'
  217. html ? link_to_version(object) : object.to_s
  218. when 'TrueClass'
  219. l(:general_text_Yes)
  220. when 'FalseClass'
  221. l(:general_text_No)
  222. when 'Issue'
  223. object.visible? && html ? link_to_issue(object) : "##{object.id}"
  224. when 'Attachment'
  225. html ? link_to_attachment(object) : object.filename
  226. when 'CustomValue', 'CustomFieldValue'
  227. if object.custom_field
  228. f = object.custom_field.format.formatted_custom_value(self, object, html)
  229. if f.nil? || f.is_a?(String)
  230. f
  231. else
  232. format_object(f, html, &block)
  233. end
  234. else
  235. object.value.to_s
  236. end
  237. else
  238. html ? h(object) : object.to_s
  239. end
  240. end
  241. def wiki_page_path(page, options={})
  242. url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  243. end
  244. def thumbnail_tag(attachment)
  245. thumbnail_size = Setting.thumbnails_size.to_i
  246. link_to(
  247. image_tag(
  248. thumbnail_path(attachment),
  249. :srcset => "#{thumbnail_path(attachment, :size => thumbnail_size * 2)} 2x",
  250. :style => "max-width: #{thumbnail_size}px; max-height: #{thumbnail_size}px;"
  251. ),
  252. attachment_path(
  253. attachment
  254. ),
  255. :title => attachment.filename
  256. )
  257. end
  258. def toggle_link(name, id, options={})
  259. onclick = "$('##{id}').toggle(); "
  260. onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
  261. onclick << "$(window).scrollTop($('##{options[:focus]}').position().top); " if options[:scroll]
  262. onclick << "return false;"
  263. link_to(name, "#", :onclick => onclick)
  264. end
  265. # Used to format item titles on the activity view
  266. def format_activity_title(text)
  267. text
  268. end
  269. def format_activity_day(date)
  270. date == User.current.today ? l(:label_today).titleize : format_date(date)
  271. end
  272. def format_activity_description(text)
  273. h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
  274. ).gsub(/[\r\n]+/, "<br />").html_safe
  275. end
  276. def format_version_name(version)
  277. if version.project == @project
  278. h(version)
  279. else
  280. h("#{version.project} - #{version}")
  281. end
  282. end
  283. def format_changeset_comments(changeset, options={})
  284. method = options[:short] ? :short_comments : :comments
  285. textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
  286. end
  287. def due_date_distance_in_words(date)
  288. if date
  289. l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
  290. end
  291. end
  292. # Renders a tree of projects as a nested set of unordered lists
  293. # The given collection may be a subset of the whole project tree
  294. # (eg. some intermediate nodes are private and can not be seen)
  295. def render_project_nested_lists(projects, &block)
  296. s = ''
  297. if projects.any?
  298. ancestors = []
  299. original_project = @project
  300. projects.sort_by(&:lft).each do |project|
  301. # set the project environment to please macros.
  302. @project = project
  303. if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
  304. s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
  305. else
  306. ancestors.pop
  307. s << "</li>"
  308. while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
  309. ancestors.pop
  310. s << "</ul></li>\n"
  311. end
  312. end
  313. classes = (ancestors.empty? ? 'root' : 'child')
  314. s << "<li class='#{classes}'><div class='#{classes}'>"
  315. s << h(block_given? ? capture(project, &block) : project.name)
  316. s << "</div>\n"
  317. ancestors << project
  318. end
  319. s << ("</li></ul>\n" * ancestors.size)
  320. @project = original_project
  321. end
  322. s.html_safe
  323. end
  324. def render_page_hierarchy(pages, node=nil, options={})
  325. content = ''
  326. if pages[node]
  327. content << "<ul class=\"pages-hierarchy\">\n"
  328. pages[node].each do |page|
  329. content << "<li>"
  330. content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
  331. :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
  332. content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
  333. content << "</li>\n"
  334. end
  335. content << "</ul>\n"
  336. end
  337. content.html_safe
  338. end
  339. # Renders flash messages
  340. def render_flash_messages
  341. s = ''
  342. flash.each do |k,v|
  343. s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
  344. end
  345. s.html_safe
  346. end
  347. # Renders tabs and their content
  348. def render_tabs(tabs, selected=params[:tab])
  349. if tabs.any?
  350. unless tabs.detect {|tab| tab[:name] == selected}
  351. selected = nil
  352. end
  353. selected ||= tabs.first[:name]
  354. render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
  355. else
  356. content_tag 'p', l(:label_no_data), :class => "nodata"
  357. end
  358. end
  359. # Returns the default scope for the quick search form
  360. # Could be 'all', 'my_projects', 'subprojects' or nil (current project)
  361. def default_search_project_scope
  362. if @project && !@project.leaf?
  363. 'subprojects'
  364. end
  365. end
  366. # Returns an array of projects that are displayed in the quick-jump box
  367. def projects_for_jump_box(user=User.current)
  368. if user.logged?
  369. user.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
  370. else
  371. []
  372. end
  373. end
  374. def render_projects_for_jump_box(projects, selected=nil)
  375. jump = params[:jump].presence || current_menu_item
  376. s = ''.html_safe
  377. project_tree(projects) do |project, level|
  378. padding = level * 16
  379. text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
  380. s << link_to(text, project_path(project, :jump => jump), :title => project.name, :class => (project == selected ? 'selected' : nil))
  381. end
  382. s
  383. end
  384. # Renders the project quick-jump box
  385. def render_project_jump_box
  386. projects = projects_for_jump_box(User.current)
  387. if @project && @project.persisted?
  388. text = @project.name_was
  389. end
  390. text ||= l(:label_jump_to_a_project)
  391. url = autocomplete_projects_path(:format => 'js', :jump => current_menu_item)
  392. trigger = content_tag('span', text, :class => 'drdn-trigger')
  393. q = text_field_tag('q', '', :id => 'projects-quick-search', :class => 'autocomplete', :data => {:automcomplete_url => url}, :autocomplete => 'off')
  394. all = link_to(l(:label_project_all), projects_path(:jump => current_menu_item), :class => (@project.nil? && controller.class.main_menu ? 'selected' : nil))
  395. content = content_tag('div',
  396. content_tag('div', q, :class => 'quick-search') +
  397. content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items projects selection') +
  398. content_tag('div', all, :class => 'drdn-items all-projects selection'),
  399. :class => 'drdn-content'
  400. )
  401. content_tag('div', trigger + content, :id => "project-jump", :class => "drdn")
  402. end
  403. def project_tree_options_for_select(projects, options = {})
  404. s = ''.html_safe
  405. if blank_text = options[:include_blank]
  406. if blank_text == true
  407. blank_text = '&nbsp;'.html_safe
  408. end
  409. s << content_tag('option', blank_text, :value => '')
  410. end
  411. project_tree(projects) do |project, level|
  412. name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
  413. tag_options = {:value => project.id}
  414. if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
  415. tag_options[:selected] = 'selected'
  416. else
  417. tag_options[:selected] = nil
  418. end
  419. tag_options.merge!(yield(project)) if block_given?
  420. s << content_tag('option', name_prefix + h(project), tag_options)
  421. end
  422. s.html_safe
  423. end
  424. # Yields the given block for each project with its level in the tree
  425. #
  426. # Wrapper for Project#project_tree
  427. def project_tree(projects, options={}, &block)
  428. Project.project_tree(projects, options, &block)
  429. end
  430. def principals_check_box_tags(name, principals)
  431. s = ''
  432. principals.each do |principal|
  433. s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
  434. end
  435. s.html_safe
  436. end
  437. # Returns a string for users/groups option tags
  438. def principals_options_for_select(collection, selected=nil)
  439. s = ''
  440. if collection.include?(User.current)
  441. s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
  442. end
  443. groups = ''
  444. collection.sort.each do |element|
  445. selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
  446. (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
  447. end
  448. unless groups.empty?
  449. s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
  450. end
  451. s.html_safe
  452. end
  453. def option_tag(name, text, value, selected=nil, options={})
  454. content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  455. end
  456. def truncate_single_line_raw(string, length)
  457. string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
  458. end
  459. # Truncates at line break after 250 characters or options[:length]
  460. def truncate_lines(string, options={})
  461. length = options[:length] || 250
  462. if string.to_s =~ /\A(.{#{length}}.*?)$/m
  463. "#{$1}..."
  464. else
  465. string
  466. end
  467. end
  468. def anchor(text)
  469. text.to_s.tr(' ', '_')
  470. end
  471. def html_hours(text)
  472. text.gsub(%r{(\d+)([\.:])(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">\2\3</span>').html_safe
  473. end
  474. def authoring(created, author, options={})
  475. l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
  476. end
  477. def time_tag(time)
  478. text = distance_of_time_in_words(Time.now, time)
  479. if @project
  480. link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
  481. else
  482. content_tag('abbr', text, :title => format_time(time))
  483. end
  484. end
  485. def syntax_highlight_lines(name, content)
  486. syntax_highlight(name, content).each_line.to_a
  487. end
  488. def syntax_highlight(name, content)
  489. Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
  490. end
  491. def to_path_param(path)
  492. str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
  493. str.blank? ? nil : str
  494. end
  495. def reorder_handle(object, options={})
  496. data = {
  497. :reorder_url => options[:url] || url_for(object),
  498. :reorder_param => options[:param] || object.class.name.underscore
  499. }
  500. content_tag('span', '',
  501. :class => "sort-handle",
  502. :data => data,
  503. :title => l(:button_sort))
  504. end
  505. def breadcrumb(*args)
  506. elements = args.flatten
  507. elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
  508. end
  509. def other_formats_links(&block)
  510. concat('<p class="other-formats">'.html_safe + l(:label_export_to))
  511. yield Redmine::Views::OtherFormatsBuilder.new(self)
  512. concat('</p>'.html_safe)
  513. end
  514. def page_header_title
  515. if @project.nil? || @project.new_record?
  516. h(Setting.app_title)
  517. else
  518. b = []
  519. ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
  520. if ancestors.any?
  521. root = ancestors.shift
  522. b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
  523. if ancestors.size > 2
  524. b << "\xe2\x80\xa6"
  525. ancestors = ancestors[-2, 2]
  526. end
  527. b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
  528. end
  529. b << content_tag(:span, h(@project), class: 'current-project')
  530. if b.size > 1
  531. separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
  532. path = safe_join(b[0..-2], separator) + separator
  533. b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
  534. end
  535. safe_join b
  536. end
  537. end
  538. # Returns a h2 tag and sets the html title with the given arguments
  539. def title(*args)
  540. strings = args.map do |arg|
  541. if arg.is_a?(Array) && arg.size >= 2
  542. link_to(*arg)
  543. else
  544. h(arg.to_s)
  545. end
  546. end
  547. html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
  548. content_tag('h2', strings.join(' &#187; ').html_safe)
  549. end
  550. # Sets the html title
  551. # Returns the html title when called without arguments
  552. # Current project name and app_title and automatically appended
  553. # Exemples:
  554. # html_title 'Foo', 'Bar'
  555. # html_title # => 'Foo - Bar - My Project - Redmine'
  556. def html_title(*args)
  557. if args.empty?
  558. title = @html_title || []
  559. title << @project.name if @project
  560. title << Setting.app_title unless Setting.app_title == title.last
  561. title.reject(&:blank?).join(' - ')
  562. else
  563. @html_title ||= []
  564. @html_title += args
  565. end
  566. end
  567. def actions_dropdown(&block)
  568. content = capture(&block)
  569. if content.present?
  570. trigger = content_tag('span', l(:button_actions), :class => 'icon-only icon-actions', :title => l(:button_actions))
  571. trigger = content_tag('span', trigger, :class => 'drdn-trigger')
  572. content = content_tag('div', content, :class => 'drdn-items')
  573. content = content_tag('div', content, :class => 'drdn-content')
  574. content_tag('span', trigger + content, :class => 'drdn')
  575. end
  576. end
  577. # Returns the theme, controller name, and action as css classes for the
  578. # HTML body.
  579. def body_css_classes
  580. css = []
  581. if theme = Redmine::Themes.theme(Setting.ui_theme)
  582. css << 'theme-' + theme.name
  583. end
  584. css << 'project-' + @project.identifier if @project && @project.identifier.present?
  585. css << 'has-main-menu' if display_main_menu?(@project)
  586. css << 'controller-' + controller_name
  587. css << 'action-' + action_name
  588. css << 'avatars-' + (Setting.gravatar_enabled? ? 'on' : 'off')
  589. if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
  590. css << "textarea-#{User.current.pref.textarea_font}"
  591. end
  592. css.join(' ')
  593. end
  594. def accesskey(s)
  595. @used_accesskeys ||= []
  596. key = Redmine::AccessKeys.key_for(s)
  597. return nil if @used_accesskeys.include?(key)
  598. @used_accesskeys << key
  599. key
  600. end
  601. # Formats text according to system settings.
  602. # 2 ways to call this method:
  603. # * with a String: textilizable(text, options)
  604. # * with an object and one of its attribute: textilizable(issue, :description, options)
  605. def textilizable(*args)
  606. options = args.last.is_a?(Hash) ? args.pop : {}
  607. case args.size
  608. when 1
  609. obj = options[:object]
  610. text = args.shift
  611. when 2
  612. obj = args.shift
  613. attr = args.shift
  614. text = obj.send(attr).to_s
  615. else
  616. raise ArgumentError, 'invalid arguments to textilizable'
  617. end
  618. return '' if text.blank?
  619. project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
  620. @only_path = only_path = options.delete(:only_path) == false ? false : true
  621. text = text.dup
  622. macros = catch_macros(text)
  623. if options[:formatting] == false
  624. text = h(text)
  625. else
  626. formatting = Setting.text_formatting
  627. text = Redmine::WikiFormatting.to_html(formatting, text, :object => obj, :attribute => attr)
  628. end
  629. @parsed_headings = []
  630. @heading_anchors = {}
  631. @current_section = 0 if options[:edit_section_links]
  632. parse_sections(text, project, obj, attr, only_path, options)
  633. text = parse_non_pre_blocks(text, obj, macros) do |text|
  634. [:parse_inline_attachments, :parse_hires_images, :parse_wiki_links, :parse_redmine_links].each do |method_name|
  635. send method_name, text, project, obj, attr, only_path, options
  636. end
  637. end
  638. parse_headings(text, project, obj, attr, only_path, options)
  639. if @parsed_headings.any?
  640. replace_toc(text, @parsed_headings)
  641. end
  642. text.html_safe
  643. end
  644. def parse_non_pre_blocks(text, obj, macros)
  645. s = StringScanner.new(text)
  646. tags = []
  647. parsed = ''
  648. while !s.eos?
  649. s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
  650. text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
  651. if tags.empty?
  652. yield text
  653. inject_macros(text, obj, macros) if macros.any?
  654. else
  655. inject_macros(text, obj, macros, false) if macros.any?
  656. end
  657. parsed << text
  658. if tag
  659. if closing
  660. if tags.last && tags.last.casecmp(tag) == 0
  661. tags.pop
  662. end
  663. else
  664. tags << tag.downcase
  665. end
  666. parsed << full_tag
  667. end
  668. end
  669. # Close any non closing tags
  670. while tag = tags.pop
  671. parsed << "</#{tag}>"
  672. end
  673. parsed
  674. end
  675. # add srcset attribute to img tags if filename includes @2x, @3x, etc.
  676. # to support hires displays
  677. def parse_hires_images(text, project, obj, attr, only_path, options)
  678. text.gsub!(/src="([^"]+@(\dx)\.(bmp|gif|jpg|jpe|jpeg|png))"/i) do |m|
  679. filename, dpr = $1, $2
  680. m + " srcset=\"#{filename} #{dpr}\""
  681. end
  682. end
  683. def parse_inline_attachments(text, project, obj, attr, only_path, options)
  684. return if options[:inline_attachments] == false
  685. # when using an image link, try to use an attachment, if possible
  686. attachments = options[:attachments] || []
  687. attachments += obj.attachments if obj.respond_to?(:attachments)
  688. if attachments.present?
  689. text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
  690. filename, ext, alt, alttext = $1.downcase, $2, $3, $4
  691. # search for the picture in attachments
  692. if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
  693. image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
  694. desc = found.description.to_s.gsub('"', '')
  695. if !desc.blank? && alttext.blank?
  696. alt = " title=\"#{desc}\" alt=\"#{desc}\""
  697. end
  698. "src=\"#{image_url}\"#{alt}"
  699. else
  700. m
  701. end
  702. end
  703. end
  704. end
  705. # Wiki links
  706. #
  707. # Examples:
  708. # [[mypage]]
  709. # [[mypage|mytext]]
  710. # wiki links can refer other project wikis, using project name or identifier:
  711. # [[project:]] -> wiki starting page
  712. # [[project:|mytext]]
  713. # [[project:mypage]]
  714. # [[project:mypage|mytext]]
  715. def parse_wiki_links(text, project, obj, attr, only_path, options)
  716. text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
  717. link_project = project
  718. esc, all, page, title = $1, $2, $3, $5
  719. if esc.nil?
  720. page = CGI.unescapeHTML(page)
  721. if page =~ /^\#(.+)$/
  722. anchor = sanitize_anchor_name($1)
  723. url = "##{anchor}"
  724. next link_to(title.present? ? title.html_safe : h(page), url, :class => 'wiki-page')
  725. end
  726. if page =~ /^([^\:]+)\:(.*)$/
  727. identifier, page = $1, $2
  728. link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
  729. title ||= identifier if page.blank?
  730. end
  731. if link_project && link_project.wiki && User.current.allowed_to?(:view_wiki_pages, link_project)
  732. # extract anchor
  733. anchor = nil
  734. if page =~ /^(.+?)\#(.+)$/
  735. page, anchor = $1, $2
  736. end
  737. anchor = sanitize_anchor_name(anchor) if anchor.present?
  738. # check if page exists
  739. wiki_page = link_project.wiki.find_page(page)
  740. url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
  741. "##{anchor}"
  742. else
  743. case options[:wiki_links]
  744. when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
  745. when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
  746. else
  747. wiki_page_id = page.present? ? Wiki.titleize(page) : nil
  748. parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
  749. url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
  750. :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
  751. end
  752. end
  753. link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
  754. else
  755. # project or wiki doesn't exist
  756. all
  757. end
  758. else
  759. all
  760. end
  761. end
  762. end
  763. # Redmine links
  764. #
  765. # Examples:
  766. # Issues:
  767. # #52 -> Link to issue #52
  768. # ##52 -> Link to issue #52, including the issue's subject
  769. # Changesets:
  770. # r52 -> Link to revision 52
  771. # commit:a85130f -> Link to scmid starting with a85130f
  772. # Documents:
  773. # document#17 -> Link to document with id 17
  774. # document:Greetings -> Link to the document with title "Greetings"
  775. # document:"Some document" -> Link to the document with title "Some document"
  776. # Versions:
  777. # version#3 -> Link to version with id 3
  778. # version:1.0.0 -> Link to version named "1.0.0"
  779. # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
  780. # Attachments:
  781. # attachment:file.zip -> Link to the attachment of the current object named file.zip
  782. # Source files:
  783. # source:some/file -> Link to the file located at /some/file in the project's repository
  784. # source:some/file@52 -> Link to the file's revision 52
  785. # source:some/file#L120 -> Link to line 120 of the file
  786. # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
  787. # export:some/file -> Force the download of the file
  788. # Forums:
  789. # forum#1 -> Link to forum with id 1
  790. # forum:Support -> Link to forum named "Support"
  791. # forum:"Technical Support" -> Link to forum named "Technical Support"
  792. # Forum messages:
  793. # message#1218 -> Link to message with id 1218
  794. # Projects:
  795. # project:someproject -> Link to project named "someproject"
  796. # project#3 -> Link to project with id 3
  797. # News:
  798. # news#2 -> Link to news item with id 1
  799. # news:Greetings -> Link to news item named "Greetings"
  800. # news:"First Release" -> Link to news item named "First Release"
  801. # Users:
  802. # user:jsmith -> Link to user with login jsmith
  803. # @jsmith -> Link to user with login jsmith
  804. # user#2 -> Link to user with id 2
  805. #
  806. # Links can refer other objects from other projects, using project identifier:
  807. # identifier:r52
  808. # identifier:document:"Some document"
  809. # identifier:version:1.0.0
  810. # identifier:source:some/file
  811. def parse_redmine_links(text, default_project, obj, attr, only_path, options)
  812. text.gsub!(LINKS_RE) do |_|
  813. tag_content = $~[:tag_content]
  814. leading = $~[:leading]
  815. esc = $~[:esc]
  816. project_prefix = $~[:project_prefix]
  817. project_identifier = $~[:project_identifier]
  818. prefix = $~[:prefix]
  819. repo_prefix = $~[:repo_prefix]
  820. repo_identifier = $~[:repo_identifier]
  821. sep = $~[:sep1] || $~[:sep2] || $~[:sep3] || $~[:sep4]
  822. identifier = $~[:identifier1] || $~[:identifier2] || $~[:identifier3]
  823. comment_suffix = $~[:comment_suffix]
  824. comment_id = $~[:comment_id]
  825. if tag_content
  826. $&
  827. else
  828. link = nil
  829. project = default_project
  830. if project_identifier
  831. project = Project.visible.find_by_identifier(project_identifier)
  832. end
  833. if esc.nil?
  834. if prefix.nil? && sep == 'r'
  835. if project
  836. repository = nil
  837. if repo_identifier
  838. repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
  839. else
  840. repository = project.repository
  841. end
  842. # project.changesets.visible raises an SQL error because of a double join on repositories
  843. if repository &&
  844. (changeset = Changeset.visible.
  845. find_by_repository_id_and_revision(repository.id, identifier))
  846. link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
  847. {:only_path => only_path, :controller => 'repositories',
  848. :action => 'revision', :id => project,
  849. :repository_id => repository.identifier_param,
  850. :rev => changeset.revision},
  851. :class => 'changeset',
  852. :title => truncate_single_line_raw(changeset.comments, 100))
  853. end
  854. end
  855. elsif sep == '#' || sep == '##'
  856. oid = identifier.to_i
  857. case prefix
  858. when nil
  859. if oid.to_s == identifier &&
  860. issue = Issue.visible.find_by_id(oid)
  861. anchor = comment_id ? "note-#{comment_id}" : nil
  862. url = issue_url(issue, :only_path => only_path, :anchor => anchor)
  863. link = if sep == '##'
  864. link_to("#{issue.tracker.name} ##{oid}#{comment_suffix}",
  865. url,
  866. :class => issue.css_classes,
  867. :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})") + ": #{issue.subject}"
  868. else
  869. link_to("##{oid}#{comment_suffix}",
  870. url,
  871. :class => issue.css_classes,
  872. :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
  873. end
  874. end
  875. when 'document'
  876. if document = Document.visible.find_by_id(oid)
  877. link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
  878. end
  879. when 'version'
  880. if version = Version.visible.find_by_id(oid)
  881. link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
  882. end
  883. when 'message'
  884. if message = Message.visible.find_by_id(oid)
  885. link = link_to_message(message, {:only_path => only_path}, :class => 'message')
  886. end
  887. when 'forum'
  888. if board = Board.visible.find_by_id(oid)
  889. link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
  890. end
  891. when 'news'
  892. if news = News.visible.find_by_id(oid)
  893. link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
  894. end
  895. when 'project'
  896. if p = Project.visible.find_by_id(oid)
  897. link = link_to_project(p, {:only_path => only_path}, :class => 'project')
  898. end
  899. when 'user'
  900. u = User.visible.find_by(:id => oid, :type => 'User')
  901. link = link_to_user(u, :only_path => only_path) if u
  902. end
  903. elsif sep == ':'
  904. name = remove_double_quotes(identifier)
  905. case prefix
  906. when 'document'
  907. if project && document = project.documents.visible.find_by_title(name)
  908. link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
  909. end
  910. when 'version'
  911. if project && version = project.versions.visible.find_by_name(name)
  912. link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
  913. end
  914. when 'forum'
  915. if project && board = project.boards.visible.find_by_name(name)
  916. link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
  917. end
  918. when 'news'
  919. if project && news = project.news.visible.find_by_title(name)
  920. link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
  921. end
  922. when 'commit', 'source', 'export'
  923. if project
  924. repository = nil
  925. if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
  926. repo_prefix, repo_identifier, name = $1, $2, $3
  927. repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
  928. else
  929. repository = project.repository
  930. end
  931. if prefix == 'commit'
  932. if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
  933. 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},
  934. :class => 'changeset',
  935. :title => truncate_single_line_raw(changeset.comments, 100)
  936. end
  937. else
  938. if repository && User.current.allowed_to?(:browse_repository, project)
  939. name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
  940. path, rev, anchor = $1, $3, $5
  941. link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
  942. :path => to_path_param(path),
  943. :rev => rev,
  944. :anchor => anchor},
  945. :class => (prefix == 'export' ? 'source download' : 'source')
  946. end
  947. end
  948. repo_prefix = nil
  949. end
  950. when 'attachment'
  951. attachments = options[:attachments] || []
  952. attachments += obj.attachments if obj.respond_to?(:attachments)
  953. if attachments && attachment = Attachment.latest_attach(attachments, name)
  954. link = link_to_attachment(attachment, :only_path => only_path, :class => 'attachment')
  955. end
  956. when 'project'
  957. if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
  958. link = link_to_project(p, {:only_path => only_path}, :class => 'project')
  959. end
  960. when 'user'
  961. u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
  962. link = link_to_user(u, :only_path => only_path) if u
  963. end
  964. elsif sep == "@"
  965. name = remove_double_quotes(identifier)
  966. u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
  967. link = link_to_user(u, :only_path => only_path) if u
  968. end
  969. end
  970. (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
  971. end
  972. end
  973. end
  974. LINKS_RE =
  975. %r{
  976. <a( [^>]+?)?>(?<tag_content>.*?)</a>|
  977. (?<leading>[\s\(,\-\[\>]|^)
  978. (?<esc>!)?
  979. (?<project_prefix>(?<project_identifier>[a-z0-9\-_]+):)?
  980. (?<prefix>attachment|document|version|forum|news|message|project|commit|source|export|user)?
  981. (
  982. (
  983. (?<sep1>\#\#?)|
  984. (
  985. (?<repo_prefix>(?<repo_identifier>[a-z0-9\-_]+)\|)?
  986. (?<sep2>r)
  987. )
  988. )
  989. (
  990. (?<identifier1>\d+)
  991. (?<comment_suffix>
  992. (\#note)?
  993. -(?<comment_id>\d+)
  994. )?
  995. )|
  996. (
  997. (?<sep3>:)
  998. (?<identifier2>[^"\s<>][^\s<>]*?|"[^"]+?")
  999. )|
  1000. (
  1001. (?<sep4>@)
  1002. (?<identifier3>[A-Za-z0-9_\-@\.]*)
  1003. )
  1004. )
  1005. (?=
  1006. (?=[[:punct:]][^A-Za-z0-9_/])|
  1007. ,|
  1008. \s|
  1009. \]|
  1010. <|
  1011. $)
  1012. }x
  1013. HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
  1014. def parse_sections(text, project, obj, attr, only_path, options)
  1015. return unless options[:edit_section_links]
  1016. text.gsub!(HEADING_RE) do
  1017. heading, level = $1, $2
  1018. @current_section += 1
  1019. if @current_section > 1
  1020. content_tag('div',
  1021. link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
  1022. :class => 'icon-only icon-edit'),
  1023. :class => "contextual heading-#{level}",
  1024. :title => l(:button_edit_section),
  1025. :id => "section-#{@current_section}") + heading.html_safe
  1026. else
  1027. heading
  1028. end
  1029. end
  1030. end
  1031. # Headings and TOC
  1032. # Adds ids and links to headings unless options[:headings] is set to false
  1033. def parse_headings(text, project, obj, attr, only_path, options)
  1034. return if options[:headings] == false
  1035. text.gsub!(HEADING_RE) do
  1036. level, attrs, content = $2.to_i, $3, $4
  1037. item = strip_tags(content).strip
  1038. anchor = sanitize_anchor_name(item)
  1039. # used for single-file wiki export
  1040. anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
  1041. @heading_anchors[anchor] ||= 0
  1042. idx = (@heading_anchors[anchor] += 1)
  1043. if idx > 1
  1044. anchor = "#{anchor}-#{idx}"
  1045. end
  1046. @parsed_headings << [level, anchor, item]
  1047. "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
  1048. end
  1049. end
  1050. MACROS_RE = /(
  1051. (!)? # escaping
  1052. (
  1053. \{\{ # opening tag
  1054. ([\w]+) # macro name
  1055. (\(([^\n\r]*?)\))? # optional arguments
  1056. ([\n\r].*?[\n\r])? # optional block of text
  1057. \}\} # closing tag
  1058. )
  1059. )/mx unless const_defined?(:MACROS_RE)
  1060. MACRO_SUB_RE = /(
  1061. \{\{
  1062. macro\((\d+)\)
  1063. \}\}
  1064. )/x unless const_defined?(:MACRO_SUB_RE)
  1065. # Extracts macros from text
  1066. def catch_macros(text)
  1067. macros = {}
  1068. text.gsub!(MACROS_RE) do
  1069. all, macro = $1, $4.downcase
  1070. if macro_exists?(macro) || all =~ MACRO_SUB_RE
  1071. index = macros.size
  1072. macros[index] = all
  1073. "{{macro(#{index})}}"
  1074. else
  1075. all
  1076. end
  1077. end
  1078. macros
  1079. end
  1080. # Executes and replaces macros in text
  1081. def inject_macros(text, obj, macros, execute=true)
  1082. text.gsub!(MACRO_SUB_RE) do
  1083. all, index = $1, $2.to_i
  1084. orig = macros.delete(index)
  1085. if execute && orig && orig =~ MACROS_RE
  1086. esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
  1087. if esc.nil?
  1088. h(exec_macro(macro, obj, args, block) || all)
  1089. else
  1090. h(all)
  1091. end
  1092. elsif orig
  1093. h(orig)
  1094. else
  1095. h(all)
  1096. end
  1097. end
  1098. end
  1099. TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
  1100. # Renders the TOC with given headings
  1101. def replace_toc(text, headings)
  1102. text.gsub!(TOC_RE) do
  1103. left_align, right_align = $2, $3
  1104. # Keep only the 4 first levels
  1105. headings = headings.select{|level, anchor, item| level <= 4}
  1106. if headings.empty?
  1107. ''
  1108. else
  1109. div_class = 'toc'
  1110. div_class << ' right' if right_align
  1111. div_class << ' left' if left_align
  1112. out = "<ul class=\"#{div_class}\"><li><strong>#{l :label_table_of_contents}</strong></li><li>"
  1113. root = headings.map(&:first).min
  1114. current = root
  1115. started = false
  1116. headings.each do |level, anchor, item|
  1117. if level > current
  1118. out << '<ul><li>' * (level - current)
  1119. elsif level < current
  1120. out << "</li></ul>\n" * (current - level) + "</li><li>"
  1121. elsif started
  1122. out << '</li><li>'
  1123. end
  1124. out << "<a href=\"##{anchor}\">#{item}</a>"
  1125. current = level
  1126. started = true
  1127. end
  1128. out << '</li></ul>' * (current - root)
  1129. out << '</li></ul>'
  1130. end
  1131. end
  1132. end
  1133. # Same as Rails' simple_format helper without using paragraphs
  1134. def simple_format_without_paragraph(text)
  1135. text.to_s.
  1136. gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
  1137. gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
  1138. gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
  1139. html_safe
  1140. end
  1141. def lang_options_for_select(blank=true)
  1142. (blank ? [["(auto)", ""]] : []) + languages_options
  1143. end
  1144. def labelled_form_for(*args, &proc)
  1145. args << {} unless args.last.is_a?(Hash)
  1146. options = args.last
  1147. if args.first.is_a?(Symbol)
  1148. options.merge!(:as => args.shift)
  1149. end
  1150. options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
  1151. form_for(*args, &proc)
  1152. end
  1153. def labelled_fields_for(*args, &proc)
  1154. args << {} unless args.last.is_a?(Hash)
  1155. options = args.last
  1156. options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
  1157. fields_for(*args, &proc)
  1158. end
  1159. # Render the error messages for the given objects
  1160. def error_messages_for(*objects)
  1161. objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
  1162. errors = objects.map {|o| o.errors.full_messages}.flatten
  1163. render_error_messages(errors)
  1164. end
  1165. # Renders a list of error messages
  1166. def render_error_messages(errors)
  1167. html = ""
  1168. if errors.present?
  1169. html << "<div id='errorExplanation'><ul>\n"
  1170. errors.each do |error|
  1171. html << "<li>#{h error}</li>\n"
  1172. end
  1173. html << "</ul></div>\n"
  1174. end
  1175. html.html_safe
  1176. end
  1177. def delete_link(url, options={})
  1178. options = {
  1179. :method => :delete,
  1180. :data => {:confirm => l(:text_are_you_sure)},
  1181. :class => 'icon icon-del'
  1182. }.merge(options)
  1183. link_to l(:button_delete), url, options
  1184. end
  1185. def link_to_function(name, function, html_options={})
  1186. content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
  1187. end
  1188. def link_to_context_menu
  1189. link_to l(:button_actions), '#', title: l(:button_actions), class: 'icon-only icon-actions js-contextmenu'
  1190. end
  1191. # Helper to render JSON in views
  1192. def raw_json(arg)
  1193. arg.to_json.to_s.gsub('/', '\/').html_safe
  1194. end
  1195. def back_url
  1196. url = params[:back_url]
  1197. if url.nil? && referer = request.env['HTTP_REFERER']
  1198. url = CGI.unescape(referer.to_s)
  1199. # URLs that contains the utf8=[checkmark] parameter added by Rails are
  1200. # parsed as invalid by URI.parse so the redirect to the back URL would
  1201. # not be accepted (ApplicationController#validate_back_url would return
  1202. # false)
  1203. url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
  1204. end
  1205. url
  1206. end
  1207. def back_url_hidden_field_tag
  1208. url = back_url
  1209. hidden_field_tag('back_url', url, :id => nil) unless url.blank?
  1210. end
  1211. def cancel_button_tag(fallback_url)
  1212. url = back_url.blank? ? fallback_url : back_url
  1213. link_to l(:button_cancel), url
  1214. end
  1215. def check_all_links(form_name)
  1216. link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
  1217. " | ".html_safe +
  1218. link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
  1219. end
  1220. def toggle_checkboxes_link(selector)
  1221. link_to_function '',
  1222. "toggleCheckboxesBySelector('#{selector}')",
  1223. :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
  1224. :class => 'icon icon-checked'
  1225. end
  1226. def progress_bar(pcts, options={})
  1227. pcts = [pcts, pcts] unless pcts.is_a?(Array)
  1228. pcts = pcts.collect(&:round)
  1229. pcts[1] = pcts[1] - pcts[0]
  1230. pcts << (100 - pcts[1] - pcts[0])
  1231. titles = options[:titles].to_a
  1232. titles[0] = "#{pcts[0]}%" if titles[0].blank?
  1233. legend = options[:legend] || ''
  1234. content_tag('table',
  1235. content_tag('tr',
  1236. (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
  1237. (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
  1238. (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
  1239. ), :class => "progress progress-#{pcts[0]}").html_safe +
  1240. content_tag('p', legend, :class => 'percent').html_safe
  1241. end
  1242. def checked_image(checked=true)
  1243. if checked
  1244. @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
  1245. end
  1246. end
  1247. def context_menu
  1248. unless @context_menu_included
  1249. content_for :header_tags do
  1250. javascript_include_tag('context_menu') +
  1251. stylesheet_link_tag('context_menu')
  1252. end
  1253. if l(:direction) == 'rtl'
  1254. content_for :header_tags do
  1255. stylesheet_link_tag('context_menu_rtl')
  1256. end
  1257. end
  1258. @context_menu_included = true
  1259. end
  1260. nil
  1261. end
  1262. def calendar_for(field_id)
  1263. include_calendar_headers_tags
  1264. javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
  1265. end
  1266. def include_calendar_headers_tags
  1267. unless @calendar_headers_tags_included
  1268. tags = ''.html_safe
  1269. @calendar_headers_tags_included = true
  1270. content_for :header_tags do
  1271. start_of_week = Setting.start_of_week
  1272. start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
  1273. # Redmine uses 1..7 (monday..sunday) in settings and locales
  1274. # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
  1275. start_of_week = start_of_week.to_i % 7
  1276. tags << javascript_tag(
  1277. "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
  1278. "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
  1279. path_to_image('/images/calendar.png') +
  1280. "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
  1281. "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
  1282. "beforeShow: beforeShowDatePicker};")
  1283. jquery_locale = l('jquery.locale', :default => current_language.to_s)
  1284. unless jquery_locale == 'en'
  1285. tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
  1286. end
  1287. tags
  1288. end
  1289. end
  1290. end
  1291. # Overrides Rails' stylesheet_link_tag with themes and plugins support.
  1292. # Examples:
  1293. # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
  1294. # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
  1295. #
  1296. def stylesheet_link_tag(*sources)
  1297. options = sources.last.is_a?(Hash) ? sources.pop : {}
  1298. plugin = options.delete(:plugin)
  1299. sources = sources.map do |source|
  1300. if plugin
  1301. "/plugin_assets/#{plugin}/stylesheets/#{source}"
  1302. elsif current_theme && current_theme.stylesheets.include?(source)
  1303. current_theme.stylesheet_path(source)
  1304. else
  1305. source
  1306. end
  1307. end
  1308. super *sources, options
  1309. end
  1310. # Overrides Rails' image_tag with themes and plugins support.
  1311. # Examples:
  1312. # image_tag('image.png') # => picks image.png from the current theme or defaults
  1313. # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
  1314. #
  1315. def image_tag(source, options={})
  1316. if plugin = options.delete(:plugin)
  1317. source = "/plugin_assets/#{plugin}/images/#{source}"
  1318. elsif current_theme && current_theme.images.include?(source)
  1319. source = current_theme.image_path(source)
  1320. end
  1321. super source, options
  1322. end
  1323. # Overrides Rails' javascript_include_tag with plugins support
  1324. # Examples:
  1325. # javascript_include_tag('scripts') # => picks scripts.js from defaults
  1326. # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
  1327. #
  1328. def javascript_include_tag(*sources)
  1329. options = sources.last.is_a?(Hash) ? sources.pop : {}
  1330. if plugin = options.delete(:plugin)
  1331. sources = sources.map do |source|
  1332. if plugin
  1333. "/plugin_assets/#{plugin}/javascripts/#{source}"
  1334. else
  1335. source
  1336. end
  1337. end
  1338. end
  1339. super *sources, options
  1340. end
  1341. def sidebar_content?
  1342. content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
  1343. end
  1344. def view_layouts_base_sidebar_hook_response
  1345. @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
  1346. end
  1347. def email_delivery_enabled?
  1348. !!ActionMailer::Base.perform_deliveries
  1349. end
  1350. # Returns the avatar image tag for the given +user+ if avatars are enabled
  1351. # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
  1352. def avatar(user, options = { })
  1353. if Setting.gravatar_enabled?
  1354. options.merge!(:default => Setting.gravatar_default)
  1355. email = nil
  1356. if user.respond_to?(:mail)
  1357. email = user.mail
  1358. elsif user.to_s =~ %r{<(.+?)>}
  1359. email = $1
  1360. end
  1361. if email.present?
  1362. gravatar(email.to_s.downcase, options) rescue nil
  1363. elsif user.is_a?(AnonymousUser)
  1364. image_tag 'anonymous.png',
  1365. GravatarHelper::DEFAULT_OPTIONS
  1366. .except(:default, :rating, :ssl).merge(options)
  1367. else
  1368. nil
  1369. end
  1370. else
  1371. ''
  1372. end
  1373. end
  1374. # Returns a link to edit user's avatar if avatars are enabled
  1375. def avatar_edit_link(user, options={})
  1376. if Setting.gravatar_enabled?
  1377. url = "https://gravatar.com"
  1378. link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
  1379. end
  1380. end
  1381. def sanitize_anchor_name(anchor)
  1382. anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
  1383. end
  1384. # Returns the javascript tags that are included in the html layout head
  1385. def javascript_heads
  1386. tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-4.3.1', 'application', 'responsive')
  1387. unless User.current.pref.warn_on_leaving_unsaved == '0'
  1388. tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
  1389. end
  1390. tags
  1391. end
  1392. def favicon
  1393. "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
  1394. end
  1395. # Returns the path to the favicon
  1396. def favicon_path
  1397. icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
  1398. image_path(icon)
  1399. end
  1400. # Returns the full URL to the favicon
  1401. def favicon_url
  1402. # TODO: use #image_url introduced in Rails4
  1403. path = favicon_path
  1404. base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
  1405. base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
  1406. end
  1407. def robot_exclusion_tag
  1408. '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
  1409. end
  1410. # Returns true if arg is expected in the API response
  1411. def include_in_api_response?(arg)
  1412. unless @included_in_api_response
  1413. param = params[:include]
  1414. @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
  1415. @included_in_api_response.collect!(&:strip)
  1416. end
  1417. @included_in_api_response.include?(arg.to_s)
  1418. end
  1419. # Returns options or nil if nometa param or X-Redmine-Nometa header
  1420. # was set in the request
  1421. def api_meta(options)
  1422. if params[:nometa].present? || request.headers['X-Redmine-Nometa']
  1423. # compatibility mode for activeresource clients that raise
  1424. # an error when deserializing an array with attributes
  1425. nil
  1426. else
  1427. options
  1428. end
  1429. end
  1430. def generate_csv(&block)
  1431. decimal_separator = l(:general_csv_decimal_separator)
  1432. encoding = l(:general_csv_encoding)
  1433. end
  1434. def export_csv_encoding_select_tag
  1435. return if l(:general_csv_encoding).casecmp('UTF-8') == 0
  1436. options = [l(:general_csv_encoding), 'UTF-8']
  1437. content_tag(:p) do
  1438. concat(
  1439. content_tag(:label) do
  1440. concat l(:label_encoding) + ' '
  1441. concat select_tag('encoding', options_for_select(options, l(:general_csv_encoding)))
  1442. end
  1443. )
  1444. end
  1445. end
  1446. # Returns an array of error messages for bulk edited items (issues, time entries)
  1447. def bulk_edit_error_messages(items)
  1448. messages = {}
  1449. items.each do |item|
  1450. item.errors.full_messages.each do |message|
  1451. messages[message] ||= []
  1452. messages[message] << item
  1453. end
  1454. end
  1455. messages.map { |message, items|
  1456. "#{message}: " + items.map {|i| "##{i.id}"}.join(', ')
  1457. }
  1458. end
  1459. private
  1460. def wiki_helper
  1461. helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
  1462. extend helper
  1463. return self
  1464. end
  1465. # remove double quotes if any
  1466. def remove_double_quotes(identifier)
  1467. name = identifier.gsub(%r{^"(.*)"$}, "\\1")
  1468. return CGI.unescapeHTML(name)
  1469. end
  1470. end