app/helpers/application_helper.rb
# frozen_string_literal: true
# rubocop:todo Metrics/ModuleLength
module ApplicationHelper
include ControllerHelper
# Should return either the custom text or a blank string
def custom_text(identifier, differential = nil)
Rails
.cache
.fetch("#{identifier}-#{differential}") do
custom_text = CustomText.find_by(identifier: identifier, differential: differential)
custom_text.try(:content) || ''
end
end
#
# Renders a non-displayed error div warning of a data failure
# Appears to have been intended to be used to provide error feedback on the studies
# in app/views/studies/information/_items.html.erb but actual behaviour will result in the
# error payload being placed in the div, but remaining invisible.
# @todo Probably remove this and the references to it in app/views/studies/information/_items.html.erb
# Or possibly restore the intended behaviour in app/assets/javascripts/sequencescape/ajax_link_handling.js
#
# @param identifier [String] The id of the element
def remote_error(identifier = 'remote_error')
tag.div(id: identifier, class: 'error', style: 'display:none;') do
'An error has occurred and the results can not be shown at the moment'
end
end
# Inserts the icon used to indicate a field is required.
# This will also be displayed on any <label> tags with the .required class
# @return [String] HTML representing the required marker
def required_marker
icon('fas', 'asterisk', class: 'text-warning', title: 'required')
end
# Returns the appropriate icon suffix for the current environment
# Returns empty string for production
# Returns "-#{environment}" for training, staging
# Returns "-development" for any other environment
# @return [String] The suffix to append to the icon name
def icon_suffix
environment = Rails.env
case environment
when 'production'
''
when 'training', 'staging'
"-#{environment}"
else
'-development'
end
end
# Return the appropriate favicon for the current environment
# @return [String] The path to the favicon
def favicon
"favicon#{icon_suffix}.ico"
end
# Return the appropriate apple icon for the current environment
# @return [String] The path to the apple icon
def apple_icon
"apple-icon#{icon_suffix}.png"
end
def render_flashes
flash.each do |key, message|
concat(alert(key, id: "message_#{key}") { Array(message).each { |m| concat tag.div(m) } })
end
nil
end
def api_data
{ api_version: RELEASE.api_version }
end
# Renders a user guide with optional link. Applies appropriate styling
#
# @param display_text [String] The text of the user guide
# @param link [String] Optional url to link the guide to.
#
# @return [type] [description]
def display_user_guide(display_text, link = nil)
alert(:user_guide) { concat link.present? ? link_to(display_text, link) : display_text }
end
def display_user_error(display_text, link = nil)
alert(:danger) { link.present? ? link_to(display_text, link) : display_text }
end
#
# Renders a badge containing the supplied text, with appropriate styling.
# By default the 'badge-#!{status}' class is applied. These states are mapped to
# bootstrap colours in components.scss (grep '// State-colour extensions')
#
# If you can't map the text directly to a style, such as if you are displaying a
# number that you want to change its colours at certain thresholds, then you can
# override the applied style with the style: argument.
#
# If the string passed in is empty, no badge will be rendered
#
# @example Render a request state badge.
# badge(request.state, type: 'request')
# @example Render the size of a batch, which is red if too large.
# status = batch.size > MAX_SIZE ? 'danger' : 'success'
# badge(batch.size, type: 'batch-size', style: status )
#
# @param status [String] The text to display in the badge. Will also be used to set the style if not otherwise
# specified
# @param type [String] Optional: Additional css-class applied to the badge (generic-badge by default)
# @param style [String] Optional: Override the badge-* class otherwise set directly from the status.
#
# @return [type] HTML to render a badge
def badge(status, type: 'generic-badge', style: status)
return if status.blank?
tag.span(status, class: "#{type} badge badge-#{style}")
end
#
# Used to add a counter to headers or links. Renders a blue badge containing the supplied number
# Only supply a suffix if it can't be worked out from the context what is being counted.
#
# @param counter [Integer] The value to show in the badge
# @param suffix [Integer, String] Optional: The type of thing being counted.
# @return [String] HTML to render a badge
def counter_badge(counter, suffix = '')
status = suffix.present? ? pluralize(counter, suffix) : counter
badge(status, type: 'counter-badge', style: 'primary')
end
# rubocop:todo Metrics/MethodLength
def dynamic_link_to(summary_item) # rubocop:todo Metrics/AbcSize
object = summary_item.object
if object.instance_of?(Submission)
link_to("Submission #{object.id}", study_information_submission_path(object.study, object))
elsif object.instance_of?(Receptacle)
link_to("#{object.label.capitalize} #{object.name}", receptacle_path(object))
elsif object.instance_of?(Labware)
link_to("#{object.label.capitalize} #{object.name}", labware_path(object))
elsif object.instance_of?(Request)
link_to("Request #{object.id}", request_path(object))
else
'No link available'
end
end
# rubocop:enable Metrics/MethodLength
def request_count_link(study, asset, state, request_type) # rubocop:todo Metrics/AbcSize
matching_requests =
asset.requests.select { |request| (request.request_type_id == request_type.id) and request.state == state }
html_options, count = { title: "#{asset.try(:human_barcode) || asset.id} #{state}" }, matching_requests.size
# 0 requests => no link, just '0'
# 1 request => request summary page
# N requests => summary overview
if count == 1
url_path = request_path(matching_requests.first)
link_to count, url_path, html_options
elsif count > 1
url_path = study_requests_path(study, state: state, request_type_id: request_type.id, asset_id: asset.id)
link_to count, url_path, html_options
end
end
# rubocop:todo Metrics/ParameterLists
def request_link(object, count, request_type, status = nil, options = {}, link_options = {})
# rubocop:enable Metrics/ParameterLists
link_to_if((count != 0), count, request_list_path(object, request_type, status, options), link_options)
end
def request_list_path(object, request_type = nil, status = nil, options = {})
options[:state] = status unless status.nil?
options[:request_type_id] = request_type.id unless request_type.nil?
if object.instance_of?(Receptacle)
receptacle_path(object, options)
elsif object.instance_of?(Labware)
labware_path(object, options)
elsif object.instance_of?(Study)
study_requests_path(object, options)
end
end
def display_follow(item, user, msg)
user.follower_of?(item) ? 'Unfollow ' + msg : 'Follow ' + msg
end
## From Pipelines
def about(title = '')
add :about, title
end
def tabulated_error_messages_for(*params) # rubocop:todo Metrics/AbcSize
options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {}
objects = params.filter_map { |object_name| instance_variable_get(:"@#{object_name}") }
count = objects.inject(0) { |sum, object| sum + object.errors.count }
if count.zero?
''
else
error_messages = objects.map { |object| object.errors.full_messages.map { |msg| tag.div(msg) } }.join
[
tag.td(class: 'error item') { "Your #{params.first} has not been created." },
tag.td(class: 'error') { raw(error_messages) }
].join.html_safe
end
end
# <li class="nav-item">
# <a class="nav-link <active>" id="name-tab" data-toggle="tab" href="#name"
# role="tab" aria-controls="name" aria-selected="true">name</a>
# </li>
def tab(name, target: nil, active: false, id: nil)
target ||= name.parameterize
active_class = active ? 'active' : ''
id ||= "#{name}-tab".parameterize
tag.li(class: 'nav-item') do
link_to name,
"##{target}",
id: id,
data: {
toggle: 'tab'
},
role: 'tab',
aria_controls: target,
class: ['nav-link', active_class]
end
end
# <div class="tab-pane fade show <active>" id="pending" role="tabpanel" aria-labelledby="peding-tab">
# yield
# </div>
def tab_pane(name, id: nil, tab_id: nil, active: false, &block)
tab_id ||= "#{name}-tab".parameterize
id ||= name.parameterize
active_class = active ? 'active' : ''
tag.div(
class: ['tab-pane', 'fade', 'show', active_class],
id: id,
role: 'tabpanel',
aria_labelledby: tab_id,
&block
)
end
def display_boolean_results(result)
return 'NA' if result.blank?
if result == 'pass' || result == '1' || result == 'true'
icon('far', 'check-circle', title: result)
else
icon('fas', 'exclamation-circle', class: 'text-danger', title: result)
end
end
def sorted_requests_for_search(requests)
sorted_requests = requests.select { |r| r.pipeline_id.nil? }
new_requests = requests - sorted_requests
new_requests.sort_by(&:pipeline_id)
requests = requests + sorted_requests
end
# Creates a label that is hidden from the view so that testing is easier
def hidden_label_tag_for_testing(name, text = nil, options = {})
label_tag(name, text, options.merge(style: 'display:none;'))
end
def help_text(&block)
tag.small(class: 'form-text text-muted col', &block)
end
def help_link(text, entry = '', options = {})
url = "#{configatron.help_link_base_url}/#{entry}"
options[:class] = "#{options[:class]} external_help"
link_to text, url, options
end
# The admin email address should be stored in config.yml for the current environment
def help_email_link
admin_address = configatron.admin_email || 'admin@test.com'
link_to admin_address.to_s, "mailto:#{admin_address}"
end
#
# Handles rendering of JSON to a series of nested lists. Does the following:
# String: Rendered as-is
# Array: Unordered list (Strictly speaking arrays are ordered, but we probably don't care.)
# Object: Descriptive list
# Other: Calls to_s
# Processes each in turn and called recursively
#
# @param [Hash, String, Array,, #to_s] json The Object to render
#
# @return [String] HTML formatted for rendering
#
# rubocop:todo Metrics/MethodLength
def render_parsed_json(json) # rubocop:todo Metrics/AbcSize
case json
when String
json
when Array
tag.ul { json.each { |elem| concat tag.li(render_parsed_json(elem)) } }
when Hash
tag.dl do
json.each do |key, value|
# Strictly speaking json should only have strings as keys. But the same constraint doesn't apply to hashes,
# so we're a little more permissive here for flexibilities sake
concat tag.dt(render_parsed_json(key))
concat tag.dd(render_parsed_json(value))
end
end
else
json.to_s
end
end
# rubocop:enable Metrics/MethodLength
#
# Ideally we don't want inline script tags, however there is a fair chunk of
# legacy code, some of which isn't trivial to migrate, as it uses erb to
# generate javascript, rather than using data-attributes.
#
# This tag:
# - Ensures we add a nonce for security
# - If the page is still loading,
# delays script execution until DOMContentLoaded to ensure that the
# modern JS has had a chance to export jQuery
# - If the page has already loaded, executes the script immediately.
# This is needed for use cases where the partial that renders this script
# is loaded after the main page has loaded
# e.g. the admin study edit page, within the admin study index page.
#
# @return [String] Script tag
#
def legacy_javascript_tag
javascript_tag nonce: true do
concat 'if (document.readyState === "loading") {window.addEventListener("DOMContentLoaded", function() {'
.html_safe
yield
concat '});} else {'.html_safe
yield
concat '}'.html_safe
end
end
end
# rubocop:enable Metrics/ModuleLength
# error_messages_for method was deprecated, however lots of the tests depend on the message format it
# was using.
# <https://apidock.com/rails/ActionView/Helpers/ActiveRecordHelper/error_messages_for>
def render_error_messages(object)
return if object.errors.count.zero?
contents = +''
contents << error_message_header(object)
contents << error_messages_ul_html_safe(object)
content_tag(:div, contents.html_safe)
end
def error_message_header(object)
count = object.errors.full_messages.count
model_name = object.class.to_s.tableize.tr('_', ' ').gsub(%r{/.*}, '').singularize
is_plural = count > 1 ? 's' : ''
header = "#{count} error#{is_plural} prohibited this #{model_name} from being saved"
content_tag(:h2, header)
end
def error_messages_ul_html_safe(object)
messages = object.errors.full_messages.map { |msg| content_tag(:li, ERB::Util.html_escape(msg)) }.join.html_safe
content_tag(:ul, messages)
end