openSUSE/open-build-service

View on GitHub
src/api/app/helpers/webui/webui_helper.rb

Summary

Maintainability
A
25 mins
Test Coverage
D
68%
# rubocop:disable Metrics/ModuleLength
module Webui::WebuiHelper
  include ActionView::Helpers::JavaScriptHelper
  include ActionView::Helpers::AssetTagHelper
  include Webui::BuildresultHelper
  include Webui::ElisionsHelper

  def format_projectname(prjname, login)
    splitted = prjname.split(':', 3)
    if splitted[0] == 'home'
      prjname = if login && splitted[1] == login
                  '~'
                else
                  "~#{splitted[1]}"
                end
      prjname += ":#{splitted[-1]}" if splitted.length > 2
    end
    prjname
  end

  REPO_STATUS_ICONS = {
    'published' => 'truck',
    'outdated_published' => 'truck',
    'publishing' => 'truck-loading',
    'outdated_publishing' => 'truck-loading',
    'unpublished' => 'dolly-flatbed',
    'outdated_unpublished' => 'dolly-flatbed',
    'building' => 'cog',
    'outdated_building' => 'cog',
    'finished' => 'check',
    'outdated_finished' => 'check',
    'blocked' => 'lock',
    'outdated_blocked' => 'lock',
    'broken' => 'exclamation-triangle',
    'outdated_broken' => 'exclamation-triangle',
    'scheduling' => 'calendar-alt',
    'outdated_scheduling' => 'calendar-alt'
  }.freeze

  REPO_STATUS_DESCRIPTIONS = {
    'published' => 'Repository has been published',
    'publishing' => 'Repository is being created right now',
    'unpublished' => 'Build finished, but repository publishing is disabled',
    'building' => 'Build jobs exist for the repository',
    'finished' => 'Build jobs have been processed, new repository is not yet created',
    'blocked' => 'No build possible at the moment, waiting for jobs in other repositories',
    'broken' => 'The repository setup is broken, build or publish not possible',
    'scheduling' => 'The repository state is being calculated right now'
  }.freeze

  def repo_status_description(status)
    REPO_STATUS_DESCRIPTIONS[status] || 'Unknown state of repository'
  end

  def repo_status_icon(status)
    REPO_STATUS_ICONS[status] || 'eye'
  end

  def check_first(first)
    first.nil? ? true : nil
  end

  def image_template_icon(template)
    default_icon = image_url('drive-optical-48.png')
    icon = template.source_path('_icon') if template.has_icon?
    capture_haml do
      content_tag(:object, data: icon || default_icon, type: 'image/png', title: template.title, width: 32, height: 32) do
        content_tag(:img, src: default_icon, alt: template.title, width: 32, height: 32)
      end
    end
  end

  def repository_status_icon(status:, details: nil, html_class: '')
    outdated = status.start_with?('outdated_')
    status = status.sub('outdated_', '')
    description = outdated ? 'State needs recalculations, former state was: ' : ''
    description << repo_status_description(status)
    description << " (#{details})" if details

    repo_state_class = repository_state_class(outdated, status)

    tag.i('', class: "repository-state-#{repo_state_class} #{html_class} fas fa-#{repo_status_icon(status)}", title: description)
  end

  def repository_info(status)
    outdated = status.sub!(/^outdated_/, '')
    description = outdated ? 'State needs recalculations, former state was: ' : ''
    description << repo_status_description(status)
  end

  def repository_state_class(outdated, status)
    return 'outdated' if outdated

    /broken|building|finished|publishing|published/.match?(status) ? status : 'default'
  end

  def force_utf8_and_transform_nonprintables(text)
    return '' if text.blank?

    text.force_encoding('UTF-8')
    text = 'The file you look at is not valid UTF-8 text. Please convert the file.' unless text.valid_encoding?
    # Ged rid of stuff that shouldn't be part of PCDATA:
    text.gsub(%r{([^a-zA-Z0-9&;<>/\n \t()])}) do
      if Regexp.last_match(1)[0].getbyte(0) < 32
        ''
      else
        Regexp.last_match(1)
      end
    end
  end

  def next_codemirror_uid
    return @codemirror_editor_setup = 0 unless @codemirror_editor_setup

    @codemirror_editor_setup += 1
  end

  def codemirror_style(opts = {})
    opts.reverse_merge!(read_only: false, no_border: false, width: 'auto', height: 'auto')

    style = ".CodeMirror {\n"
    style += "border-width: 0 0 0 0;\n" if opts[:no_border] || opts[:read_only]
    style += "height: #{opts[:height]};\n" unless opts[:height] == 'auto'
    style += "width: #{opts[:width]}; \n" unless opts[:width] == 'auto'
    "#{style}}\n"
  end

  def package_link(pack, opts = {})
    opts[:project] = pack.project.name
    opts[:package] = pack.name
    project_or_package_link(opts)
  end

  def link_to_package(prj, pkg, opts)
    opts[:project_text] ||= opts[:project]
    opts[:package_text] ||= opts[:package]

    unless opts[:trim_to].nil?
      opts[:project_text], opts[:package_text] =
        elide_two(opts[:project_text], opts[:package_text], opts[:trim_to])
    end

    out = if opts[:short]
            ''.html_safe
          else
            'package '.html_safe
          end

    opts[:short] = true # for project
    out += link_to_project(prj, opts) + ' / ' +
           link_to_if(pkg, opts[:package_text],
                      { controller: '/webui/package', action: 'show',
                        project: opts[:project],
                        package: opts[:package] }, class: 'package', title: opts[:package])
    if opts[:rev] && pkg
      out += ' ('.html_safe +
             link_to("revision #{elide(opts[:rev], 10)}",
                     { controller: '/webui/package', action: 'show',
                       project: opts[:project], package: opts[:package], rev: opts[:rev] },
                     class: 'package', title: opts[:rev]) + ')'.html_safe
    end
    out
  end

  def link_to_project(prj, opts)
    opts[:project_text] ||= opts[:project]
    out = if opts[:short]
            ''.html_safe
          else
            'project '.html_safe
          end
    project_text = opts[:trim_to].nil? ? opts[:project_text] : elide(opts[:project_text], opts[:trim_to])
    out + link_to_if(prj, project_text,
                     { controller: '/webui/project', action: 'show', project: opts[:project] },
                     class: 'project', title: opts[:project])
  end

  def project_or_package_link(opts)
    defaults = { package: nil, rev: nil, short: false, trim_to: 40 }
    opts = defaults.merge(opts)

    # only care for database entries
    prj = Project.where(name: opts[:project]).select(:id, :name, :updated_at).first
    # Expires in 2 hours so that changes of local and remote packages eventually result in an update
    Rails.cache.fetch(['project_or_package_link', prj.try(:id), opts], expires_in: 2.hours) do
      opts[:project_text] ||= format_projectname(opts[:project], opts[:creator]) if prj && opts[:creator]
      pkg = prj.packages.where(name: opts[:package]).select(:id, :name, :project_id).first if opts[:package] && prj && opts[:package] != :multiple
      if opts[:package]
        link_to_package(prj, pkg, opts)
      else
        link_to_project(prj, opts)
      end
    end
  end

  def creator_intentions(role = nil)
    role.blank? ? 'become bugowner (previous bugowners will be deleted)' : "get the role #{role}"
  end

  def word_break(string, length = 80)
    return '' unless string

    # adds a <wbr> tag after an amount of given characters
    safe_join(string.scan(/.{1,#{length}}/), '<wbr>'.html_safe)
  end

  # paths param will accept one or more paths to match to make this tab active.
  # Only the first one will be used as link though if more than one is present.
  def tab_link(label, paths, html_class = 'nav-link text-nowrap', active: false)
    paths = [paths] unless paths.respond_to?(:select)
    paths_match = paths.any? { |path| request.path.eql?(path) }
    html_class << ' active' if active || paths_match

    link_to(label, paths.first, class: html_class)
  end

  # TODO: remove once all the calls to image_tag_for are replaced by AvatarComponent
  def image_tag_for(object, size: 500, custom_class: 'img-fluid')
    return unless object

    alt = "#{object.name}'s avatar"
    image_tag(gravatar_icon(object.email, size), alt: alt, size: size, title: object.name, class: custom_class)
  end

  # TODO: remove once all the calls to image_tag_for are replaced by AvatarComponent
  def gravatar_icon(email, size)
    if ::Configuration.gravatar && email
      "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email.downcase)}?s=#{size}&d=robohash"
    else
      'default_face.png'
    end
  end

  def home_title
    @configuration ? @configuration['title'] : 'Open Build Service'
  end

  def pick_max_problems(remaining_checks, remaining_build_problems, max_shown)
    show_checks = [max_shown, remaining_checks.length].min
    show_builds = [max_shown - show_checks, remaining_build_problems.length].min
    # always prefer one build fail
    if show_builds.zero? && remaining_build_problems.present?
      show_builds += 1
      show_checks -= 1
    end

    checks = remaining_checks.shift(show_checks)
    build_problems = remaining_build_problems.shift(show_builds)
    [checks, build_problems, remaining_checks, remaining_build_problems]
  end

  def feature_enabled?(feature)
    Flipper.enabled?(feature, User.possibly_nobody)
  end

  def feature_css_class
    css_classes = []
    # NOTE: add a CSS class to wrap the CSS under the new feature flag. Example:
    # css_classes << 'new-foo' if feature_enabled?(:new_foo)
    css_classes.join(' ')
  end

  def sign_up_link(css_class: nil)
    return unless can_sign_up?

    if ::Configuration.proxy_auth_mode_enabled?
      link_to(sign_up_params[:url], class: css_class) do
        link_content('Sign Up', css_class, 'fa-user-plus')
      end
    else
      link_to('#', class: css_class, data: { 'bs-toggle': 'modal', 'bs-target': '#sign-up-modal' }) do
        link_content('Sign Up', css_class, 'fa-user-plus')
      end
    end
  end

  def log_in_link(css_class: nil)
    if CONFIG['proxy_auth_mode'] == :mellon
      link_to(CONFIG['proxy_auth_login_page'], class: css_class) do
        link_content('Log In', css_class, 'fa-sign-in-alt')
      end
    elsif kerberos_mode?
      link_to(new_session_path, class: css_class) do
        link_content('Log In', css_class, 'fa-sign-in-alt')
      end
    else
      link_to('#', class: css_class, data: { 'bs-toggle': 'modal', 'bs-target': '#log-in-modal' }) do
        link_content('Log In', css_class, 'fa-sign-in-alt')
      end
    end
  end

  def link_content(text, css_class, icon)
    if css_class && css_class.include?('nav-link')
      capture do
        concat(tag.i('', class: "fas #{icon}"))
        concat(tag.div(text))
      end
    else
      text
    end
  end

  def sidebar_collapsed?
    cookies[:sidebar_collapsed].eql?('true')
  end

  def valid_xml_id(rawid)
    rawid = "_#{rawid}" unless /^[A-Za-z_]/.match?(rawid) # xs:ID elements have to start with character or '_'
    CGI.escapeHTML(rawid.gsub(%r{[+&: ./~()@#]}, '_'))
  end

  def theme_from_user
    return 'light' unless feature_enabled?('color_themes')

    User.session&.color_theme || 'system'
  end

  def contact_link
    blank_contact = ::Configuration.contact_name.blank? || ::Configuration.contact_url.blank?
    blank_email = ::Configuration[:admin_email] == 'unconfigured@openbuildservice.org'
    return 'contacting instance administrators' if blank_contact && blank_email

    name = ::Configuration.contact_name || 'email'
    url = ::Configuration.contact_url || "mailto:#{::Configuration.admin_email}"
    link_to(name, url)
  end
end

# rubocop:enable Metrics/ModuleLength