Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/jekyll-jsonld.rb
2607 views
1
# frozen_string_literal: true
2
3
require 'json'
4
require './_plugins/gtn'
5
require './_plugins/gtn/git'
6
require './_plugins/util'
7
8
module Jekyll
9
module Filters
10
11
# Generate JSON-LD metadata for the GTN.
12
module JsonldFilter
13
GTN = {
14
'@type': 'Organization',
15
'http://purl.org/dc/terms/conformsTo': {
16
# Bioschemas profile
17
'@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT',
18
'@type': 'CreativeWork'
19
},
20
id: 'https://training.galaxyproject.org',
21
email: '[email protected]',
22
name: 'Galaxy Training Network',
23
description: 'An organization providing a collection of tutorials developed and maintained by the worldwide Galaxy community',
24
legalName: 'Galaxy Training Network',
25
alternateName: 'GTN',
26
url: 'https://training.galaxyproject.org',
27
logo: 'https://training.galaxyproject.org/training-material/assets/images/GTNLogo1000.png',
28
keywords: %w[galaxy bioinformatics training fair accessible],
29
status: 'active',
30
foundingDate: Gtn::Git.discover['founding_date'].to_s,
31
}.freeze
32
33
A11Y = {
34
accessMode: %w[textual visual],
35
accessModeSufficient: %w[textual visual],
36
# "accessibilityAPI": ,
37
accessibilityControl: %w[fullKeyboardControl fullMouseControl],
38
accessibilityFeature: %w[alternativeText tableOfContents],
39
# "accessibilityHazard": [],
40
accessibilitySummary: 'The text aims to be as accessible as possible. Image descriptions will vary per ' \
41
'tutorial, from images being completely inaccessible, to images with good descriptions ' \
42
'for non-visual users.',
43
}.freeze
44
45
EDU_ROLES = {
46
'use' => 'Students',
47
'admin-dev' => 'Galaxy Administrators',
48
'basics' => 'Students',
49
'data-science' => 'Data-Science Students',
50
'instructors' => 'Instructors',
51
}
52
53
##
54
# Generate the Dublin Core metadata for a material.
55
# Parmaeters:
56
# +material+:: The material to generate the metadata for.
57
# +site+:: The site object.
58
# Returns:
59
# A string containing the metadata.
60
#
61
# Example:
62
# {{ material | generate_dublin_core: site }}
63
# => <meta name="DC.identifier" content="..." />
64
def generate_dublin_core(material, site)
65
return if material.key?('data') && material['data'].fetch('type', 'none') != 'tutorial_hands_on'
66
67
attributes = [
68
['DC.identifier', site['github_repository']],
69
['DC.type', 'text'],
70
['DC.title', material['title']],
71
['DC.publisher', 'Galaxy Training Network'],
72
['DC.date', Gtn::ModificationTimes.obtain_time(material['path'])]
73
]
74
75
attributes += Gtn::Contributors.get_authors(material).map do |user|
76
['DC.creator', Gtn::Contributors.fetch_name(site, user)]
77
end
78
79
attributes.map { |a, b| "<meta name=\"#{a}\" content=\"#{b}\">" }.join("\n")
80
end
81
82
##
83
# Generate the JSON-LD metadata for a person
84
# Parameters:
85
# +id+:: The id of the person.
86
# +contributor+:: The contributor object from CONTRIBUTORS.yaml.
87
# +site+:: The site object.
88
# Returns:
89
# +Hash+:: The JSON-LD metadata.
90
#
91
# Example:
92
# generate_person_jsonld("hexylena", site['data']['contributors']['hexylena'], site)
93
# => {
94
# "@context": "https://schema.org",
95
# "@type": "Person",
96
# "http://purl.org/dc/terms/conformsTo": {
97
# # Bioschemas profile
98
# "@id": "https://bioschemas.org/profiles/Person/0.2-DRAFT-2019_07_19",
99
# "@type": "Person"
100
# },
101
# "url": "https://training.galaxyproject.org/hall-of-fame/hexylena/",
102
# "mainEntityOfPage": "https://training.galaxyproject.org/hall-of-fame/hexylena/",
103
# "name": "hexylena",
104
# "image": "https://avatars.githubusercontent.com/hexylena",
105
# "description": "A contributor to the GTN project.",
106
# "memberOf": [...],
107
# "identifier": "https://orcid.org/0000-0002-6601-2165",
108
# "orcid": "https://orcid.org/0000-0002-6601-2165"
109
# }
110
#
111
def generate_person_jsonld(id, contributor, site)
112
member_of = Gtn::Contributors.fetch_contributor(site, id)['affiliations'] || []
113
member_of = member_of.map do |org_id|
114
org = Gtn::Contributors.fetch_contributor(site, org_id)
115
generate_org_jsonld(org_id, org, site)
116
end
117
118
person = {
119
'@context': 'https://schema.org',
120
'@type': 'Person',
121
'http://purl.org/dc/terms/conformsTo': {
122
'@id': 'https://bioschemas.org/profiles/Person/0.3-DRAFT',
123
'@type': 'CreativeWork'
124
},
125
# I guess these are identical?
126
url: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/",
127
mainEntityOfPage: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/",
128
name: Gtn::Contributors.fetch_name(site, id),
129
image: "https://avatars.githubusercontent.com/#{id}",
130
# No clue what to put here it's a person.
131
description: if contributor.nil?
132
'A contributor to the GTN project.'
133
else
134
contributor.fetch('bio',
135
'A contributor to the GTN project.')
136
end,
137
memberOf: [GTN] + member_of,
138
}
139
if !contributor.nil? && contributor.key?('orcid') && contributor['orcid']
140
person['identifier'] = "https://orcid.org/#{contributor['orcid']}"
141
person['@id'] = "https://orcid.org/#{contributor['orcid']}"
142
end
143
144
person
145
end
146
147
##
148
# Generate the JSON-LD metadata for an organisation
149
# Parameters:
150
# +id+:: The id of the org.
151
# +contributor+:: The contributor object from ORGANISATIONS.yaml.
152
# +site+:: The site object.
153
# Returns:
154
# +Hash+:: The JSON-LD metadata.
155
def generate_org_jsonld(id, contributor, site)
156
organization = {
157
'@context': 'https://schema.org',
158
'@type': 'Organization',
159
'http://purl.org/dc/terms/conformsTo': {
160
'@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT',
161
'@type': 'CreativeWork'
162
},
163
id: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/",
164
name: Gtn::Contributors.fetch_name(site, id),
165
description: 'An organization supporting the Galaxy Training Network',
166
}
167
168
organization['url'] = contributor['url'] if contributor.key?('url') && contributor['url']
169
170
organization
171
end
172
173
##
174
# Generate the JSON-LD metadata for a funding organisation
175
# Parameters:
176
# +id+:: The id of the person.
177
# +contributor+:: The contributor object from ORGANISATIONS.yaml.
178
# +site+:: The site object.
179
# Returns:
180
# +Hash+:: The JSON-LD metadata.
181
def generate_funder_jsonld(id, contributor, site)
182
{
183
'@context': 'https://schema.org',
184
'@type': 'Organization',
185
'http://purl.org/dc/terms/conformsTo': {
186
'@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT',
187
'@type': 'CreativeWork'
188
},
189
name: Gtn::Contributors.fetch_name(site, id),
190
description: contributor.fetch('funding_statement', 'An organization supporting the Galaxy Training Network'),
191
url: contributor.fetch('url', "https://training.galaxyproject.org/training-material/hall-of-fame/#{id}/"),
192
logo: contributor.fetch('avatar', "https://github.com/#{id}.png"),
193
}
194
end
195
196
##
197
# Generate the JSON-LD metadata for a grant
198
# Parameters:
199
# +id+:: The id of the grant.
200
# +contributor+:: The contributor object from GRANTS.yaml.
201
# +site+:: The site object.
202
# Returns:
203
# +Hash+:: The JSON-LD metadata.
204
def generate_grant_jsonld(id, contributor, site)
205
organization = {
206
'@context': 'https://schema.org',
207
'@type': 'Grant',
208
identifier: contributor['funding_id'],
209
url: Gtn::Contributors.fetch_funding_url(contributor) || contributor['url'],
210
funder: generate_funder_jsonld(id, contributor, site)
211
}
212
213
organization['startDate'] = contributor['start_date'] if contributor.key?('start_date')
214
organization['endDate'] = contributor['end_date'] if contributor.key?('end_date')
215
216
organization
217
end
218
219
##
220
# Generate the JSON-LD metadata for a person, funder, or organisation as JSON.
221
# Parameters:
222
# +id+:: The id of the person.
223
# +site+:: The site object.
224
# +json+:: Should the output be rendered as JSON (only really used in contributor page.)
225
# Returns:
226
# +String+:: The JSON-LD metadata.
227
def to_pfo_jsonld(id, site, json: true)
228
contributor = Gtn::Contributors.fetch_contributor(site, id)
229
d = if Gtn::Contributors.person?(site, id)
230
generate_person_jsonld(id, contributor, site)
231
elsif Gtn::Contributors.grant?(site, id)
232
generate_grant_jsonld(id, contributor, site)
233
else
234
generate_org_jsonld(id, contributor, site)
235
end
236
237
if json
238
JSON.pretty_generate(d)
239
else
240
d
241
end
242
end
243
244
##
245
# Generate the JSON-LD metadata for a news article (blog)
246
# Parameters:
247
# +page+:: The page object.
248
# +site+:: The +Jekyll::Site+ site object.
249
# Returns:
250
# +Hash+:: The JSON-LD metadata.
251
def generate_news_jsonld(page, site)
252
authors = Gtn::Contributors.get_authors(page.to_h).map do |x|
253
to_pfo_jsonld(x, site, json: false)
254
end
255
256
data = {
257
'@context': 'https://schema.org',
258
'@type': 'BlogPosting',
259
url: "#{site['url']}#{site['baseurl']}#{page['url']}",
260
name: page['title'],
261
headline: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags.
262
keywords: page['tags'] || [],
263
description: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags
264
articleBody: page.content, # TODO: remove html tags
265
datePublished: page.date,
266
dateModified: Gtn::ModificationTimes.obtain_time(page.path),
267
author: authors,
268
publisher: GTN,
269
mainEntityOfPage: {
270
'@type': 'WebPage',
271
'@id': "#{site['url']}#{page['url']}"
272
},
273
image: {
274
'@type': 'ImageObject',
275
width: 60,
276
height: 60,
277
url: "#{site['baseurl']}/assets/images/GTN-60px.png"
278
}
279
}
280
data.update(A11Y)
281
282
JSON.pretty_generate(data)
283
end
284
285
##
286
# Generate the JSON-LD metadata for an event
287
# Parameters:
288
# +page+:: The page object.
289
# +site+:: The +Jekyll::Site+ site object.
290
# Returns:
291
# +Hash+:: The JSON-LD metadata.
292
#
293
# Examples:
294
# {{ page | generate_event_jsonld: site }}
295
def generate_event_jsonld(page, site)
296
organisers = Gtn::Contributors.get_organisers(page.to_h).map do |x|
297
to_pfo_jsonld(x, site, json: false)
298
end
299
instructors = Gtn::Contributors.get_instructors(page.to_h).map do |x|
300
to_pfo_jsonld(x, site, json: false)
301
end
302
funders = Gtn::Contributors.get_funders(site, page.to_h).map do |x|
303
to_pfo_jsonld(x, site, json: false)
304
end
305
funding = Gtn::Contributors.get_grants(site, page.to_h).map do |x|
306
to_pfo_jsonld(x, site, json: false)
307
end
308
309
materials = []
310
if page['program']
311
page['program'].each do |section|
312
if !section.key? 'tutorials'
313
next
314
end
315
316
section['tutorials'].each do |tutorial|
317
if tutorial.key?('custom')
318
next
319
end
320
321
material = Gtn::TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name'])
322
materials.push(material)
323
end
324
end
325
end
326
materials.compact!
327
328
# Extract EDAM terms from all materials
329
edam_terms = materials.map do |material|
330
material.fetch('edam_ontology', []).map do |term|
331
{
332
'@type': 'DefinedTerm',
333
'@id': "http://edamontology.org/#{term}",
334
inDefinedTermSet: 'http://edamontology.org',
335
termCode: term,
336
}
337
end
338
end.flatten.uniq
339
340
learning_objectives = materials.map do |material|
341
material.fetch('objectives', [])
342
end.flatten.compact
343
344
# TODO: add topic edam terms too? Not sure.
345
parts = []
346
materials.each do |material|
347
mat = generate_material_jsonld(material, site['data'][material['topic_name']], site)
348
if !mat.nil? && !mat.empty?
349
parts.push(mat)
350
end
351
end
352
353
if page['program']
354
syllab = page['program'].reject { |s| s['section'].nil? }.map do |section|
355
{
356
'@type': 'Syllabus',
357
name: section['section'],
358
description: section.fetch('description', nil),
359
}
360
end
361
end
362
363
data = {
364
'@context': 'https://schema.org',
365
'@type': 'Course',
366
url: "#{site['url']}#{site['baseurl']}#{page['url']}",
367
name: page['title'],
368
keywords: page['tags'] || [],
369
description: page['description'],
370
371
about: edam_terms, # TeSS, "scientific topics".
372
audience: page['audience'], # TeSS: target audience
373
# If 'online' is present in the mode, the course is online.
374
# Will fail on "this is NOT an online course"
375
# Acceptable.
376
courseMode: page['mode'],
377
startDate: page['date_start'],
378
endDate: page['date_end'],
379
organizer: organisers, # TeSS only, US spelling, non-standard
380
381
location: page['location'], # TODO, TeSS location
382
teaches: learning_objectives, # TeSS, "learning objectives"
383
# timeRequired: 'P1D', # TeSS, "duration", TODO: calculate from start/end date, not implemented in scraper currently.
384
385
availableLanguage: ['en'], # TODO: support other languages
386
inLanguage: ['en'], # TODO: support other languages
387
# courseCode
388
# coursePrerequisites
389
# educationalCredentialAwarded
390
# financialAidEligible
391
# hasCourseInstance
392
# numberOfCredits
393
# occupationalCredentialAwarded
394
# syllabusSections
395
# totalHistoricalEnrollment
396
397
# assesses
398
# competencyRequired
399
# educationalAlignment
400
# educationalLevel
401
# educationalUse
402
# learningResourceType
403
# teaches
404
405
funder: funders, # Org or person
406
funding: funding, # Grant
407
publisher: GTN,
408
provider: GTN,
409
syllabusSections: syllab,
410
# Session materials
411
# TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out.
412
# hasPart: parts,
413
}
414
415
begin
416
data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path)
417
data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path)
418
rescue StandardError
419
data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path'])
420
data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path'])
421
end
422
423
if page['cover']
424
data['image'] = if page['cover'] =~ /^http/
425
[page['cover']]
426
else
427
["#{site['url']}#{site['baseurl']}#{page['cover']}"]
428
end
429
end
430
431
# We CANNOT guarantee A11Y
432
# data.update(A11Y)
433
if page['cost'] and page['cost'].downcase == 'free'
434
data['isAccessibleForFree'] = true
435
offer = {
436
'@type': 'Offer',
437
price: 0,
438
priceCurrency: 'EUR',
439
category: 'Free',
440
isAccessibleForFree: true,
441
}
442
elsif page['cost']
443
data['isAccessibleForFree'] = false
444
offer = {
445
'@type': 'Offer',
446
price: page['cost'].split[0],
447
priceCurrency: page['cost'].split[1],
448
isAccessibleForFree: false,
449
category: 'Paid',
450
# TODO: this can be more advanced but we need to collect start/end times, and timezone.
451
}
452
end
453
454
# TODO: this is wrong in a whole host of scenarios like incl weekends.
455
course_days = (page.fetch('date_end', page['date_start']) - page['date_start']).to_i
456
if course_days < 1
457
course_days = 1
458
end
459
data['hasCourseInstance'] = [
460
{
461
'@type': 'CourseInstance',
462
courseMode: page['mode'],
463
# courseWorkload: "A daily course running from #{page['date_start']} to #{page['date_end']}",
464
offers: offer,
465
instructor: instructors,
466
isAccessibleForFree: data['isAccessibleForFree'],
467
courseSchedule: {
468
'@type': 'Schedule',
469
startDate: page['date_start'],
470
endDate: page.fetch('date_end', page['date_start']),
471
repeatCount: course_days,
472
repeatFrequency: 'daily', # Contrary to schema.org spec, this is what Google wants.
473
},
474
courseWorkload: "P#{course_days}D",
475
}
476
]
477
478
data['offers'] = [offer]
479
480
if page.key?('location') && page['location'].keys.length > 1
481
data['location'] = {
482
'@type': 'Place',
483
name: page['location']['name'],
484
address: {
485
'@type': 'PostalAddress',
486
streetAddress: page['location'].fetch('address', nil),
487
addressLocality: page['location'].fetch('city', nil),
488
addressRegion: page['location'].fetch('region', nil),
489
postalCode: page['location'].fetch('postcode', nil),
490
addressCountry: page['location'].fetch('country', nil)
491
}
492
}
493
end
494
495
JSON.pretty_generate(data)
496
end
497
498
##
499
# Generate the JSON-LD metadata for a learning pathway
500
# Parameters:
501
# +page+:: The page object.
502
# +site+:: The +Jekyll::Site+ site object.
503
# Returns:
504
# +Hash+:: The JSON-LD metadata.
505
#
506
# Examples:
507
# {{ page | generate_learning_pathway_jsonld: site }}
508
def generate_learning_pathway_jsonld(page, site)
509
materials = []
510
page['pathway'].each do |section|
511
if !section.key? 'tutorials'
512
next
513
end
514
515
section['tutorials'].each do |tutorial|
516
if tutorial.key?('custom')
517
next
518
end
519
520
material = Gtn::TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name'])
521
materials.push(material)
522
end
523
end
524
materials.compact!
525
526
# Extract EDAM terms from all materials
527
edam_terms = materials.map do |material|
528
material.fetch('edam_ontology', []).map do |term|
529
{
530
'@type': 'DefinedTerm',
531
'@id': "http://edamontology.org/#{term}",
532
inDefinedTermSet: 'http://edamontology.org',
533
termCode: term,
534
}
535
end
536
end.flatten.uniq
537
538
learning_objectives = materials.map do |material|
539
material.fetch('objectives', [])
540
end.flatten.compact
541
542
funders = materials.map do |material|
543
Gtn::Contributors.get_funders(site, material).map do |x|
544
to_pfo_jsonld(x, site, json: false)
545
end
546
end.flatten.uniq.compact
547
548
funding = materials.map do |material|
549
Gtn::Contributors.get_grants(site, material).map do |x|
550
to_pfo_jsonld(x, site, json: false)
551
end
552
end.flatten.uniq.compact
553
554
# TODO: add topic edam terms too? Not sure.
555
parts = []
556
materials.each do |material|
557
mat = generate_material_jsonld(material, site['data'][material['topic_name']], site)
558
if !mat.nil? && !mat.empty?
559
parts.push(mat)
560
end
561
end
562
563
syllab = page['pathway'].reject { |s| s['section'].nil? }.map do |section|
564
{
565
'@type': 'Syllabus',
566
name: section['section'],
567
description: section.fetch('description', nil),
568
}
569
end
570
571
data = {
572
'@context': 'https://schema.org',
573
'@type': 'Course',
574
url: "#{site['url']}#{site['baseurl']}#{page['url']}",
575
name: "Learning Pathway #{page['title']}",
576
keywords: page['tags'] || [],
577
description: page['description'],
578
about: edam_terms, # TeSS, "scientific topics".
579
audience: page['audience'], # TeSS: target audience
580
teaches: learning_objectives, # TeSS, "learning objectives"
581
availableLanguage: ['en'], # TODO: support other languages
582
inLanguage: ['en'], # TODO: support other languages
583
# courseCode
584
# coursePrerequisites
585
# educationalCredentialAwarded
586
# financialAidEligible
587
# hasCourseInstance
588
# numberOfCredits
589
# occupationalCredentialAwarded
590
# syllabusSections
591
# totalHistoricalEnrollment
592
593
# assesses
594
# competencyRequired
595
# educationalAlignment
596
# educationalLevel
597
# educationalUse
598
# learningResourceType
599
# teaches
600
601
funder: funders, # Org or person
602
funding: funding, # Grant
603
publisher: GTN,
604
provider: GTN,
605
syllabusSections: syllab,
606
# Session materials
607
# TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out.
608
# hasPart: parts,
609
}
610
611
begin
612
data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path)
613
data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path)
614
rescue StandardError
615
data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path'])
616
data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path'])
617
end
618
619
if page['cover']
620
data['image'] = if page['cover'] =~ /^http/
621
[page['cover']]
622
else
623
["#{site['url']}#{site['baseurl']}#{page['cover']}"]
624
end
625
end
626
627
# We CANNOT guarantee A11Y
628
# data.update(A11Y)
629
data['isAccessibleForFree'] = true
630
offer = {
631
'@type': 'Offer',
632
price: 0,
633
priceCurrency: 'EUR',
634
category: 'Free',
635
isAccessibleForFree: true,
636
}
637
data['offers'] = [offer]
638
639
# TODO: this is basically just wrong.
640
data['hasCourseInstance'] = [
641
{
642
'@type': 'CourseInstance',
643
courseMode: 'online',
644
offers: offer,
645
isAccessibleForFree: data['isAccessibleForFree'],
646
}
647
]
648
649
JSON.pretty_generate(data)
650
end
651
652
##
653
# Convert a material to JSON-LD, intended to be used in Jekyll Liquid templates.
654
# Parameters:
655
# +material+:: The material object.
656
# +topic+:: The topic object.
657
# +site+:: The +Jekyll::Site+ site object.
658
#
659
# Returns:
660
# +String+:: The JSON-LD metadata.
661
#
662
# Examples:
663
# {{ material | to_jsonld: topic, site }}
664
def to_jsonld(material, topic, site)
665
JSON.pretty_generate(generate_material_jsonld(material, topic, site))
666
end
667
668
##
669
# Convert a material to JSON-LD. Intended to be used by the filters which you should call in templates.
670
#
671
# Parameters:
672
# +material+:: The material object.
673
# +topic+:: The topic object.
674
# +site+:: The +Jekyll::Site+ site object.
675
#
676
# Returns:
677
# +Hash+:: The JSON-LD metadata.
678
def generate_material_jsonld(material, topic, site)
679
langCodeMap = {
680
"en" => 'English',
681
"es" => 'Español',
682
"fr" => 'Français',
683
}
684
685
eduLevel = {
686
'Introductory' => 'Beginner',
687
'Intermediate' => 'Intermediate',
688
'Advanced' => 'Advanced'
689
}
690
return '{}' if !topic
691
692
topic_desc = {
693
'@type': 'CreativeWork',
694
name: (topic['title']).to_s,
695
description: (topic['summary']).to_s,
696
url: "#{site['url']}#{site['baseurl']}/topics/#{topic['name']}/"
697
}
698
699
# aggregate everything
700
data = {
701
# Properties from Course
702
'@context': 'http://schema.org',
703
'@type': 'LearningResource',
704
705
# Required for BioSchemas
706
'http://purl.org/dc/terms/conformsTo': {
707
'@id': 'https://bioschemas.org/profiles/TrainingMaterial/1.0-RELEASE',
708
'@type': 'CreativeWork'
709
},
710
711
# Properties from CreativeWork
712
# "about" described below
713
#
714
# "accountablePerson":,
715
# "aggregateRating":,
716
# "alternativeHeadline":,
717
# "associatedMedia":,
718
audience: {
719
'@type': 'EducationalAudience',
720
educationalRole: EDU_ROLES[topic['type']]
721
},
722
# "audio":,
723
# "award":,
724
# "author" described below
725
# "character":,
726
citation: [
727
{
728
'@type': 'CreativeWork',
729
name: 'Galaxy Training: A Powerful Framework for Teaching!',
730
url: 'https://doi.org/10.1371/journal.pcbi.1010752'
731
},
732
{
733
'@type': 'CreativeWork',
734
name: 'Community-Driven Data Analysis Training for Biology',
735
url: 'https://doi.org/10.1016/j.cels.2018.05.012'
736
}
737
],
738
# "comment":,
739
# "commentCount":,
740
# "contentLocation":,
741
# "contentRating":,
742
# "contentReferenceTime":,
743
# "contributor" described below
744
# copyrightHolder: GTN,
745
# copyrightNotice: m
746
# "copyrightYear":,
747
# "correction":,
748
# "creator":,
749
# "dateCreated":,
750
# "datePublished":,
751
discussionUrl: site['gitter_url'],
752
# "editor":,
753
# "educationalAlignment":,
754
# "educationalUse":,
755
# "encoding":,
756
# "encodingFormat":,
757
# "exampleOfWork":,
758
# "expires":,
759
# "funder": funding,
760
# "genre":,
761
# "hasPart" described below
762
headline: (material['title']).to_s,
763
# "interactionStatistic":,
764
interactivityType: 'mixed',
765
isAccessibleForFree: true,
766
# "isBasedOn":,
767
isFamilyFriendly: true,
768
# "isPartOf" described below
769
# "keywords": described below
770
# "learningResourceType" described below
771
license: 'https://spdx.org/licenses/CC-BY-4.0.html',
772
# "locationCreated":,
773
# "mainEntity":,
774
# "material":,
775
# "mentions" described below
776
# "offers":,
777
# "position":,
778
producer: GTN,
779
provider: GTN,
780
# "publication":,
781
# "publisher":,
782
# "publisherImprint":,
783
# "publishingPrinciples":,
784
# "recordedAt":,
785
# "releasedEvent":,
786
# "review":,
787
# "schemaVersion":,
788
# "sdDatePublished":,
789
# "sdLicense":,
790
# "sdPublisher":,
791
sourceOrganization: GTN,
792
# "spatialCoverage":,
793
# "sponsor":,
794
# "temporalCoverage":,
795
# "text":,
796
# "thumbnailUrl":,
797
# "timeRequired" described below
798
# "translationOfWork":,
799
# "translator": Google Translate???,
800
# "typicalAgeRange":,
801
# "version":,
802
# "video":,
803
# "workExample":,
804
# "workTranslation":,
805
806
# Properties from Thing
807
# "additionalType":,
808
# "alternateName":,
809
# "description" described below
810
# "disambiguatingDescription":,
811
# "image":,
812
# "mainEntityOfPage":,
813
# "name" described below
814
# "potentialAction":,
815
# "sameAs":,
816
# "subjectOf":,
817
# "url" described below
818
workTranslation: [],
819
creativeWorkStatus: material['draft'] ? 'Draft' : 'Active',
820
}
821
822
if material.key?('pub_date')
823
data['dateModified'] = material['mod_date']
824
data['datePublished'] = material['pub_date']
825
else
826
begin
827
data['dateModified'] = Gtn::ModificationTimes.obtain_time(material.path)
828
data['datePublished'] = Gtn::PublicationTimes.obtain_time(material.path)
829
rescue StandardError
830
data['dateModified'] = Gtn::ModificationTimes.obtain_time(material['path'])
831
data['datePublished'] = Gtn::PublicationTimes.obtain_time(material['path'])
832
end
833
end
834
835
if material.key?('copyright')
836
# copyrightHolder: GTN,
837
data['copyrightNotice'] = material['copyright']
838
else
839
# I'm not sure this is accurate.
840
data['copyrightHolder'] = GTN
841
end
842
843
funders = Gtn::Contributors.get_funders(site, material).map do |x|
844
to_pfo_jsonld(x, site, json: false)
845
end
846
grants = Gtn::Contributors.get_grants(site, material).map do |x|
847
to_pfo_jsonld(x, site, json: false)
848
end
849
850
data['funder'] = funders
851
data['funding'] = grants
852
853
data['identifier'] = "https://gxy.io/GTN:#{material['short_id']}" if material.key?('short_id')
854
855
data.update(A11Y)
856
857
actual_material = Gtn::TopicFilter.fetch_tutorial_material(site, material['topic_name'], material['tutorial_name'])
858
859
# info depending if tutorial, hands-on or slide level
860
# parts = []
861
# data['hasPart'] = parts
862
863
mentions = []
864
description = []
865
866
data['isPartOf'] = topic_desc
867
868
data['abstract'] = material
869
.fetch('content', '')
870
.strip
871
.split("\n")
872
.first
873
874
if ! data['abstract'].nil?
875
data['abstract'] = data['abstract']
876
.gsub(/\{\{\s*site.baseurl\s*\}\}/, url_prefix(site))
877
.gsub(/\[{{\s*site.url\s*}}/, '[' + url_prefix(site))
878
.gsub(/{% link (topics[^%]*).md %}/, url_prefix(site) + '\1.html')
879
.gsub(/{% link (topics[^%]*).html %}/, url_prefix(site) + '\1.html')
880
.gsub(/\s*\(?{%\s*cite [^}]+\s*%}\)?/, '')
881
.gsub('{{ site.github_repository }}', safe_site_config(site, 'github_repository', 'https://example.com'))
882
.gsub(/{% snippet ([^%]*) %}/, '')
883
.gsub(/{% include ([^%]*) %}/, '')
884
end
885
886
description.push("## Abstract\n\n#{data['abstract']}\n\n")
887
888
if (material['name'] == 'tutorial.md') || (material['name'] == 'slides.html')
889
890
if material['name'] == 'tutorial.md'
891
data['learningResourceType'] = 'e-learning'
892
description.push("## About This Material\n\nThis is a Hands-on Tutorial from the GTN which is usable either for individual self-study, or as a teaching material in a classroom.\n\n")
893
else
894
data['learningResourceType'] = 'slides'
895
end
896
897
data['name'] = material['title']
898
data['url'] = "#{site['url']}#{site['baseurl']}#{material['url']}"
899
900
# Requires https://github.com/galaxyproject/training-material/pull/4271
901
data['version'] = Gtn::ModificationTimes.obtain_modification_count(material['path'])
902
903
# Time required
904
if material.key?('time_estimation') && !material['time_estimation'].nil?
905
data['timeRequired'] = "PT#{material['time_estimation'].upcase}"
906
end
907
908
# Description with questions, objectives and keypoints
909
if material.key?('questions') && !material['questions'].nil? && material['questions'].length.positive?
910
questions = material['questions'].join("\n - ")
911
description.push("## Questions this #{material['type']} will address\n\n - #{questions}\n\n")
912
end
913
if material.key?('objectives') && !material['objectives'].nil? && material['objectives'].length.positive?
914
objectives = material['objectives'].map{|x| "- #{x}"}.join("\n")
915
description.push("## Learning Objectives\n\n#{objectives}\n\n")
916
data['teaches'] = objectives
917
end
918
if material.key?('keypoints') && !material['keypoints'].nil? && material['keypoints'].length.positive?
919
keypoints = material['keypoints'].join("\n - ")
920
description.push("## Key Points\n\n - #{keypoints}\n\n")
921
end
922
923
# Keywords
924
data['keywords'] = [topic['title']] + (material['tags'] || [])
925
# Zenodo links
926
end
927
928
# Mentions are 'external resources' in TeSS.
929
# This could be expanded with
930
# - supported servers
931
# - tools and resources used (e.g. Galaxy) or tools linked to the TS.
932
# - slides (if tutorial) and tutorial (if slides)
933
# - other materials in the same topic?
934
if actual_material.key?('workflows')
935
mentions.push({
936
'@type': 'Thing',
937
url: "#{site['url']}#{site['baseurl']}#{material['dir']}workflows/",
938
name: "Associated Workflows"
939
})
940
end
941
942
# Notebooks
943
if actual_material.key?('notebook')
944
if actual_material['notebook']['language'] != 'r'
945
# Python, Bash, SQL (all via jupyter)
946
url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.ipynb"
947
mentions.push({
948
'@type': 'Thing',
949
url: url,
950
name: "Jupyter Notebook (with Solutions)"
951
})
952
mentions.push({
953
'@type': 'Thing',
954
url: url.gsub(/\.ipynb$/, '-course.ipynb'),
955
name: "Jupyter Notebook (without Solutions)"
956
})
957
elsif actual_material['notebook']['language'] == 'r' # Actual R
958
url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.Rmd"
959
mentions.push({
960
'@type': 'Thing',
961
url: url,
962
name: "Quarto/RMarkdown Notebook"
963
})
964
mentions.push({
965
'@type': 'Thing',
966
url: "https://bio.tools/tool/rstudio",
967
name: "RStudio"
968
})
969
end
970
end
971
972
# Tools
973
uses_tools = false
974
(actual_material['tools'] || []).each do |tool|
975
if site.data['tool-meta'].nil?
976
next
977
end
978
979
toolmeta = site.data['tool-meta'][tool]
980
if toolmeta.nil?
981
next
982
end
983
984
if toolmeta['bio.tools'].length.positive?
985
mentions.push({
986
'@type': 'Thing',
987
url: "https://bio.tools/tool/#{toolmeta['bio.tools']}",
988
name: toolmeta.fetch('bio.tools_name', toolmeta['name'])
989
})
990
end
991
uses_tools = true
992
end
993
if uses_tools
994
mentions.push({
995
'@type': 'Thing',
996
url: "https://bio.tools/tool/galaxy",
997
name: "Galaxy"
998
})
999
end
1000
1001
# Zenodo link out
1002
if actual_material.key?('zenodo_link') && ! actual_material['zenodo_link'].nil?
1003
if actual_material['zenodo_link'].length.positive?
1004
mentions.push({
1005
'@type': 'Thing',
1006
url: (actual_material['zenodo_link']).to_s,
1007
name: "Associated Training Datasets"
1008
})
1009
end
1010
end
1011
1012
if description.empty?
1013
description.push(material.fetch('content', '').strip.split("\n").first)
1014
end
1015
data['description'] = description.join("\n")
1016
1017
data['inLanguage'] = if material.key?('lang')
1018
{
1019
'@type': 'Language',
1020
name: langCodeMap[material['lang']],
1021
alternateName: material['lang']
1022
}
1023
else
1024
{
1025
'@type': 'Language',
1026
name: 'English',
1027
alternateName: 'en'
1028
}
1029
end
1030
1031
# Course requirements (material + topic)
1032
reqs = []
1033
reqs.push(*topic['requirements']) if topic.key?('requirements')
1034
reqs.push(*material['requirements']) if material.key?('requirements')
1035
if !reqs.empty?
1036
coursePrerequisites = []
1037
reqs.each do |req|
1038
if req['type'] == 'internal'
1039
if req.key?('tutorials')
1040
(req['tutorials']).each do |tuto|
1041
(site['pages']).each do |page|
1042
if ((page['name'] == 'tutorial.md') || (page['name'] == 'slides.html')) &&
1043
((page['topic_name'] == req['topic_name']) && (page['tutorial_name'] == tuto))
1044
# slides
1045
if page['name'] == 'slides.html'
1046
coursePrerequisites.push(
1047
{
1048
'@context': 'http://schema.org',
1049
'@type': 'LearningResource',
1050
url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/" \
1051
"tutorials/#{tuto}/slides.html",
1052
name: (page['title']).to_s,
1053
description: "Slides for '#{page['title']}' tutorial",
1054
learningResourceType: 'slides',
1055
interactivityType: 'expositive',
1056
provider: GTN
1057
}
1058
)
1059
if page['hands_on_url']
1060
coursePrerequisites.push(
1061
{
1062
'@context': 'http://schema.org',
1063
'@type': 'LearningResource',
1064
url: (page['hands_on_url']).to_s,
1065
learningResourceType: 'e-learning',
1066
interactivityType: 'expositive',
1067
}
1068
)
1069
end
1070
end
1071
# hands-on
1072
if page['name'] == 'tutorial.md'
1073
coursePrerequisites.push(
1074
{
1075
'@context': 'http://schema.org',
1076
'@type': 'LearningResource',
1077
url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/tutorials" \
1078
"/#{tuto}/tutorial.html",
1079
name: (page['title']).to_s,
1080
description: "Hands-on for '#{page['title']}' tutorial",
1081
learningResourceType: 'e-learning',
1082
interactivityType: 'expositive',
1083
provider: GTN
1084
}
1085
)
1086
end
1087
end
1088
end
1089
end
1090
else
1091
coursePrerequisites.push(
1092
{
1093
'@context': 'http://schema.org',
1094
'@type': 'LearningResource',
1095
url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/",
1096
name: (site['data'][req['topic_name']]['title']).to_s,
1097
description: (site['data'][req['topic_name']]['title']).to_s,
1098
provider: GTN
1099
}
1100
)
1101
end
1102
elsif req['type'] == 'external'
1103
coursePrerequisites.push({
1104
'@type': 'CreativeWork',
1105
url: (req['link']).to_s,
1106
name: (req['title']).to_s
1107
})
1108
else
1109
coursePrerequisites.push((req['title']).to_s)
1110
end
1111
end
1112
data['competencyRequired'] = coursePrerequisites.uniq
1113
end
1114
1115
# Add contributors/authors
1116
if material.key?('contributors') || material.key?('contributions')
1117
authors = Gtn::Contributors.get_authors(material).map do |x|
1118
generate_person_jsonld(x, Gtn::Contributors.fetch_contributor(site, x), site)
1119
end
1120
1121
data['author'] = authors
1122
end
1123
1124
# Add non-author contributors
1125
if material.key?('contributions')
1126
data['contributor'] = Gtn::Contributors.get_non_authors(material).map do |x|
1127
generate_person_jsonld(x, site['data']['contributors'][x], site)
1128
end
1129
end
1130
1131
about = []
1132
about.push(topic_desc)
1133
edam_terms = topic.fetch('edam_ontology', []) | material.fetch('edam_ontology', [])
1134
1135
about += edam_terms.map do |term|
1136
{
1137
'@type': 'DefinedTerm',
1138
'@id': "http://edamontology.org/#{term}",
1139
inDefinedTermSet: 'http://edamontology.org',
1140
termCode: term,
1141
# "name": ,
1142
url: 'https://bioportal.bioontology.org/ontologies/EDAM/?p=classes&conceptid=' \
1143
"http%3A%2F%2Fedamontology.org%2F#{term}"
1144
}
1145
end
1146
1147
data['about'] = about
1148
1149
data['educationalLevel'] = material.key?('level') ? eduLevel[material['level']] : 'Beginner'
1150
data['mentions'] = mentions
1151
1152
data
1153
end
1154
end
1155
end
1156
end
1157
1158
Liquid::Template.register_filter(Jekyll::Filters::JsonldFilter)
1159
1160