-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Copy pathapplication_helper.rb
459 lines (393 loc) · 14.5 KB
/
application_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "forwardable"
require "cgi"
module ApplicationHelper
include OpenProject::TextFormatting
include OpenProject::ObjectLinking
include OpenProject::SafeParams
include I18n
include ERB::Util
include Redmine::I18n
include HookHelper
include IconsHelper
include AdditionalUrlHelpers
include OpenProject::PageHierarchyHelper
# Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action, project: @project)
User.current.allowed_in_project?({ controller:, action: }, project)
rescue Authorization::UnknownPermissionError
# TODO: Temporary fix until we find something better
false
end
# Display a link if user is authorized
#
# @param [String] name Anchor text (passed to link_to)
# @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
# @param [optional, Hash] html_options Options passed to link_to
# @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
#
# When a block is given, skip the name parameter
def link_to_if_authorized(*args, &)
name = args.shift unless block_given?
options = args.shift || {}
html_options = args.shift
parameters_for_method_reference = args
return unless authorize_for(options[:controller] || params[:controller], options[:action])
if block_given?
link_to(options, html_options, *parameters_for_method_reference, &)
else
link_to(name, options, html_options, *parameters_for_method_reference)
end
end
def required_field_name(name = "")
safe_join [name, " ", content_tag("span", "*", class: "required")]
end
def li_unless_nil(link, options = {})
content_tag(:li, link, options) if link
end
# Show a sorted linkified (if active) comma-joined list of users
def list_users(users, options = {})
users.sort.map { |u| link_to_user(u, options) }.join(", ")
end
# returns a class name based on the user's status
def user_status_class(user)
"status_" + user.status
end
def user_status_i18n(user)
t "status_#{user.status}"
end
def delete_link(url, options = {})
options = {
method: :delete,
data: { confirm: I18n.t(:text_are_you_sure) },
class: "icon icon-delete"
}.merge(options)
link_to I18n.t(:button_delete), url, options
end
def format_activity_day(date)
date == User.current.today ? I18n.t(:label_today).titleize : format_date(date)
end
def due_date_distance_in_words(date)
if date
label = date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in
I18n.t(label, value: distance_of_date_in_words(Date.today, date))
end
end
# Yields the given block for each project with its level in the tree
#
# Wrapper for Project#project_tree
def project_tree(projects, &)
Project.project_tree(projects, &)
end
def project_nested_ul(projects, &)
s = ""
if projects.any?
ancestors = []
Project.project_tree(projects) do |project, _level|
if ancestors.empty? || project.is_descendant_of?(ancestors.last)
s << "<ul>\n"
else
ancestors.pop
s << "</li>"
while ancestors.any? && !project.is_descendant_of?(ancestors.last)
ancestors.pop
s << "</ul></li>\n"
end
end
s << "<li>"
s << yield(project).to_s
ancestors << project
end
s << ("</li></ul>\n" * ancestors.size)
end
s.html_safe
end
def principals_check_box_tags(name, principals)
labeled_check_box_tags(name, principals,
title: :user_status_i18n,
class: :user_status_class)
end
def labeled_check_box_tags(name, collection, options = {})
collection.sort.map do |object|
id = name.gsub(/[\[\]]+/, "_") + object.id.to_s
object_options = options.inject({}) do |h, (k, v)|
h[k] = v.is_a?(Symbol) ? send(v, object) : v
h
end
object_options[:class] = Array(object_options[:class]) + %w(form--label-with-check-box)
content_tag :div, class: "form--field" do
label_tag(id, object, object_options) do
styled_check_box_tag(name, object.id, false, id:) + object.to_s
end
end
end.join.html_safe
end
def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)},
'<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
.html_safe
end
def authoring(created, author, options = {})
label = options[:label] || :label_added_time_by
I18n.t(label, author: link_to_user(author), age: time_tag(created)).html_safe
end
def authoring_at(created, author)
return if author.nil?
I18n.t(:"js.label_added_time_by",
author: html_escape(author.name),
age: created,
authorLink: user_path(author)).html_safe
end
def time_tag(time)
text = distance_of_time_in_words(Time.now, time)
if @project and @project.module_enabled?("activity")
link_to(text, { controller: "/activities",
action: "index",
project_id: @project,
from: time.to_date },
title: format_time(time))
else
datetime = time.acts_like?(:time) ? time.xmlschema : time.iso8601
content_tag(:time, text, datetime:,
title: format_time(time), class: "timestamp")
end
end
def syntax_highlight(name, content)
highlighted = OpenProject::SyntaxHighlighting.highlight_by_filename(content, name)
highlighted.each_line do |line|
yield highlighted.html_safe? ? line.html_safe : line
end
end
def to_path_param(path)
path.to_s
end
def other_formats_links(&)
formats = capture(Redmine::Views::OtherFormatsBuilder.new(self), &)
unless formats.nil? || formats.strip.empty?
content_tag "p", class: "other-formats" do
(I18n.t(:label_export_to) + formats).html_safe
end
end
end
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes
css = ["theme-" + OpenProject::CustomStyles::Design.identifier.to_s]
if params[:controller] && params[:action]
css << ("controller-" + params[:controller])
css << ("action-" + params[:action])
end
css << "ee-banners-#{EnterpriseToken.show_banners? ? 'visible' : 'hidden'}"
css << "env-#{Rails.env}"
# Add browser specific classes to aid css fixes
css += browser_specific_classes
css.join(" ")
end
def accesskey(s)
OpenProject::AccessKeys.key_for s
end
# Same as Rails' simple_format helper without using paragraphs
def simple_format_without_paragraph(text)
text.to_s
.gsub(/\r\n?/, "\n") # \r\n and \r -> \n
.gsub(/\n\n+/, "<br /><br />") # 2+ newline -> 2 br
.gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
.html_safe
end
def lang_options_for_select(blank = true)
auto = if blank && (valid_languages - all_languages) == (all_languages - valid_languages)
[["(auto)", ""]]
else
[]
end
mapped_languages = valid_languages.map { |lang| translate_language(lang) }
auto + mapped_languages.sort_by(&:last)
end
def all_lang_options_for_select
all_languages
.map { |lang| translate_language(lang) }
.sort_by(&:last)
end
def theme_options_for_select
options = [
[t("themes.light"), "light"],
[t("themes.light_high_contrast"), "light_high_contrast"]
]
options.push([t("themes.dark"), "dark"]) if OpenProject::FeatureDecisions.dark_mode_active?
options
end
def user_theme_data_attributes
mode, _theme_suffix = User.current.pref.theme.split("_", 2)
"data-color-mode=#{mode} data-#{mode}-theme=#{User.current.pref.theme}"
end
def highlight_default_language(lang_options)
lang_options.map do |(language_name, code)|
if code == Setting.default_language
[I18n.t("settings.language_name_being_default", language_name:), code, { disabled: true, checked: true }]
else
[language_name, code]
end
end
end
def labelled_tabular_form_for(record, options = {}, &)
options.reverse_merge!(builder: TabularFormBuilder, html: {})
options[:html][:class] = "form" unless options[:html].has_key?(:class)
form_for(record, options, &)
end
def back_url_hidden_field_tag(use_referer: true)
back_url = params[:back_url] || (use_referer ? request.env["HTTP_REFERER"] : nil)
back_url = CGI.unescape(back_url.to_s)
hidden_field_tag("back_url", CGI.escape(back_url), id: nil) if back_url.present?
end
def back_url_to_current_page_hidden_field_tag
back_url = params[:back_url]
if back_url.present?
back_url = back_url.to_s
elsif request.get? and params.present?
back_url = request.url
end
hidden_field_tag("back_url", back_url) if back_url.present?
end
def check_all_links(form_name)
link_to_function(t(:button_check_all), "OpenProject.helpers.checkAll('#{form_name}', true)") +
" | " +
link_to_function(t(:button_uncheck_all), "OpenProject.helpers.checkAll('#{form_name}', false)")
end
def current_layout
controller.send :_layout, ["html"]
end
# Generates the HTML for a progress bar
# Params:
# * pcts:
# * a number indicating the percentage done
# * or an array of two numbers -> [percentage_closed, percentage_done]
# where percentage_closed <= percentage_done
# and percentage_close + percentage_done <= 100
# * options:
# A hash containing the following keys:
# * width: (default '100px') the css-width for the progress bar
# * legend: (default: '') the text displayed alond with the progress bar
def progress_bar(pcts, options = {})
pcts = Array(pcts).map(&:round)
closed = pcts[0]
done = pcts[1] || 0
width = options[:width] || "100px;"
legend = options[:legend] || ""
total_progress = options[:hide_total_progress] ? "" : t(:total_progress)
percent_sign = options[:hide_percent_sign] ? "" : "%"
content_tag :span do
progress = content_tag :span, class: "progress-bar", style: "width: #{width}" do
concat content_tag(:span, "", class: "inner-progress closed", style: "width: #{closed}%")
concat content_tag(:span, "", class: "inner-progress done", style: "width: #{done}%")
end
progress + content_tag(:span, "#{legend}#{percent_sign} #{total_progress}", class: "progress-bar-legend")
end
end
def checked_image(checked = true)
if checked
icon_wrapper("icon-context icon-checkmark", t(:label_checked))
end
end
def calendar_for(*_args)
ActiveSupport::Deprecation.warn "calendar_for has been removed. Please use the op-basic-single-date-picker angular component instead",
caller
end
def locale_first_day_of_week
case Setting.start_of_week.to_i
when 1
"1" # Monday
when 7
"0" # Sunday
when 6
"6" # Saturday
else
# use language default (pass a blank string) and moment.js will reuse existing info
# /frontend/src/main.ts
""
end
end
def locale_first_week_of_year
case Setting.first_week_of_year.to_i
when 1
"1" # Monday
when 4
"4" # Thursday
else
# use language default (pass a blank string) and moment.js will reuse existing info
# /frontend/src/main.ts
""
end
end
# To avoid the menu flickering, disable it
# by default unless we're in test mode
def initial_menu_styles(side_displayed)
Rails.env.test? || !side_displayed ? "" : "display:none"
end
def initial_menu_classes(side_displayed, show_decoration)
classes = "can-hide-navigation"
classes << " nosidebar" unless side_displayed
classes << " nomenus" unless show_decoration
classes
end
# Add a HTML meta tag to control robots (web spiders)
#
# @param [optional, String] content the content of the ROBOTS tag.
# defaults to no index, follow, and no archive
def robot_exclusion_tag(content = "NOINDEX,FOLLOW,NOARCHIVE")
"<meta name='ROBOTS' content='#{h(content)}' />".html_safe
end
def permitted_params
PermittedParams.new(params, current_user)
end
# Returns the language name in its own language for a given locale
#
# @param lang_code [String] the locale for the desired language, like `en`,
# `de`, `fil`, `zh-CN`, and so on.
# @return [String] the language name translated in its own language
def translate_language(lang_code)
# rename in-context translation language name for the language select box
if lang_code.to_sym == Redmine::I18n::IN_CONTEXT_TRANSLATION_CODE &&
::I18n.locale != Redmine::I18n::IN_CONTEXT_TRANSLATION_CODE
[Redmine::I18n::IN_CONTEXT_TRANSLATION_NAME, lang_code.to_s]
else
[I18n.t("cldr.language_name", locale: lang_code), lang_code.to_s]
end
end
def link_to_content_update(text, url_params = {}, html_options = {})
link_to(text, url_params, html_options)
end
def password_complexity_requirements
rules = OpenProject::Passwords::Evaluator.rules_description
# use 0..0, so this doesn't fail if rules is an empty string
rules[0] = rules[0..0].upcase
s = raw "<em>" + OpenProject::Passwords::Evaluator.min_length_description + "</em>"
s += raw "<br /><em>" + rules + "</em>" unless rules.empty?
s
end
end