Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/gtn.rb
2593 views
1
# frozen_string_literal: true
2
3
require 'English'
4
require './_plugins/gtn/contributors'
5
require './_plugins/gtn/boxify'
6
require './_plugins/gtn/mod'
7
require './_plugins/gtn/ro-crate'
8
require './_plugins/gtn/images'
9
require './_plugins/gtn/synthetic'
10
require './_plugins/gtn/metrics'
11
require './_plugins/gtn/scholar'
12
require './_plugins/gtn/supported'
13
require './_plugins/gtn/toolshed'
14
require './_plugins/gtn/usegalaxy'
15
require './_plugins/util'
16
require './_plugins/jekyll-topic-filter'
17
require 'time'
18
require 'net/http'
19
20
21
Jekyll.logger.info "[GTN] Jekyll env: #{Jekyll.env}"
22
Jekyll.logger.info "[GTN] You are running #{RUBY_VERSION} released on #{RUBY_RELEASE_DATE} for #{RUBY_PLATFORM}"
23
version_parts = RUBY_VERSION.split('.')
24
Jekyll.logger.warn '[GTN] WARNING: This Ruby is pretty old, you might want to update.' if version_parts[0].to_i < 3
25
26
##
27
# We have several sub-areas of Jekyll namespaced things that are useful to know about.
28
#
29
# - Jekyll::Filters - Liquid Filters that are useful in rendering your HTML
30
# - Jekyll::Tags - Liquid Tags can be used to access certain internals in HTML
31
# - Jekyll::Generators - Generators emit files at runtime, e.g. the hall of fame pages.
32
# - Jekyll::GtnFunctions - Generally miscellaneous Liquid Functions, could be refactored into Jekyll::Filters and Jekyll::Tags
33
module Jekyll
34
##
35
# This module contains functions that are used in the GTN, our internal functions that is.
36
module GtnFunctions
37
# rubocop:disable Naming/PredicateName
38
39
def self.cache
40
@@cache ||= Jekyll::Cache.new('GtnFunctions')
41
end
42
43
##
44
# List of elixir node country IDs (ISO 3166-1 alpha-2) and their names
45
ELIXIR_NODES = {
46
'au' => 'Australia',
47
'be' => 'Belgium',
48
'ch' => 'Switzerland',
49
'cz' => 'Czechia',
50
'de' => 'Germany',
51
'dk' => 'Denmark',
52
'ee' => 'Estonia',
53
'es' => 'Spain',
54
'fi' => 'Finland',
55
'fr' => 'France',
56
'gr' => 'Greece',
57
'hu' => 'Hungary',
58
'ie' => 'Ireland',
59
'il' => 'Israel',
60
'it' => 'Italy',
61
'lu' => 'Luxembourg',
62
'nl' => 'the Netherlands',
63
'no' => 'Norway',
64
'pt' => 'Portugal',
65
'se' => 'Sweden',
66
'si' => 'Slovenia',
67
'uk' => 'United Kingdom',
68
}.freeze
69
70
##
71
# Returns the name of an elixir node, given its country ID
72
# Params:
73
# +name+:: The country ID of the node (ISO 3166-1 alpha-2)
74
# Returns:
75
# +String+:: The name of the node
76
def elixirnode2name(name)
77
ELIXIR_NODES[name]
78
end
79
80
def url_exists(url)
81
cache.getset("url-exists-#{url}") do
82
uri = URI.parse(url)
83
http = Net::HTTP.new(uri.host, uri.port)
84
http.use_ssl = true if uri.scheme == 'https'
85
response = http.request_head(uri.path)
86
#Jekyll.logger.warn response
87
response.code == '200'
88
end
89
end
90
91
##
92
# Obtain the most cited paper in the GTN
93
# Params:
94
# +citations+:: The citations to search through
95
#
96
# Returns:
97
# +Hash+:: The papers including their text citation and citation count
98
def top_citations(citations)
99
if citations.nil?
100
{}
101
else
102
citations.sort_by { |_k, v| v }.reverse.to_h.first(20).to_h do |k, v|
103
[k, { 'count' => v, 'text' => Gtn::Scholar.render_citation(k) }]
104
end
105
end
106
end
107
108
##
109
# A slightly more unsafe slugify function
110
# Params:
111
# +text+:: The text to slugify
112
# Returns:
113
# +String+:: The slugified text
114
#
115
# Example:
116
# slugify_unsafe("Hello, World!") # => "Hello-World"
117
def slugify_unsafe(text)
118
# Gets rid of *most* things without making it completely unusable?
119
unsafe_slugify(text)
120
end
121
122
##
123
# Return human text for ruby types
124
# Params:
125
# +type+:: The type to humanize
126
# Returns:
127
# +String+:: The humanized type
128
#
129
# Example:
130
# humanize_types("seq") # => "List of Items"
131
def humanize_types(type)
132
data = {
133
'seq' => 'List of Items',
134
'str' => 'Free Text',
135
'map' => 'A dictionary/map',
136
'float' => 'Decimal Number',
137
'int' => 'Integer Number',
138
'bool' => 'Boolean'
139
}
140
data[type]
141
end
142
143
##
144
# Replaces newlines with newline + two spaces
145
def replace_newline_doublespace(text)
146
text.gsub(/\n/, "\n ")
147
end
148
149
##
150
# Returns the publication date of a page, when it was merged into `main`
151
# Params:
152
# +page+:: The page to get the publication date of
153
# Returns:
154
# +String+:: The publication date of the page
155
#
156
def gtn_pub_date(path)
157
# if it's not a string then log a warning
158
path = path['path'] if !path.is_a?(String)
159
# Automatically strips any leading slashes.
160
Gtn::PublicationTimes.obtain_time(path.gsub(%r{^/}, ''))
161
end
162
163
##
164
# Returns the last modified date of a page
165
# Params:
166
# +page+:: The page to get the last modified date of
167
# Returns:
168
# +String+:: The last modified date of the page
169
#
170
# TODO: These two could be unified tbh
171
def last_modified_at(page)
172
Gtn::ModificationTimes.obtain_time(page['path'])
173
end
174
175
##
176
# Returns the last modified date of a page
177
# Params:
178
# +page+:: The page to get the last modified date of
179
# Returns:
180
# +String+:: The last modified date of the page
181
#
182
def gtn_mod_date(path)
183
# Automatically strips any leading slashes.
184
Gtn::ModificationTimes.obtain_time(path.gsub(%r{^/}, ''))
185
end
186
187
##
188
# How many times has a topic been mentioned in feedback?
189
# Params:
190
# +feedback+:: The feedback to search through
191
# +name+:: The name of the topic to search for
192
# Returns:
193
# +Integer+:: The number of times the topic has been mentioned
194
def how_many_topic_feedbacks(feedback, name)
195
if feedback.nil?
196
return 0
197
end
198
199
feedback.select { |x| x['topic'] == name }.length
200
end
201
202
##
203
# How many times has a tutorial been mentioned in feedback?
204
# Params:
205
# +feedback+:: The feedback to search through
206
# +name+:: The name of the tutorial to search for
207
# Returns:
208
# +Integer+:: The number of times the tutorial has been mentioned
209
def how_many_tutorial_feedbacks(feedback, name)
210
if feedback.nil?
211
return 0
212
end
213
214
feedback.select { |x| x['tutorial'] == name }.length
215
end
216
217
##
218
# Fix the titles of boxes in a page
219
# Params:
220
# +content+:: The content to fix
221
# +lang+:: The language of the content
222
# +key+:: The key of the content
223
# Returns:
224
# +String+:: The fixed content
225
def fix_box_titles(content, lang, key)
226
Gtn::Boxify.replace_elements(content, lang, key)
227
end
228
229
##
230
# Params:
231
# +data+:: The page data
232
# Returns:
233
# +Array+:: The "authors" of the material (list of strings)
234
#
235
# Example:
236
# {% assign authors = page | filter_authors -%}
237
def filter_authors(data)
238
Gtn::Contributors.get_authors(data)
239
end
240
241
##
242
# Params:
243
# +data+:: The site data
244
# +string+:: The contributor id
245
# Returns:
246
# +Hash+:: The contributing entity
247
#
248
# Example:
249
# {% assign contrib = site | fetch_contributor: page.contributor -%}
250
def fetch_contributor(site, id)
251
Gtn::Contributors.fetch_contributor(site, id)
252
end
253
254
##
255
# Params:
256
# +data+:: The contributor's data
257
# Returns:
258
# +String+:: The funding URL
259
#
260
# Example:
261
# {{ entity | fetch_funding_url }}
262
def fetch_funding_url(entity)
263
Gtn::Contributors.fetch_funding_url(entity)
264
end
265
266
##
267
# Params:
268
# +data+:: The contributor's data
269
# Returns:
270
# +String+:: The avatar's URL
271
#
272
# Example:
273
# {{ entity | fetch_entity_avatar: 'alice', 120 }}
274
def fetch_entity_avatar_url(entity, id, width)
275
return 'ERROR_NO_ENTITY' if entity.nil?
276
277
width.nil? ? '' : "width=\"#{width}\""
278
if !entity['avatar'].nil?
279
entity['avatar']
280
elsif entity['github'] != false
281
qp = width.nil? ? '' : "?s=#{width}"
282
"https://avatars.githubusercontent.com/#{id}#{qp}"
283
else
284
'/training-material/assets/images/avatar.png'
285
end
286
end
287
288
##
289
# Params:
290
# +data+:: The contributor's data
291
# Returns:
292
# +String+:: The funding URL
293
#
294
# Example:
295
# {{ entity | fetch_entity_avatar: 'alice', 120 }}
296
def fetch_entity_avatar(entity, id, width)
297
if entity.nil?
298
return '<img src="/training-material/assets/images/avatar.png" alt="ERROR_NO_ENTITY avatar" class="avatar"/>'
299
end
300
301
w = width.nil? ? '' : "width=\"#{width}\""
302
url = fetch_entity_avatar_url(entity, id, width)
303
%(<img src="#{url}" alt="#{entity['name']} avatar" #{w} class="avatar" />)
304
end
305
306
##
307
# Convert a fedi address to a link
308
# Params:
309
# +fedi_address+:: The fedi address to convert
310
# Returns:
311
# +String+:: The URL at which their profile is accessible
312
#
313
# Example:
314
# {{ contributors[page.contributor].fediverse | fedi2link }}
315
#
316
# fedi2link("@[email protected]") => "https://galaxians.garden/@hexylena"
317
def fedi2link(fedi_address)
318
fedi_address.gsub(/^@?(?<user>.*)@(?<host>.*)$/) { |_m| "https://#{$LAST_MATCH_INFO[:host]}/@#{$LAST_MATCH_INFO[:user]}" }
319
end
320
321
##
322
# Load an SVG file directly into the page
323
# Params:
324
# +path+:: The path of the SVG file (relative to GTN workspace root)
325
# Returns:
326
# +String+:: The SVG file contents
327
#
328
# Example:
329
# {{ "assets/images/mastodon.svg" | load_svg }}
330
def load_svg(path)
331
File.read(path).gsub(/\R+/, '')
332
end
333
334
def regex_replace(str, regex_search, value_replace)
335
regex = /#{regex_search}/m
336
str.gsub(regex, value_replace)
337
end
338
339
##
340
# This method does a single regex replacement
341
#
342
# = Example
343
#
344
# {{ content | regex_replace: '<hr>', '' }}
345
def regex_replace_once(str, regex_search, value_replace)
346
regex = /#{regex_search}/m
347
str.sub(regex, value_replace)
348
end
349
350
##
351
# Check if a match is found
352
def matches(str, regex_search)
353
r = /#{regex_search}/
354
str.match?(r)
355
end
356
357
def convert_to_material_list(site, materials)
358
# [{"name"=>"introduction", "topic"=>"admin"}]
359
return [] if materials.nil?
360
361
materials.map do |m|
362
if m.key?('name') && m.key?('topic')
363
found = Gtn::TopicFilter.fetch_tutorial_material(site, m['topic'], m['name'])
364
Jekyll.logger.warn "Could not find material #{m['topic']}/#{m['name']} in the site data" if found.nil?
365
366
if m.key?('time')
367
found['time'] = m['time']
368
else
369
found['time'] = nil
370
end
371
found
372
elsif m.key?('type') && m['type'] == 'faq'
373
{
374
'type' => 'faq',
375
'name' => m['name'],
376
'title' => m['name'],
377
'faq_url' => m['link'],
378
}
379
elsif m.key?('external') && m['external']
380
{
381
'layout' => 'tutorial_hands_on',
382
'name' => m['name'],
383
'title' => m['name'],
384
'hands_on' => 'external',
385
'hands_on_url' => m['link'],
386
}
387
elsif m.key?('type') && m['type'] == 'custom'
388
{
389
'layout' => 'custom',
390
'name' => m['name'],
391
'title' => m['name'],
392
'description' => m['description'],
393
'time' => m['time'],
394
}
395
else
396
Jekyll.logger.warn "[GTN] Unsure how to render #{m}"
397
end
398
end
399
end
400
401
##
402
# Convert a workflow path to a TRS path
403
# Params:
404
# +str+:: The workflow path
405
# Returns:
406
# +String+:: The TRS path
407
#
408
# Example:
409
# {{ "topics/metagenomics/tutorials/mothur-miseq-sop-short/workflows/workflow1_quality_control.ga" |
410
# convert_workflow_path_to_trs }}
411
# => "/api/ga4gh/trs/v2/tools/metagenomics-mothur-miseq-sop-short/versions/workflow1_quality_control"
412
def convert_workflow_path_to_trs(str)
413
return 'GTN_TRS_ERROR_NIL' if str.nil?
414
415
m = str.match(%r{topics/(?<topic>.*)/tutorials/(?<tutorial>.*)/workflows/(?<workflow>.*)\.ga})
416
return "/api/ga4gh/trs/v2/tools/#{m[:topic]}-#{m[:tutorial]}/versions/#{m[:workflow].downcase}" if m
417
418
'GTN_TRS_ERROR'
419
end
420
421
def layout_to_human(layout)
422
case layout
423
when /_slides/ # excludes slides-plain
424
'Slides'
425
when /tutorial_hands_on/
426
'Hands-on'
427
when 'faq'
428
'FAQs'
429
when 'news'
430
'News'
431
when 'workflow'
432
'Workflow'
433
end
434
end
435
436
def get_version_number(page)
437
Gtn::ModificationTimes.obtain_modification_count(page['path'])
438
end
439
440
def get_rating_histogram(site, material_id, recent: false)
441
return {} if material_id.nil?
442
443
feedbacks = recent ? get_recent_feedbacks_time(site, material_id) : get_feedbacks(site, material_id)
444
445
return {} if feedbacks.nil? || feedbacks.empty?
446
447
ratings = feedbacks.map { |f| f['rating'] }
448
ratings.each_with_object(Hash.new(0)) { |w, counts| counts[w] += 1 }
449
end
450
451
def get_rating_histogram_chart(site, material_id)
452
histogram = get_rating_histogram(site, material_id)
453
return {} if histogram.empty?
454
455
highest = histogram.map { |_k, v| v }.max
456
histogram
457
.map { |k, v| [k, [v, v / highest.to_f]] }
458
.sort_by { |k, _v| -k }
459
.to_h
460
end
461
462
def get_rating(site, material_id, recent: false)
463
f = get_rating_histogram(site, material_id, recent: recent)
464
rating = f.map { |k, v| k * v }.sum / f.map { |_k, v| v }.sum.to_f
465
rating.round(1)
466
end
467
468
def get_rating_recent(site, material_id)
469
r = get_rating(site, material_id, recent: true)
470
r.nan? ? get_rating(site, material_id, recent: false) : r
471
end
472
473
# Only accepts an integer rating
474
def to_stars(rating)
475
if rating.nil? || (rating.to_i < 1) || (rating == '0') || rating.zero?
476
%(<span class="sr-only">0 stars</span>) +
477
'<i class="far fa-star" aria-hidden="true"></i>'
478
elsif rating.to_i < 1
479
'<span class="sr-only">0 stars</span><i class="far fa-star" aria-hidden="true"></i>'
480
else
481
%(<span class="sr-only">#{rating} stars</span>) +
482
('<i class="fa fa-star" aria-hidden="true"></i>' * rating.to_i)
483
end
484
end
485
486
def get_feedbacks(site, material_id)
487
return [] if material_id.nil?
488
489
begin
490
topic, tutorial = material_id.split('/')
491
492
if tutorial.include?(':')
493
language = tutorial.split(':')[1]
494
tutorial = tutorial.split(':')[0]
495
# If a language is supplied, then
496
feedbacks = site.data['feedback2'][topic][tutorial]
497
.select { |f| (f['lang'] || '').downcase == language.downcase }
498
else
499
# English is the default
500
feedbacks = site.data['feedback2'][topic][tutorial]
501
.select { |f| f['lang'].nil? }
502
end
503
rescue StandardError
504
return []
505
end
506
507
return [] if feedbacks.nil? || feedbacks.empty?
508
509
feedbacks
510
.sort_by { |f| f['date'] }
511
.reverse
512
.map do |f|
513
f['stars'] = to_stars(f['rating'])
514
f
515
end
516
end
517
518
def get_feedback_count(site, material_id)
519
get_feedbacks(site, material_id).length
520
end
521
522
def get_feedback_count_recent(site, material_id)
523
get_recent_feedbacks_time(site, material_id).length
524
end
525
526
def get_recent_feedbacks_time(site, material_id)
527
feedbacks = get_feedbacks(site, material_id)
528
.select do |f|
529
f['pro']&.length&.positive? ||
530
f['con']&.length&.positive?
531
end
532
.map do |f|
533
f['f_date'] = Date.parse(f['date']).strftime('%B %Y')
534
f
535
end
536
537
feedbacks.select { |f| Date.parse(f['date']) > Date.today - 365 }
538
end
539
540
def get_recent_feedbacks(site, material_id)
541
feedbacks = get_feedbacks(site, material_id)
542
.select do |f|
543
f['pro']&.length&.positive? ||
544
f['con']&.length&.positive?
545
end
546
.map do |f|
547
f['f_date'] = Date.parse(f['date']).strftime('%B %Y')
548
f
549
end
550
551
last_year = feedbacks.select { |f| Date.parse(f['date']) > Date.today - 365 }
552
# If we have fewer than 20 in the last year, then extend further.
553
if last_year.length < 20
554
feedbacks
555
.first(20)
556
.group_by { |f| f['f_date'] }
557
else
558
# Otherwise just everything last year.
559
last_year
560
.group_by { |f| f['f_date'] }
561
end
562
end
563
564
def tutorials_over_time_bar_chart(site)
565
graph = Hash.new(0)
566
Gtn::TopicFilter.list_all_materials(site).each do |material|
567
if material['pub_date']
568
yymm = material['pub_date'].strftime('%Y-%m')
569
graph[yymm] += 1
570
end
571
end
572
573
# Cumulative over time
574
# https://stackoverflow.com/questions/71745593/how-to-do-a-single-line-cumulative-count-for-hash-values-in-ruby
575
graph
576
# Turns it into an array
577
.sort_by { |k, _v| k }
578
# Cumulative sum
579
.each_with_object([]) { |(k, v), a| a << [k, v + a.last&.last.to_i] }.to_h
580
.map { |k, v| { 'x' => k, 'y' => v } }
581
.to_json
582
end
583
584
def list_usegalaxy_servers(_site)
585
Gtn::Usegalaxy.servers.map { |x| x.transform_keys(&:to_s) }
586
end
587
588
def list_usegalaxy_servers_shuffle(_site)
589
Gtn::Usegalaxy.servers.map { |x| x.transform_keys(&:to_s) }.shuffle
590
end
591
592
def topic_name_from_page(page, site)
593
if page.key? 'topic_name'
594
site.data[page['topic_name']]['title']
595
elsif page['url'] =~ /^\/faqs\/gtn/
596
'GTN FAQ'
597
elsif page['url'] =~ /^\/faqs\/galaxy/
598
'Galaxy FAQ'
599
else
600
site.data.fetch(page['url'].split('/')[2], { 'title' => '' })['title']
601
end
602
end
603
604
def format_location(location)
605
url = 'https://www.openstreetmap.org/search?query='
606
# location:
607
# name: Bioinf Dept
608
# address: 42 E Main St.
609
# city: Reyjkjavik
610
# country: Iceland
611
# #region: # optional
612
# postcode: 912NM
613
loc = [
614
location.fetch('name', nil),
615
location.fetch('address', nil),
616
location.fetch('city', nil),
617
location.fetch('region', nil),
618
location.fetch('country', nil),
619
location.fetch('postcode', nil)
620
].compact
621
622
if loc.length > 1
623
"<a href=\"#{url}#{loc.join(', ')}\">#{loc.join(', ')}</a>"
624
else
625
# Just e.g. the name
626
loc.join(', ')
627
end
628
end
629
630
def format_location_simple(location)
631
loc = [
632
location.fetch('name', nil),
633
location.fetch('address', nil),
634
location.fetch('city', nil),
635
location.fetch('region', nil),
636
location.fetch('country', nil),
637
location.fetch('postcode', nil)
638
].compact
639
640
loc.join(', ')
641
end
642
643
def format_location_short(location)
644
url = 'https://www.openstreetmap.org/search?query='
645
# location:
646
# name: Bioinf Dept
647
# address: 42 E Main St.
648
# city: Reyjkjavik
649
# country: Iceland
650
# #region: # optional
651
# postcode: 912NM
652
loc = [
653
location.fetch('name', nil),
654
location.fetch('address', nil),
655
location.fetch('city', nil),
656
location.fetch('region', nil),
657
location.fetch('country', nil),
658
location.fetch('postcode', nil)
659
].compact
660
661
loc2 = [
662
location.fetch('name', nil),
663
location.fetch('city', nil),
664
location.fetch('country', nil)
665
].compact
666
667
if loc.length > 1
668
"<a href=\"#{url}#{loc.join(', ')}\">#{loc2.join(', ')}</a>"
669
else
670
# Just e.g. the name
671
loc.join(', ')
672
end
673
end
674
675
def collapse_date_pretty(event)
676
collapse_event_date_pretty(event)
677
end
678
679
##
680
# Get the topic of a page's path
681
# Params:
682
# +page+:: The page to get the topic of, it will inspect page['path']
683
# Returns:
684
# +String+:: The topic of the page
685
#
686
# Example:
687
# {{ page | get_topic }}
688
def get_topic(page)
689
# Arrays that will store all introduction slides and tutorials we discover.
690
page['path'].split('/')[1]
691
end
692
693
##
694
# Get the list of 'upcoming' events (i.e. reg deadline or start is 30 days away.)
695
# Params:
696
# +site+:: The site object
697
# Returns:
698
# +Array+:: List of events
699
#
700
# Example:
701
# {{ site | get_upcoming_events }}
702
def get_upcoming_events(site)
703
cache.getset('upcoming-events') do
704
site.pages
705
.select { |p| p.data['layout'] == 'event' || p.data['layout'] == 'event-external' }
706
.reject { |p| p.data['program'].nil? } # Only those with programs
707
.select { |p| p.data['event_upcoming'] == true } # Only those coming soon
708
.map do |p|
709
materials = p.data['program']
710
.map { |section| section['tutorials'] }
711
.flatten
712
.compact # Remove nil entries
713
.reject { |x| x.fetch('type', nil) == 'custom' } # Remove custom entries
714
.map { |x| "#{x['topic']}/#{x['name']}" } # Just the material IDs.
715
.sort.uniq
716
[p, materials]
717
end
718
end
719
end
720
721
##
722
# Get the list of 'upcoming' events that include this material's ID
723
# Params:
724
# +site+:: The site object
725
# +material+:: The 'material' to get the topic of, it will inspect page.id (use new_material)
726
# Returns:
727
# +Array+:: List of events
728
#
729
# Example:
730
#
731
# {{ site | get_upcoming_events }}
732
def get_upcoming_events_for_this(site, material)
733
if material.nil?
734
[]
735
else
736
get_upcoming_events(site)
737
.select { |_p, materials| materials.include? material['id'] }
738
.map { |p, _materials| p }
739
end
740
end
741
742
##
743
# Get the list of all videos for the site (the automated + manual.)
744
# Params:
745
# +site+:: The site object
746
# Returns:
747
# +Array+:: List of [topic_id, topic_name, automated_videos, manual_videos]
748
#
749
# Example:
750
#
751
# {{ site | get_videos_for_videos_page }}
752
def get_videos_for_videos_page(site)
753
res = {}
754
Gtn::TopicFilter.list_all_materials(site).each do |material|
755
next unless material['video'] || material['recordings'] || material['slides_recordings']
756
757
if ! res.key? material['topic_name']
758
res[material['topic_name']] = {
759
'topic_id' => material['topic_name'],
760
'topic_name' => site.data[material['topic_name']]['title'],
761
'automated_videos' => [],
762
'manual_videos' => []
763
}
764
end
765
766
# Automated recording
767
if material['video']
768
vid = "#{material['topic_name']}/tutorials/#{material['tutorial_name']}/slides"
769
res[material['topic_name']]['automated_videos'].push({
770
'title' => material['title'],
771
'vid' => vid,
772
'type' => 'internal',
773
'speakers' => ['awspolly'],
774
'captioners' => Gtn::Contributors.get_authors(material),
775
'cover' => "https://training.galaxyproject.org/videos/topics/#{vid}.mp4.png"
776
})
777
end
778
779
if material['slides_recordings']
780
rec = material['slides_recordings'].max_by { |x| x['date'] }
781
res[material['topic_name']]['manual_videos'].push({
782
'title' => material['title'],
783
'vid' => rec['youtube_id'],
784
'type' => 'youtube',
785
'speakers' => rec['speakers'],
786
'captioners' => rec['captioners'],
787
'cover' => "https://img.youtube.com/vi/#{rec['youtube_id']}/sddefault.jpg"
788
})
789
end
790
791
if material['recordings']
792
rec = material['recordings'].max_by { |x| x['date'] }
793
res[material['topic_name']]['manual_videos'].push({
794
'title' => material['title'],
795
'vid' => rec['youtube_id'],
796
'type' => 'youtube',
797
'speakers' => rec['speakers'],
798
'captioners' => rec['captioners'],
799
'cover' => "https://img.youtube.com/vi/#{rec['youtube_id']}/sddefault.jpg"
800
})
801
end
802
end
803
804
res.each do |k, v|
805
if v['automated_videos'].empty?
806
v.delete('automated_videos')
807
end
808
if v['manual_videos'].empty?
809
v.delete('manual_videos')
810
end
811
end
812
813
res
814
end
815
816
def shuffle(array)
817
array.shuffle
818
end
819
820
def unix_time_to_date(time)
821
Time.at(time.to_i).strftime('%Y-%m-%d %H:%M:%S')
822
end
823
824
def is_date_passed(date)
825
if date.nil?
826
false
827
elsif date.is_a?(String)
828
Date.parse(date) < Date.today
829
else
830
date < Date.today
831
end
832
end
833
834
def get_og_desc(site, page); end
835
836
def get_og_title(site, page, reverse)
837
og_title = []
838
topic_id = page['path'].gsub(%r{^\./}, '').split('/')[1]
839
840
if site.data.key?(topic_id)
841
if site.data[topic_id].is_a?(Hash) && site.data[topic_id].key?('title')
842
og_title = [site.data[topic_id]['title'].clone]
843
else
844
Jekyll.logger.warn "Missing title for #{topic_id}"
845
end
846
end
847
848
if page['layout'] == 'topic'
849
og_title.push 'Tutorial List'
850
return og_title.join(' / ')
851
end
852
853
material_id = page['path'].gsub(%r{^\./}, '').split('/')[3]
854
material = nil
855
material = fetch_tutorial_material(site, topic_id, material_id) if site.data.key? topic_id
856
857
og_title.push material['title'] if !material.nil?
858
859
case page['layout']
860
when 'workflow-list'
861
og_title.push 'Workflows'
862
when 'faq-page', 'faqs'
863
if page['path'] =~ %r{faqs/gtn}
864
og_title.push 'GTN FAQs'
865
elsif page['path'] =~ %r{faqs/galaxy}
866
og_title.push 'Galaxy FAQs'
867
else
868
og_title.push 'FAQs'
869
end
870
when 'faq'
871
og_title.push "FAQ: #{page['title']}"
872
when 'learning-pathway'
873
og_title.push "Learning Pathway: #{page['title']}"
874
when 'tutorial_hands_on'
875
og_title.push "Hands-on: #{page['title']}"
876
when /slides/
877
og_title.push "Slides: #{page['title']}"
878
else
879
og_title.push page['title']
880
end
881
882
if reverse.to_s == 'true'
883
og_title.compact.reverse.join(' / ').gsub(/Hands-on: Hands-on:/, 'Hands-on:')
884
else
885
og_title.compact.join(' / ').gsub(/Hands-on: Hands-on:/, 'Hands-on:')
886
end
887
end
888
889
##
890
# Gets the 'default' link for a material, hands on if it exists, otherwise slides.
891
# Params:
892
# +material+:: The material to get the link for
893
# Returns:
894
# +String+:: The URL of the default link
895
def get_default_link(material)
896
return 'NO LINK' if material.nil?
897
return 'NO LINK' if material == true
898
899
url = nil
900
901
url = "topics/#{material['topic_name']}/tutorials/#{material['tutorial_name']}/slides.html" if material['slides']
902
903
if material['hands_on'] && (material['hands_on'] != 'external' && material['hands_on'] != '')
904
url = "topics/#{material['topic_name']}/tutorials/#{material['tutorial_name']}/tutorial.html"
905
end
906
907
url
908
end
909
910
def group_icons(icons)
911
icons.group_by { |_k, v| v }.transform_values { |v| v.map { |z| z[0] } }.invert
912
end
913
914
def materials_for_pathway(page)
915
d = if page.is_a?(Jekyll::Page)
916
page.data.fetch('pathway', [])
917
else
918
page.fetch('pathway', [])
919
end
920
921
d.map do |m|
922
m.fetch('tutorials', [])
923
.select { |t| t.key?('name') && t.key?('topic') }
924
.map { |t| [t['topic'], t['name']] }
925
end.flatten.compact.sort.uniq
926
end
927
928
def find_learningpaths_including_topic(site, topic_id)
929
site.pages
930
.select { |p| p['layout'] == 'learning-pathway' }
931
.select do |p|
932
materials_for_pathway(p)
933
.map { |topic, _tutorial| topic }
934
.include?(topic_id)
935
end
936
end
937
# rubocop:enable Naming/PredicateName
938
end
939
end
940
941
Liquid::Template.register_filter(Jekyll::GtnFunctions)
942
943
##
944
# We're going to do some find and replace, to replace `@gtn:contributorName` with a link to their profile.
945
Jekyll::Hooks.register :site, :pre_render do |site|
946
pfo_keys = site.data['contributors'].keys + site.data['grants'].keys + site.data['organisations'].keys
947
site.posts.docs.each do |post|
948
if post.content
949
post.content = post.content.gsub(/@gtn:([a-zA-Z0-9_-]+)/) do |match|
950
# Get first capture
951
name = match.gsub('@gtn:', '')
952
if pfo_keys.include?(name)
953
"{% include _includes/contributor-badge-inline.html id=\"#{name}\" %}"
954
else
955
match
956
end
957
end
958
end
959
end
960
site.pages.each do |page|
961
if page.content
962
page.content = page.content.gsub(/@gtn:([a-zA-Z0-9_-]+)/) do |match|
963
name = match.gsub('@gtn:', '')
964
if pfo_keys.include?(name)
965
"{% include _includes/contributor-badge-inline.html id=\"#{name}\" %}"
966
else
967
match
968
end
969
end
970
971
# This would also need to modify the box types themselves, not sure how is best to do that.
972
page.content = page.content.gsub(/> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/) do |match|
973
if match =~ /(CAUTION|WARNING)/
974
'> <warning-title></warning-title>'
975
elsif match =~ /TIP/
976
'> <tip-title></tip-title>'
977
else
978
'> <comment-title></comment-title>'
979
end
980
end
981
end
982
end
983
end
984
985
# Create back-refs for affiliations
986
Jekyll::Hooks.register :site, :post_read do |site|
987
# Users list affiliations on their profile in site.data['contributors']
988
# And we want to create a back-ref to the user from the affiliation
989
site.data['contributors'].each do |name, contributor|
990
if contributor.key?('affiliations')
991
contributor['affiliations'].each do |affiliation|
992
if site.data['organisations'].key?(affiliation)
993
if !site.data['organisations'][affiliation].key?('members')
994
site.data['organisations'][affiliation]['members'] = []
995
end
996
997
site.data['organisations'][affiliation]['members'] << name
998
elsif site.data['grants'].key?(affiliation)
999
site.data['grants'][affiliation]['members'] = [] if !site.data['grants'][affiliation].key?('members')
1000
1001
site.data['grants'][affiliation]['members'] << name
1002
end
1003
end
1004
end
1005
1006
if contributor.key?('former_affiliations')
1007
contributor['former_affiliations'].each do |affiliation|
1008
if site.data['organisations'].key?(affiliation)
1009
if !site.data['organisations'][affiliation].key?('former_members')
1010
site.data['organisations'][affiliation]['former_members'] = []
1011
end
1012
1013
site.data['organisations'][affiliation]['former_members'] << name
1014
elsif site.data['grants'].key?(affiliation)
1015
if !site.data['grants'][affiliation].key?('former_members')
1016
site.data['grants'][affiliation]['former_members'] = []
1017
end
1018
1019
site.data['grants'][affiliation]['former_members'] << name
1020
end
1021
end
1022
end
1023
end
1024
1025
# Add shortlinks
1026
Jekyll.logger.info '[GTN] Loading shortlinks'
1027
shortlinks = site.data['shortlinks']
1028
shortlinks_reversed = shortlinks['id'].invert
1029
1030
posts = if site.posts.respond_to?(:docs)
1031
site.posts.docs
1032
else
1033
site.posts
1034
end
1035
1036
posts.each do |post|
1037
post.data['short_id'] = shortlinks_reversed[post.url]
1038
end
1039
1040
site.pages.each do |page|
1041
page.data['short_id'] = shortlinks_reversed[page.url]
1042
end
1043
1044
# Annotate symlinks
1045
site.pages.each do |page|
1046
page.data['symlink'] = File.symlink?(page.path)
1047
# Elsewhere we checked more levels deep, maybe enable if needed.
1048
# || File.symlink?(File.dirname(page.path)) || File.symlink?(File.dirname(File.dirname(page.path)))
1049
end
1050
1051
Jekyll.logger.info '[GTN] Annotating events'
1052
site.pages.select { |p| p.data['layout'] == 'event' || p.data['layout'] == 'event-external' }.each do |page|
1053
1054
unless page.data['date_start']
1055
# if no date set, use a mock date to prevent build from failihng
1056
page.data['date_start'] = Date.parse('2121-01-01')
1057
end
1058
page.data['not_started'] = page.data['date_start'] > Date.today
1059
page.data['event_over'] = (page.data['date_end'] || page.data['date_start']) < Date.today
1060
1061
event_start = page.data['date_start']
1062
event_end = page.data['date_end'] || page.data['date_start']
1063
1064
page.data['event_state'] = if Date.today < event_start
1065
'upcoming'
1066
elsif (event_start - 3) < Date.today && Date.today < (event_end + 3) # Some lee way
1067
'ongoing'
1068
else
1069
'ended'
1070
end
1071
1072
page.data['duration'] = if page.data['date_end'].nil?
1073
1
1074
else
1075
(page.data['date_end'] - page.data['date_start']).to_i + 1
1076
end
1077
1078
# reg deadline
1079
deadline = if page.data.key?('registration') && page.data['registration'].key?('deadline')
1080
page.data['registration']['deadline']
1081
else
1082
page.data['date_start']
1083
end
1084
1085
# If it's an 'upcoming event'
1086
if deadline - 30 <= Date.today && Date.today <= deadline
1087
page.data['event_upcoming'] = true
1088
end
1089
end
1090
1091
# This exists because the jekyll-feed plugin expects those fields to look like that.
1092
posts.each do |post|
1093
post.data['author'] = Gtn::Contributors.get_authors(post.data).map do |c|
1094
Gtn::Contributors.fetch_name(post.site, c)
1095
end.join(', ')
1096
if post.data.key? 'cover'
1097
post.data['image'] = post.data['cover']
1098
end
1099
end
1100
end
1101
1102
Jekyll::Hooks.register :site, :post_write do |site|
1103
if Jekyll.env == 'production'
1104
Gtn::Shortlinks.fix_missing_redirs(site)
1105
end
1106
end
1107
1108
if $PROGRAM_NAME == __FILE__
1109
result = Gtn::ModificationTimes.obtain_time(ARGV[0].gsub(%r{^/}, ''))
1110
puts "Modification time of #{ARGV[0].gsub(%r{^/}, '')} is #{result}"
1111
end
1112
1113