Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/api.rb
1677 views
1
# frozen_string_literal: true
2
3
require 'json'
4
5
require './_plugins/jekyll-topic-filter'
6
require './_plugins/gtn/metrics'
7
require './_plugins/gtn/scholar'
8
require './_plugins/gtn/git'
9
require './_plugins/gtn/hooks'
10
require './_plugins/gtn/ro-crate'
11
require './_plugins/gtn'
12
require './_plugins/util'
13
14
##
15
# Use Jekyll's Markdown converter to convert text to HTML
16
# Params:
17
# +site+:: +Jekyll::Site+ object
18
# +text+:: +String+ of text to convert
19
# Returns:
20
# +String+ of markdown text
21
def markdownify(site, text)
22
site.find_converter_instance(
23
Jekyll::Converters::Markdown
24
).convert(text.to_s)
25
end
26
27
##
28
# Recursively visit a hash and markdownify all strings inside
29
# Params:
30
# +site+:: +Jekyll::Site+ object
31
# +f+:: +Hash+ to visit
32
# Returns:
33
# +Hash+ with all strings markdownified
34
def visitAndMarkdownify(site, f)
35
case f
36
when Array
37
f.map! { |x| visitAndMarkdownify(site, x) }
38
when Hash
39
f = f.transform_values do |v|
40
visitAndMarkdownify(site, v)
41
end
42
when String
43
f = markdownify(site, f).strip.gsub(/<p>/, '').gsub(%r{</p>}, '')
44
end
45
f
46
end
47
48
##
49
# Map a contributor ID to a JSON object which includes links to their profile page and API endpoint
50
# Params:
51
# +site+:: +Jekyll::Site+ object
52
# +c+:: +String+ of contributor ID
53
# Returns:
54
# +Hash+ of contributor information
55
def mapContributor(site, c)
56
contrib_type, contrib = Gtn::Contributors.fetch(site, c)
57
x = contrib
58
.merge({
59
'id' => c,
60
'url' => site.config['url'] + site.config['baseurl'] + "/api/#{contrib_type}s/#{c}.json",
61
'page' => site.config['url'] + site.config['baseurl'] + "/hall-of-fame/#{c}/",
62
})
63
visitAndMarkdownify(site, x)
64
end
65
66
module Jekyll
67
module Generators
68
##
69
# This class generates the GTN's "api" by writing out a folder full of JSON files.
70
class APIGenerator
71
72
def copy(site, source, dest)
73
# It isn't unusual that some of these might not exist in dev envs.
74
if File.exist?(site.in_source_dir(source))
75
if ! Dir.exist?(File.dirname(site.in_dest_dir(dest)))
76
FileUtils.mkdir_p(File.dirname(site.in_dest_dir(dest)))
77
end
78
79
FileUtils.cp(site.in_source_dir(source), site.in_dest_dir(dest))
80
end
81
end
82
83
def write(site, dest, data, json: true, pretty: true)
84
# Since we're doing this ourselves, need to be responsible for ensuring
85
# the directory exists.
86
if ! Dir.exist?(File.dirname(site.in_dest_dir(dest)))
87
FileUtils.mkdir_p(File.dirname(site.in_dest_dir(dest)))
88
end
89
90
if json
91
if pretty
92
File.write(site.in_dest_dir(dest), JSON.pretty_generate(data))
93
else
94
File.write(site.in_dest_dir(dest), JSON.generate(data))
95
end
96
else
97
# Pretty isn't relevant.
98
File.write(site.in_dest_dir(dest), data)
99
end
100
end
101
102
##
103
# Runs the generation process
104
# Params:
105
# +site+:: +Jekyll::Site+ object
106
def generate(site)
107
Jekyll.logger.info '[GTN/API] Generating API'
108
109
write(site, 'api/configuration.json', site.config.reject { |k, _v| k.to_s.start_with?('cached_') })
110
write(site, 'api/swagger.json', site.data['swagger'])
111
write(site, 'api/version.json', Gtn::Git.discover)
112
113
# Full Bibliography
114
Jekyll.logger.debug '[GTN/API] Bibliography'
115
write(site, 'api/gtn.bib', site.config['cached_global_bib'].to_s, json: false)
116
117
# Metrics endpoint, /metrics
118
write(site, 'api/metrics', Gtn::Metrics.generate_metrics(site), json: false)
119
120
# Public tool listing
121
write(site, 'api/psl.json', site.data['public-server-tools'], pretty: false)
122
123
# Tool Categories
124
write(site, 'api/toolcats.json', site.data['toolcats'], pretty: false)
125
126
# Tool Categories
127
write(site, 'api/toolshed-revisions.json', site.data['toolshed-revisions'], pretty: false)
128
129
# Feedback Data
130
write(site, 'api/feedback2.json', site.data['feedback2'], pretty: false)
131
copy(site, 'metadata/feedback.csv', 'api/feedback.csv')
132
copy(site, 'metadata/feedback2.yaml', 'api/feedback2.yaml')
133
134
# Contributors
135
Jekyll.logger.debug '[GTN/API] Contributors, Funders, Organisations'
136
%w[contributors grants organisations].each do |type|
137
pfo = site.data[type].map { |c, _| mapContributor(site, c) }
138
write(site, "api/#{type}.json", pfo, pretty: false)
139
140
site.data['contributors'].each do |c, _|
141
write(site, "api/#{type}/#{c}.json", mapContributor(site, c))
142
end
143
end
144
145
geojson = {
146
'type' => 'FeatureCollection',
147
'features' => site.data['contributors']
148
.select { |_k, v| v.key? 'location' }
149
.map do |k, v|
150
{
151
'type' => 'Feature',
152
'geometry' => { 'type' => 'Point', 'coordinates' => [v['location']['lon'], v['location']['lat']] },
153
'properties' => {
154
'name' => v.fetch('name', k),
155
'url' => "https://training.galaxyproject.org/training-material/hall-of-fame/#{k}/",
156
'joined' => v['joined'],
157
'orcid' => v['orcid'],
158
'id' => k,
159
'contact_for_training' => v.fetch('contact_for_training', false),
160
}
161
}
162
end
163
}
164
write(site, "api/contributors.geojson", geojson)
165
166
167
# Trigger the topic cache to generate if it hasn't already
168
Jekyll.logger.debug '[GTN/API] Tutorials'
169
Gtn::TopicFilter.topic_filter(site, 'does-not-matter')
170
Gtn::TopicFilter.list_topics(site).map do |topic|
171
out = site.data[topic].dup
172
out['materials'] = Gtn::TopicFilter.topic_filter(site, topic).map do |x|
173
q = x.dup
174
q['contributors'] = Gtn::Contributors.get_contributors(q).dup.map do |c|
175
mapContributor(site, c)
176
end
177
178
q['urls'] = {}
179
180
if !q['hands_on'].nil?
181
q['urls']['hands_on'] = site.config['url'] + site.config['baseurl'] + "/api/topics/#{q['url'][8..-6]}.json"
182
end
183
184
if !q['slides'].nil?
185
q['urls']['slides'] = site.config['url'] + site.config['baseurl'] + "/api/topics/#{q['url'][8..-6]}.json"
186
end
187
188
# Write out the individual page
189
# Delete the ref to avoid including it by accident
190
q.delete('ref')
191
q.delete('ref_tutorials')
192
q.delete('ref_slides')
193
write(site, "api/topics/#{q['url'][7..-6]}.json", q)
194
195
q
196
end
197
out['editorial_board'] = out['editorial_board'].map do |c|
198
mapContributor(site, c)
199
end
200
201
write(site, "api/topics/#{topic}.json", out)
202
end
203
204
topics = {}
205
Jekyll.logger.debug '[GTN/API] Topics'
206
# Individual Topic Indexes
207
site.data.each_pair do |k, v|
208
if v.is_a?(Hash) && v.key?('type') && v.key?('editorial_board')
209
210
topics[k] = {
211
'name' => v['name'],
212
'title' => v['title'],
213
'summary' => v['summary'],
214
'url' => site.config['url'] + site.config['baseurl'] + "/api/topics/#{k}.json",
215
'editorial_board' => v['editorial_board'].map { |c| mapContributor(site, c) }
216
}
217
end
218
end
219
220
# Videos.json
221
# {
222
# "id": "transcriptomics/tutorials/mirna-target-finder/slides",
223
# "topic": "Transcriptomics",
224
# "title": "Whole transcriptome analysis of Arabidopsis thaliana"
225
# },
226
227
videos = Gtn::TopicFilter.list_videos(site).map do |m|
228
{
229
id: "#{m['topic_name']}/tutorials/#{m['tutorial_name']}/slides",
230
topic: m['topic_name_human'],
231
title: m['title']
232
}
233
end
234
write(site, "api/videos.json", videos)
235
236
237
# Overall topic index
238
write(site, "api/topics.json", topics)
239
240
Jekyll.logger.debug '[GTN/API] Tutorial and Slide pages'
241
242
# Deploy the feedback file as well
243
write(site, "api/feedback.json", site.data['feedback'])
244
245
# Top Tools
246
Jekyll.logger.debug '[GTN/API] Top Tools'
247
write(site, "api/top-tools.json", Gtn::TopicFilter.list_materials_by_tool(site))
248
249
# GA4GH TRS Endpoint
250
# Please note that this is all a fun hack
251
Gtn::TopicFilter.list_all_materials(site).select { |m| m['workflows'] }.each do |material|
252
material['workflows'].each do |workflow|
253
wfid = workflow['wfid']
254
wfname = workflow['wfname']
255
256
ga4gh_blob = {
257
'id' => wfname,
258
'url' => site.config['url'] + site.config['baseurl'] + material['url'],
259
'name' => 'v1',
260
'author' => [],
261
'descriptor_type' => ['GALAXY'],
262
}
263
write(site, "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}.json", ga4gh_blob)
264
265
266
267
descriptor = {
268
'content' => File.read("#{material['dir']}/workflows/#{workflow['workflow']}"),
269
'checksum' => [],
270
'url' => nil,
271
}
272
write(site, "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}/GALAXY/descriptor.json", descriptor)
273
end
274
end
275
end
276
end
277
end
278
end
279
280
281
Jekyll::Hooks.register :site, :post_read do |site|
282
if Jekyll.env == 'production'
283
Gtn::Hooks.by_tool(site)
284
end
285
end
286
287
# Basically like `PageWithoutAFile`, we just write out the ones we'd created earlier.
288
Jekyll::Hooks.register :site, :post_write do |site|
289
# No need to run this except in prod.
290
if Jekyll.env == 'production'
291
# Build our API
292
api = Jekyll::Generators::APIGenerator.new
293
api.generate(site)
294
295
# Public tool listing: reorganised
296
if site.data['public-server-tools'] && site.data['public-server-tools']['tools']
297
site.data['public-server-tools']['tools'].each do |tool, version_data|
298
path = File.join(site.dest, 'api', 'psl', "#{tool}.json")
299
dir = File.dirname(path)
300
FileUtils.mkdir_p(dir) unless File.directory?(dir)
301
302
d = version_data.dup
303
d.each_key do |k|
304
# Replace the indexes with the server URLs from site['public-server-tools']['servers']
305
d[k] = d[k].map { |v| site.data['public-server-tools']['servers'][v] }
306
end
307
308
File.write(path, JSON.generate(d))
309
end
310
Jekyll.logger.debug '[GTN/API/PSL] PSL written'
311
else
312
Jekyll.logger.debug '[GTN/API/PSL] PSL Dataset not available, are you in a CI environment?'
313
end
314
315
Gtn::TopicFilter.list_all_materials(site).each do |material|
316
directory = material['dir']
317
318
if material['slides']
319
path = File.join(site.dest, 'api', directory, 'slides.json')
320
p = material.dup
321
p.delete('ref')
322
p.delete('ref_tutorials')
323
p.delete('ref_slides')
324
p['contributors'] = Gtn::Contributors.get_contributors(p).dup.map { |c| mapContributor(site, c) }
325
326
# Here we un-do the tutorial metadata priority, and overwrite with
327
# slides metadata when available.
328
slides_data = site.pages.select { |p2| p2.url == "/#{directory}/slides.html" }[0]
329
p.update(slides_data.data) if slides_data&.data
330
331
if !Dir.exist?(File.dirname(path))
332
FileUtils.mkdir_p(File.dirname(path))
333
end
334
File.write(path, JSON.generate(p))
335
end
336
337
if material['hands_on']
338
path = File.join(site.dest, 'api', directory, 'tutorial.json')
339
p = material.dup
340
p.delete('ref')
341
p.delete('ref_tutorials')
342
p.delete('ref_slides')
343
p['contributors'] = Gtn::Contributors.get_contributors(p).dup.map { |c| mapContributor(site, c) }
344
if !Dir.exist?(File.dirname(path))
345
FileUtils.mkdir_p(File.dirname(path))
346
end
347
File.write(path, JSON.generate(p))
348
end
349
end
350
351
# Import on-demand
352
require 'securerandom'
353
354
355
dir = File.join(site.dest, 'api', 'workflows')
356
# ro-crate-metadata.json
357
crate_start = Time.now
358
count = 0
359
Gtn::TopicFilter.list_all_materials(site).select { |m| m['workflows'] }.each do |material|
360
material['workflows'].each do |workflow|
361
Gtn::RoCrate.write(site, dir, material, workflow, site.config['url'], site.config['baseurl'])
362
count += 1
363
end
364
end
365
366
Jekyll.logger.debug "[GTN/API/WFRun] RO-Crate Metadata written in #{Time.now - crate_start} seconds for #{count} workflows"
367
end
368
end
369
370