Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/gtn.rb
1677 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
end
369
found
370
elsif m.key?('type') && m['type'] == 'faq'
371
{
372
'type' => 'faq',
373
'name' => m['name'],
374
'title' => m['name'],
375
'faq_url' => m['link'],
376
}
377
elsif m.key?('external') && m['external']
378
{
379
'layout' => 'tutorial_hands_on',
380
'name' => m['name'],
381
'title' => m['name'],
382
'hands_on' => 'external',
383
'hands_on_url' => m['link'],
384
}
385
elsif m.key?('type') && m['type'] == 'custom'
386
{
387
'layout' => 'custom',
388
'name' => m['name'],
389
'title' => m['name'],
390
'description' => m['description'],
391
'time' => m['time'],
392
}
393
else
394
Jekyll.logger.warn "[GTN] Unsure how to render #{m}"
395
end
396
end
397
end
398
399
##
400
# Convert a workflow path to a TRS path
401
# Params:
402
# +str+:: The workflow path
403
# Returns:
404
# +String+:: The TRS path
405
#
406
# Example:
407
# {{ "topics/metagenomics/tutorials/mothur-miseq-sop-short/workflows/workflow1_quality_control.ga" |
408
# convert_workflow_path_to_trs }}
409
# => "/api/ga4gh/trs/v2/tools/metagenomics-mothur-miseq-sop-short/versions/workflow1_quality_control"
410
def convert_workflow_path_to_trs(str)
411
return 'GTN_TRS_ERROR_NIL' if str.nil?
412
413
m = str.match(%r{topics/(?<topic>.*)/tutorials/(?<tutorial>.*)/workflows/(?<workflow>.*)\.ga})
414
return "/api/ga4gh/trs/v2/tools/#{m[:topic]}-#{m[:tutorial]}/versions/#{m[:workflow].downcase}" if m
415
416
'GTN_TRS_ERROR'
417
end
418
419
def layout_to_human(layout)
420
case layout
421
when /_slides/ # excludes slides-plain
422
'Slides'
423
when /tutorial_hands_on/
424
'Hands-on'
425
when 'faq'
426
'FAQs'
427
when 'news'
428
'News'
429
when 'workflow'
430
'Workflow'
431
end
432
end
433
434
def get_version_number(page)
435
Gtn::ModificationTimes.obtain_modification_count(page['path'])
436
end
437
438
def get_rating_histogram(site, material_id, recent: false)
439
return {} if material_id.nil?
440
441
feedbacks = recent ? get_recent_feedbacks_time(site, material_id) : get_feedbacks(site, material_id)
442
443
return {} if feedbacks.nil? || feedbacks.empty?
444
445
ratings = feedbacks.map { |f| f['rating'] }
446
ratings.each_with_object(Hash.new(0)) { |w, counts| counts[w] += 1 }
447
end
448
449
def get_rating_histogram_chart(site, material_id)
450
histogram = get_rating_histogram(site, material_id)
451
return {} if histogram.empty?
452
453
highest = histogram.map { |_k, v| v }.max
454
histogram
455
.map { |k, v| [k, [v, v / highest.to_f]] }
456
.sort_by { |k, _v| -k }
457
.to_h
458
end
459
460
def get_rating(site, material_id, recent: false)
461
f = get_rating_histogram(site, material_id, recent: recent)
462
rating = f.map { |k, v| k * v }.sum / f.map { |_k, v| v }.sum.to_f
463
rating.round(1)
464
end
465
466
def get_rating_recent(site, material_id)
467
r = get_rating(site, material_id, recent: true)
468
r.nan? ? get_rating(site, material_id, recent: false) : r
469
end
470
471
# Only accepts an integer rating
472
def to_stars(rating)
473
if rating.nil? || (rating.to_i < 1) || (rating == '0') || rating.zero?
474
%(<span class="sr-only">0 stars</span>) +
475
'<i class="far fa-star" aria-hidden="true"></i>'
476
elsif rating.to_i < 1
477
'<span class="sr-only">0 stars</span><i class="far fa-star" aria-hidden="true"></i>'
478
else
479
%(<span class="sr-only">#{rating} stars</span>) +
480
('<i class="fa fa-star" aria-hidden="true"></i>' * rating.to_i)
481
end
482
end
483
484
def get_feedbacks(site, material_id)
485
return [] if material_id.nil?
486
487
begin
488
topic, tutorial = material_id.split('/')
489
490
if tutorial.include?(':')
491
language = tutorial.split(':')[1]
492
tutorial = tutorial.split(':')[0]
493
# If a language is supplied, then
494
feedbacks = site.data['feedback2'][topic][tutorial]
495
.select { |f| (f['lang'] || '').downcase == language.downcase }
496
else
497
# English is the default
498
feedbacks = site.data['feedback2'][topic][tutorial]
499
.select { |f| f['lang'].nil? }
500
end
501
rescue StandardError
502
return []
503
end
504
505
return [] if feedbacks.nil? || feedbacks.empty?
506
507
feedbacks
508
.sort_by { |f| f['date'] }
509
.reverse
510
.map do |f|
511
f['stars'] = to_stars(f['rating'])
512
f
513
end
514
end
515
516
def get_feedback_count(site, material_id)
517
get_feedbacks(site, material_id).length
518
end
519
520
def get_feedback_count_recent(site, material_id)
521
get_recent_feedbacks_time(site, material_id).length
522
end
523
524
def get_recent_feedbacks_time(site, material_id)
525
feedbacks = get_feedbacks(site, material_id)
526
.select do |f|
527
f['pro']&.length&.positive? ||
528
f['con']&.length&.positive?
529
end
530
.map do |f|
531
f['f_date'] = Date.parse(f['date']).strftime('%B %Y')
532
f
533
end
534
535
feedbacks.select { |f| Date.parse(f['date']) > Date.today - 365 }
536
end
537
538
def get_recent_feedbacks(site, material_id)
539
feedbacks = get_feedbacks(site, material_id)
540
.select do |f|
541
f['pro']&.length&.positive? ||
542
f['con']&.length&.positive?
543
end
544
.map do |f|
545
f['f_date'] = Date.parse(f['date']).strftime('%B %Y')
546
f
547
end
548
549
last_year = feedbacks.select { |f| Date.parse(f['date']) > Date.today - 365 }
550
# If we have fewer than 20 in the last year, then extend further.
551
if last_year.length < 20
552
feedbacks
553
.first(20)
554
.group_by { |f| f['f_date'] }
555
else
556
# Otherwise just everything last year.
557
last_year
558
.group_by { |f| f['f_date'] }
559
end
560
end
561
562
def tutorials_over_time_bar_chart(site)
563
graph = Hash.new(0)
564
Gtn::TopicFilter.list_all_materials(site).each do |material|
565
if material['pub_date']
566
yymm = material['pub_date'].strftime('%Y-%m')
567
graph[yymm] += 1
568
end
569
end
570
571
# Cumulative over time
572
# https://stackoverflow.com/questions/71745593/how-to-do-a-single-line-cumulative-count-for-hash-values-in-ruby
573
graph
574
# Turns it into an array
575
.sort_by { |k, _v| k }
576
# Cumulative sum
577
.each_with_object([]) { |(k, v), a| a << [k, v + a.last&.last.to_i] }.to_h
578
.map { |k, v| { 'x' => k, 'y' => v } }
579
.to_json
580
end
581
582
def list_usegalaxy_servers(_site)
583
Gtn::Usegalaxy.servers.map { |x| x.transform_keys(&:to_s) }
584
end
585
586
def list_usegalaxy_servers_shuffle(_site)
587
Gtn::Usegalaxy.servers.map { |x| x.transform_keys(&:to_s) }.shuffle
588
end
589
590
def topic_name_from_page(page, site)
591
if page.key? 'topic_name'
592
site.data[page['topic_name']]['title']
593
elsif page['url'] =~ /^\/faqs\/gtn/
594
'GTN FAQ'
595
elsif page['url'] =~ /^\/faqs\/galaxy/
596
'Galaxy FAQ'
597
else
598
site.data.fetch(page['url'].split('/')[2], { 'title' => '' })['title']
599
end
600
end
601
602
def format_location(location)
603
url = 'https://www.openstreetmap.org/search?query='
604
# location:
605
# name: Bioinf Dept
606
# address: 42 E Main St.
607
# city: Reyjkjavik
608
# country: Iceland
609
# #region: # optional
610
# postcode: 912NM
611
loc = [
612
location.fetch('name', nil),
613
location.fetch('address', nil),
614
location.fetch('city', nil),
615
location.fetch('region', nil),
616
location.fetch('country', nil),
617
location.fetch('postcode', nil)
618
].compact
619
620
if loc.length > 1
621
"<a href=\"#{url}#{loc.join(', ')}\">#{loc.join(', ')}</a>"
622
else
623
# Just e.g. the name
624
loc.join(', ')
625
end
626
end
627
628
def format_location_simple(location)
629
loc = [
630
location.fetch('name', nil),
631
location.fetch('address', nil),
632
location.fetch('city', nil),
633
location.fetch('region', nil),
634
location.fetch('country', nil),
635
location.fetch('postcode', nil)
636
].compact
637
638
loc.join(', ')
639
end
640
641
def format_location_short(location)
642
url = 'https://www.openstreetmap.org/search?query='
643
# location:
644
# name: Bioinf Dept
645
# address: 42 E Main St.
646
# city: Reyjkjavik
647
# country: Iceland
648
# #region: # optional
649
# postcode: 912NM
650
loc = [
651
location.fetch('name', nil),
652
location.fetch('address', nil),
653
location.fetch('city', nil),
654
location.fetch('region', nil),
655
location.fetch('country', nil),
656
location.fetch('postcode', nil)
657
].compact
658
659
loc2 = [
660
location.fetch('name', nil),
661
location.fetch('city', nil),
662
location.fetch('country', nil)
663
].compact
664
665
if loc.length > 1
666
"<a href=\"#{url}#{loc.join(', ')}\">#{loc2.join(', ')}</a>"
667
else
668
# Just e.g. the name
669
loc.join(', ')
670
end
671
end
672
673
def collapse_date_pretty(event)
674
collapse_event_date_pretty(event)
675
end
676
677
##
678
# Get the topic of a page's path
679
# Params:
680
# +page+:: The page to get the topic of, it will inspect page['path']
681
# Returns:
682
# +String+:: The topic of the page
683
#
684
# Example:
685
# {{ page | get_topic }}
686
def get_topic(page)
687
# Arrays that will store all introduction slides and tutorials we discover.
688
page['path'].split('/')[1]
689
end
690
691
##
692
# Get the list of 'upcoming' events (i.e. reg deadline or start is 30 days away.)
693
# Params:
694
# +site+:: The site object
695
# Returns:
696
# +Array+:: List of events
697
#
698
# Example:
699
# {{ site | get_upcoming_events }}
700
def get_upcoming_events(site)
701
cache.getset('upcoming-events') do
702
site.pages
703
.select { |p| p.data['layout'] == 'event' || p.data['layout'] == 'event-external' }
704
.reject { |p| p.data['program'].nil? } # Only those with programs
705
.select { |p| p.data['event_upcoming'] == true } # Only those coming soon
706
.map do |p|
707
materials = p.data['program']
708
.map { |section| section['tutorials'] }
709
.flatten
710
.compact # Remove nil entries
711
.reject { |x| x.fetch('type', nil) == 'custom' } # Remove custom entries
712
.map { |x| "#{x['topic']}/#{x['name']}" } # Just the material IDs.
713
.sort.uniq
714
[p, materials]
715
end
716
end
717
end
718
719
##
720
# Get the list of 'upcoming' events that include this material's ID
721
# Params:
722
# +site+:: The site object
723
# +material+:: The 'material' to get the topic of, it will inspect page.id (use new_material)
724
# Returns:
725
# +Array+:: List of events
726
#
727
# Example:
728
#
729
# {{ site | get_upcoming_events }}
730
def get_upcoming_events_for_this(site, material)
731
if material.nil?
732
[]
733
else
734
get_upcoming_events(site)
735
.select { |_p, materials| materials.include? material['id'] }
736
.map { |p, _materials| p }
737
end
738
end
739
740
##
741
# Get the list of all videos for the site (the automated + manual.)
742
# Params:
743
# +site+:: The site object
744
# Returns:
745
# +Array+:: List of [topic_id, topic_name, automated_videos, manual_videos]
746
#
747
# Example:
748
#
749
# {{ site | get_videos_for_videos_page }}
750
def get_videos_for_videos_page(site)
751
res = {}
752
Gtn::TopicFilter.list_all_materials(site).each do |material|
753
next unless material['video'] || material['recordings'] || material['slides_recordings']
754
755
if ! res.key? material['topic_name']
756
res[material['topic_name']] = {
757
'topic_id' => material['topic_name'],
758
'topic_name' => site.data[material['topic_name']]['title'],
759
'automated_videos' => [],
760
'manual_videos' => []
761
}
762
end
763
764
# Automated recording
765
if material['video']
766
vid = "#{material['topic_name']}/tutorials/#{material['tutorial_name']}/slides"
767
res[material['topic_name']]['automated_videos'].push({
768
'title' => material['title'],
769
'vid' => vid,
770
'type' => 'internal',
771
'speakers' => ['awspolly'],
772
'captioners' => Gtn::Contributors.get_authors(material),
773
'cover' => "https://training.galaxyproject.org/videos/topics/#{vid}.mp4.png"
774
})
775
end
776
777
if material['slides_recordings']
778
rec = material['slides_recordings'].max_by { |x| x['date'] }
779
res[material['topic_name']]['manual_videos'].push({
780
'title' => material['title'],
781
'vid' => rec['youtube_id'],
782
'type' => 'youtube',
783
'speakers' => rec['speakers'],
784
'captioners' => rec['captioners'],
785
'cover' => "https://img.youtube.com/vi/#{rec['youtube_id']}/sddefault.jpg"
786
})
787
end
788
789
if material['recordings']
790
rec = material['recordings'].max_by { |x| x['date'] }
791
res[material['topic_name']]['manual_videos'].push({
792
'title' => material['title'],
793
'vid' => rec['youtube_id'],
794
'type' => 'youtube',
795
'speakers' => rec['speakers'],
796
'captioners' => rec['captioners'],
797
'cover' => "https://img.youtube.com/vi/#{rec['youtube_id']}/sddefault.jpg"
798
})
799
end
800
end
801
802
res.each do |k, v|
803
if v['automated_videos'].empty?
804
v.delete('automated_videos')
805
end
806
if v['manual_videos'].empty?
807
v.delete('manual_videos')
808
end
809
end
810
811
res
812
end
813
814
def shuffle(array)
815
array.shuffle
816
end
817
818
def unix_time_to_date(time)
819
Time.at(time.to_i).strftime('%Y-%m-%d %H:%M:%S')
820
end
821
822
def is_date_passed(date)
823
if date.nil?
824
false
825
elsif date.is_a?(String)
826
Date.parse(date) < Date.today
827
else
828
date < Date.today
829
end
830
end
831
832
def get_og_desc(site, page); end
833
834
def get_og_title(site, page, reverse)
835
og_title = []
836
topic_id = page['path'].gsub(%r{^\./}, '').split('/')[1]
837
838
if site.data.key?(topic_id)
839
if site.data[topic_id].is_a?(Hash) && site.data[topic_id].key?('title')
840
og_title = [site.data[topic_id]['title'].clone]
841
else
842
Jekyll.logger.warn "Missing title for #{topic_id}"
843
end
844
end
845
846
if page['layout'] == 'topic'
847
og_title.push 'Tutorial List'
848
return og_title.join(' / ')
849
end
850
851
material_id = page['path'].gsub(%r{^\./}, '').split('/')[3]
852
material = nil
853
material = fetch_tutorial_material(site, topic_id, material_id) if site.data.key? topic_id
854
855
og_title.push material['title'] if !material.nil?
856
857
case page['layout']
858
when 'workflow-list'
859
og_title.push 'Workflows'
860
when 'faq-page', 'faqs'
861
if page['path'] =~ %r{faqs/gtn}
862
og_title.push 'GTN FAQs'
863
elsif page['path'] =~ %r{faqs/galaxy}
864
og_title.push 'Galaxy FAQs'
865
else
866
og_title.push 'FAQs'
867
end
868
when 'faq'
869
og_title.push "FAQ: #{page['title']}"
870
when 'learning-pathway'
871
og_title.push "Learning Pathway: #{page['title']}"
872
when 'tutorial_hands_on'
873
og_title.push "Hands-on: #{page['title']}"
874
when /slides/
875
og_title.push "Slides: #{page['title']}"
876
else
877
og_title.push page['title']
878
end
879
880
if reverse.to_s == 'true'
881
og_title.compact.reverse.join(' / ').gsub(/Hands-on: Hands-on:/, 'Hands-on:')
882
else
883
og_title.compact.join(' / ').gsub(/Hands-on: Hands-on:/, 'Hands-on:')
884
end
885
end
886
887
##
888
# Gets the 'default' link for a material, hands on if it exists, otherwise slides.
889
# Params:
890
# +material+:: The material to get the link for
891
# Returns:
892
# +String+:: The URL of the default link
893
def get_default_link(material)
894
return 'NO LINK' if material.nil?
895
return 'NO LINK' if material == true
896
897
url = nil
898
899
url = "topics/#{material['topic_name']}/tutorials/#{material['tutorial_name']}/slides.html" if material['slides']
900
901
if material['hands_on'] && (material['hands_on'] != 'external' && material['hands_on'] != '')
902
url = "topics/#{material['topic_name']}/tutorials/#{material['tutorial_name']}/tutorial.html"
903
end
904
905
url
906
end
907
908
def group_icons(icons)
909
icons.group_by { |_k, v| v }.transform_values { |v| v.map { |z| z[0] } }.invert
910
end
911
912
def materials_for_pathway(page)
913
d = if page.is_a?(Jekyll::Page)
914
page.data.fetch('pathway', [])
915
else
916
page.fetch('pathway', [])
917
end
918
919
d.map do |m|
920
m.fetch('tutorials', [])
921
.select { |t| t.key?('name') && t.key?('topic') }
922
.map { |t| [t['topic'], t['name']] }
923
end.flatten.compact.sort.uniq
924
end
925
926
def find_learningpaths_including_topic(site, topic_id)
927
site.pages
928
.select { |p| p['layout'] == 'learning-pathway' }
929
.select do |p|
930
materials_for_pathway(p)
931
.map { |topic, _tutorial| topic }
932
.include?(topic_id)
933
end
934
end
935
# rubocop:enable Naming/PredicateName
936
end
937
end
938
939
Liquid::Template.register_filter(Jekyll::GtnFunctions)
940
941
##
942
# We're going to do some find and replace, to replace `@gtn:contributorName` with a link to their profile.
943
Jekyll::Hooks.register :site, :pre_render do |site|
944
pfo_keys = site.data['contributors'].keys + site.data['grants'].keys + site.data['organisations'].keys
945
site.posts.docs.each do |post|
946
if post.content
947
post.content = post.content.gsub(/@gtn:([a-zA-Z0-9_-]+)/) do |match|
948
# Get first capture
949
name = match.gsub('@gtn:', '')
950
if pfo_keys.include?(name)
951
"{% include _includes/contributor-badge-inline.html id=\"#{name}\" %}"
952
else
953
match
954
end
955
end
956
end
957
end
958
site.pages.each do |page|
959
if page.content
960
page.content = page.content.gsub(/@gtn:([a-zA-Z0-9_-]+)/) do |match|
961
name = match.gsub('@gtn:', '')
962
if pfo_keys.include?(name)
963
"{% include _includes/contributor-badge-inline.html id=\"#{name}\" %}"
964
else
965
match
966
end
967
end
968
969
# This would also need to modify the box types themselves, not sure how is best to do that.
970
page.content = page.content.gsub(/> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/) do |match|
971
if match =~ /(CAUTION|WARNING)/
972
'> <warning-title></warning-title>'
973
elsif match =~ /TIP/
974
'> <tip-title></tip-title>'
975
else
976
'> <comment-title></comment-title>'
977
end
978
end
979
end
980
end
981
end
982
983
# Create back-refs for affiliations
984
Jekyll::Hooks.register :site, :post_read do |site|
985
# Users list affiliations on their profile in site.data['contributors']
986
# And we want to create a back-ref to the user from the affiliation
987
site.data['contributors'].each do |name, contributor|
988
if contributor.key?('affiliations')
989
contributor['affiliations'].each do |affiliation|
990
if site.data['organisations'].key?(affiliation)
991
if !site.data['organisations'][affiliation].key?('members')
992
site.data['organisations'][affiliation]['members'] = []
993
end
994
995
site.data['organisations'][affiliation]['members'] << name
996
elsif site.data['grants'].key?(affiliation)
997
site.data['grants'][affiliation]['members'] = [] if !site.data['grants'][affiliation].key?('members')
998
999
site.data['grants'][affiliation]['members'] << name
1000
end
1001
end
1002
end
1003
1004
if contributor.key?('former_affiliations')
1005
contributor['former_affiliations'].each do |affiliation|
1006
if site.data['organisations'].key?(affiliation)
1007
if !site.data['organisations'][affiliation].key?('former_members')
1008
site.data['organisations'][affiliation]['former_members'] = []
1009
end
1010
1011
site.data['organisations'][affiliation]['former_members'] << name
1012
elsif site.data['grants'].key?(affiliation)
1013
if !site.data['grants'][affiliation].key?('former_members')
1014
site.data['grants'][affiliation]['former_members'] = []
1015
end
1016
1017
site.data['grants'][affiliation]['former_members'] << name
1018
end
1019
end
1020
end
1021
end
1022
1023
# Add shortlinks
1024
Jekyll.logger.info '[GTN] Loading shortlinks'
1025
shortlinks = site.data['shortlinks']
1026
shortlinks_reversed = shortlinks['id'].invert
1027
1028
posts = if site.posts.respond_to?(:docs)
1029
site.posts.docs
1030
else
1031
site.posts
1032
end
1033
1034
posts.each do |post|
1035
post.data['short_id'] = shortlinks_reversed[post.url]
1036
end
1037
1038
site.pages.each do |page|
1039
page.data['short_id'] = shortlinks_reversed[page.url]
1040
end
1041
1042
# Annotate symlinks
1043
site.pages.each do |page|
1044
page.data['symlink'] = File.symlink?(page.path)
1045
# Elsewhere we checked more levels deep, maybe enable if needed.
1046
# || File.symlink?(File.dirname(page.path)) || File.symlink?(File.dirname(File.dirname(page.path)))
1047
end
1048
1049
Jekyll.logger.info '[GTN] Annotating events'
1050
site.pages.select { |p| p.data['layout'] == 'event' || p.data['layout'] == 'event-external' }.each do |page|
1051
1052
unless page.data['date_start']
1053
# if no date set, use a mock date to prevent build from failihng
1054
page.data['date_start'] = Date.parse('2121-01-01')
1055
end
1056
page.data['not_started'] = page.data['date_start'] > Date.today
1057
page.data['event_over'] = (page.data['date_end'] || page.data['date_start']) < Date.today
1058
1059
event_start = page.data['date_start']
1060
event_end = page.data['date_end'] || page.data['date_start']
1061
1062
page.data['event_state'] = if Date.today < event_start
1063
'upcoming'
1064
elsif (event_start - 3) < Date.today && Date.today < (event_end + 3) # Some lee way
1065
'ongoing'
1066
else
1067
'ended'
1068
end
1069
1070
page.data['duration'] = if page.data['date_end'].nil?
1071
1
1072
else
1073
(page.data['date_end'] - page.data['date_start']).to_i + 1
1074
end
1075
1076
# reg deadline
1077
deadline = if page.data.key?('registration') && page.data['registration'].key?('deadline')
1078
page.data['registration']['deadline']
1079
else
1080
page.data['date_start']
1081
end
1082
1083
# If it's an 'upcoming event'
1084
if deadline - 30 <= Date.today && Date.today <= deadline
1085
page.data['event_upcoming'] = true
1086
end
1087
end
1088
1089
# This exists because the jekyll-feed plugin expects those fields to look like that.
1090
posts.each do |post|
1091
post.data['author'] = Gtn::Contributors.get_authors(post.data).map do |c|
1092
Gtn::Contributors.fetch_name(post.site, c)
1093
end.join(', ')
1094
if post.data.key? 'cover'
1095
post.data['image'] = post.data['cover']
1096
end
1097
end
1098
end
1099
1100
Jekyll::Hooks.register :site, :post_write do |site|
1101
if Jekyll.env == 'production'
1102
Gtn::Shortlinks.fix_missing_redirs(site)
1103
end
1104
end
1105
1106
if $PROGRAM_NAME == __FILE__
1107
result = Gtn::ModificationTimes.obtain_time(ARGV[0].gsub(%r{^/}, ''))
1108
puts "Modification time of #{ARGV[0].gsub(%r{^/}, '')} is #{result}"
1109
end
1110
1111