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 64KB

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