module Jekyll::GtnFunctions

The main GTN function library

Constants

ELIXIR_NODES

List of elixir node country IDs (ISO 3166-1 alpha-2) and their names

Public Class Methods

cache() click to toggle source

rubocop:disable Naming/PredicateName

# File _plugins/gtn.rb, line 34
def self.cache
  @@cache ||= Jekyll::Cache.new('GtnFunctions')
end

Public Instance Methods

collapse_date_pretty(event) click to toggle source
# File _plugins/gtn.rb, line 648
def collapse_date_pretty(event)
  collapse_event_date_pretty(event)
end
convert_to_material_list(site, materials) click to toggle source
# File _plugins/gtn.rb, line 345
def convert_to_material_list(site, materials)
  # [{"name"=>"introduction", "topic"=>"admin"}]
  return [] if materials.nil?

  materials.map do |m|
    if m.key?('name') && m.key?('topic')
      found = TopicFilter.fetch_tutorial_material(site, m['topic'], m['name'])
      Jekyll.logger.warn "Could not find material #{m['topic']}/#{m['name']} in the site data" if found.nil?

      if m.key?('time')
        found['time'] = m['time']
      end
      found
    elsif m.key?('external') && m['external']
      {
        'layout' => 'tutorial_hands_on',
        'name' => m['name'],
        'title' => m['name'],
        'hands_on' => 'external',
        'hands_on_url' => m['link'],
      }
    elsif m.key?('type') && m['type'] == 'custom'
      {
        'layout' => 'custom',
        'name' => m['name'],
        'title' => m['name'],
        'description' => m['description'],
        'time' => m['time'],
      }
    else
      Jekyll.logger.warn "[GTN] Unsure how to render #{m}"
    end
  end
end
convert_workflow_path_to_trs(str) click to toggle source

Convert a workflow path to a TRS path Params:

str

The workflow path

Returns:

String

The TRS path

Example:

{{ "topics/metagenomics/tutorials/mothur-miseq-sop-short/workflows/workflow1_quality_control.ga" |
   convert_workflow_path_to_trs }}
=> "/api/ga4gh/trs/v2/tools/metagenomics-mothur-miseq-sop-short/versions/workflow1_quality_control"
# File _plugins/gtn.rb, line 391
def convert_workflow_path_to_trs(str)
  return 'GTN_TRS_ERROR_NIL' if str.nil?

  m = str.match(%r{topics/(?<topic>.*)/tutorials/(?<tutorial>.*)/workflows/(?<workflow>.*)\.ga})
  return "/api/ga4gh/trs/v2/tools/#{m[:topic]}-#{m[:tutorial]}/versions/#{m[:workflow].downcase}" if m

  'GTN_TRS_ERROR'
end
elixirnode2name(name) click to toggle source

Returns the name of an elixir node, given its country ID Params:

name

The country ID of the node (ISO 3166-1 alpha-2)

Returns:

String

The name of the node

# File _plugins/gtn.rb, line 71
def elixirnode2name(name)
  ELIXIR_NODES[name]
end
fetch_contributor(site, id) click to toggle source

Params:

data

The site data

string

The contributor id

Returns:

Hash

The contributing entity

Example:

{% assign contrib = site | fetch_contributor: page.contributor -%}
# File _plugins/gtn.rb, line 245
def fetch_contributor(site, id)
  Gtn::Contributors.fetch_contributor(site, id)
end
fetch_entity_avatar(entity, id, width) click to toggle source

Params:

data

The contributor’s data

Returns:

String

The funding URL

Example:

{{ entity | fetch_entity_avatar: 'alice', 120 }}
# File _plugins/gtn.rb, line 291
def fetch_entity_avatar(entity, id, width)
  if entity.nil?
    return '<img src="/training-material/assets/images/avatar.png" alt="ERROR_NO_ENTITY avatar" class="avatar"/>'
  end

  w = width.nil? ? '' : "width=\"#{width}\""
  url = fetch_entity_avatar_url(entity, id, width)
  %(<img src="#{url}" alt="#{entity['name']} avatar" #{w} class="avatar" />)
end
fetch_entity_avatar_url(entity, id, width) click to toggle source

Params:

data

The contributor’s data

Returns:

String

The avatar’s URL

Example:

{{ entity | fetch_entity_avatar: 'alice', 120 }}
# File _plugins/gtn.rb, line 269
def fetch_entity_avatar_url(entity, id, width)
  return 'ERROR_NO_ENTITY' if entity.nil?

  width.nil? ? '' : "width=\"#{width}\""
  if !entity['avatar'].nil?
    entity['avatar']
  elsif entity['github'] != false
    qp = width.nil? ? '' : "?s=#{width}"
    "https://avatars.githubusercontent.com/#{id}#{qp}"
  else
    '/training-material/assets/images/avatar.png'
  end
end
fetch_funding_url(entity) click to toggle source

Params:

data

The contributor’s data

Returns:

String

The funding URL

Example:

{{ entity | fetch_funding_url }}
# File _plugins/gtn.rb, line 257
def fetch_funding_url(entity)
  Gtn::Contributors.fetch_funding_url(entity)
end
filter_authors(data) click to toggle source

Params:

data

The page data

Returns:

Array

The “authors” of the material (list of strings)

Example:

{% assign authors = page | filter_authors -%}
# File _plugins/gtn.rb, line 232
def filter_authors(data)
  Gtn::Contributors.get_authors(data)
end
find_learningpaths_including_topic(site, topic_id) click to toggle source
# File _plugins/gtn.rb, line 822
def find_learningpaths_including_topic(site, topic_id)
  site.pages
      .select { |p| p['layout'] == 'learning-pathway' }
      .select do |p|
    materials_for_pathway(p)
      .map { |topic, _tutorial| topic }
      .include?(topic_id)
  end
end
fix_box_titles(content, lang, key) click to toggle source

Fix the titles of boxes in a page Params:

content

The content to fix

lang

The language of the content

key

The key of the content

Returns:

String

The fixed content

# File _plugins/gtn.rb, line 220
def fix_box_titles(content, lang, key)
  Gtn::Boxify.replace_elements(content, lang, key)
end
format_location(location) click to toggle source
# File _plugins/gtn.rb, line 577
def format_location(location)
  url = 'https://www.openstreetmap.org/search?query='
  # location:
  #   name: Bioinf Dept
  #   address: 42 E Main St.
  #   city: Reyjkjavik
  #   country: Iceland
  #   #region: # optional
  #   postcode: 912NM
  loc = [
    location.fetch('name', nil),
    location.fetch('address', nil),
    location.fetch('city', nil),
    location.fetch('region', nil),
    location.fetch('country', nil),
    location.fetch('postcode', nil)
  ].compact

  if loc.length > 1
    "<a href=\"#{url}#{loc.join(', ')}\">#{loc.join(', ')}</a>"
  else
    # Just e.g. the name
    loc.join(', ')
  end
end
format_location_short(location) click to toggle source
# File _plugins/gtn.rb, line 616
def format_location_short(location)
  url = 'https://www.openstreetmap.org/search?query='
  # location:
  #   name: Bioinf Dept
  #   address: 42 E Main St.
  #   city: Reyjkjavik
  #   country: Iceland
  #   #region: # optional
  #   postcode: 912NM
  loc = [
    location.fetch('name', nil),
    location.fetch('address', nil),
    location.fetch('city', nil),
    location.fetch('region', nil),
    location.fetch('country', nil),
    location.fetch('postcode', nil)
  ].compact

  loc2 = [
    location.fetch('name', nil),
    location.fetch('city', nil),
    location.fetch('country', nil)
  ].compact

  if loc.length > 1
    "<a href=\"#{url}#{loc.join(', ')}\">#{loc2.join(', ')}</a>"
  else
    # Just e.g. the name
    loc.join(', ')
  end
end
format_location_simple(location) click to toggle source
# File _plugins/gtn.rb, line 603
def format_location_simple(location)
  loc = [
    location.fetch('name', nil),
    location.fetch('address', nil),
    location.fetch('city', nil),
    location.fetch('region', nil),
    location.fetch('country', nil),
    location.fetch('postcode', nil)
  ].compact

  loc.join(', ')
end
get_feedback_count(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 495
def get_feedback_count(site, material_id)
  get_feedbacks(site, material_id).length
end
get_feedback_count_recent(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 499
def get_feedback_count_recent(site, material_id)
  get_recent_feedbacks_time(site, material_id).length
end
get_feedbacks(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 463
def get_feedbacks(site, material_id)
  return [] if material_id.nil?

  begin
    topic, tutorial = material_id.split('/')

    if tutorial.include?(':')
      language = tutorial.split(':')[1]
      tutorial = tutorial.split(':')[0]
      # If a language is supplied, then
      feedbacks = site.data['feedback2'][topic][tutorial]
                      .select { |f| (f['lang'] || '').downcase == language.downcase }
    else
      # English is the default
      feedbacks = site.data['feedback2'][topic][tutorial]
                      .select { |f| f['lang'].nil? }
    end
  rescue StandardError
    return []
  end

  return [] if feedbacks.nil? || feedbacks.empty?

  feedbacks
    .sort_by { |f| f['date'] }
    .reverse
    .map do |f|
    f['stars'] = to_stars(f['rating'])
    f
  end
end
get_og_desc(site, page) click to toggle source
# File _plugins/gtn.rb, line 728
def get_og_desc(site, page); end
get_og_title(site, page, reverse) click to toggle source
# File _plugins/gtn.rb, line 730
def get_og_title(site, page, reverse)
  og_title = []
  topic_id = page['path'].gsub(%r{^\./}, '').split('/')[1]

  if site.data.key?(topic_id)
    if site.data[topic_id].is_a?(Hash) && site.data[topic_id].key?('title')
      og_title = [site.data[topic_id]['title'].clone]
    else
      Jekyll.logger.warn "Missing title for #{topic_id}"
    end
  end

  if page['layout'] == 'topic'
    og_title.push 'Tutorial List'
    return og_title.join(' / ')
  end

  material_id = page['path'].gsub(%r{^\./}, '').split('/')[3]
  material = nil
  material = fetch_tutorial_material(site, topic_id, material_id) if site.data.key? topic_id

  og_title.push material['title'] if !material.nil?

  case page['layout']
  when 'workflow-list'
    og_title.push 'Workflows'
  when 'faq-page', 'faqs'
    if page['path'] =~ %r{faqs/gtn}
      og_title.push 'GTN FAQs'
    elsif page['path'] =~ %r{faqs/galaxy}
      og_title.push 'Galaxy FAQs'
    else
      og_title.push 'FAQs'
    end
  when 'faq'
    og_title.push "FAQ: #{page['title']}"
  when 'learning-pathway'
    og_title.push "Learning Pathway: #{page['title']}"
  when 'tutorial_hands_on'
    og_title.push "Hands-on: #{page['title']}"
  when /slides/
    og_title.push "Slides: #{page['title']}"
  else
    og_title.push page['title']
  end

  if reverse.to_s == 'true'
    og_title.compact.reverse.join(' / ').gsub(/Hands-on: Hands-on:/, 'Hands-on:')
  else
    og_title.compact.join(' / ').gsub(/Hands-on: Hands-on:/, 'Hands-on:')
  end
end
get_rating(site, material_id, recent: false) click to toggle source
# File _plugins/gtn.rb, line 439
def get_rating(site, material_id, recent: false)
  f = get_rating_histogram(site, material_id, recent: recent)
  rating = f.map { |k, v| k * v }.sum / f.map { |_k, v| v }.sum.to_f
  rating.round(1)
end
get_rating_histogram(site, material_id, recent: false) click to toggle source
# File _plugins/gtn.rb, line 417
def get_rating_histogram(site, material_id, recent: false)
  return {} if material_id.nil?

  feedbacks = recent ? get_recent_feedbacks_time(site, material_id) : get_feedbacks(site, material_id)

  return {} if feedbacks.nil? || feedbacks.empty?

  ratings = feedbacks.map { |f| f['rating'] }
  ratings.each_with_object(Hash.new(0)) { |w, counts| counts[w] += 1 }
end
get_rating_histogram_chart(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 428
def get_rating_histogram_chart(site, material_id)
  histogram = get_rating_histogram(site, material_id)
  return {} if histogram.empty?

  highest = histogram.map { |_k, v| v }.max
  histogram
    .map { |k, v| [k, [v, v / highest.to_f]] }
    .sort_by { |k, _v| -k }
    .to_h
end
get_rating_recent(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 445
def get_rating_recent(site, material_id)
  r = get_rating(site, material_id, recent: true)
  r.nan? ? get_rating(site, material_id, recent: false) : r
end
get_recent_feedbacks(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 517
def get_recent_feedbacks(site, material_id)
  feedbacks = get_feedbacks(site, material_id)
              .select do |f|
                f['pro']&.length&.positive? ||
                  f['con']&.length&.positive?
              end
              .map do |f|
    f['f_date'] = Date.parse(f['date']).strftime('%B %Y')
    f
  end

  last_year = feedbacks.select { |f| Date.parse(f['date']) > Date.today - 365 }
  # If we have fewer than 20 in the last year, then extend further.
  if last_year.length < 20
    feedbacks
      .first(20)
      .group_by { |f| f['f_date'] }
  else
    # Otherwise just everything last year.
    last_year
      .group_by { |f| f['f_date'] }
  end
end
get_recent_feedbacks_time(site, material_id) click to toggle source
# File _plugins/gtn.rb, line 503
def get_recent_feedbacks_time(site, material_id)
  feedbacks = get_feedbacks(site, material_id)
              .select do |f|
                f['pro']&.length&.positive? ||
                  f['con']&.length&.positive?
              end
              .map do |f|
    f['f_date'] = Date.parse(f['date']).strftime('%B %Y')
    f
  end

  feedbacks.select { |f| Date.parse(f['date']) > Date.today - 365 }
end
get_topic(page) click to toggle source

Get the topic of a page’s path Params:

page

The page to get the topic of, it will inspect page

Returns:

String

The topic of the page

Example:

{{ page | get_topic }}
# File _plugins/gtn.rb, line 661
def get_topic(page)
  # Arrays that will store all introduction slides and tutorials we discover.
  page['path'].split('/')[1]
end
get_upcoming_events(site) click to toggle source

Get the list of ‘upcoming’ events (i.e. reg deadline or start is 30 days away.) Params:

site

The site object

Returns:

Array

List of events

Example:

{{ site | get_upcoming_events }}
# File _plugins/gtn.rb, line 675
def get_upcoming_events(site)
  cache.getset('upcoming-events') do
    site.pages
        .select { |p| p.data['layout'] == 'event' || p.data['layout'] == 'event-external' }
        .reject { |p| p.data['program'].nil? } # Only those with programs
        .select { |p| p.data['event_upcoming'] == true } # Only those coming soon
        .map do |p|
      materials = p.data['program']
                   .map { |section| section['tutorials'] }
                   .flatten
                   .compact # Remove nil entries
                   .reject { |x| x.fetch('type', nil) == 'custom' } # Remove custom entries
                   .map { |x| "#{x['topic']}/#{x['name']}" } # Just the material IDs.
                   .sort.uniq
      [p, materials]
    end
  end
end
get_upcoming_events_for_this(site, material) click to toggle source

Get the list of ‘upcoming’ events that include this material’s ID Params:

site

The site object

material

The ‘material’ to get the topic of, it will inspect page.id (use new_material)

Returns:

Array

List of events

Example:

{{ site | get_upcoming_events }}
# File _plugins/gtn.rb, line 704
def get_upcoming_events_for_this(site, material)
  get_upcoming_events(site)
    .select { |_p, materials| materials.include? material['id'] }
    .map { |p, _materials| p }
end
get_version_number(page) click to toggle source
# File _plugins/gtn.rb, line 413
def get_version_number(page)
  Gtn::ModificationTimes.obtain_modification_count(page['path'])
end
group_icons(icons) click to toggle source
# File _plugins/gtn.rb, line 804
def group_icons(icons)
  icons.group_by { |_k, v| v }.transform_values { |v| v.map { |z| z[0] } }.invert
end
gtn_mod_date(path) click to toggle source

Returns the last modified date of a page Params:

page

The page to get the last modified date of

Returns:

String

The last modified date of the page

# File _plugins/gtn.rb, line 177
def gtn_mod_date(path)
  # Automatically strips any leading slashes.
  Gtn::ModificationTimes.obtain_time(path.gsub(%r{^/}, ''))
end
gtn_pub_date(path) click to toggle source

Returns the publication date of a page, when it was merged into ‘main` Params:

page

The page to get the publication date of

Returns:

String

The publication date of the page

# File _plugins/gtn.rb, line 151
def gtn_pub_date(path)
  # if it's not a string then log a warning
  path = path['path'] if !path.is_a?(String)
  # Automatically strips any leading slashes.
  Gtn::PublicationTimes.obtain_time(path.gsub(%r{^/}, ''))
end
how_many_topic_feedbacks(feedback, name) click to toggle source

How many times has a topic been mentioned in feedback? Params:

feedback

The feedback to search through

name

The name of the topic to search for

Returns:

Integer

The number of times the topic has been mentioned

# File _plugins/gtn.rb, line 189
def how_many_topic_feedbacks(feedback, name)
  if feedback.nil?
    return 0
  end

  feedback.select { |x| x['topic'] == name }.length
end
how_many_tutorial_feedbacks(feedback, name) click to toggle source

How many times has a tutorial been mentioned in feedback? Params:

feedback

The feedback to search through

name

The name of the tutorial to search for

Returns:

Integer

The number of times the tutorial has been mentioned

# File _plugins/gtn.rb, line 204
def how_many_tutorial_feedbacks(feedback, name)
  if feedback.nil?
    return 0
  end

  feedback.select { |x| x['tutorial'] == name }.length
end
humanize_types(type) click to toggle source

Return human text for ruby types Params:

type

The type to humanize

Returns:

String

The humanized type

Example:

humanize_types("seq") # => "List of Items"
# File _plugins/gtn.rb, line 126
def humanize_types(type)
  data = {
    'seq' => 'List of Items',
    'str' => 'Free Text',
    'map' => 'A dictionary/map',
    'float' => 'Decimal Number',
    'int' => 'Integer Number',
    'bool' => 'Boolean'
  }
  data[type]
end
is_date_passed(date) click to toggle source
# File _plugins/gtn.rb, line 718
def is_date_passed(date)
  if date.nil?
    false
  elsif date.is_a?(String)
    Date.parse(date) < Date.today
  else
    date < Date.today
  end
end
last_modified_at(page) click to toggle source

Returns the last modified date of a page Params:

page

The page to get the last modified date of

Returns:

String

The last modified date of the page

TODO: These two could be unified tbh

# File _plugins/gtn.rb, line 166
def last_modified_at(page)
  Gtn::ModificationTimes.obtain_time(page['path'])
end
layout_to_human(layout) click to toggle source
# File _plugins/gtn.rb, line 400
def layout_to_human(layout)
  case layout
  when /slides/
    'Slides'
  when /tutorial_hands_on/
    'Hands-on'
  when 'faq'
    'FAQs'
  when 'news'
    'News'
  end
end
list_usegalaxy_servers(_site) click to toggle source
# File _plugins/gtn.rb, line 561
def list_usegalaxy_servers(_site)
  Gtn::Usegalaxy.servers.map { |x| x.transform_keys(&:to_s) }
end
list_usegalaxy_servers_shuffle(_site) click to toggle source
# File _plugins/gtn.rb, line 565
def list_usegalaxy_servers_shuffle(_site)
  Gtn::Usegalaxy.servers.map { |x| x.transform_keys(&:to_s) }.shuffle
end
load_svg(path) click to toggle source

Load an SVG file directly into the page Params:

path

The path of the SVG file (relative to GTN workspace root)

Returns:

String

The SVG file contents

Example:

{{ "assets/images/mastodon.svg" | load_svg }}
# File _plugins/gtn.rb, line 325
def load_svg(path)
  File.read(path).gsub(/\R+/, '')
end
materials_for_pathway(page) click to toggle source
# File _plugins/gtn.rb, line 808
def materials_for_pathway(page)
  d = if page.is_a?(Jekyll::Page)
        page.data.fetch('pathway', [])
      else
        page.fetch('pathway', [])
      end

  d.map do |m|
    m.fetch('tutorials', [])
     .select { |t| t.key?('name') && t.key?('topic') }
     .map { |t| [t['topic'], t['name']] }
  end.flatten.compact.sort.uniq
end
regex_replace(str, regex_search, value_replace) click to toggle source
# File _plugins/gtn.rb, line 329
def regex_replace(str, regex_search, value_replace)
  regex = /#{regex_search}/m
  str.gsub(regex, value_replace)
end
regex_replace_once(str, regex_search, value_replace) click to toggle source

This method does a single regex replacement

Example

{{ content | regex_replace: '<hr>', '' }}
# File _plugins/gtn.rb, line 340
def regex_replace_once(str, regex_search, value_replace)
  regex = /#{regex_search}/m
  str.sub(regex, value_replace)
end
replace_newline_doublespace(text) click to toggle source

Replaces newlines with newline + two spaces

# File _plugins/gtn.rb, line 140
def replace_newline_doublespace(text)
  text.gsub(/\n/, "\n  ")
end
shuffle(array) click to toggle source
# File _plugins/gtn.rb, line 710
def shuffle(array)
  array.shuffle
end
slugify_unsafe(text) click to toggle source

A slightly more unsafe slugify function Params:

text

The text to slugify

Returns:

String

The slugified text

Example:

slugify_unsafe("Hello, World!") # => "Hello-World"
# File _plugins/gtn.rb, line 112
def slugify_unsafe(text)
  # Gets rid of *most* things without making it completely unusable?
  text.gsub(%r{["'\\/;:,.!@#$%^&*()]}, '').gsub(/\s/, '-').gsub(/-+/, '-')
end
to_stars(rating) click to toggle source

Only accepts an integer rating

# File _plugins/gtn.rb, line 451
def to_stars(rating)
  if rating.nil? || (rating.to_i < 1) || (rating == '0') || rating.zero?
    %(<span class="sr-only">0 stars</span>) +
      '<i class="far fa-star" aria-hidden="true"></i>'
  elsif rating.to_i < 1
    '<span class="sr-only">0 stars</span><i class="far fa-star" aria-hidden="true"></i>'
  else
    %(<span class="sr-only">#{rating} stars</span>) +
      ('<i class="fa fa-star" aria-hidden="true"></i>' * rating.to_i)
  end
end
top_citations(citations) click to toggle source

Obtain the most cited paper in the GTN Params:

citations

The citations to search through

Returns:

Hash

The papers including their text citation and citation count

# File _plugins/gtn.rb, line 93
def top_citations(citations)
  if citations.nil?
    {}
  else
    citations.sort_by { |_k, v| v }.reverse.to_h.first(20).to_h do |k, v|
      [k, { 'count' => v, 'text' => Gtn::Scholar.render_citation(k) }]
    end
  end
end
topic_name_from_page(page, site) click to toggle source
# File _plugins/gtn.rb, line 569
def topic_name_from_page(page, site)
  if page.key? 'topic_name'
    site.data[page['topic_name']]['title']
  else
    site.data.fetch(page['url'].split('/')[2], { 'title' => '' })['title']
  end
end
tutorials_over_time_bar_chart(site) click to toggle source
# File _plugins/gtn.rb, line 541
def tutorials_over_time_bar_chart(site)
  graph = Hash.new(0)
  TopicFilter.list_all_materials(site).each do |material|
    if material['pub_date']
      yymm = material['pub_date'].strftime('%Y-%m')
      graph[yymm] += 1
    end
  end

  # Cumulative over time
  # https://stackoverflow.com/questions/71745593/how-to-do-a-single-line-cumulative-count-for-hash-values-in-ruby
  graph
    # Turns it into an array
    .sort_by { |k, _v| k }
    # Cumulative sum
    .each_with_object([]) { |(k, v), a| a << [k, v + a.last&.last.to_i] }.to_h
    .map { |k, v| { 'x' => k, 'y' => v } }
    .to_json
end
unix_time_to_date(time) click to toggle source
# File _plugins/gtn.rb, line 714
def unix_time_to_date(time)
  Time.at(time.to_i).strftime('%Y-%m-%d %H:%M:%S')
end
url_exists(url) click to toggle source
# File _plugins/gtn.rb, line 75
def url_exists(url)
  cache.getset("url-exists-#{url}") do
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true if uri.scheme == 'https'
    response = http.request_head(uri.path)
    #Jekyll.logger.warn response
    response.code == '200'
  end
end