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

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