diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ef204a88fc005..2c1fdb06309dbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,8 @@ jobs: - name: Validate all links, enforce alt text run: | bundle exec htmlproofer \ - --ignore-urls "/.*localhost.*/","/.*vimeo\.com.*/","/.*gitter\.im.*/","/.*drmaa\.org.*/","/.*slides.html#.*/,/#embedded_jbrowse/","/.*videos.*.mp4.png/","/krona_(all|multisample).html/","/workflows\/trs_import/" \ - --ignore-files "/.*krona.*\.html/","/.*\/files\/.*/","/.*\/node_modules\/.*/","/\/tutorials\/.*\/docker\//","/.*content.html/","/.*recentrifuge.*\.html/" \ + --ignore-urls "/.*localhost.*/","/.*vimeo\.com.*/","/.*gitter\.im.*/","/.*drmaa\.org.*/","/.*slides.html#.*/,/#embedded_jbrowse/","/.*videos.*.mp4.png/","/krona_(all|multisample).html/","/workflows\/trs_import/","/api/","/by-tool/" \ + --ignore-files "/.*krona.*\.html/","/.*\/files\/.*/","/.*\/node_modules\/.*/","/\/tutorials\/.*\/docker\//","/.*content.html/","/.*recentrifuge.*\.html/","/short/" \ --swap-urls "github.com/galaxyproject/training-material/tree/main:github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_HEAD_REF}" \ --disable-external=true \ --enforce-https=false \ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0a3be6358e3a0a..977ec51bfc90cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -70,15 +70,13 @@ jobs: make annotate ACTIVATE_ENV=pwd curl -L https://docs.google.com/spreadsheets/d/1NfZhi5Jav7kl9zFCkeb7rIC2F8xW1isruv1TeO4WpNI/export\?format\=tsv | ruby bin/prepare_feedback.rb curl -L https://hexylena.github.io/toolshed-version-database/tool-meta.json > metadata/tool-meta.json + curl -L https://edamontology.org/EDAM.csv > metadata/EDAM.csv make rebuild-search-index ACTIVATE_ENV=pwd - cat metadata/swagger.yaml | python bin/yaml2json.py > api/swagger.json - rdoc bin _plugins/ --output gtn_rdoc + bundle exec rdoc bin _plugins/ --output gtn_rdoc --template rorvswild - name: Build Site run: | JEKYLL_ENV=production bundle exec jekyll build --strict_front_matter -d _site/training-material - cp metadata/feedback.csv _site/training-material/api/feedback.csv - cp metadata/feedback2.yaml _site/training-material/api/feedback2.yaml env: GTN_FORK: ${{ github.repository_owner }} @@ -96,6 +94,10 @@ jobs: jupyter lite build --contents /tmp/notebook mv _output _site/training-material/jupyter/ + - name: Mandatory pre-deploy checks + run: | + bundle exec ruby bin/lint-deploy.rb + - name: Deploy 🚀 uses: peaceiris/actions-gh-pages@v3 with: diff --git a/.github/workflows/monthly-release-backfill.yml b/.github/workflows/monthly-release-backfill.yml index 2251f34e9fe703..5560c627ff7aad 100644 --- a/.github/workflows/monthly-release-backfill.yml +++ b/.github/workflows/monthly-release-backfill.yml @@ -71,7 +71,6 @@ jobs: sed -i s"|^title: .*|title: 'GTN Archive ${SOURCE_TAG}'|g" _config.yml curl -L https://hexylena.github.io/toolshed-version-database/tool-meta.json > metadata/tool-meta.json make rebuild-search-index ACTIVATE_ENV=pwd - cat metadata/swagger.yaml | python bin/yaml2json.py > api/swagger.json JEKYLL_ENV=production bundle exec jekyll build --strict_front_matter -d _site/training-material env: SOURCE_TAG: ${{ github.event.inputs.selected_tag }} diff --git a/.github/workflows/monthly-release.yml b/.github/workflows/monthly-release.yml index 83ae951d604549..d7808dc6c33721 100644 --- a/.github/workflows/monthly-release.yml +++ b/.github/workflows/monthly-release.yml @@ -76,7 +76,6 @@ jobs: sed -i s"|^title: .*|title: 'GTN Archive ${SOURCE_TAG}'|g" _config.yml curl -L https://hexylena.github.io/toolshed-version-database/tool-meta.json > metadata/tool-meta.json make rebuild-search-index ACTIVATE_ENV=pwd - cat metadata/swagger.yaml | ruby bin/yaml2json.rb > api/swagger.json JEKYLL_ENV=production bundle exec jekyll build --strict_front_matter -d _site/training-material env: SOURCE_TAG: ${{ env.release_tag }} diff --git a/CONTRIBUTORS.yaml b/CONTRIBUTORS.yaml index 8a9a770e48130e..52db9ec54dfcf5 100644 --- a/CONTRIBUTORS.yaml +++ b/CONTRIBUTORS.yaml @@ -707,6 +707,12 @@ dyusuf: affiliations: - uni-freiburg +dometto: + name: Dawa Ometto + joined: 2024-12 + affiliations: + - surf + eancelet: name: Estelle Ancelet email: estelle.ancelet@inrae.fr @@ -1694,6 +1700,12 @@ miRlyKayleigh: email: kayleigh.smith@open.ac.uk joined: 2022-12 +mirelaminkova: + name: Mirela Minkova + joined: 2024-12 + affiliations: + - surf + moffmade: name: Cory Maughmer joined: 2018-06 @@ -1765,6 +1777,11 @@ nagoue: email: nadia.goue@uca.fr orcid: 0000-0003-2750-1473 joined: 2019-07 + +Najatamk: + name: Najat Amoukou + email: najatibrahim21@gmail.com + joined: 2024-07 nakucher: name: Natalie Kucher @@ -2691,6 +2708,10 @@ yvesvdb: affiliations: - ifb +yuliiaorlova: + name: Yuliia Orlova + joined: 2024-12 + zargham-ahmad: name: Zargham Ahmad email: zargham.ahmad@recetox.muni.cz diff --git a/GRANTS.yaml b/GRANTS.yaml index e7a73e90b46121..0af219d601c5a4 100644 --- a/GRANTS.yaml +++ b/GRANTS.yaml @@ -204,6 +204,27 @@ h2020-defend: This work has received funding from the DEFEND project (www.defend2020.eu) with funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No 773701. url: https://www.defend2020.eu +nfdi4bioimage: + name: NFDI4Bioimage + short_name: "NFDI4Bioimage" + github: false + joined: 2024-12 + funding_id: "501864659" + funding_database: dfg + funder_name: DFG + url: https://nfdi4bioimage.de + avatar: "/training-material/shared/images/nfdi4bioimage.jpeg" + +nfdi4plants: + short_name: DataPLANT + name: DataPLANT (NFDI4Plants) + joined: 2024-07 + funding_id: "442077441" + funding_database: dfg + funder_name: DFG + url: https://nfdi4plants.org + avatar: "/training-material/shared/images/logo-dataplant.svg" + nhgri-anvil: name: National Human Genome Research Institute Genomic Data Science Analysis, Visualization, and Informatics Lab-Space short_name: "NHGRI ANVIL" @@ -235,3 +256,6 @@ skills4eosc: + + + diff --git a/Gemfile b/Gemfile index 7161f854323a3a..2448aaad0cc533 100644 --- a/Gemfile +++ b/Gemfile @@ -27,3 +27,7 @@ gem 'commander' # RO-Crates gem 'rubyzip', '~> 2.3.0' + +# Documentation +gem 'rdoc', '~> 6.7' +gem 'rorvswild_theme_rdoc' diff --git a/Gemfile.lock b/Gemfile.lock index cc8816b16efee3..e974c7e631da9d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,7 @@ GEM rexml csl-styles (2.0.1) csl (~> 2.0) + date (3.4.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -89,13 +90,19 @@ GEM pathutil (0.16.2) forwardable-extended (~> 2.6) pkg-config (1.5.5) + psych (5.2.1) + date + stringio public_suffix (5.0.3) racc (1.8.0) rainbow (3.1.1) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) + rdoc (6.9.1) + psych (>= 4.0.0) rexml (3.3.9) + rorvswild_theme_rdoc (0.2) rouge (4.1.3) rubyzip (2.3.2) safe_yaml (1.0.5) @@ -103,6 +110,7 @@ GEM google-protobuf (~> 3.23) sass-embedded (1.69.5-x86_64-linux-gnu) google-protobuf (~> 3.23) + stringio (3.1.2) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) typhoeus (1.4.0) @@ -131,6 +139,8 @@ DEPENDENCIES kwalify nokogiri (>= 1.10.4) pkg-config + rdoc (~> 6.7) + rorvswild_theme_rdoc rubyzip (~> 2.3.0) webrick diff --git a/Makefile b/Makefile index 6d2269b630c8b6..cd5397f5520920 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ bundle-update: bundle-install ## install gems if Ruby is already present (e.g. bundle update .PHONE: bundle-update -serve: api/swagger.json ## run a local server (You can specify PORT=, HOST=, and FLAGS= to set the port, host or to pass additional flags) +serve: ## run a local server (You can specify PORT=, HOST=, and FLAGS= to set the port, host or to pass additional flags) @echo "Tip: Want faster builds? Use 'serve-quick' in place of 'serve'." @echo "Tip: to serve in incremental mode (faster rebuilds), use the command: make serve FLAGS=--incremental" && echo "" && \ $(ACTIVATE_ENV) && \ @@ -78,7 +78,7 @@ serve: api/swagger.json ## run a local server (You can specify PORT=, HOST=, and ${JEKYLL} serve --trace --strict_front_matter -d _site/training-material -P ${PORT} -H ${HOST} ${FLAGS} .PHONY: serve -serve-quick: api/swagger.json ## run a local server (faster, some plugins disabled for speed) +serve-quick: ## run a local server (faster, some plugins disabled for speed) @echo "This will build the website with citations and other content disabled, and incremental on by default. To run the full preview (slower), use make serve" && echo "" && \ $(ACTIVATE_ENV) && \ mv Gemfile Gemfile.backup || true && \ @@ -110,7 +110,7 @@ build-gitpod: bundle-install ## run a build on a gitpod.io environment bundle exec jekyll build --config _config.yml .PHONY: build-gitpod -build: clean api/swagger.json ## build files but do not run a server (You can specify FLAGS= to pass additional flags to Jekyll) +build: clean ## build files but do not run a server (You can specify FLAGS= to pass additional flags to Jekyll) $(ACTIVATE_ENV) && \ mv Gemfile Gemfile.backup || true && \ mv Gemfile.lock Gemfile.lock.backup || true && \ @@ -267,10 +267,6 @@ annotate: ## annotate the tutorials with usable Galaxy instances rebuild-search-index: ## Rebuild search index node bin/lunr-index.js > search.json -api/swagger.json: metadata/swagger.yaml - $(ACTIVATE_ENV) && \ - cat metadata/swagger.yaml | python bin/yaml2json.py > api/swagger.json - clean: ## clean up junk files @rm -rf _site @rm -rf .sass-cache diff --git a/ORGANISATIONS.yaml b/ORGANISATIONS.yaml index f6ce6930982f41..d4d671947c3243 100644 --- a/ORGANISATIONS.yaml +++ b/ORGANISATIONS.yaml @@ -39,6 +39,14 @@ deNBI: name: de.NBI url: https://www.denbi.de/ +dfg: + name: Deutsche Forschungsgemeinschaft + short_name: "DFG" + github: false + joined: 2024-12 + url: https://www.dfg.de/de + avatar: "/training-material/shared/images/DFG_grant.png" + earlham: name: Earlham Institute joined: 2017-09 @@ -208,13 +216,6 @@ ncbi: avatar: "/training-material/shared/images/ncbi.png" ror: "02meqm098" -nfdi4plants: - short_name: DataPLANT - name: DataPLANT (NFDI4Plants) - joined: 2024-07 - url: https://nfdi4plants.org - avatar: "/training-material/shared/images/logo-dataplant.svg" - NIH: name: National Institutes of Health short_name: "NIH" @@ -317,6 +318,14 @@ swiss-tph: github: false ror: "03adhka07" +surf: + short_name: SURF + name: SURF + url: https://www.surf.nl/ + avatar: "/training-material/shared/images/surf.png" + github: false + ror: "009vhk114" + tb-capt: short_name: TB-CAPT name: TB-CAPT @@ -363,9 +372,3 @@ vib: github: false ror: "03xrhmk39" -nfdi4bioimage: - short_name: NFDI4BIOIMAGE - name: NFDI4BIOIMAGE - joined: 2024-11 - url: https://nfdi4bioimage.de - avatar: "/training-material/shared/images/nfdi4bioimage.jpeg" diff --git a/_includes/contributor-badge.html b/_includes/contributor-badge.html index 02b68829c51e89..fa94c6b7552174 100644 --- a/_includes/contributor-badge.html +++ b/_includes/contributor-badge.html @@ -3,11 +3,11 @@ {%- else -%} {%- assign contributor_badge_entity = site | fetch_contributor: include.id -%} {%- assign name = contributor_badge_entity.short_name | default: contributor_badge_entity.name | default: include.id -%} - {% if include.small %} - - {{ contributor_badge_entity | fetch_entity_avatar:include.id, 36 }} + {%- if include.small -%} + + {{- contributor_badge_entity | fetch_entity_avatar:include.id, 36 -}} - {% else %} + {%- else -%} {%- if contributor_badge_entity.orcid -%}orcid logo{%- endif -%} {{ contributor_badge_entity | fetch_entity_avatar:include.id, 36 }} @@ -16,5 +16,5 @@ {%- include _includes/contributor-human-icons.html activity=include.activity -%} {%- endif -%} - {% endif %} + {%- endif -%} {%- endif -%} diff --git a/_includes/cyoa-choices.html b/_includes/cyoa-choices.html index c0a836e10b4de5..4c3a37315e432d 100644 --- a/_includes/cyoa-choices.html +++ b/_includes/cyoa-choices.html @@ -1,6 +1,8 @@
-
Hands-on: Choose Your Own Tutorial
+
{{ include.title | default: "Hands-on: Choose Your Own Tutorial" }}
+ {% unless include.brief %}

This is a "Choose Your Own Tutorial" section, where you can select between multiple paths. Click one of the buttons below to select how you want to follow the tutorial

+ {% endunless %} {% if include.text %}

{{ include.text }} diff --git a/_includes/news-card.html b/_includes/news-card.html index def48d39b62b02..6e09585cb59321 100644 --- a/_includes/news-card.html +++ b/_includes/news-card.html @@ -1,4 +1,3 @@ -

{% assign n = include.news %} {% if n.cover %} @@ -18,38 +17,46 @@ {% assign coverimagealt = "GTN logo with a multi-coloured star and the words Galaxy Training Network" %} {% endif %} -
- -
-
- {{ coverimagealt }} -
-
-
- -
- {{ n.date | date: "%-d %B %Y" }} + +
+
+ +
+ {{ coverimagealt }} +
+
+
+
{{n.title}}
+
+ {% for id in n.contributions.authorship %} + {% include _includes/contributor-badge.html id=id small=true %} + {% endfor %} +
+ {{ n.date | date: "%-d %B %Y" }} +
+ + + + {% if n.external %} + This is an external post, please follow the link to read it. + {% else %} + {{ n.excerpt | strip_html }} + {% endif %} + +
- {% include _includes/contributors-line.html page=n %} +
+ + -
-
+
+ +
-
+ + diff --git a/_includes/quiz.html b/_includes/quiz.html index 0fd5cbe14efd00..24320c8ffefd1f 100644 --- a/_includes/quiz.html +++ b/_includes/quiz.html @@ -5,11 +5,94 @@ {% endif %} {% endfor %} -
->
Quiz: {{ quiz.title }}
-> Check your knowledge with a quiz! -> -> - [Self Study Mode]({% link quiz/quiz.html %}?mode=self&quiz={{ site.baseurl }}/{{ quiz.path }}) - do the quiz at your own pace, to check your understanding. -> - [Classroom Mode]({% link quiz/quiz.html %}?mode=teacher&quiz={{ site.baseurl }}/{{ quiz.path }}) - do the quiz synchronously with a classroom of students. -{: .question} +
+
+ + Quiz: {{ quiz.title }} +
+ + +{% for question in quiz.questions %} + {% assign qnum = forloop.index %} +
+ Question {{ qnum }}: {{ question.title }} + + {% if question.image %} + Image for question {{ forloop.index }} + {% endif %} + + {% unless question.type == "poll" %} +
    + {% for answer in question.answers %} + + {%- capture is_correct -%} + {%- if question.type == "choose-1" -%} + {%- if answer == question.correct -%}correct{%- endif -%} + {%- elsif question.type == "choose-many" -%} + {%- for a in question.correct -%}{%- if answer == a -%}correct{%- endif -%}{%- endfor -%} + {%- endif -%} + {%- endcapture -%} + +
  • + + {% if question.type == "choose-1" %} + + {% elsif question.type == "choose-many" %} + + {% elsif question.type == "free-text" %} + + {% endif %} + + +
  • + {% endfor %} +
+ {% endunless %} +
+{% endfor %} + +
+ + +
+ + diff --git a/_includes/tutorial_list.html b/_includes/tutorial_list.html index a49f8713d4e49c..adf12c093e5ea2 100644 --- a/_includes/tutorial_list.html +++ b/_includes/tutorial_list.html @@ -40,12 +40,32 @@ {% assign hidetime = material.async | default: async%} {% if page.layout == 'event' and hidetimes != true %} - {{material.time | markdownify }} + {{material.time | markdownify }} + + + + {% endif %} {% if material.layout == 'custom' %} - {{material.title | markdownify }} + {{material.title | markdownify }} diff --git a/_layouts/by_tool.html b/_layouts/by_tool.html index 3c6837296ea004..60071a09ccd519 100644 --- a/_layouts/by_tool.html +++ b/_layouts/by_tool.html @@ -2,14 +2,14 @@ layout: base --- -{% assign tool_id_path = page.observed_tool_ids[0][0] %} +{% assign tool_id_path = page.latest_tool_id %}

{{ site.data['tool-meta'][tool_id_path].name }}: {{ site.data['tool-meta'][tool_id_path].desc }}

-
{{ page.observed_tool_ids[0] }}
+
{{ tool_id_path }}
@@ -17,45 +17,88 @@

Metadata

-

Servers Offering this Tool

- UseGalaxy.eu ({{ page.observed_tool_ids[0][1] }}) -

This is a guess, we are not currently checking if that is actually present.

-
-

Observed Tool Versions

-

Within GTN tutorials

-
+

Links

- {% if site.data['tool-meta'][tool_id_path].edam_operations %} + {% assign edam_op_count = site.data['tool-meta'][tool_id_path].edam_operations | size %} + {% if edam_op_count > 0 %}

EDAM Operations

{% endif %} - {% if site.data['tool-meta'][tool_id_path].edam_topics%} + {% assign edam_topic_count = site.data['tool-meta'][tool_id_path].edam_topics | size %} + {% if edam_topic_count > 0 %}

EDAM Topics

{% endif %} + +
+

Observed Tool Versions

+

Within GTN tutorials

+
+ + {% for tool in page.observed_tool_ids %} +
+ {{ tool[1] }} + {% assign v_id = tool[0] %} + {% assign supported = site | tool_version_support: v_id %} + {% assign usegalaxy_exact = supported.exact | where: "usegalaxy", "true" %} + {% assign other_exact = supported.exact | where: "usegalaxy", "false" %} + + +
+ {% endfor %} +
@@ -74,3 +117,21 @@

Relevant Tutorials

+ + diff --git a/_layouts/community.html b/_layouts/community.html new file mode 100644 index 00000000000000..00f3b42322fc4b --- /dev/null +++ b/_layouts/community.html @@ -0,0 +1,291 @@ +--- +layout: base +--- + +{% assign topic = site.data[page.topic_name] | default: page.topic %} +{% assign topic_material = site | list_materials_structured:topic.name %} +{% assign topic_material_flat = site | list_materials_flat:topic.name %} + +

{{ topic.title }} — Community Home

+ +This is a new, experimental "Community Home" for a given topic. It is intended to highlight community contributions over the years to a topic! + +{% assign prio = "news events learning-pathways tutorials slides recordings faqs workflows grants organisations contributors" | split:" " %} + + +{% assign topic_material_by_years = site | list_topic_materials_yearly:topic.name %} +{% assign cumulative_counts_by_years = site | count_topic_materials_yearly:topic.name %} + +
+
+
+ +
Materials Over Time
+
+
+
+
+ +
Contributors Over Time
+
+
+
+
+ +
Workflows Over Time
+
+
+
+
+ +
Other Contributions Over Time
+
+
+
+ +{% for year in topic_material_by_years %} +
+
+
+

{{ year[0] }} Year in Review

+

So many new additions to our community!

+
+
+
+
+ {% for p in prio %} + {% if year[1][p] %} + {% assign group = year[1][p] %} +
+
+ {% assign group_len = group | size %} +
{{ group_len }}
+
+ {{ p | regex_replace: '-', ' ' | titlecase }} +
+
+
+ {% endif %} + {% endfor %} +
+
+
+
+
+ + {% for p in prio %} + {% if year[1][p] %} + {% assign group = year[1][p] %} + +

{{ p | regex_replace: '-', ' ' | titlecase }}

+ + {% endif %} + {% endfor %} +
+
+ +
+ +
+{% endfor %} + +

Cumulative Data as CSV

+
+year,{% for point in cumulative_counts_by_years['tutorials'] %}{{ point.x | regex_replace:'-.*', '' }}{%unless forloop.last%},{%endunless%}{% endfor %}
+{% for p in prio %}{{ p }},{% for point in cumulative_counts_by_years[p] %}{{ point.y }}{%unless forloop.last%},{%endunless%}{% endfor %}
+{% endfor %}
+
+
+ + + + + + + + + + diff --git a/_layouts/contributor_index.html b/_layouts/contributor_index.html index 205e2a3e207ae2..76f5f23fe57ba6 100644 --- a/_layouts/contributor_index.html +++ b/_layouts/contributor_index.html @@ -249,8 +249,8 @@

GitHub Activity

{% if page.gh_prs_count > 0 %}

{{ page.gh_prs_count }} Merged Pull Requests

- See all of the - {% icon github %} Pull Requests and + See all of the + {% icon github %} Pull Requests and {% icon github %} Commits by {{ entity.name | default: page.contributor }}.

{% endif %} -

Favourite Topics

+

Favourite Topics

-

Favourite Formats

+

Favourite Formats

diff --git a/_layouts/event.html b/_layouts/event.html index c58d546822daac..f4d4af5d11ca05 100644 --- a/_layouts/event.html +++ b/_layouts/event.html @@ -173,13 +173,14 @@

Get set up for the course!

Follow the steps below to get set up with everything you need to participate in the course!

- {% unless event.infrastructure.no_galaxy %} + {% if event.infrastructure.servers %}
Galaxy logo
+
Create a Galaxy Account

@@ -200,7 +201,7 @@

Create a Galaxy Account
- {% endunless %} + {% endif %} {% if event.infrastructure.tiaas %} diff --git a/_layouts/home.html b/_layouts/home.html index 70452694caa617..7fbdbc90d5a86f 100644 --- a/_layouts/home.html +++ b/_layouts/home.html @@ -209,12 +209,11 @@

Contributor Hall of Fame

Quickstart

- diff --git a/_layouts/topic-maintainer.html b/_layouts/topic-maintainer.html index 19bb848e8a6110..2b8044cc06260c 100644 --- a/_layouts/topic-maintainer.html +++ b/_layouts/topic-maintainer.html @@ -6,7 +6,7 @@ {% assign topic_material = site | list_materials_structured:topic.name %} {% assign topic_material_flat = site | list_materials_flat:topic.name %} -

{{ topic.title }} Editorial Board Home

+

{{ topic.title }} — Editorial Board Home

This is a new, experimental "Editorial Board Home" for a given topic. It is intended to provide a single place for maintainers and editorial board members to find out key information about their topic and identify action items. @@ -249,7 +249,7 @@

Events using materials from this Topic

TODO once this is merged: https://github.com/galaxyproject/training-material/pull/4963 -

Statistics For Your Materials

+

Statistics For Your Materials

Stats powered by Plausible Analytics
diff --git a/_layouts/topic.html b/_layouts/topic.html index 059d2b75076dc4..5d81bdf93e66b1 100644 --- a/_layouts/topic.html +++ b/_layouts/topic.html @@ -16,20 +16,31 @@

{{ topic.title }}

{{ topic.summary | markdownify }}

- {% if topic.gitter %} -

For any question or discussions related to this topic, or to connect with others in the community, please visit the community chat:

-

{% icon comment %}   Community Matrix Chat

- {% endif%} - - - - {% if topic.requirements %} -

Requirements

-

Before diving into this topic, we recommend you to have a look at:

- -
    - {% include _includes/display_extra_training.md extra_trainings=topic.requirements %} -
+ {% if topic.community_ctas %} +
+ {% for cta in topic.community_ctas %} +
+ {{ cta.description | markdownify }} + + {% icon_var cta.icon %}   {{ cta.link_text }} + +
+ {% endfor %} +
+ {% else %} + + {% if topic.gitter %} +

For any question or discussions related to this topic, or to connect with others in the community, please visit the community chat:

+

{% icon comment %}   Community Matrix Chat

+ {% endif%} + + {% if topic.requirements %} +

Requirements

+

Before diving into this topic, we recommend you to have a look at:

+
    + {% include _includes/display_extra_training.md extra_trainings=topic.requirements %} +
+ {% endif %} {% endif %} {% if topic.learning_path_cta %} @@ -44,6 +55,22 @@

Not sure where to start?

{% endif %} + {% if topic.learning_path_ctas %} +
+
+

Not sure where to start?

+

+ Try a {{ topic.title }} Learning Pathway! + + {% for cta in topic.learning_path_ctas %} + {{ cta[0] }} + {% endfor %} + +

+
+
+ {% endif %} +

Material

{% if topic.toc %} @@ -85,6 +112,12 @@

Frequently Asked Questions

{% endif %} {% endunless %} + {% unless topic.tag_based %} +

Community Resources

+ Community Home + Maintainer Home + {% endunless %} + {% if topic.editorial_board %}

Editorial Board

This material is reviewed by our Editorial Board:

diff --git a/_layouts/tutorial_hands_on.html b/_layouts/tutorial_hands_on.html index 441ab982b9298b..e06c83835a9a57 100644 --- a/_layouts/tutorial_hands_on.html +++ b/_layouts/tutorial_hands_on.html @@ -322,6 +322,7 @@

{{ locale['faqs'] | default: "Frequently Asked Questions" }}

If not, please ask your question on the GTN Gitter Channel or the Galaxy Help Forum + {% if topic.references %}

Useful literature

diff --git a/_plugins/abbr.rb b/_plugins/abbr.rb index 5d09dd661daee4..e4b96e7d682861 100644 --- a/_plugins/abbr.rb +++ b/_plugins/abbr.rb @@ -3,61 +3,63 @@ require 'jekyll' module Jekyll - ## - # This class modifies the page contents to replace {abbr} with the associated - # abbreviation in a proper tag. It's done as a generator because it's - # easier to operate once per page and be able to easily see if we've generated - # the abbreviation before. - # - # This could in theory operate as a simple tag, but I'm not sure how to keep - # track of "# of times seen per page" there. - class Abbreviate < Jekyll::Generator - safe true - - def initialize(config) # :nodoc: - super - @config = config['abbreviate'] ||= {} - end + module Generators + ## + # This class modifies the page contents to replace {abbr} with the associated + # abbreviation in a proper tag. It's done as a generator because it's + # easier to operate once per page and be able to easily see if we've generated + # the abbreviation before. + # + # This could in theory operate as a simple tag, but I'm not sure how to keep + # track of "# of times seen per page" there. + class Abbreviate < Jekyll::Generator + safe true - def generate(site) # :nodoc: - site.pages - .reject { |page| skip_layout?(page.data['layout']) } - .each { |page| abbreviate page } - site.posts.docs - .reject { |post| skip_layout?(post.data['layout']) } - .each { |post| abbreviate post } - end + def initialize(config) # :nodoc: + super + @config = config['abbreviate'] ||= {} + end + + def generate(site) # :nodoc: + site.pages + .reject { |page| skip_layout?(page.data['layout']) } + .each { |page| abbreviate page } + site.posts.docs + .reject { |post| skip_layout?(post.data['layout']) } + .each { |post| abbreviate post } + end - private + private - def abbreviate(page) # :nodoc: - return unless page.data.key?('abbreviations') + def abbreviate(page) # :nodoc: + return unless page.data.key?('abbreviations') - seen = {} - page.data['abbreviations'].each do |abbr, definition| - page.content = page.content.gsub(/\{(#{abbr})\}/) do - if seen.key?(abbr) - firstdef = false - else - firstdef = true - seen[abbr] = true - end + seen = {} + page.data['abbreviations'].each do |abbr, definition| + page.content = page.content.gsub(/\{(#{abbr})\}/) do + if seen.key?(abbr) + firstdef = false + else + firstdef = true + seen[abbr] = true + end - if firstdef - "#{definition} (#{abbr})" - else - "#{abbr}" + if firstdef + "#{definition} (#{abbr})" + else + "#{abbr}" + end end end end - end - def skip_layout?(layout) - to_skip = @config['skip_layouts'] || [] + def skip_layout?(layout) + to_skip = @config['skip_layouts'] || [] - true if to_skip.empty? + true if to_skip.empty? - to_skip.include?(layout) + to_skip.include?(layout) + end end end end diff --git a/_plugins/api.rb b/_plugins/api.rb index 263701d4d1df18..597b5a066c2372 100644 --- a/_plugins/api.rb +++ b/_plugins/api.rb @@ -6,8 +6,10 @@ require './_plugins/gtn/metrics' require './_plugins/gtn/scholar' require './_plugins/gtn/git' +require './_plugins/gtn/hooks' require './_plugins/gtn/ro-crate' require './_plugins/gtn' +require './_plugins/util' ## # Use Jekyll's Markdown converter to convert text to HTML @@ -62,123 +64,85 @@ def mapContributor(site, c) end module Jekyll - ## - # This class generates the GTN's "api" by writing out a folder full of JSON files. - class APIGenerator < Generator - + module Generators ## - # Generates /api/configuration.json - # Params: - # +site+:: +Jekyll::Site+ object - # Returns: - # nil - def generateConfiguration(site) - page2 = PageWithoutAFile.new(site, '', 'api/', 'configuration.json') - site.config.update(Gtn::Git.discover) - # Remove every key that starts with "cached_" - conf = site.config.reject { |k, _v| k.to_s.start_with?('cached_') } - page2.content = JSON.pretty_generate(conf) - page2.data['layout'] = nil - site.pages << page2 - end + # This class generates the GTN's "api" by writing out a folder full of JSON files. + class APIGenerator + + def copy(site, source, dest) + # It isn't unusual that some of these might not exist in dev envs. + if File.exist?(site.in_source_dir(source)) + if ! Dir.exist?(File.dirname(site.in_dest_dir(dest))) + FileUtils.mkdir_p(File.dirname(site.in_dest_dir(dest))) + end - ## - # Generates /api/version.json - # Params: - # +site+:: +Jekyll::Site+ object - # Returns: - # nil - def generateVersion(site) - page2 = PageWithoutAFile.new(site, '', 'api/', 'version.json') - page2.content = JSON.pretty_generate(Gtn::Git.discover) - page2.data['layout'] = nil - site.pages << page2 - end + FileUtils.cp(site.in_source_dir(source), site.in_dest_dir(dest)) + end + end - ## - # Generates /api/data-library.yaml - # Params: - # +site+:: +Jekyll::Site+ object - # Returns: - # nil - def generateLibrary(site) - Jekyll.logger.info '[GTN/API] Data Library' - page2 = PageWithoutAFile.new(site, '', 'api/', 'data-library.yaml') - data_libraries = Dir.glob('topics/**/data-library.yaml') - data_libraries.map! { |x| YAML.load_file(x) } - page2.content = JSON.pretty_generate(Gtn::Git.discover) - page2.data['layout'] = nil - site.pages << page2 - end + def write(site, dest, data, json: true, pretty: true) + # Since we're doing this ourselves, need to be responsible for ensuring + # the directory exists. + if ! Dir.exist?(File.dirname(site.in_dest_dir(dest))) + FileUtils.mkdir_p(File.dirname(site.in_dest_dir(dest))) + end - ## - # Runs the generation process - # Params: - # +site+:: +Jekyll::Site+ object - def generate(site) - generateConfiguration(site) - # For some reason the templating isn't working right here. - # generateVersion(site) - # TODO: - # generateLibrary(site) - - Jekyll.logger.info '[GTN/API] Generating API' - # Full Bibliography - Gtn::Scholar.load_bib(site) - Jekyll.logger.debug '[GTN/API] Bibliography' - page3 = PageWithoutAFile.new(site, '', 'api/', 'gtn.bib') - page3.content = site.config['cached_global_bib'].to_s - page3.data['layout'] = nil - site.pages << page3 - - # Metrics endpoint, /metrics - page2 = PageWithoutAFile.new(site, '', '', 'metrics') - page2.content = "{% raw %}\n#{Gtn::Metrics.generate_metrics(site)}{% endraw %}" - page2.data['layout'] = nil - site.pages << page2 - - # Public tool listing - page2 = PageWithoutAFile.new(site, '', 'api/', 'psl.json') - page2.content = JSON.generate(site.data['public-server-tools']) - page2.data['layout'] = nil - site.pages << page2 - - # Tool Categories - page2 = PageWithoutAFile.new(site, '', 'api/', 'toolcats.json') - page2.content = JSON.generate(site.data['toolcats']) - page2.data['layout'] = nil - site.pages << page2 - - # Tool Categories - page2 = PageWithoutAFile.new(site, '', 'api/', 'toolshed-revisions.json') - page2.content = JSON.generate(site.data['toolshed-revisions']) - page2.data['layout'] = nil - site.pages << page2 - - # Feedback Data - page2 = PageWithoutAFile.new(site, '', 'api/', 'feedback2.json') - page2.content = JSON.generate(site.data['feedback2']) - page2.data['layout'] = nil - site.pages << page2 - - # Contributors - Jekyll.logger.debug '[GTN/API] Contributors, Funders, Organisations' - %w[contributors grants organisations].each do |type| - page2 = PageWithoutAFile.new(site, '', 'api/', "#{type}.json") - page2.content = JSON.pretty_generate(site.data[type].map { |c, _| mapContributor(site, c) }) - page2.data['layout'] = nil - site.pages << page2 - site.data['contributors'].each do |c, _| - page4 = PageWithoutAFile.new(site, '', 'api/', "#{type}s/#{c}.json") - page4.content = JSON.pretty_generate(mapContributor(site, c)) - page4.data['layout'] = nil - site.pages << page4 + if json + if pretty + File.write(site.in_dest_dir(dest), JSON.pretty_generate(data)) + else + File.write(site.in_dest_dir(dest), JSON.generate(data)) + end + else + # Pretty isn't relevant. + File.write(site.in_dest_dir(dest), data) end end - page2 = PageWithoutAFile.new(site, '', 'api/', 'contributors.geojson') - page2.content = JSON.pretty_generate( - { + ## + # Runs the generation process + # Params: + # +site+:: +Jekyll::Site+ object + def generate(site) + Jekyll.logger.info '[GTN/API] Generating API' + + write(site, 'api/configuration.json', site.config.reject { |k, _v| k.to_s.start_with?('cached_') }) + write(site, 'api/swagger.json', site.data['swagger']) + write(site, 'api/version.json', Gtn::Git.discover) + + # Full Bibliography + Jekyll.logger.debug '[GTN/API] Bibliography' + write(site, 'api/gtn.bib', site.config['cached_global_bib'].to_s, json: false) + + # Metrics endpoint, /metrics + write(site, 'api/metrics', Gtn::Metrics.generate_metrics(site), json: false) + + # Public tool listing + write(site, 'api/psl.json', site.data['public-server-tools'], pretty: false) + + # Tool Categories + write(site, 'api/toolcats.json', site.data['toolcats'], pretty: false) + + # Tool Categories + write(site, 'api/toolshed-revisions.json', site.data['toolshed-revisions'], pretty: false) + + # Feedback Data + write(site, 'api/feedback2.json', site.data['feedback2'], pretty: false) + copy(site, 'metadata/feedback.csv', 'api/feedback.csv') + copy(site, 'metadata/feedback2.yaml', 'api/feedback2.yaml') + + # Contributors + Jekyll.logger.debug '[GTN/API] Contributors, Funders, Organisations' + %w[contributors grants organisations].each do |type| + pfo = site.data[type].map { |c, _| mapContributor(site, c) } + write(site, "api/#{type}.json", pfo, pretty: false) + + site.data['contributors'].each do |c, _| + write(site, "api/#{type}/#{c}.json", mapContributor(site, c)) + end + end + + geojson = { 'type' => 'FeatureCollection', 'features' => site.data['contributors'] .select { |_k, v| v.key? 'location' } @@ -197,161 +161,136 @@ def generate(site) } end } - ) - page2.data['layout'] = nil - site.pages << page2 - - # Trigger the topic cache to generate if it hasn't already - Jekyll.logger.debug '[GTN/API] Tutorials' - TopicFilter.topic_filter(site, 'does-not-matter') - TopicFilter.list_topics(site).map do |topic| - out = site.data[topic].dup - out['materials'] = TopicFilter.topic_filter(site, topic).map do |x| - q = x.dup - q['contributors'] = Gtn::Contributors.get_contributors(q).dup.map do |c| - mapContributor(site, c) - end + write(site, "api/contributors.geojson", geojson) + + + # Trigger the topic cache to generate if it hasn't already + Jekyll.logger.debug '[GTN/API] Tutorials' + Gtn::TopicFilter.topic_filter(site, 'does-not-matter') + Gtn::TopicFilter.list_topics(site).map do |topic| + out = site.data[topic].dup + out['materials'] = Gtn::TopicFilter.topic_filter(site, topic).map do |x| + q = x.dup + q['contributors'] = Gtn::Contributors.get_contributors(q).dup.map do |c| + mapContributor(site, c) + end - q['urls'] = {} + q['urls'] = {} - if !q['hands_on'].nil? - q['urls']['hands_on'] = site.config['url'] + site.config['baseurl'] + "/api/topics/#{q['url'][8..-6]}.json" - end + if !q['hands_on'].nil? + q['urls']['hands_on'] = site.config['url'] + site.config['baseurl'] + "/api/topics/#{q['url'][8..-6]}.json" + end + + if !q['slides'].nil? + q['urls']['slides'] = site.config['url'] + site.config['baseurl'] + "/api/topics/#{q['url'][8..-6]}.json" + end + + # Write out the individual page + # Delete the ref to avoid including it by accident + q.delete('ref') + q.delete('ref_tutorials') + q.delete('ref_slides') + write(site, "api/topics/#{q['url'][7..-6]}.json", q) - if !q['slides'].nil? - q['urls']['slides'] = site.config['url'] + site.config['baseurl'] + "/api/topics/#{q['url'][8..-6]}.json" + q + end + out['editorial_board'] = out['editorial_board'].map do |c| + mapContributor(site, c) end - # Write out the individual page - page6 = PageWithoutAFile.new(site, '', 'api/topics/', "#{q['url'][7..-6]}.json") - # Delete the ref to avoid including it by accident - q.delete('ref') - q.delete('ref_tutorials') - q.delete('ref_slides') - page6.content = JSON.pretty_generate(q) - page6.data['layout'] = nil - site.pages << page6 - - q - end - out['editorial_board'] = out['editorial_board'].map do |c| - mapContributor(site, c) + write(site, "api/topics/#{topic}.json", out) end - page2 = PageWithoutAFile.new(site, '', 'api/topics/', - "#{topic}.json") - page2.content = JSON.pretty_generate(out) - page2.data['layout'] = nil - site.pages << page2 - end + topics = {} + Jekyll.logger.debug '[GTN/API] Topics' + # Individual Topic Indexes + site.data.each_pair do |k, v| + if v.is_a?(Hash) && v.key?('type') && v.key?('editorial_board') + + topics[k] = { + 'name' => v['name'], + 'title' => v['title'], + 'summary' => v['summary'], + 'url' => site.config['url'] + site.config['baseurl'] + "/api/topics/#{k}.json", + 'editorial_board' => v['editorial_board'].map { |c| mapContributor(site, c) } + } + end + end - topics = {} - Jekyll.logger.debug '[GTN/API] Topics' - # Individual Topic Indexes - site.data.each_pair do |k, v| - if v.is_a?(Hash) && v.key?('type') && v.key?('editorial_board') - - topics[k] = { - 'name' => v['name'], - 'title' => v['title'], - 'summary' => v['summary'], - 'url' => site.config['url'] + site.config['baseurl'] + "/api/topics/#{k}.json", - 'editorial_board' => v['editorial_board'].map { |c| mapContributor(site, c) } + # Videos.json + # { + # "id": "transcriptomics/tutorials/mirna-target-finder/slides", + # "topic": "Transcriptomics", + # "title": "Whole transcriptome analysis of Arabidopsis thaliana" + # }, + + videos = Gtn::TopicFilter.list_videos(site).map do |m| + { + id: "#{m['topic_name']}/tutorials/#{m['tutorial_name']}/slides", + topic: m['topic_name_human'], + title: m['title'] } end - end + write(site, "api/videos.json", videos) - # Videos.json - # { - # "id": "transcriptomics/tutorials/mirna-target-finder/slides", - # "topic": "Transcriptomics", - # "title": "Whole transcriptome analysis of Arabidopsis thaliana" - # }, - - page2 = PageWithoutAFile.new(site, '', 'api/', 'videos.json') - page2.content = JSON.pretty_generate(TopicFilter.list_videos(site).map do |m| - { - id: "#{m['topic_name']}/tutorials/#{m['tutorial_name']}/slides", - topic: m['topic_name_human'], - title: m['title'] - } - end) - page2.data['layout'] = nil - site.pages << page2 - - # Overall topic index - page2 = PageWithoutAFile.new(site, '', 'api/', 'topics.json') - page2.content = JSON.pretty_generate(topics) - page2.data['layout'] = nil - site.pages << page2 - - Jekyll.logger.debug '[GTN/API] Tutorial and Slide pages' - - # Deploy the feedback file as well - page2 = PageWithoutAFile.new(site, '', 'api/', 'feedback.json') - page2.content = JSON.pretty_generate(site.data['feedback']) - page2.data['layout'] = nil - site.pages << page2 - - # Top Tools - Jekyll.logger.debug '[GTN/API] Top Tools' - page2 = PageWithoutAFile.new(site, '', 'api/', 'top-tools.json') - page2.content = JSON.pretty_generate(TopicFilter.list_materials_by_tool(site)) - page2.data['layout'] = nil - site.pages << page2 - - # Not really an API - TopicFilter.list_materials_by_tool(site).each do |tool, tutorials| - page2 = PageWithoutAFile.new(site, '', 'by-tool/', "#{tool.gsub('%20', ' ')}.html") - page2.content = nil - page2.data['layout'] = 'by_tool' - page2.data['short_tool'] = tool - page2.data['observed_tool_ids'] = tutorials['tool_id'] - page2.data['tutorial_list'] = tutorials['tutorials'] - site.pages << page2 - end - # GA4GH TRS Endpoint - # Please note that this is all a fun hack - TopicFilter.list_all_materials(site).select { |m| m['workflows'] }.each do |material| - material['workflows'].each do |workflow| - wfid = workflow['wfid'] - wfname = workflow['wfname'] + # Overall topic index + write(site, "api/topics.json", topics) - page2 = PageWithoutAFile.new(site, '', "api/ga4gh/trs/v2/tools/#{wfid}/versions/", "#{wfname}.json") - page2.content = JSON.pretty_generate( - { + Jekyll.logger.debug '[GTN/API] Tutorial and Slide pages' + + # Deploy the feedback file as well + write(site, "api/feedback.json", site.data['feedback']) + + # Top Tools + Jekyll.logger.debug '[GTN/API] Top Tools' + write(site, "api/top-tools.json", Gtn::TopicFilter.list_materials_by_tool(site)) + + # GA4GH TRS Endpoint + # Please note that this is all a fun hack + Gtn::TopicFilter.list_all_materials(site).select { |m| m['workflows'] }.each do |material| + material['workflows'].each do |workflow| + wfid = workflow['wfid'] + wfname = workflow['wfname'] + + ga4gh_blob = { 'id' => wfname, 'url' => site.config['url'] + site.config['baseurl'] + material['url'], 'name' => 'v1', 'author' => [], 'descriptor_type' => ['GALAXY'], } - ) - page2.data['layout'] = nil - site.pages << page2 - - page2 = PageWithoutAFile.new(site, '', "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}/GALAXY", - 'descriptor.json') - page2.content = JSON.pretty_generate( - { + write(site, "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}.json", ga4gh_blob) + + + + descriptor = { 'content' => File.read("#{material['dir']}/workflows/#{workflow['workflow']}"), 'checksum' => [], 'url' => nil, } - ) - page2.data['layout'] = nil - site.pages << page2 + write(site, "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}/GALAXY/descriptor.json", descriptor) + end end end end end end + +Jekyll::Hooks.register :site, :post_read do |site| + if Jekyll.env == 'production' + Gtn::Hooks.by_tool(site) + end +end + # Basically like `PageWithoutAFile`, we just write out the ones we'd created earlier. Jekyll::Hooks.register :site, :post_write do |site| # No need to run this except in prod. if Jekyll.env == 'production' + # Build our API + api = Jekyll::Generators::APIGenerator.new + api.generate(site) # Public tool listing: reorganised if site.data['public-server-tools'] && site.data['public-server-tools']['tools'] @@ -373,7 +312,7 @@ def generate(site) Jekyll.logger.debug '[GTN/API/PSL] PSL Dataset not available, are you in a CI environment?' end - TopicFilter.list_all_materials(site).each do |material| + Gtn::TopicFilter.list_all_materials(site).each do |material| directory = material['dir'] if material['slides'] @@ -417,7 +356,7 @@ def generate(site) # ro-crate-metadata.json crate_start = Time.now count = 0 - TopicFilter.list_all_materials(site).select { |m| m['workflows'] }.each do |material| + Gtn::TopicFilter.list_all_materials(site).select { |m| m['workflows'] }.each do |material| material['workflows'].each do |workflow| Gtn::RoCrate.write(site, dir, material, workflow, site.config['url'], site.config['baseurl']) count += 1 diff --git a/_plugins/author-page.rb b/_plugins/author-page.rb index a82bccdfcff80e..1666a93baf847a 100644 --- a/_plugins/author-page.rb +++ b/_plugins/author-page.rb @@ -4,167 +4,169 @@ require './_plugins/gtn' module Jekyll - ## - # This class generates the GTN's author pags - class AuthorPageGenerator < Generator - safe true - + module Generators ## - # This extracts the contributions and pushes them on to an existing - # datastructure, modifying it in the process. It's pretty gross. - # - # Params - # +t+:: The tutorial, slide, or news item - # +datastructure+:: The hash of contributors that the author information should be pushed onto - # +flat+:: Whether the datastructure is a flat array or a nested array - # - # Returns - # +datastructure+:: The modified datastructure - def pusher(t, datastructure, flat) - if t.data.key?('contributors') - if flat - t.data['contributors'].each { |c| datastructure[c].push(t) } - else - t.data['contributors'].each { |c| datastructure[c].push([t, nil]) } - end - elsif t.data.key?('contributions') - t.data['contributions'].each do |contribution_type, contributor| - contributor.each do |c| - if flat - datastructure[c].push(t) - else - datastructure[c].push([t, contribution_type]) + # This class generates the GTN's author pags + class AuthorPageGenerator < Generator + safe true + + ## + # This extracts the contributions and pushes them on to an existing + # datastructure, modifying it in the process. It's pretty gross. + # + # Params + # +t+:: The tutorial, slide, or news item + # +datastructure+:: The hash of contributors that the author information should be pushed onto + # +flat+:: Whether the datastructure is a flat array or a nested array + # + # Returns + # +datastructure+:: The modified datastructure + def pusher(t, datastructure, flat) + if t.data.key?('contributors') + if flat + t.data['contributors'].each { |c| datastructure[c].push(t) } + else + t.data['contributors'].each { |c| datastructure[c].push([t, nil]) } + end + elsif t.data.key?('contributions') + t.data['contributions'].each do |contribution_type, contributor| + contributor.each do |c| + if flat + datastructure[c].push(t) + else + datastructure[c].push([t, contribution_type]) + end end end end - end - t.data['maintainers'].each { |c| datastructure[c].push([t, 'maintainer']) } if t.data.key?('maintainers') - t.data['funding'].each { |c| datastructure[c].push([t, 'funding']) } if t.data.key?('funding') + t.data['maintainers'].each { |c| datastructure[c].push([t, 'maintainer']) } if t.data.key?('maintainers') + t.data['funding'].each { |c| datastructure[c].push([t, 'funding']) } if t.data.key?('funding') - datastructure - end - - ## - # This generates the author pages - # Params - # +site+:: The site object - def generate(site) - return unless site.layouts.key? 'contributor_index' - - dir = 'hall-of-fame' - - # pre-calculating this hash saves about 4.9 seconds off the previous - # build time of 5 seconds. - tutorials_by_author = Hash.new { |hash, key| hash[key] = [] } - learning_pathways_by_author = Hash.new { |hash, key| hash[key] = [] } - slides_by_author = Hash.new { |hash, key| hash[key] = [] } - news_by_author = Hash.new { |hash, key| hash[key] = [] } - events_by_author = Hash.new { |hash, key| hash[key] = [] } - videos_by_author = Hash.new { |hash, key| hash[key] = [] } - faqs_by_author = Hash.new { |hash, key| hash[key] = [] } - has_philosophy = Hash.new { false } - - prs_by_author = Hash.new { |hash, key| hash[key] = [] } - reviews_by_author = Hash.new { |hash, key| hash[key] = [] } - - site.data['github'].each do |num, pr| - prs_by_author[pr['author']['login']] << [num, pr['mergedAt']] - - pr['reviews'].each do |review| - reviews_by_author[review['author']['login']] << [num, review['submittedAt'], review['state']] - end + datastructure end - site.pages.each do |t| - # Skip Symlinks - if t.data['symlink'] - next + ## + # This generates the author pages + # Params + # +site+:: The site object + def generate(site) + return unless site.layouts.key? 'contributor_index' + + dir = 'hall-of-fame' + + # pre-calculating this hash saves about 4.9 seconds off the previous + # build time of 5 seconds. + tutorials_by_author = Hash.new { |hash, key| hash[key] = [] } + learning_pathways_by_author = Hash.new { |hash, key| hash[key] = [] } + slides_by_author = Hash.new { |hash, key| hash[key] = [] } + news_by_author = Hash.new { |hash, key| hash[key] = [] } + events_by_author = Hash.new { |hash, key| hash[key] = [] } + videos_by_author = Hash.new { |hash, key| hash[key] = [] } + faqs_by_author = Hash.new { |hash, key| hash[key] = [] } + has_philosophy = Hash.new { false } + + prs_by_author = Hash.new { |hash, key| hash[key] = [] } + reviews_by_author = Hash.new { |hash, key| hash[key] = [] } + + site.data['github'].each do |num, pr| + prs_by_author[pr['author']['login']] << [num, pr['mergedAt']] + + pr['reviews'].each do |review| + reviews_by_author[review['author']['login']] << [num, review['submittedAt'], review['state']] + end end - # Tutorials - pusher(t, tutorials_by_author, false) if t['layout'] == 'tutorial_hands_on' - - # Slides - if !%w[base_slides introduction_slides tutorial_slides].index(t['layout']).nil? - pusher(t, slides_by_author, false) - end + site.pages.each do |t| + # Skip Symlinks + if t.data['symlink'] + next + end - pusher(t, events_by_author, false) if t['layout'] == 'event' + # Tutorials + pusher(t, tutorials_by_author, false) if t['layout'] == 'tutorial_hands_on' - pusher(t, faqs_by_author, false) if t['layout'] == 'faq' + # Slides + if !%w[base_slides introduction_slides tutorial_slides].index(t['layout']).nil? + pusher(t, slides_by_author, false) + end - t.data.fetch('recordings', []).each do |r| - r.fetch('captioners', []).each { |ent| videos_by_author[ent].push([t, 'captioner', r]) } - r.fetch('speakers', []).each { |ent| videos_by_author[ent].push([t, 'speaker', r]) } - end + pusher(t, events_by_author, false) if t['layout'] == 'event' - pusher(t, learning_pathways_by_author, false) if t['layout'] == 'learning-pathway' + pusher(t, faqs_by_author, false) if t['layout'] == 'faq' - # Philosophies - has_philosophy[t.data['username']] = true if t['layout'] == 'training_philosophy' && !t.data['username'].nil? - end + t.data.fetch('recordings', []).each do |r| + r.fetch('captioners', []).each { |ent| videos_by_author[ent].push([t, 'captioner', r]) } + r.fetch('speakers', []).each { |ent| videos_by_author[ent].push([t, 'speaker', r]) } + end - site.posts.docs.each do |t| - # News - pusher(t, news_by_author, true) if t['layout'] == 'news' - end + pusher(t, learning_pathways_by_author, false) if t['layout'] == 'learning-pathway' - Gtn::Contributors.list(site).each_key do |contributor| - # Using PageWithoutAFile instead of a custom class which reads files - # from disk each time, saves some time, but it is unclear how much - # due to how the previous was accounted. But assuming 0.040s per page * 193 should be about 8 seconds. - page2 = PageWithoutAFile.new(site, '', File.join(dir, contributor), 'index.html') - page2.content = nil - name = Gtn::Contributors.fetch_contributor(site, contributor).fetch('name', contributor) - - # Their tutorials - page2.data['contributor'] = contributor - page2.data['personname'] = name - page2.data['title'] = "GTN Contributor: #{name}" - page2.data['layout'] = 'contributor_index' - - page2.data['tutorials'] = tutorials_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.compact]} - page2.data['slides'] = slides_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.compact]} - page2.data['news'] = news_by_author[contributor] - page2.data['learning_pathways'] = learning_pathways_by_author[contributor] - page2.data['events'] = events_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.compact]} - page2.data['videos'] = videos_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.uniq.compact]} - page2.data['faqs'] = faqs_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.uniq.compact]} - - page2.data['tutorials_count'] = tutorials_by_author[contributor].length - page2.data['slides_count'] = slides_by_author[contributor].length - page2.data['news_count'] = news_by_author[contributor].length - page2.data['learning_pathways_count'] = learning_pathways_by_author[contributor].length - page2.data['events_count'] = events_by_author[contributor].length - page2.data['videos_count'] = videos_by_author[contributor].length - page2.data['faqs_count'] = faqs_by_author[contributor].length - - page2.data['editors'] = TopicFilter.enumerate_topics(site).select do |t| - t.fetch('editorial_board', []).include?(contributor) + # Philosophies + has_philosophy[t.data['username']] = true if t['layout'] == 'training_philosophy' && !t.data['username'].nil? end - # Also their learning pathways - page2.data['editors'] += site.pages.select do |t| - t['layout'] == 'learning-pathway' && t.data.fetch('editorial_board', []).include?(contributor) + + site.posts.docs.each do |t| + # News + pusher(t, news_by_author, true) if t['layout'] == 'news' end - page2.data['editor_count'] = page2.data['editors'].length - page2.data['has_philosophy'] = has_philosophy[contributor] + Gtn::Contributors.list(site).each_key do |contributor| + # Using PageWithoutAFile instead of a custom class which reads files + # from disk each time, saves some time, but it is unclear how much + # due to how the previous was accounted. But assuming 0.040s per page * 193 should be about 8 seconds. + page2 = PageWithoutAFile.new(site, '', File.join(dir, contributor), 'index.html') + page2.content = nil + name = Gtn::Contributors.fetch_contributor(site, contributor).fetch('name', contributor) + + # Their tutorials + page2.data['contributor'] = contributor + page2.data['personname'] = name + page2.data['title'] = "GTN Contributor: #{name}" + page2.data['layout'] = 'contributor_index' + + page2.data['tutorials'] = tutorials_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.compact]} + page2.data['slides'] = slides_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.compact]} + page2.data['news'] = news_by_author[contributor] + page2.data['learning_pathways'] = learning_pathways_by_author[contributor] + page2.data['events'] = events_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.compact]} + page2.data['videos'] = videos_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.uniq.compact]} + page2.data['faqs'] = faqs_by_author[contributor].group_by{|x| x[0] }.map{|k, v| [k, v.map{|vv| vv[1]}.uniq.compact]} + + page2.data['tutorials_count'] = tutorials_by_author[contributor].length + page2.data['slides_count'] = slides_by_author[contributor].length + page2.data['news_count'] = news_by_author[contributor].length + page2.data['learning_pathways_count'] = learning_pathways_by_author[contributor].length + page2.data['events_count'] = events_by_author[contributor].length + page2.data['videos_count'] = videos_by_author[contributor].length + page2.data['faqs_count'] = faqs_by_author[contributor].length + + page2.data['editors'] = Gtn::TopicFilter.enumerate_topics(site).select do |t| + t.fetch('editorial_board', []).include?(contributor) + end + # Also their learning pathways + page2.data['editors'] += site.pages.select do |t| + t['layout'] == 'learning-pathway' && t.data.fetch('editorial_board', []).include?(contributor) + end + page2.data['editor_count'] = page2.data['editors'].length + + page2.data['has_philosophy'] = has_philosophy[contributor] - countable_reviews = reviews_by_author[contributor] - .reject{|x| x[1].nil?} # Group by PRs. - .group_by{|x| x[0]}.map{|x, r| r.sort_by{|r1| r1[1]}.max}.sort_by{|w| w[1]}.reverse + countable_reviews = reviews_by_author[contributor] + .reject{|x| x[1].nil?} # Group by PRs. + .group_by{|x| x[0]}.map{|x, r| r.sort_by{|r1| r1[1]}.max}.sort_by{|w| w[1]}.reverse - page2.data['gh_prs_count'] = prs_by_author[contributor].count - page2.data['gh_reviews_count'] = countable_reviews.count + page2.data['gh_prs_count'] = prs_by_author[contributor].count + page2.data['gh_reviews_count'] = countable_reviews.count - page2.data['gh_prs_recent'] = prs_by_author[contributor] - .reject{|x| x[1].nil?}.sort_by { |x| x[1] }.reverse.take(5) - .map{|x| x[0]} - page2.data['gh_reviews_recent'] = countable_reviews.take(5) - .map{|x| x[0]} + page2.data['gh_prs_recent'] = prs_by_author[contributor] + .reject{|x| x[1].nil?}.sort_by { |x| x[1] }.reverse.take(5) + .map{|x| x[0]} + page2.data['gh_reviews_recent'] = countable_reviews.take(5) + .map{|x| x[0]} - site.pages << page2 + site.pages << page2 + end end end end diff --git a/_plugins/by-tag-pages.rb b/_plugins/by-tag-pages.rb index 339594a860a7e8..af315b785d6c34 100644 --- a/_plugins/by-tag-pages.rb +++ b/_plugins/by-tag-pages.rb @@ -1,45 +1,53 @@ require './_plugins/jekyll-topic-filter' module Jekyll - ## - # This class generates the GTN's author pags - class TagPageGenerator < Generator - safe true - + module Generators ## - # This generates the author pages - # Params - # +site+:: The site object - def generate(site) - Jekyll.logger.info '[GTN/SyntheticTopics] Generating By-Tag Indexes' - TopicFilter.list_all_tags(site).map do |tag| - site.data["by_tag_#{tag}"] = { - 'name' => "by_tag_#{tag}", - 'type' => 'use', - 'title' => tag, - 'summary' => "Tutorials covering #{tag}", - 'tag_based' => true, - 'hidden' => true, - } - - topic_index = PageWithoutAFile.new(site, '', "tags/#{Jekyll::Utils.slugify(tag)}", 'index.md') - topic_index.content = '' - topic_index.data['layout'] = 'topic' - topic_index.data['topic_name'] = "by_tag_#{tag}" - topic_index.data['topic'] = site.data["by_tag_#{tag}"] + # This class generates the GTN's Tag Pages + class TagPageGenerator < Generator + safe true - site.pages << topic_index + ## + # This generates the author pages + # Params + # +site+:: The site object + def generate(site) + if Jekyll.env == 'production' + _generate(site) + end end - Jekyll.logger.info '[GTN/SyntheticTopics] Generating By-Tag Embeds' - TopicFilter.list_all_tags(site).map do |tag| - topic_index = PageWithoutAFile.new(site, '', "tags/#{Jekyll::Utils.slugify(tag)}", 'embed.html') - topic_index.content = '' - topic_index.data['layout'] = 'topic-embed' - topic_index.data['topic_name'] = "by_tag_#{tag}" - topic_index.data['topic'] = site.data["by_tag_#{tag}"] + def _generate(site) + Jekyll.logger.info '[GTN/SyntheticTopics] Generating By-Tag Indexes' + Gtn::TopicFilter.list_all_tags(site).map do |tag| + site.data["by_tag_#{tag}"] = { + 'name' => "by_tag_#{tag}", + 'type' => 'use', + 'title' => tag, + 'summary' => "Tutorials covering #{tag}", + 'tag_based' => true, + 'hidden' => true, + } + + topic_index = PageWithoutAFile.new(site, '', "tags/#{Jekyll::Utils.slugify(tag)}", 'index.md') + topic_index.content = '' + topic_index.data['layout'] = 'topic' + topic_index.data['topic_name'] = "by_tag_#{tag}" + topic_index.data['topic'] = site.data["by_tag_#{tag}"] + + site.pages << topic_index + end + + Jekyll.logger.info '[GTN/SyntheticTopics] Generating By-Tag Embeds' + Gtn::TopicFilter.list_all_tags(site).map do |tag| + topic_index = PageWithoutAFile.new(site, '', "tags/#{Jekyll::Utils.slugify(tag)}", 'embed.html') + topic_index.content = '' + topic_index.data['layout'] = 'topic-embed' + topic_index.data['topic_name'] = "by_tag_#{tag}" + topic_index.data['topic'] = site.data["by_tag_#{tag}"] - site.pages << topic_index + site.pages << topic_index + end end end end diff --git a/_plugins/colour-tags.rb b/_plugins/colour-tags.rb index f8a8106d0e2704..a7e0703acf714a 100644 --- a/_plugins/colour-tags.rb +++ b/_plugins/colour-tags.rb @@ -5,48 +5,58 @@ # Our automatic colour tag generator # It makes tags colourful in a reproducible way -module ColourTag - ## - # This function generates the CSS for a colour tag - # Params - # +contents+:: The contents of the tag - # - # Returns - # +String+:: The CSS for the tag - # - # Example - # ColourTag.colour_tag("test") => "--color-primary: #f799ff; --color-darker: #f571ff; --color-dimmed: #f686ff;" - def self.colour_tag(contents) - d = (Digest::SHA256.hexdigest contents).to_i(16) - - hue = ((d >> 4) % 360).abs - lightnessOffset = 75 - lightness = lightnessOffset + (hash & 0xf) - - # randomly make yellow tags bright - lightness += (100 - lightness) * 0.75 if (hue > 70) && (hue < 96) && ((d & 0x100) == 0x100) - - primary = Hsluv.hsluv_to_hex(hue, 100, lightness) - darker = Hsluv.hsluv_to_hex(hue, 100, lightness * 0.9) - dimmed = Hsluv.hsluv_to_hex(hue, 100, lightness * 0.95) - - "--color-primary: #{primary}; --color-darker: #{darker}; --color-dimmed: #{dimmed};" +module Gtn + module HashedColours + ## + # This function generates the CSS for a colour tag + # Params + # +contents+:: The contents of the tag + # + # Returns + # +String+:: The CSS for the tag + # + # Example + # HashedColours.colour_tag("test") => "--color-primary: #f799ff; --color-darker: #f571ff; --color-dimmed: #f686ff;" + def self.colour_tag(contents) + d = (Digest::SHA256.hexdigest contents).to_i(16) + + hue = ((d >> 4) % 360).abs + lightnessOffset = 75 + lightness = lightnessOffset + (hash & 0xf) + + # randomly make yellow tags bright + lightness += (100 - lightness) * 0.75 if (hue > 70) && (hue < 96) && ((d & 0x100) == 0x100) + + primary = Hsluv.hsluv_to_hex(hue, 100, lightness) + darker = Hsluv.hsluv_to_hex(hue, 100, lightness * 0.9) + dimmed = Hsluv.hsluv_to_hex(hue, 100, lightness * 0.95) + + "--color-primary: #{primary}; --color-darker: #{darker}; --color-dimmed: #{dimmed};" + end end end module Jekyll # :nodoc: - # The jekyll implementation of the colour tag - module ImplColourTag - def cache - @@cache ||= Jekyll::Cache.new('ColorTags') - end + module Filters + # The jekyll implementation of the colour tag + module HashedColours + def cache + @@cache ||= Jekyll::Cache.new('ColorTags') + end - def colour_tag(contents) - cache.getset(contents) do - ColourTag.colour_tag(contents) + ## + # colour_tag will turn some contents into a set of CSS variables for use in CSS. + # + # Examples: + # + # {{ tag }} + def colour_tag(contents) + cache.getset(contents) do + Gtn::HashedColours.colour_tag(contents) + end end end end end -Liquid::Template.register_filter(Jekyll::ImplColourTag) +Liquid::Template.register_filter(Jekyll::Filters::HashedColours) diff --git a/_plugins/feeds.rb b/_plugins/feeds.rb index 1c960301a89b99..d934634795cbc9 100644 --- a/_plugins/feeds.rb +++ b/_plugins/feeds.rb @@ -6,6 +6,12 @@ require 'json' class DateTime + ## + # Convert a given DateTime stamp roughly to an African/European lunch time + # + # Why that time? It is when the majority of our users are online and wanting to read the news. + # + # This is really only available in the feeds plugin, should not be assumed to be available elsewhere. def to_euro_lunch self.to_date.to_datetime + 0.6 end @@ -37,30 +43,6 @@ def track(url) end end -def objectify(attrs, url, path) - obj = attrs.clone - obj['__path'] = path - obj['__url'] = url - - def obj.data - self - end - - def obj.path - self['__path'] - end - - def obj.url - self['__url'] - end - - def obj.content - self.fetch('content', 'NO CONTENT AVAILABLE') - end - - obj -end - FEED_WIDGET_XSLT = Nokogiri::XSLT(File.read('feed-widget.xslt.xml')) def serialise(site, feed_path, builder) @@ -215,135 +197,6 @@ def generate_tag_topic_feeds(_site) '' end -def all_date_sorted_materials(site) - events = site.pages.select { |x| x['layout'] == 'event' || x['layout'] == 'event-external' } - materials = TopicFilter.list_all_materials(site).reject { |k, _v| k['draft'] } - news = site.posts.select { |x| x['layout'] == 'news' } - faqs = site.pages.select { |x| x['layout'] == 'faq' } - pathways = site.pages.select { |x| x['layout'] == 'learning-pathway' } - workflows = Dir.glob('topics/**/*.ga') - - bucket = events.map do |e| - [Gtn::PublicationTimes.obtain_time(e.path).to_datetime, 'events', e, ['event'] + e.data.fetch('tags', [])] - end - - materials.each do |m| - tags = [m['topic_name']] + (m['tags'] || []) - m.fetch('ref_tutorials', []).map do |t| - bucket << [Gtn::PublicationTimes.obtain_time(t.path).to_datetime, 'tutorials', t, tags] - - (t['recordings'] || []).map do |r| - url = '/' + t.path.gsub(/tutorial(_[A_Z_]*)?.(html|md)$/, 'recordings/') - url += "#tutorial-recording-#{Date.parse(r['date']).strftime('%-d-%B-%Y').downcase}" - attr = {'title' => "Recording of " + t['title'], - 'contributors' => r['speakers'] + (r['captions'] || []), - 'content' => "A #{r['length']} long recording is now available."} - - obj = objectify(attr, url, t.path) - bucket << [DateTime.parse(r['date'].to_s), 'recordings', obj, tags] - end - end - - - - m.fetch('ref_slides', []).reject { |s| s.url =~ /-plain.html/ }.map do |s| - bucket << [Gtn::PublicationTimes.obtain_time(s.path).to_datetime, 'slides', s, tags] - - (s['recordings'] || []).map do |r| - url = '/' + s.path.gsub(/slides(_[A_Z_]*)?.(html|md)$/, 'recordings/') - url += "#tutorial-recording-#{Date.parse(r['date']).strftime('%-d-%B-%Y').downcase}" - attr = {'title' => "Recording of " + s['title'], - 'contributors' => r['speakers'] + (r['captions'] || []), - 'content' => "A #{r['length']} long recording is now available."} - obj = objectify(attr, url, s.path) - bucket << [DateTime.parse(r['date'].to_s), 'recordings', obj, tags] - end - end - end - - bucket += news.map do |n| - [n.date.to_datetime, 'news', n, ['news'] + n.data.fetch('tags', [])] - end - - bucket += faqs.map do |n| - tag = Gtn::PublicationTimes.clean_path(n.path).split('/')[1] - [Gtn::PublicationTimes.obtain_time(n.path).to_datetime, 'faqs', n, ['faqs', tag]] - end - - bucket += pathways.map do |n| - tags = ['learning-pathway'] + (n['tags'] || []) - [Gtn::PublicationTimes.obtain_time(n.path).to_datetime, 'learning-pathways', n, tags] - end - - bucket += workflows.map do |n| - tag = Gtn::PublicationTimes.clean_path(n).split('/')[1] - wf_data = JSON.parse(File.read(n)) - - attrs = { - 'title' => wf_data['name'], - 'description' => wf_data['annotation'], - 'tags' => wf_data['tags'], - 'contributors' => wf_data.fetch('creator', []).map do |c| - matched = site.data['contributors'].select{|k, v| - v.fetch('orcid', "does-not-exist") == c.fetch('identifier', "").gsub('https://orcid.org/', '') - }.first - if matched - matched[0] - else - c['name'] - end - end - } - - # These aren't truly stable. I'm not sure what to do about that. - obj = objectify(attrs, '/' + n.gsub(/\.ga$/, '.html'), n) - # obj = objectify(attrs, '/' + n.path[0..n.path.rindex('/')], n) - - - [Gtn::PublicationTimes.obtain_time(n).to_datetime, 'workflows', obj, ['workflows', tag] + obj['tags']] - end - - # Remove symlinks from bucket. - bucket = bucket.reject { |date, type, page, tags| - File.symlink?(page.path) || File.symlink?(File.dirname(page.path)) || File.symlink?(File.dirname(File.dirname(page.path))) - } - - bucket += site.data['contributors'].map do |k, v| - a = {'title' => "@#{k}", - 'content' => "GTN Contributions from #{k}"} - obj = objectify(a, "/hall-of-fame/#{k}/", k) - - [DateTime.parse("#{v['joined']}-01T12:00:00", 'content' => "GTN Contributions from #{k}"), 'contributors', obj, ['contributor']] - end - - bucket += site.data['grants'].map do |k, v| - a = {'title' => "@#{k}", - 'content' => "GTN Contributions from #{k}"} - obj = objectify(a, "/hall-of-fame/#{k}/", k) - - # TODO: backdate grants, organisations - if v['joined'] - [DateTime.parse("#{v['joined']}-01T12:00:00"), 'grants', obj, ['grant']] - end - end.compact - - bucket += site.data['organisations'].map do |k, v| - a = {'title' => "@#{k}", - 'content' => "GTN Contributions from #{k}"} - obj = objectify(a, "/hall-of-fame/#{k}/", k) - - if v['joined'] - [DateTime.parse("#{v['joined']}-01T12:00:00"), 'organisations', obj, ['organisation']] - end - end.compact - - bucket - .reject{|x| x[0] > DateTime.now } # Remove future-dated materials - .reject{|x| x[2]['draft'] == true } # Remove drafts - .sort_by {|x| x[0] } # Date-sorted, not strictly necessary since will be grouped. - .reverse -end - def group_bucket_by(bucket, group_by: 'day') case group_by when 'day' @@ -736,12 +589,12 @@ def generate_event_feeds(site) {title: 'Events', url: "#{site.config['url']}#{site.baseurl}/events/feed.xml"} ] - bucket = all_date_sorted_materials(site) + bucket = Gtn::TopicFilter.all_date_sorted_resources(site) bucket.freeze opml['GTN Topics'] = [] opml['GTN Topics - Digests'] = [] - TopicFilter.list_topics(site).each do |topic| + Gtn::TopicFilter.list_topics(site).each do |topic| generate_topic_feeds(site, topic, bucket) opml['GTN Topics'] << {title: "#{topic} all changes", url: "#{site.config['url']}#{site.baseurl}/topic/feed.xml"} diff --git a/_plugins/file_exists.rb b/_plugins/file_exists.rb index 10b6b8cc2b3eda..4f27b6da096326 100644 --- a/_plugins/file_exists.rb +++ b/_plugins/file_exists.rb @@ -1,28 +1,32 @@ module Jekyll - ## - # This class adds a tag that checks if a file exists. - class FileExistsTag < Liquid::Tag - def initialize(tag_name, path, tokens) # :nodoc: - super - @path = path - end - + module Tags ## - # Render the tag - # Params: - # +context+:: The context of the page - def render(context) - # Pipe parameter through Liquid to make additional replacements possible - url = Liquid::Template.parse(@path).render context + # This class adds a tag that checks if a file exists. + class FileExistsTag < Liquid::Tag + def initialize(tag_name, path, tokens) # :nodoc: + super + @path = path + end + + ## + # file_exists - Check if a file exists and return an appropriate boolean + # + # Examples: + # + # {% capture hasfaq %}{% file_exists {{faqpage}} %}{% endcapture %} + def render(context) + # Pipe parameter through Liquid to make additional replacements possible + url = Liquid::Template.parse(@path).render context - # Adds the site source, so that it also works with a custom one - site_source = context.registers[:site].config['source'] - file_path = "#{site_source}/#{url}" + # Adds the site source, so that it also works with a custom one + site_source = context.registers[:site].config['source'] + file_path = "#{site_source}/#{url}" - # Check if file exists (returns true or false) - File.exist?(file_path.strip!).to_s + # Check if file exists (returns true or false) + File.exist?(file_path.strip!).to_s + end end end end -Liquid::Template.register_tag('file_exists', Jekyll::FileExistsTag) +Liquid::Template.register_tag('file_exists', Jekyll::Tags::FileExistsTag) diff --git a/_plugins/generator-recordings.rb b/_plugins/generator-recordings.rb index 1d592c760c27e6..8710416a4c1448 100644 --- a/_plugins/generator-recordings.rb +++ b/_plugins/generator-recordings.rb @@ -3,33 +3,35 @@ require './_plugins/gtn' module Jekyll - ## - # This class generates the GTN's author pags - class RecordingPageGenerator < Generator - safe true - + module Generators ## - # This generates the recording pages, where needed. - # Params - # +site+:: The site object - def generate(site) - Jekyll.logger.info "[GTN/Videos] Generating recording pages" - materials = TopicFilter - .list_all_materials(site) + # This class generates the GTN's recording pages + class RecordingPageGenerator < Generator + safe true + + ## + # This generates the recording pages, where needed. + # Params + # +site+:: The site object + def generate(site) + Jekyll.logger.info "[GTN/Videos] Generating recording pages" + materials = Gtn::TopicFilter + .list_all_materials(site) - with_video = materials - .select{|m| m.has_key? 'recordings' or m.has_key? 'slide_recordings'} + with_video = materials + .select{|m| m.has_key? 'recordings' or m.has_key? 'slide_recordings'} - Jekyll.logger.info "[GTN/Videos] #{with_video.length} materials with recordings found." - materials.each do |material| - page2 = PageWithoutAFile.new(site, '', material['dir'], 'recordings/index.html') - page2.content = nil - page2.data['layout'] = 'recordings' - page2.data['topic_name'] = material['topic_name'] - page2.data['tutorial_name'] = material['tutorial_name'] - page2.data['material'] = material - page2.data['title'] = 'Recordings for ' + material['title'] - site.pages << page2 + Jekyll.logger.info "[GTN/Videos] #{with_video.length} materials with recordings found." + materials.each do |material| + page2 = PageWithoutAFile.new(site, '', material['dir'], 'recordings/index.html') + page2.content = nil + page2.data['layout'] = 'recordings' + page2.data['topic_name'] = material['topic_name'] + page2.data['tutorial_name'] = material['tutorial_name'] + page2.data['material'] = material + page2.data['title'] = 'Recordings for ' + material['title'] + site.pages << page2 + end end end end diff --git a/_plugins/generator-workflows.rb b/_plugins/generator-workflows.rb index 3470d03b296b73..02f12298c57093 100644 --- a/_plugins/generator-workflows.rb +++ b/_plugins/generator-workflows.rb @@ -4,54 +4,82 @@ module Jekyll ## - # This class generates the GTN's author pags - class WorkflowPageGenerator < Generator - safe true + # {Jekyll Generators}[https://jekyllrb.com/docs/plugins/generators/] are a way to let you generate files at runtime, without needing them to exist on disk. + # + # We use generators for lots of purposes, e.g. + # + # Real Generators, the way Jekyll intended: + # + # - Jekyll::Generators::PlaintextSlidesGenerator - turns slides.html into plain text non-JS versions. + # - Jekyll::Generators::RecordingPageGenerator - emits a webpage for every tutorial that has recordings, in the GTN + # - Jekyll::Generators::WorkflowPageGenerator - emits a webpage for every workflow in the GTN + # - Jekyll::Generators::AuthorPageGenerator - emits a hall-of-fame entry for every contributor, organisation, and grant listed in our site metadata. + # - Jekyll::Generators::RmarkdownGenerator - outputs the RMarkdown notebooks for tutorials that want them. + # - Jekyll::Generators::SitemapGenerator2 - alternative for the jekyll-sitemap plugin that's a bit faster. + # - Jekyll::Generators::SyntheticTopicGenerator - our synthetic tag based topics + # - Jekyll::Generators::TagPageGenerator - topic page for every tag + # - Jekyll::Generators::WorkflowPageGenerator + # + # Muck with page contents generators (probably should be hooks): + # + # - Jekyll::Generators::Abbreviate - turns +{something}+ into an abbreviation tag + # - Jekyll::Generators::Figurify - handles our modified markdown for images + # - Jekyll::Generators::Boxify - turns +blah ... {: .box}+ into GTN boxes. + # + # Other generators (also probably should be hooks): + # + # - Jekyll::Generators::EnvironmentVariablesGenerator - adds git revision, tags, and other environment variables to the site object + module Generators ## - # This generates the recording pages, where needed. - # Params - # +site+:: The site object - def generate(site) - Jekyll.logger.info "[GTN/Workflows] Generating workflow pages" - materials = TopicFilter - .list_all_materials(site) + # This class generates the GTN's workflow pages. + class WorkflowPageGenerator < Generator + safe true - # [{"workflow"=>"Calling_variants_in_non-diploid_systems.ga", - # "tests"=>true, - # "url"=>"https://training.galaxyproject.org/training-material/topics/variant-analysis/tutorials/non-dip/workflows/Calling_variants_in_non-diploid_systems.ga", - # "path"=>"topics/variant-analysis/tutorials/non-dip/workflows/Calling_variants_in_non-diploid_systems.ga", - # "wfid"=>"variant-analysis-non-dip", - # "wfname"=>"calling-variants-in-non-diploid-systems", - # "trs_endpoint"=>"https://training.galaxyproject.org/training-material/api/ga4gh/trs/v2/tools/variant-analysis-non-dip/versions/calling-variants-in-non-diploid-systems", - # "license"=>nil, - # "parent_id"=>"variant-analysis/non-dip", - # "topic_id"=>"variant-analysis", - # "tutorial_id"=>"non-dip", - # "creators"=>[], - # "name"=>"Calling variants in non-diploid systems", - # "title"=>"Calling variants in non-diploid systems", - # "test_results"=>nil, - # "modified"=>2024-03-18 12:38:46.293831071 +0100, - # "mermaid"=> - # "graph_dot"=> - # }] - # /api/workflows/#{topic_id}/#{tutorial_id}/#{wfid}/rocrate.zip - shortlinks = site.data['shortlinks']['id'].invert + ## + # Params + # +site+:: The site object + def generate(site) + Jekyll.logger.info "[GTN/Workflows] Generating workflow pages" + materials = Gtn::TopicFilter + .list_all_materials(site) - materials.each do |material| - (material['workflows'] || []).each do |workflow| - page2 = PageWithoutAFile.new(site, '', '', "#{workflow['path'].gsub(/.ga$/, '.html')}") - path = File.join('/', workflow['path'].gsub(/.ga$/, '.html')) - page2.content = nil - page2.data['title'] = workflow['title'] - page2.data['layout'] = 'workflow' - page2.data['material'] = material - page2.data['workflow'] = workflow - page2.data['js_requirements'] = {'mathjax' => false, 'mermaid' => true} - page2.data['short_id'] = shortlinks[path] - page2.data['redirect_from'] = ["/short/#{shortlinks[path]}"] - site.pages << page2 + # [{"workflow"=>"Calling_variants_in_non-diploid_systems.ga", + # "tests"=>true, + # "url"=>"https://training.galaxyproject.org/training-material/topics/variant-analysis/tutorials/non-dip/workflows/Calling_variants_in_non-diploid_systems.ga", + # "path"=>"topics/variant-analysis/tutorials/non-dip/workflows/Calling_variants_in_non-diploid_systems.ga", + # "wfid"=>"variant-analysis-non-dip", + # "wfname"=>"calling-variants-in-non-diploid-systems", + # "trs_endpoint"=>"https://training.galaxyproject.org/training-material/api/ga4gh/trs/v2/tools/variant-analysis-non-dip/versions/calling-variants-in-non-diploid-systems", + # "license"=>nil, + # "parent_id"=>"variant-analysis/non-dip", + # "topic_id"=>"variant-analysis", + # "tutorial_id"=>"non-dip", + # "creators"=>[], + # "name"=>"Calling variants in non-diploid systems", + # "title"=>"Calling variants in non-diploid systems", + # "test_results"=>nil, + # "modified"=>2024-03-18 12:38:46.293831071 +0100, + # "mermaid"=> + # "graph_dot"=> + # }] + # /api/workflows/#{topic_id}/#{tutorial_id}/#{wfid}/rocrate.zip + shortlinks = site.data['shortlinks']['id'].invert + + materials.each do |material| + (material['workflows'] || []).each do |workflow| + page2 = PageWithoutAFile.new(site, '', '', "#{workflow['path'].gsub(/.ga$/, '.html')}") + path = File.join('/', workflow['path'].gsub(/.ga$/, '.html')) + page2.content = nil + page2.data['title'] = workflow['title'] + page2.data['layout'] = 'workflow' + page2.data['material'] = material + page2.data['workflow'] = workflow + page2.data['js_requirements'] = {'mathjax' => false, 'mermaid' => true} + page2.data['short_id'] = shortlinks[path] + page2.data['redirect_from'] = ["/short/#{shortlinks[path]}"] + site.pages << page2 + end end end end diff --git a/_plugins/gtn.rb b/_plugins/gtn.rb index e043c53d1d4a23..07453792e3d28c 100644 --- a/_plugins/gtn.rb +++ b/_plugins/gtn.rb @@ -24,10 +24,15 @@ Jekyll.logger.warn '[GTN] WARNING: This Ruby is pretty old, you might want to update.' if version_parts[0].to_i < 3 ## -# This module contains functions that are used in the GTN, our internal functions that is. - +# We have several sub-areas of Jekyll namespaced things that are useful to know about. +# +# - Jekyll::Filters - Liquid Filters that are useful in rendering your HTML +# - Jekyll::Tags - Liquid Tags can be used to access certain internals in HTML +# - Jekyll::Generators - Generators emit files at runtime, e.g. the hall of fame pages. +# - Jekyll::GtnFunctions - Generally miscellaneous Liquid Functions, could be refactored into Jekyll::Filters and Jekyll::Tags module Jekyll - # The main GTN function library + ## + # This module contains functions that are used in the GTN, our internal functions that is. module GtnFunctions # rubocop:disable Naming/PredicateName @@ -342,13 +347,20 @@ def regex_replace_once(str, regex_search, value_replace) str.sub(regex, value_replace) end + ## + # Check if a match is found + def matches(str, regex_search) + r = /#{regex_search}/ + str.match?(r) + end + def convert_to_material_list(site, materials) # [{"name"=>"introduction", "topic"=>"admin"}] return [] if materials.nil? materials.map do |m| if m.key?('name') && m.key?('topic') - found = TopicFilter.fetch_tutorial_material(site, m['topic'], m['name']) + found = Gtn::TopicFilter.fetch_tutorial_material(site, m['topic'], m['name']) Jekyll.logger.warn "Could not find material #{m['topic']}/#{m['name']} in the site data" if found.nil? if m.key?('time') @@ -542,7 +554,7 @@ def get_recent_feedbacks(site, material_id) def tutorials_over_time_bar_chart(site) graph = Hash.new(0) - TopicFilter.list_all_materials(site).each do |material| + Gtn::TopicFilter.list_all_materials(site).each do |material| if material['pub_date'] yymm = material['pub_date'].strftime('%Y-%m') graph[yymm] += 1 @@ -706,7 +718,8 @@ def get_upcoming_events(site) # +Array+:: List of events # # Example: - # {{ site | get_upcoming_events }} + # + # {{ site | get_upcoming_events }} def get_upcoming_events_for_this(site, material) if material.nil? [] @@ -717,6 +730,80 @@ def get_upcoming_events_for_this(site, material) end end + ## + # Get the list of all videos for the site (the automated + manual.) + # Params: + # +site+:: The site object + # Returns: + # +Array+:: List of [topic_id, topic_name, automated_videos, manual_videos] + # + # Example: + # + # {{ site | get_videos_for_videos_page }} + def get_videos_for_videos_page(site) + res = {} + Gtn::TopicFilter.list_all_materials(site).each do |material| + next unless material['video'] || material['recordings'] || material['slides_recordings'] + + if ! res.key? material['topic_name'] + res[material['topic_name']] = { + 'topic_id' => material['topic_name'], + 'topic_name' => site.data[material['topic_name']]['title'], + 'automated_videos' => [], + 'manual_videos' => [] + } + end + + # Automated recording + if material['video'] + vid = "#{material['topic_name']}/tutorials/#{material['tutorial_name']}/slides" + res[material['topic_name']]['automated_videos'].push({ + 'title' => material['title'], + 'vid' => vid, + 'type' => 'internal', + 'speakers' => ['awspolly'], + 'captioners' => Gtn::Contributors.get_authors(material), + 'cover' => "https://training.galaxyproject.org/videos/topics/#{vid}.mp4.png" + }) + end + + if material['slides_recordings'] + rec = material['slides_recordings'].max_by { |x| x['date'] } + res[material['topic_name']]['manual_videos'].push({ + 'title' => material['title'], + 'vid' => rec['youtube_id'], + 'type' => 'youtube', + 'speakers' => rec['speakers'], + 'captioners' => rec['captioners'], + 'cover' => "https://img.youtube.com/vi/#{rec['youtube_id']}/sddefault.jpg" + }) + end + + if material['recordings'] + rec = material['recordings'].max_by { |x| x['date'] } + res[material['topic_name']]['manual_videos'].push({ + 'title' => material['title'], + 'vid' => rec['youtube_id'], + 'type' => 'youtube', + 'speakers' => rec['speakers'], + 'captioners' => rec['captioners'], + 'cover' => "https://img.youtube.com/vi/#{rec['youtube_id']}/sddefault.jpg" + }) + end + end + + res.each do |k, v| + if v['automated_videos'].empty? + v.delete('automated_videos') + end + if v['manual_videos'].empty? + v.delete('manual_videos') + end + end + + res + end + def shuffle(array) array.shuffle end @@ -1003,6 +1090,12 @@ def find_learningpaths_including_topic(site, topic_id) end end +Jekyll::Hooks.register :site, :post_write do |site| + if Jekyll.env == 'production' + Gtn::Shortlinks.fix_missing_redirs(site) + end +end + if $PROGRAM_NAME == __FILE__ result = Gtn::ModificationTimes.obtain_time(ARGV[0].gsub(%r{^/}, '')) puts "Modification time of #{ARGV[0].gsub(%r{^/}, '')} is #{result}" diff --git a/_plugins/gtn/boxify.rb b/_plugins/gtn/boxify.rb index 6e41eb69d96492..ec35408b2eac66 100644 --- a/_plugins/gtn/boxify.rb +++ b/_plugins/gtn/boxify.rb @@ -221,7 +221,7 @@ def self.replace_elements(text, lang = 'en', key) if $PROGRAM_NAME == __FILE__ require 'test/unit' # Test the box ID algorithm - class BoxIdTest < Test::Unit::TestCase + class Gtn::Boxify::BoxIdTest < Test::Unit::TestCase def test_single_page assert_equal(Gtn::Boxify.get_id('hands-on', 'a box', 'index.md'), 'hands-on-a-box') assert_equal(Gtn::Boxify.get_id('hands-on', 'a box', 'index.md'), 'hands-on-a-box-1') diff --git a/_plugins/gtn/contributors.rb b/_plugins/gtn/contributors.rb index 349349aae18559..c99f6b1ef08bf6 100644 --- a/_plugins/gtn/contributors.rb +++ b/_plugins/gtn/contributors.rb @@ -235,6 +235,8 @@ def self.fetch_funding_url(contributor) "https://gtr.ukri.org/projects?ref=#{contributor['funding_id']}" when 'highergov' "https://www.highergov.com/contract/#{contributor['funding_id']}/" + when 'dfg' + "https://gepris-extern.dfg.de/gepris/projekt/#{contributor['funding_id']}?language=en" else Jekyll.logger.error "Unknown funding system #{contributor['funding_database']}. Please let us know so we can add support for it!" 'ERROR' diff --git a/_plugins/gtn/hooks.rb b/_plugins/gtn/hooks.rb new file mode 100644 index 00000000000000..0d00a8ab079500 --- /dev/null +++ b/_plugins/gtn/hooks.rb @@ -0,0 +1,72 @@ +require './_plugins/jekyll-topic-filter' +require 'jekyll' + + +module Gtn + # Parse the git repo to get some facts + module Hooks + + ## + # Generate the by-tool pages + # Params: + # +site+:: Jekyll site object + def self.by_tool(site) + Jekyll.logger.debug "[GTN/Hooks/by_tool] Started" + init_count = site.pages.size + start_time = Time.now + + tools = Gtn::TopicFilter.list_materials_by_tool(site) + tools.reject!{|tool, _| tool.include?('{{')} + + tools.each do |tool, tutorials| + # tool: e.g. `saskia-hiltemann/krona_text/krona-text` + + ordered_tool_ids = tutorials['tool_id'] + .map{|x| + if x[0] == x[1] + # TODO: collect versions of builtins. + [x[0], '0.0.0'] # Fake version for local only tools + else + x + end + } + .reject{|x| x[0] == x[1]} + .map{|x| [x[0], x[1], Gem::Version.new(fix_version(x[1]))]} + .sort_by{|x| x[2]} + + # Redirect from the older, shorter IDs that have more potential for conflicts. + if tool.include?('/') + previous_id = tool.split('/')[0] + '/' + tool.split('/')[2] + else + previous_id = tool # No change + end + + page2 = Jekyll::PageWithoutAFile.new(site, '', 'by-tool/', "#{tool.gsub('%20', ' ')}.html") + page2.content = nil + page2.data['layout'] = 'by_tool' + page2.data['short_tool'] = tool + page2.data['observed_tool_ids'] = ordered_tool_ids.map{|x| x[0..1]}.reverse + page2.data['tutorial_list'] = tutorials['tutorials'] + page2.data['latest_tool_id'] = ordered_tool_ids.map{|x| x[0]}.last + # page2.data['redirect_from'] = ["/by-tool/#{previous_id.gsub('%20', ' ')}"] + site.pages << page2 + + # TODO: For whatever reason the redirect_from does NOT work, even this + # early in the hooks, so we're just going to write the file and call it + # a day. Someone should fix this someday. My apologies for leaving it like this. + if previous_id != tool + page2 = Jekyll::PageWithoutAFile.new(site, '', 'by-tool/', "#{previous_id}.html") + page2.content = nil + page2.data['layout'] = 'by_tool' + page2.data['short_tool'] = tool + page2.data['observed_tool_ids'] = ordered_tool_ids.map{|x| x[0..1]}.reverse + page2.data['tutorial_list'] = tutorials['tutorials'] + page2.data['latest_tool_id'] = ordered_tool_ids.map{|x| x[0]}.last + site.pages << page2 + end + + end + Jekyll.logger.info "[GTN/Hooks/by_tool] #{site.pages.size - init_count} pages added in #{Time.now - start_time}s" + end + end +end diff --git a/_plugins/gtn/shortlinks.rb b/_plugins/gtn/shortlinks.rb index 9e9460279e4205..a4c23cbdcc4733 100644 --- a/_plugins/gtn/shortlinks.rb +++ b/_plugins/gtn/shortlinks.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true module Gtn - # This module is responsible for generating shortlinks for tutorials and FAQs + # This module is responsible for generating shortlinks for tutorials and FAQs and any other pages we add. + # + # Every category gets its own prefix letter. module Shortlinks CATEGORY_TUTORIAL = 'T' CATEGORY_SLIDES = 'S' @@ -11,10 +13,51 @@ module Shortlinks CATEGORY_EVENTS = 'E' CATEGORY_WORKFLOW = 'W' + REDIRECT_TEMPLATE = <<~REDIR + + + + Redirecting… + + + + +

Redirecting…

+ Click here if you are not redirected. + + REDIR + def self.mapped?(tutorial, current_mapping) current_mapping['id'].values.include? tutorial end + ## + # Duplicate of the jekyll-redirect-from plugin template. + # We can't use that for, reasons. + def self.html_redirect(target) + REDIRECT_TEMPLATE.gsub('REDIRECT_URL', target) + end + + ## + # Fix missing symlinks (usually exist because the target file has been + # renamed and doesn't exist anymore.) However, a redirect *will* be present + # for the original filename so we just fix the missing symlink. + # + # Params: + # +site+:: The Jekyll site object + def self.fix_missing_redirs(site) + missing_redirs = site.data['shortlinks']['id'].select do |id, target| + short_link = "short/#{id}.html" + ! File.exist?(site.in_dest_dir(short_link)) + end + + missing_redirs.each do |id, target| + short_link = "short/#{id}.html" + Jekyll.logger.warn "[GTN/Shortlink]" "Shortlink target #{target} does not exist for shortlink #{short_link}, fixing." + File.write(site.in_dest_dir(short_link), Gtn::Shortlinks.html_redirect(target)) + end + end + def self.update(current_mapping) current_mapping['id'] = {} if !current_mapping.key? 'id' diff --git a/_plugins/gtn/supported.rb b/_plugins/gtn/supported.rb index 0617c7c189fba6..e898ad5e50c60f 100644 --- a/_plugins/gtn/supported.rb +++ b/_plugins/gtn/supported.rb @@ -72,17 +72,17 @@ def self.calculate(data, tool_list) # generate a 'false' value when merging sets. inexact_support -= exact_support - usegalaxy_server_urls = Gtn::Usegalaxy.servers.map { |x| x[:url] } + usegalaxy_server_urls = Gtn::Usegalaxy.servers.map { |x| x[:url].downcase.gsub(/\/$/, '')} { 'exact' => (exact_support || []).map do |id| data['servers'][id].update( - { 'usegalaxy' => usegalaxy_server_urls.include?(data['servers'][id]['url']) } + { 'usegalaxy' => usegalaxy_server_urls.include?(data['servers'][id]['url'].downcase.gsub(/\/$/, '')) } ) end, 'inexact' => (inexact_support || []).map do |id| data['servers'][id].update( - { 'usegalaxy' => usegalaxy_server_urls.include?(data['servers'][id]['url']) } + { 'usegalaxy' => usegalaxy_server_urls.include?(data['servers'][id]['url'].downcase.gsub(/\/$/, '')) } ) end } @@ -183,7 +183,7 @@ def self.calculate_matrix(data, tool_list) if ARGV.length.positive? && (ARGV[0] == 'test') require 'test/unit' # Testing for the class - class IntersectionTest < Test::Unit::TestCase + class Gtn::Test::IntersectionTest < Test::Unit::TestCase def test_exact data = { 'servers' => { 0 => 's0', 1 => 's1', 2 => 's2' }, diff --git a/_plugins/gtn/synthetic.rb b/_plugins/gtn/synthetic.rb index 29f748da8b360d..38efd8fd9b3b02 100644 --- a/_plugins/gtn/synthetic.rb +++ b/_plugins/gtn/synthetic.rb @@ -3,26 +3,28 @@ require './_plugins/jekyll-topic-filter' module Jekyll - # Generates synthetic topics from tutorials with specific tags - class SyntheticTopicGenerator < Generator - def generate(site) - # Full Bibliography - Jekyll.logger.info '[GTN/SyntheticTopics] Generating Indexes' + module Generators + # Generates synthetic topics from tutorials with specific tags + class SyntheticTopicGenerator < Generator + def generate(site) + # Full Bibliography + Jekyll.logger.info '[GTN/SyntheticTopics] Generating Indexes' - TopicFilter.list_topics(site).select { |t| site.data[t]['tag_based'] }.each do |topic| - Jekyll.logger.debug "[GTN/SyntheticTopics] Creating #{topic} topic" + Gtn::TopicFilter.list_topics(site).select { |t| site.data[t]['tag_based'] }.each do |topic| + Jekyll.logger.debug "[GTN/SyntheticTopics] Creating #{topic} topic" - topic_index = PageWithoutAFile.new(site, '', "topics/#{topic}", 'index.md') - topic_index.content = '' - topic_index.data['layout'] = 'topic' - topic_index.data['topic_name'] = topic - site.pages << topic_index + topic_index = PageWithoutAFile.new(site, '', "topics/#{topic}", 'index.md') + topic_index.content = '' + topic_index.data['layout'] = 'topic' + topic_index.data['topic_name'] = topic + site.pages << topic_index - # For now, intentionally no FAQ - # faq_index = PageWithoutAFile.new(site, "", "topics/#{topic}/faqs", "index.md") - # faq_index.content = "" - # faq_index.data["layout"] = "faq-page" - # site.pages << faq_index + # For now, intentionally no FAQ + # faq_index = PageWithoutAFile.new(site, "", "topics/#{topic}/faqs", "index.md") + # faq_index.content = "" + # faq_index.data["layout"] = "faq-page" + # site.pages << faq_index + end end end end diff --git a/_plugins/jekyll-boxify.rb b/_plugins/jekyll-boxify.rb index 192bac83973cea..ed712064942a49 100644 --- a/_plugins/jekyll-boxify.rb +++ b/_plugins/jekyll-boxify.rb @@ -4,71 +4,73 @@ require './_plugins/gtn' module Jekyll - # The GTN Box generation process - class Boxify < Jekyll::Generator - def initialize(config) # :nodoc: - super - @config = config['boxify'] ||= {} - end + module Generators + # The GTN Box generation process + class Boxify < Jekyll::Generator + def initialize(config) # :nodoc: + super + @config = config['boxify'] ||= {} + end - def generate(site) # :nodoc: - Jekyll.logger.info '[GTN/Boxify]' - site.pages.each { |page| boxify page, site } - site.posts.docs.each { |post| boxify post, site } - end + def generate(site) # :nodoc: + Jekyll.logger.info '[GTN/Boxify]' + site.pages.each { |page| boxify page, site } + site.posts.docs.each { |post| boxify post, site } + end - ## - # This function adds boxes to the page content. - # Params: - # +page+:: The page to add boxes to - # +site+:: The +Jekyll::Site+ object - def boxify(page, _site) - return if page.content.nil? + ## + # This function adds boxes to the page content. + # Params: + # +page+:: The page to add boxes to + # +site+:: The +Jekyll::Site+ object + def boxify(page, _site) + return if page.content.nil? - lang = page['lang'] || 'en' + lang = page['lang'] || 'en' - # Interim solution, fancier box titles - # rubocop:disable Layout/LineLength - page.content = page.content.gsub(%r{<(?#{Gtn::Boxify.box_classes})-title( ?(?noprefix))>(?.*?)</\s*\k<boxclass>-title\s*>}) do - # rubocop:enable Layout/LineLength - m = ::Regexp.last_match - box_type = m[:boxclass] - title = m[:title] - noprefix = m[:noprefix] - if page.data['citation_target'] == 'jupyter' - title = Gtn::Boxify.safe_title(title) - title = Gtn::Boxify.format_box_title(title, box_type, lang, noprefix: noprefix) - icon = Gtn::Boxify.get_icon(box_type, emoji: true) - box = "<div class=\"box-title\" aria-description=\"#{box_type} box: " \ - "#{title}\" style=\"font-size: 150%\">#{icon} #{title}</div>" - box.gsub!(/\\"/, '"') - box.gsub!(/([^\\])"/, '\1\\"') - else - _, box = Gtn::Boxify.generate_title(box_type, title, lang, page.path, noprefix: noprefix) - end + # Interim solution, fancier box titles + # rubocop:disable Layout/LineLength + page.content = page.content.gsub(%r{<(?<boxclass>#{Gtn::Boxify.box_classes})-title( ?(?<noprefix>noprefix))>(?<title>.*?)</\s*\k<boxclass>-title\s*>}) do + # rubocop:enable Layout/LineLength + m = ::Regexp.last_match + box_type = m[:boxclass] + title = m[:title] + noprefix = m[:noprefix] + if page.data['citation_target'] == 'jupyter' + title = Gtn::Boxify.safe_title(title) + title = Gtn::Boxify.format_box_title(title, box_type, lang, noprefix: noprefix) + icon = Gtn::Boxify.get_icon(box_type, emoji: true) + box = "<div class=\"box-title\" aria-description=\"#{box_type} box: " \ + "#{title}\" style=\"font-size: 150%\">#{icon} #{title}</div>" + box.gsub!(/\\"/, '"') + box.gsub!(/([^\\])"/, '\1\\"') + else + _, box = Gtn::Boxify.generate_title(box_type, title, lang, page.path, noprefix: noprefix) + end - box - end + box + end - # Long term solution, proper new boxes - # BUT: does not work with <details></details> that are actual HTML elements, so we'll need to rename those. - # page.content = page.content.gsub(/<(#{Gtn::Boxify.box_classes})>/) { - # box_type = $1 - # box = Gtn::Boxify.generate_box(box_type, nil, lang, page.path) - # box - # } + # Long term solution, proper new boxes + # BUT: does not work with <details></details> that are actual HTML elements, so we'll need to rename those. + # page.content = page.content.gsub(/<(#{Gtn::Boxify.box_classes})>/) { + # box_type = $1 + # box = Gtn::Boxify.generate_box(box_type, nil, lang, page.path) + # box + # } - # page.content = page.content.gsub(/<(#{Gtn::Boxify.box_classes}) title="([^"]*)">/) { - # box_type = $1 - # title = $2 - # box = Gtn::Boxify.generate_box(box_type, title, lang, page.path) - # box - # } + # page.content = page.content.gsub(/<(#{Gtn::Boxify.box_classes}) title="([^"]*)">/) { + # box_type = $1 + # title = $2 + # box = Gtn::Boxify.generate_box(box_type, title, lang, page.path) + # box + # } - # page.content = page.content.gsub(/<\/\s*(#{Gtn::Boxify::box_classes})\s*>/) { - # box_type = $1 - # "\n</div></div><!--#{box_type}-->" - # } + # page.content = page.content.gsub(/<\/\s*(#{Gtn::Boxify::box_classes})\s*>/) { + # box_type = $1 + # "\n</div></div><!--#{box_type}-->" + # } + end end end end diff --git a/_plugins/jekyll-bundler.rb b/_plugins/jekyll-bundler.rb index c5a2c4d00b30b1..2c65b3a09ca4b3 100644 --- a/_plugins/jekyll-bundler.rb +++ b/_plugins/jekyll-bundler.rb @@ -43,77 +43,92 @@ end module Jekyll - # The main GTN function library - module JsBundle - # Return the preloads for the bundles, when in production - # +test+:: ignore this - # Returns the HTML to load the bundle - # - # Example: - # {{ 'load' | bundle_preloads }} - def bundle_preloads(_test) - if Jekyll.env == 'production' - bundle_preloads_prod - else - '' + module Filters + + # Our (very simple) JS Bundler + module JsBundle + ## + # Setup the local cache via +Jekyll::Cache+ + def cache + @@cache ||= Jekyll::Cache.new('GtnJsBundle') end - end - - # (Internal) Return the production preloads for the bundles - def bundle_preloads_prod - bundles = @context.registers[:site].config['javascript_bundles'] - baseurl = @context.registers[:site].config['baseurl'] - # Select the ones wishing to be preloaded - bundles = bundles.select do |_name, bundle| - bundle['preload'] == true + # Return the preloads for the bundles, when in production + # +test+:: ignore this + # Returns the HTML to load the bundle + # + # Example: + # {{ 'load' | bundle_preloads }} + def bundle_preloads(_test) + if Jekyll.env == 'production' + bundle_preloads_prod + else + '' + end end - bundles.map do |_name, bundle| - bundle_path = "#{baseurl}#{bundle['path']}" - "<link rel='preload' href='#{bundle_path}' as='script'>" - end.join("\n") - end + # (Internal) Return the production preloads for the bundles + def bundle_preloads_prod + bundles = @context.registers[:site].config['javascript_bundles'] + baseurl = @context.registers[:site].config['baseurl'] + + # Select the ones wishing to be preloaded + bundles = bundles.select do |_name, bundle| + bundle['preload'] == true + end - # Load a specific bundle, in liquid - # +name+:: the name of the bundle to load - # Returns the HTML to load the bundle - # - # Example: - # {{ 'main' | load_bundle }} - def load_bundle(name) - if Jekyll.env == 'production' - load_bundle_production(name) - else - load_bundle_dev(name) + bundles.map do |_name, bundle| + bundle_path = "#{baseurl}#{bundle['path']}" + "<link rel='preload' href='#{bundle_path}' as='script'>" + end.join("\n") end - end - def load_bundle_dev(name) - bundle = @context.registers[:site].config['javascript_bundles'][name] - raise "Bundle #{name} not found in site config" if bundle.nil? + # Load a specific bundle, in liquid + # +name+:: the name of the bundle to load + # Returns the HTML to load the bundle + # + # Example: + # {{ 'main' | load_bundle }} + def load_bundle(name) + cache.getset("#{Jekyll.env}-#{name}") do + if Jekyll.env == 'production' + load_bundle_production(name) + else + load_bundle_dev(name) + end + end + end - Jekyll.logger.debug "[GTN/Bundler] Bundle #{bundle}" + ## + # Dev version of the bundle loader, just direct script links + def load_bundle_dev(name) + bundle = @context.registers[:site].config['javascript_bundles'][name] + raise "Bundle #{name} not found in site config" if bundle.nil? - baseurl = @context.registers[:site].config['baseurl'] + Jekyll.logger.debug "[GTN/Bundler] Bundle #{bundle}" - bundle['resources'].map do |f| - "<script src='#{baseurl}/#{f}'></script>" - end.join("\n") - end + baseurl = @context.registers[:site].config['baseurl'] + + bundle['resources'].map do |f| + "<script src='#{baseurl}/#{f}'></script>" + end.join("\n") + end - def load_bundle_production(name) - bundle = @context.registers[:site].config['javascript_bundles'][name] - raise "Bundle #{name} not found in site config" if bundle.nil? + ## + # Production version of the bundle loader, with cache busting + def load_bundle_production(name) + bundle = @context.registers[:site].config['javascript_bundles'][name] + raise "Bundle #{name} not found in site config" if bundle.nil? - baseurl = @context.registers[:site].config['baseurl'] - attrs = '' - attrs += ' async' if bundle['async'] - attrs += ' defer' if bundle['defer'] - bundle_path = "#{baseurl}#{bundle['path']}" - "<script #{attrs} src='#{bundle_path}'></script>" + baseurl = @context.registers[:site].config['baseurl'] + attrs = '' + attrs += ' async' if bundle['async'] + attrs += ' defer' if bundle['defer'] + bundle_path = "#{baseurl}#{bundle['path']}" + "<script #{attrs} src='#{bundle_path}'></script>" + end end end end -Liquid::Template.register_filter(Jekyll::JsBundle) +Liquid::Template.register_filter(Jekyll::Filters::JsBundle) diff --git a/_plugins/jekyll-color-picker.rb b/_plugins/jekyll-color-picker.rb index 40ca6507d53887..aab893161b5c3d 100644 --- a/_plugins/jekyll-color-picker.rb +++ b/_plugins/jekyll-color-picker.rb @@ -1,25 +1,28 @@ # frozen_string_literal: true module Jekyll - # Convert a color into a cute little box - class ColorPickerTag < Liquid::Tag - def initialize(tag_name, text, tokens) # :nodoc: - super - @text = text.strip - end + module Tags + # Convert a color into a cute little box + class ColorPickerTag < Liquid::Tag + def initialize(tag_name, text, tokens) # :nodoc: + super + @text = text.strip + end - ## - # This function renders the color box - # Params: - # +context+:: The context of the page - # - # Example: - # {% color_picker #ff0000 %} - def render(_context) - "<span style='background-color:#{@text};border-radius:3px;border:1px solid #000;width:12px;" \ - "height:12px;margin-right:5px;'>     </span>" + ## + # This function renders the color box + # Params: + # +context+:: The context of the page + # + # Example: + # + # {% color_picker #ff0000 %} + def render(_context) + "<span style='background-color:#{@text};border-radius:3px;border:1px solid #000;width:12px;" \ + "height:12px;margin-right:5px;'>     </span>" + end end end end -Liquid::Template.register_tag('color_picker', Jekyll::ColorPickerTag) +Liquid::Template.register_tag('color_picker', Jekyll::Tags::ColorPickerTag) diff --git a/_plugins/jekyll-duration.rb b/_plugins/jekyll-duration.rb index 315229d916c3a9..574564e3d8d84f 100644 --- a/_plugins/jekyll-duration.rb +++ b/_plugins/jekyll-duration.rb @@ -1,123 +1,126 @@ # frozen_string_literal: true module Jekyll - # This module contains a filter for converting a duration string into a human readable string. - module DurationFilter - ## - # This function converts a duration string into a human readable string. - # Params: - # +duration+:: The duration string to convert (e.g. 1H30M, RFC 3339 formatted minus the leading P/T) - # Returns: - # +String+:: The human readable string - # - # Example: - # {{ "T1H30M" | duration_to_human }} - # => "1 hour 30 minutes" - def duration_to_human(duration) - seconds = parse_rfc3339(duration) - if seconds.nil? - return duration + module Filters + + # This module contains a filter for converting a duration string into a human readable string. + module DurationFilter + ## + # This function converts a duration string into a human readable string. + # Params: + # +duration+:: The duration string to convert (e.g. 1H30M, RFC 3339 formatted minus the leading P/T) + # Returns: + # +String+:: The human readable string + # + # Example: + # {{ "T1H30M" | duration_to_human }} + # => "1 hour 30 minutes" + def duration_to_human(duration) + seconds = parse_rfc3339(duration) + if seconds.nil? + return duration + end + return fmt_duration(seconds) end - return fmt_duration(seconds) - end - def fmt_duration(seconds) - d = resolve_hms(seconds) + def fmt_duration(seconds) + d = resolve_hms(seconds) - # Otherwise append english terms for the various parts - duration_parts = [] - - hour = 'hour' - hours = 'hours' - minutes = 'minutes' - if @context.registers[:page]&.key?('lang') && (@context.registers[:page]['lang'] != 'en') - lang = @context.registers[:page]['lang'] - hour = @context.registers[:site].data['lang'][lang]['hour'] - hours = @context.registers[:site].data['lang'][lang]['hours'] - minutes = @context.registers[:site].data['lang'][lang]['minutes'] - end + # Otherwise append english terms for the various parts + duration_parts = [] - # Hours - if d[:hours] > 0 - if d[:hours] == 1 - duration_parts.push("#{d[:hours]} " + hour) - else - duration_parts.push("#{d[:hours]} " + hours) + hour = 'hour' + hours = 'hours' + minutes = 'minutes' + if @context.registers[:page]&.key?('lang') && (@context.registers[:page]['lang'] != 'en') + lang = @context.registers[:page]['lang'] + hour = @context.registers[:site].data['lang'][lang]['hour'] + hours = @context.registers[:site].data['lang'][lang]['hours'] + minutes = @context.registers[:site].data['lang'][lang]['minutes'] end - end - # Minutes - assuming no one uses `1 minute` - duration_parts.push("#{d[:minutes]} " + minutes) if d[:minutes] > 0 + # Hours + if d[:hours] > 0 + if d[:hours] == 1 + duration_parts.push("#{d[:hours]} " + hour) + else + duration_parts.push("#{d[:hours]} " + hours) + end + end - # Hopefully no one uses seconds - duration_parts.push("#{d[:seconds]} seconds") if d[:seconds] > 0 + # Minutes - assuming no one uses `1 minute` + duration_parts.push("#{d[:minutes]} " + minutes) if d[:minutes] > 0 - duration_parts.join(' ') - end + # Hopefully no one uses seconds + duration_parts.push("#{d[:seconds]} seconds") if d[:seconds] > 0 - ## - # Sum the durations correctly for multiple RFC3339 formatted durations. - # Params: - # +s+:: The RFC3339 formatted duration string - # Returns: - # +d+:: a number of seconds - def parse_rfc3339(s) - if s == 0 - return 0 + duration_parts.join(' ') end - # Match the different parts of the string, must match entire string or it - # will fail. - match = /^T?(?:([0-9]*)[Hh])*(?:([0-9]*)[Mm])*(?:([0-9.]*)[Ss])*$/.match(s) + ## + # Sum the durations correctly for multiple RFC3339 formatted durations. + # Params: + # +s+:: The RFC3339 formatted duration string + # Returns: + # +d+:: a number of seconds + def parse_rfc3339(s) + if s == 0 + return 0 + end - # If it doesn't match, pass through unedited so we don't cause unexpected - # issues. - if match.nil? - Jekyll.logger.debug "[GTN/Durations]:", "Could not parse time: #{s}" - return nil - end + # Match the different parts of the string, must match entire string or it + # will fail. + match = /^T?(?:([0-9]*)[Hh])*(?:([0-9]*)[Mm])*(?:([0-9.]*)[Ss])*$/.match(s) - return match[1].to_i * 3600 + match[2].to_i * 60 + match[3].to_i - end + # If it doesn't match, pass through unedited so we don't cause unexpected + # issues. + if match.nil? + Jekyll.logger.debug "[GTN/Durations]:", "Could not parse time: #{s}" + return nil + end - ## - # Turn a count of seconds into hours/minutes/seconds. - # Params: - # +Int+:: A number of seconds - # Returns: - # +Hash+:: A hash with keys for hours, minutes, and seconds - # - # Example: - # resolve_hms(5400) - # => { hours: 1, minutes: 30, seconds: 0 } - def resolve_hms(seconds) - # Normalize the total - minutes = seconds / 60 - seconds = seconds % 60 - hours = minutes / 60 - minutes = minutes % 60 - - { hours: hours, minutes: minutes, seconds: seconds } - end + return match[1].to_i * 3600 + match[2].to_i * 60 + match[3].to_i + end + + ## + # Turn a count of seconds into hours/minutes/seconds. + # Params: + # +Int+:: A number of seconds + # Returns: + # +Hash+:: A hash with keys for hours, minutes, and seconds + # + # Example: + # resolve_hms(5400) + # => { hours: 1, minutes: 30, seconds: 0 } + def resolve_hms(seconds) + # Normalize the total + minutes = seconds / 60 + seconds = seconds % 60 + hours = minutes / 60 + minutes = minutes % 60 + + { hours: hours, minutes: minutes, seconds: seconds } + end - ## - # Sum the durations correctly for multiple RFC3339 formatted durations. - # Params: - # +materials+:: The GTN material objects - # Returns: - # +String+:: The human total duration - def sum_duration(materials) - Jekyll.logger.debug "[GTN/Durations]: sum durations with #{materials.length} materials." - total = 0 - materials.each do |material| - if ! material['time_estimation'].nil? - Jekyll.logger.debug " [GTN/Durations]: #{material['time_estimation']} #{material['title']} -> #{parse_rfc3339(material['time_estimation'])}" - total += parse_rfc3339(material['time_estimation']) + ## + # Sum the durations correctly for multiple RFC3339 formatted durations. + # Params: + # +materials+:: The GTN material objects + # Returns: + # +String+:: The human total duration + def sum_duration(materials) + Jekyll.logger.debug "[GTN/Durations]: sum durations with #{materials.length} materials." + total = 0 + materials.each do |material| + if ! material['time_estimation'].nil? + Jekyll.logger.debug " [GTN/Durations]: #{material['time_estimation']} #{material['title']} -> #{parse_rfc3339(material['time_estimation'])}" + total += parse_rfc3339(material['time_estimation']) + end end + fmt_duration(total) end - fmt_duration(total) end end end -Liquid::Template.register_filter(Jekyll::DurationFilter) +Liquid::Template.register_filter(Jekyll::Filters::DurationFilter) diff --git a/_plugins/jekyll-environment_variables.rb b/_plugins/jekyll-environment_variables.rb index d1a6431b6d3cd2..becfdcbf4bce4e 100644 --- a/_plugins/jekyll-environment_variables.rb +++ b/_plugins/jekyll-environment_variables.rb @@ -7,29 +7,32 @@ require './_plugins/gtn/git' module Jekyll - # This module contains a generator for adding environment variables to the `site` object in Liquid templates - class EnvironmentVariablesGenerator < Generator - ## - # Environment variables are added to the `site` object in Liquid templates. - # Here we add the following: - # - `site.config['git_revision']` - the current git revision - # - `site.config['git_tags']` - an array of all git tags - # - `site.config['git_tags_recent']` - an array of the 3 most recent git tags - # - `site.config['gtn_fork']` - the fork of the GTN repo - # - `site.config['age']` - the age of the site in years - def generate(site) - # Add other environment variables to `site.config` here... - Gtn::Scholar.load_bib(site) - site.config.update(Gtn::Git.discover) + module Generators + # This module contains a generator for adding environment variables to the `site` object in Liquid templates + # TODO: definitely could be a hook instead of a generator + class EnvironmentVariablesGenerator < Generator + ## + # Environment variables are added to the `site` object in Liquid templates. + # Here we add the following: + # - `site.config['git_revision']` - the current git revision + # - `site.config['git_tags']` - an array of all git tags + # - `site.config['git_tags_recent']` - an array of the 3 most recent git tags + # - `site.config['gtn_fork']` - the fork of the GTN repo + # - `site.config['age']` - the age of the site in years + def generate(site) + # Add other environment variables to `site.config` here... + Gtn::Scholar.load_bib(site) + site.config.update(Gtn::Git.discover) - site.data['build'] = { - 'today' => Date.today, - 'now' => Time.now, - 'jekyll' => { - 'version' => Jekyll::VERSION, - 'environment' => Jekyll.env, + site.data['build'] = { + 'today' => Date.today, + 'now' => Time.now, + 'jekyll' => { + 'version' => Jekyll::VERSION, + 'environment' => Jekyll.env, + } } - } + end end end end diff --git a/_plugins/jekyll-figurify.rb b/_plugins/jekyll-figurify.rb index 711bab1e51d31b..0aa1825283744d 100644 --- a/_plugins/jekyll-figurify.rb +++ b/_plugins/jekyll-figurify.rb @@ -3,137 +3,140 @@ require 'jekyll' module Jekyll - # Our modifications to the markdown renderer to process images with figure captions - class Figurify < Jekyll::Generator - safe true - - def initialize(config) - super - @config = config['figurify'] ||= {} - end + module Generators + # Our modifications to the markdown renderer to process images with figure captions + # TODO: probably could be a hook post_read. + class Figurify < Jekyll::Generator + safe true + + def initialize(config) + super + @config = config['figurify'] ||= {} + end - def generate(site) - site.pages - .reject { |page| skip_layout? page.data['layout'] } - .each { |page| figurify page, site } - site.posts.docs - .reject { |post| skip_layout? post.data['layout'] } - .each { |post| figurify post, site } - end + def generate(site) + site.pages + .reject { |page| skip_layout? page.data['layout'] } + .each { |page| figurify page, site } + site.posts.docs + .reject { |post| skip_layout? post.data['layout'] } + .each { |post| figurify post, site } + end - private + private - def insert_image(url, alt, style, dimensions, actual_path) - # If it's a *local* SVG (we don't want to do this with remote SVGs, doesn't work right) - if url =~ (/svg$/) && !actual_path.nil? - fallback = '' - if actual_path.nil? - # External image, no fallback possible + def insert_image(url, alt, style, dimensions, actual_path) + # If it's a *local* SVG (we don't want to do this with remote SVGs, doesn't work right) + if url =~ (/svg$/) && !actual_path.nil? fallback = '' - elsif File.exist?(actual_path.gsub(/svg$/, 'png')) - fallback = "<img src=\"#{url.gsub(/svg$/, 'png')}\" alt=\"#{alt}\">" - elsif File.exist?(actual_path.gsub(/svg$/, 'jpg')) - fallback = "<img src=\"#{url.gsub(/svg$/, 'jpg')}\" alt=\"#{alt}\">" - elsif File.exist?(actual_path.gsub(/svg$/, 'jpeg')) - fallback = "<img src=\"#{url.gsub(/svg$/, 'jpeg')}\" alt=\"#{alt}\">" - end + if actual_path.nil? + # External image, no fallback possible + fallback = '' + elsif File.exist?(actual_path.gsub(/svg$/, 'png')) + fallback = "<img src=\"#{url.gsub(/svg$/, 'png')}\" alt=\"#{alt}\">" + elsif File.exist?(actual_path.gsub(/svg$/, 'jpg')) + fallback = "<img src=\"#{url.gsub(/svg$/, 'jpg')}\" alt=\"#{alt}\">" + elsif File.exist?(actual_path.gsub(/svg$/, 'jpeg')) + fallback = "<img src=\"#{url.gsub(/svg$/, 'jpeg')}\" alt=\"#{alt}\">" + end - %( - <div style="overflow-x: auto"> - <object data="#{url}" #{style} type="image/svg+xml" alt="#{alt}"> - #{fallback} - #{alt} - </object> - </div> - ) - else - %( - <img src="#{url}" alt="#{alt}" #{style} #{dimensions} loading="lazy"> - ) + %( + <div style="overflow-x: auto"> + <object data="#{url}" #{style} type="image/svg+xml" alt="#{alt}"> + #{fallback} + #{alt} + </object> + </div> + ) + else + %( + <img src="#{url}" alt="#{alt}" #{style} #{dimensions} loading="lazy"> + ) + end end - end - def figurify(page, site) - num_figure = 0 - return if page.content.nil? + def figurify(page, site) + num_figure = 0 + return if page.content.nil? + + tuto_dir = File.dirname(page.path) + page.content = page.content.gsub(/!\[([^\]]*)\]\((.+?)\s*(?:"(.*)")\)({:(.*)})?/) do + alt = ::Regexp.last_match(1) + url = ::Regexp.last_match(2) + title = ::Regexp.last_match(3) + style = ::Regexp.last_match(5) + + if skip_titles?(title) || (title.to_s.empty? && skip_empty?) + Regexp.last_match + else + num_figure += 1 + + alt.gsub!(/"/, '"') + if alt.strip.length.positive? && !(alt.end_with?('.') || alt.end_with?('!') || alt.end_with?('?')) + alt = "#{alt}. " + end + + dimensions, actual_path = Gtn::Images.html_image_dimensions(tuto_dir, url) + prefix = figcaption_prefix(page, site) + image = insert_image(url, alt, style, dimensions, actual_path) + + %( + <figure id="figure-#{num_figure}" style="max-width: 90%;"> + #{image} + <a target="_blank" href="#{url}" rel="noopener noreferrer"><small>Open image in new tab</small></a><br/><br/> + <figcaption> + <span class="figcaption-prefix"><strong>#{prefix}#{num_figure}</strong>:</span> #{title} + </figcaption> + </figure> + ).split("\n").map(&:strip).join + end + end - tuto_dir = File.dirname(page.path) - page.content = page.content.gsub(/!\[([^\]]*)\]\((.+?)\s*(?:"(.*)")\)({:(.*)})?/) do - alt = ::Regexp.last_match(1) - url = ::Regexp.last_match(2) - title = ::Regexp.last_match(3) - style = ::Regexp.last_match(5) + page.content = page.content.gsub(/!\[([^\]]*)\]\((.+?)?\)({:(.*)})?/) do + alt = ::Regexp.last_match(1) + url = ::Regexp.last_match(2) + style = ::Regexp.last_match(4) - if skip_titles?(title) || (title.to_s.empty? && skip_empty?) - Regexp.last_match - else - num_figure += 1 + dimensions, _actual_path = Gtn::Images.html_image_dimensions(tuto_dir, url) alt.gsub!(/"/, '"') if alt.strip.length.positive? && !(alt.end_with?('.') || alt.end_with?('!') || alt.end_with?('?')) alt = "#{alt}. " end - dimensions, actual_path = Gtn::Images.html_image_dimensions(tuto_dir, url) - prefix = figcaption_prefix(page, site) - image = insert_image(url, alt, style, dimensions, actual_path) - %( - <figure id="figure-#{num_figure}" style="max-width: 90%;"> - #{image} - <a target="_blank" href="#{url}" rel="noopener noreferrer"><small>Open image in new tab</small></a><br/><br/> - <figcaption> - <span class="figcaption-prefix"><strong>#{prefix}#{num_figure}</strong>:</span> #{title} - </figcaption> - </figure> + <a href="#{url}" rel="noopener noreferrer"> + <img src="#{url}" alt="#{alt}" #{style} #{dimensions} loading="lazy"> + </a> ).split("\n").map(&:strip).join end end - page.content = page.content.gsub(/!\[([^\]]*)\]\((.+?)?\)({:(.*)})?/) do - alt = ::Regexp.last_match(1) - url = ::Regexp.last_match(2) - style = ::Regexp.last_match(4) - - dimensions, _actual_path = Gtn::Images.html_image_dimensions(tuto_dir, url) - - alt.gsub!(/"/, '"') - if alt.strip.length.positive? && !(alt.end_with?('.') || alt.end_with?('!') || alt.end_with?('?')) - alt = "#{alt}. " + def figcaption_prefix(page, site) + fig = 'Figure' + if page['lang'] + lang = page['lang'] + fig = site.data['lang'][lang]['figure'] end - - %( - <a href="#{url}" rel="noopener noreferrer"> - <img src="#{url}" alt="#{alt}" #{style} #{dimensions} loading="lazy"> - </a> - ).split("\n").map(&:strip).join + @config['prefix'] || "#{fig} " end - end - def figcaption_prefix(page, site) - fig = 'Figure' - if page['lang'] - lang = page['lang'] - fig = site.data['lang'][lang]['figure'] + def skip_empty? + @config['skip_empty'] || false end - @config['prefix'] || "#{fig} " - end - - def skip_empty? - @config['skip_empty'] || false - end - def skip_layout?(layout) - to_skip = @config['skip_layouts'] || [] + def skip_layout?(layout) + to_skip = @config['skip_layouts'] || [] - true if to_skip.empty? + true if to_skip.empty? - to_skip.include?(layout) - end + to_skip.include?(layout) + end - def skip_titles?(title) - to_skip = @config['skip_titles'] || [] - to_skip.include?(title) + def skip_titles?(title) + to_skip = @config['skip_titles'] || [] + to_skip.include?(title) + end end end end diff --git a/_plugins/jekyll-icon-tag.rb b/_plugins/jekyll-icon-tag.rb index f4ea6858a4f0b7..dad6241f679765 100644 --- a/_plugins/jekyll-icon-tag.rb +++ b/_plugins/jekyll-icon-tag.rb @@ -1,80 +1,99 @@ # frozen_string_literal: true module Jekyll - # Our {% icon X %} tag - class IconTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - parts = text.strip.split - @text = parts[0] - @aria = true - return unless parts[1] == 'aria=false' + module Tags - @aria = false - end + # Our {% icon X %} tag + class IconTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + parts = text.strip.split + @text = parts[0] + @aria = true + return unless parts[1] == 'aria=false' - ## - # This function renders the icon tag - # Params: - # +icon+:: The icon to render - # +@area+:: Whether to add aria-hidden - # +@text+:: The text to add to the icon - # - # Returns: - # The HTML for the icon - # Note: The icon text label is wrapped in a span with class - # "visually-hidden" to make it accessible to screen readers. - # - # Example: - # {% icon fa fa-github %} - # => <i class="fa fa-github" aria-hidden="true"></i> - # {% icon fa fa-github aria=false %} - # => <i class="fa fa-github"></i> - def render_for_text(icon) - if icon.empty? - raise SyntaxError, "No icon defined for: '#{@text}'. Please define it in `_config.yml` (under `icon-tag:`)." + @aria = false end - if icon.start_with?('fa') - if @aria - %(<i class="#{icon}" aria-hidden="true"></i><span class="visually-hidden">#{@text}</span>) - else - %(<i class="#{icon}" aria-hidden="true"></i>) + ## + # This function renders the icon tag + # Params: + # +icon+:: The icon to render + # +@area+:: Whether to add aria-hidden + # +@text+:: The text to add to the icon + # + # Returns: + # The HTML for the icon + # Note: The icon text label is wrapped in a span with class + # "visually-hidden" to make it accessible to screen readers. + # + # Example: + # {% icon fa fa-github %} + # => <i class="fa fa-github" aria-hidden="true"></i> + # {% icon fa fa-github aria=false %} + # => <i class="fa fa-github"></i> + def render_for_text(icon) + if icon.empty? + raise SyntaxError, "No icon defined for: '#{@text}'. Please define it in `_config.yml` (under `icon-tag:`)." end - elsif icon.start_with?('ai') - if @aria - %(<i class="ai #{icon}" aria-hidden="true"></i><span class="visually-hidden">#{@text}</span>) - else - %(<i class="ai #{icon}" aria-hidden="true"></i>) + + if icon.start_with?('fa') + if @aria + %(<i class="#{icon}" aria-hidden="true"></i><span class="visually-hidden">#{@text}</span>) + else + %(<i class="#{icon}" aria-hidden="true"></i>) + end + elsif icon.start_with?('ai') + if @aria + %(<i class="ai #{icon}" aria-hidden="true"></i><span class="visually-hidden">#{@text}</span>) + else + %(<i class="ai #{icon}" aria-hidden="true"></i>) + end end end - end - def render(context) - cfg = get_config(context) - icon = cfg[@text] || '' - render_for_text(icon) - end + ## + # icon - Include an icon from our _config.yml into your tutorial + # + # Examples: + # + # {% icon email %} + # {% icon galaxy-history %} + # + def render(context) + cfg = get_config(context) + icon = cfg[@text] || '' + render_for_text(icon) + end - def get_config(context) - context.registers[:site].config['icon-tag'] + def get_config(context) + context.registers[:site].config['icon-tag'] + end end - end - # The variable version that can accept a variable name instead of a string - class IconTagVar < IconTag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end + # The variable version that can accept a variable name instead of a string + class IconTagVar < IconTag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end - def render(context) - cfg = get_config(context) - icon = cfg[context[@text]] || '' - render_for_text(icon) + ## + # icon_var - Include an icon from our _config.yml into your tutorial, but accessing a variable rather than expecting a string. + # + # Examples: + # + # {% icon_var var1 %} + # {% icon_var var2 %} + # + def render(context) + cfg = get_config(context) + icon = cfg[context[@text]] || '' + render_for_text(icon) + end end end end -Liquid::Template.register_tag('icon_var', Jekyll::IconTagVar) -Liquid::Template.register_tag('icon', Jekyll::IconTag) +Liquid::Template.register_tag('icon_var', Jekyll::Tags::IconTagVar) +Liquid::Template.register_tag('icon', Jekyll::Tags::IconTag) diff --git a/_plugins/jekyll-jsonld.rb b/_plugins/jekyll-jsonld.rb index 783d8229cfdb90..0c155e11f75c70 100644 --- a/_plugins/jekyll-jsonld.rb +++ b/_plugins/jekyll-jsonld.rb @@ -6,284 +6,512 @@ require './_plugins/util' module Jekyll - # Generate JSON-LD metadata for the GTN. - module JsonldFilter - GTN = { - '@type': 'Organization', - 'http://purl.org/dc/terms/conformsTo': { - # Bioschemas profile - '@id': 'https://bioschemas.org/profiles/Organization/0.2-DRAFT-2019_07_19', - '@type': 'Organization' - }, - id: 'https://training.galaxyproject.org', - email: 'galaxytrainingnetwork@gmail.com', - name: 'Galaxy Training Network', - legalName: 'Galaxy Training Network', - alternateName: 'GTN', - url: 'https://training.galaxyproject.org', - logo: 'https://training.galaxyproject.org/training-material/assets/images/GTNLogo1000.png', - fundingModel: "The GTN's infrastructure relies on GitHub and the Galaxy Project for hosting costs. " \ - 'There are no full time paid staff members of the GTN. Individuals are occasionally funded on ' \ - 'GTN-adjacent projects.', - keywords: %w[galaxy bioinformatics training fair accessible], - status: 'active', - foundingDate: Gtn::Git.discover['founding_date'].to_s, - socialMedia: 'https://mstdn.science/@gtn', - type: 'project', - }.freeze - - A11Y = { - accessMode: %w[textual visual], - accessModeSufficient: %w[textual visual], - # "accessibilityAPI": , - accessibilityControl: %w[fullKeyboardControl fullMouseControl], - accessibilityFeature: %w[alternativeText tableOfContents], - # "accessibilityHazard": [], - accessibilitySummary: 'The text aims to be as accessible as possible. Image descriptions will vary per ' \ - 'tutorial, from images being completely inaccessible, to images with good descriptions ' \ - 'for non-visual users.', - }.freeze - - EDU_ROLES = { - 'use' => 'Students', - 'admin-dev' => 'Galaxy Administrators', - 'basics' => 'Students', - 'data-science' => 'Data-Science Students', - 'instructors' => 'Instructors', - } - - ## - # Generate the Dublin Core metadata for a material. - # Parmaeters: - # +material+:: The material to generate the metadata for. - # +site+:: The site object. - # Returns: - # A string containing the metadata. - # - # Example: - # {{ material | generate_dublin_core: site }} - # => <meta name="DC.identifier" content="..." /> - def generate_dublin_core(material, site) - return if material.key?('data') && material['data'].fetch('type', 'none') != 'tutorial_hands_on' - - attributes = [ - ['DC.identifier', site['github_repository']], - ['DC.type', 'text'], - ['DC.title', material['title']], - ['DC.publisher', 'Galaxy Training Network'], - ['DC.date', Gtn::ModificationTimes.obtain_time(material['path'])] - ] - - attributes += Gtn::Contributors.get_authors(material).map do |user| - ['DC.creator', Gtn::Contributors.fetch_name(site, user)] - end - - attributes.map { |a, b| "<meta name=\"#{a}\" content=\"#{b}\">" }.join("\n") - end - - ## - # Generate the JSON-LD metadata for a person - # Parameters: - # +id+:: The id of the person. - # +contributor+:: The contributor object from CONTRIBUTORS.yaml. - # +site+:: The site object. - # Returns: - # +Hash+:: The JSON-LD metadata. - # - # Example: - # generate_person_jsonld("hexylena", site['data']['contributors']['hexylena'], site) - # => { - # "@context": "https://schema.org", - # "@type": "Person", - # "http://purl.org/dc/terms/conformsTo": { - # # Bioschemas profile - # "@id": "https://bioschemas.org/profiles/Person/0.2-DRAFT-2019_07_19", - # "@type": "Person" - # }, - # "url": "https://training.galaxyproject.org/hall-of-fame/hexylena/", - # "mainEntityOfPage": "https://training.galaxyproject.org/hall-of-fame/hexylena/", - # "name": "hexylena", - # "image": "https://avatars.githubusercontent.com/hexylena", - # "description": "A contributor to the GTN project.", - # "memberOf": [...], - # "identifier": "https://orcid.org/0000-0002-6601-2165", - # "orcid": "https://orcid.org/0000-0002-6601-2165" - # } - # - def generate_person_jsonld(id, contributor, site) - member_of = Gtn::Contributors.fetch_contributor(site, id)['affiliations'] || [] - member_of = member_of.map do |org_id| - org = Gtn::Contributors.fetch_contributor(site, org_id) - generate_org_jsonld(org_id, org, site) - end + module Filters - person = { - '@context': 'https://schema.org', - '@type': 'Person', - 'http://purl.org/dc/terms/conformsTo': { - '@id': 'https://bioschemas.org/profiles/Person/0.3-DRAFT', - '@type': 'CreativeWork' - }, - # I guess these are identical? - url: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/", - mainEntityOfPage: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/", - name: Gtn::Contributors.fetch_name(site, id), - image: "https://avatars.githubusercontent.com/#{id}", - # No clue what to put here it's a person. - description: if contributor.nil? - 'A contributor to the GTN project.' - else - contributor.fetch('bio', - 'A contributor to the GTN project.') - end, - memberOf: [GTN] + member_of, - } - if !contributor.nil? && contributor.key?('orcid') && contributor['orcid'] - person['identifier'] = "https://orcid.org/#{contributor['orcid']}" - person['orcid'] = "https://orcid.org/#{contributor['orcid']}" - end - - person - end - - def generate_org_jsonld(id, contributor, site) - organization = { - '@context': 'https://schema.org', + # Generate JSON-LD metadata for the GTN. + module JsonldFilter + GTN = { '@type': 'Organization', 'http://purl.org/dc/terms/conformsTo': { - '@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT', - '@type': 'CreativeWork' + # Bioschemas profile + '@id': 'https://bioschemas.org/profiles/Organization/0.2-DRAFT-2019_07_19', + '@type': 'Organization' }, - id: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/", - name: Gtn::Contributors.fetch_name(site, id), - description: 'An organization supporting the Galaxy Training Network', + id: 'https://training.galaxyproject.org', + email: 'galaxytrainingnetwork@gmail.com', + name: 'Galaxy Training Network', + legalName: 'Galaxy Training Network', + alternateName: 'GTN', + url: 'https://training.galaxyproject.org', + logo: 'https://training.galaxyproject.org/training-material/assets/images/GTNLogo1000.png', + fundingModel: "The GTN's infrastructure relies on GitHub and the Galaxy Project for hosting costs. " \ + 'There are no full time paid staff members of the GTN. Individuals are occasionally funded on ' \ + 'GTN-adjacent projects.', + keywords: %w[galaxy bioinformatics training fair accessible], + status: 'active', + foundingDate: Gtn::Git.discover['founding_date'].to_s, + socialMedia: 'https://mstdn.science/@gtn', + type: 'project', + }.freeze + + A11Y = { + accessMode: %w[textual visual], + accessModeSufficient: %w[textual visual], + # "accessibilityAPI": , + accessibilityControl: %w[fullKeyboardControl fullMouseControl], + accessibilityFeature: %w[alternativeText tableOfContents], + # "accessibilityHazard": [], + accessibilitySummary: 'The text aims to be as accessible as possible. Image descriptions will vary per ' \ + 'tutorial, from images being completely inaccessible, to images with good descriptions ' \ + 'for non-visual users.', + }.freeze + + EDU_ROLES = { + 'use' => 'Students', + 'admin-dev' => 'Galaxy Administrators', + 'basics' => 'Students', + 'data-science' => 'Data-Science Students', + 'instructors' => 'Instructors', } - organization['url'] = contributor['url'] if contributor.key?('url') && contributor['url'] + ## + # Generate the Dublin Core metadata for a material. + # Parmaeters: + # +material+:: The material to generate the metadata for. + # +site+:: The site object. + # Returns: + # A string containing the metadata. + # + # Example: + # {{ material | generate_dublin_core: site }} + # => <meta name="DC.identifier" content="..." /> + def generate_dublin_core(material, site) + return if material.key?('data') && material['data'].fetch('type', 'none') != 'tutorial_hands_on' + + attributes = [ + ['DC.identifier', site['github_repository']], + ['DC.type', 'text'], + ['DC.title', material['title']], + ['DC.publisher', 'Galaxy Training Network'], + ['DC.date', Gtn::ModificationTimes.obtain_time(material['path'])] + ] + + attributes += Gtn::Contributors.get_authors(material).map do |user| + ['DC.creator', Gtn::Contributors.fetch_name(site, user)] + end - organization - end + attributes.map { |a, b| "<meta name=\"#{a}\" content=\"#{b}\">" }.join("\n") + end - def generate_funder_jsonld(id, contributor, site) - { - '@context': 'https://schema.org', - '@type': 'Organization', - 'http://purl.org/dc/terms/conformsTo': { - '@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT', - '@type': 'CreativeWork' - }, - name: Gtn::Contributors.fetch_name(site, id), - description: contributor.fetch('funding_statement', 'An organization supporting the Galaxy Training Network'), - url: contributor.fetch('url', "https://training.galaxyproject.org/training-material/hall-of-fame/#{id}/"), - logo: contributor.fetch('avatar', "https://github.com/#{id}.png"), - } - end + ## + # Generate the JSON-LD metadata for a person + # Parameters: + # +id+:: The id of the person. + # +contributor+:: The contributor object from CONTRIBUTORS.yaml. + # +site+:: The site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + # + # Example: + # generate_person_jsonld("hexylena", site['data']['contributors']['hexylena'], site) + # => { + # "@context": "https://schema.org", + # "@type": "Person", + # "http://purl.org/dc/terms/conformsTo": { + # # Bioschemas profile + # "@id": "https://bioschemas.org/profiles/Person/0.2-DRAFT-2019_07_19", + # "@type": "Person" + # }, + # "url": "https://training.galaxyproject.org/hall-of-fame/hexylena/", + # "mainEntityOfPage": "https://training.galaxyproject.org/hall-of-fame/hexylena/", + # "name": "hexylena", + # "image": "https://avatars.githubusercontent.com/hexylena", + # "description": "A contributor to the GTN project.", + # "memberOf": [...], + # "identifier": "https://orcid.org/0000-0002-6601-2165", + # "orcid": "https://orcid.org/0000-0002-6601-2165" + # } + # + def generate_person_jsonld(id, contributor, site) + member_of = Gtn::Contributors.fetch_contributor(site, id)['affiliations'] || [] + member_of = member_of.map do |org_id| + org = Gtn::Contributors.fetch_contributor(site, org_id) + generate_org_jsonld(org_id, org, site) + end - def generate_grant_jsonld(id, contributor, site) - organization = { - '@context': 'https://schema.org', - '@type': 'Grant', - identifier: contributor['funding_id'], - url: Gtn::Contributors.fetch_funding_url(contributor) || contributor['url'], - funder: generate_funder_jsonld(id, contributor, site) - } + person = { + '@context': 'https://schema.org', + '@type': 'Person', + 'http://purl.org/dc/terms/conformsTo': { + '@id': 'https://bioschemas.org/profiles/Person/0.3-DRAFT', + '@type': 'CreativeWork' + }, + # I guess these are identical? + url: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/", + mainEntityOfPage: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/", + name: Gtn::Contributors.fetch_name(site, id), + image: "https://avatars.githubusercontent.com/#{id}", + # No clue what to put here it's a person. + description: if contributor.nil? + 'A contributor to the GTN project.' + else + contributor.fetch('bio', + 'A contributor to the GTN project.') + end, + memberOf: [GTN] + member_of, + } + if !contributor.nil? && contributor.key?('orcid') && contributor['orcid'] + person['identifier'] = "https://orcid.org/#{contributor['orcid']}" + person['orcid'] = "https://orcid.org/#{contributor['orcid']}" + end - organization['startDate'] = contributor['start_date'] if contributor.key?('start_date') - organization['endDate'] = contributor['end_date'] if contributor.key?('end_date') + person + end - organization - end + ## + # Generate the JSON-LD metadata for an organisation + # Parameters: + # +id+:: The id of the org. + # +contributor+:: The contributor object from ORGANISATIONS.yaml. + # +site+:: The site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + def generate_org_jsonld(id, contributor, site) + organization = { + '@context': 'https://schema.org', + '@type': 'Organization', + 'http://purl.org/dc/terms/conformsTo': { + '@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT', + '@type': 'CreativeWork' + }, + id: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/", + name: Gtn::Contributors.fetch_name(site, id), + description: 'An organization supporting the Galaxy Training Network', + } - ## - # Generate the JSON-LD metadata for a person, funder, or organisation as JSON. - # Parameters: - # +id+:: The id of the person. - # +site+:: The site object. - # +json+:: Should the output be rendered as JSON (only really used in contributor page.) - # Returns: - # +String+:: The JSON-LD metadata. - def to_pfo_jsonld(id, site, json: true) - contributor = Gtn::Contributors.fetch_contributor(site, id) - d = if Gtn::Contributors.person?(site, id) - generate_person_jsonld(id, contributor, site) - elsif Gtn::Contributors.grant?(site, id) - generate_grant_jsonld(id, contributor, site) - else - generate_org_jsonld(id, contributor, site) - end + organization['url'] = contributor['url'] if contributor.key?('url') && contributor['url'] - if json - JSON.pretty_generate(d) - else - d + organization end - end - ## - # Generate the JSON-LD metadata for a news article (blog) - # Parameters: - # +page+:: The page object. - # +site+:: The +Jekyll::Site+ site object. - # Returns: - # +Hash+:: The JSON-LD metadata. - def generate_news_jsonld(page, site) - authors = Gtn::Contributors.get_authors(page.to_h).map do |x| - to_pfo_jsonld(x, site, json: false) + ## + # Generate the JSON-LD metadata for a funding organisation + # Parameters: + # +id+:: The id of the person. + # +contributor+:: The contributor object from ORGANISATIONS.yaml. + # +site+:: The site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + def generate_funder_jsonld(id, contributor, site) + { + '@context': 'https://schema.org', + '@type': 'Organization', + 'http://purl.org/dc/terms/conformsTo': { + '@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT', + '@type': 'CreativeWork' + }, + name: Gtn::Contributors.fetch_name(site, id), + description: contributor.fetch('funding_statement', 'An organization supporting the Galaxy Training Network'), + url: contributor.fetch('url', "https://training.galaxyproject.org/training-material/hall-of-fame/#{id}/"), + logo: contributor.fetch('avatar', "https://github.com/#{id}.png"), + } end - data = { - '@context': 'https://schema.org', - '@type': 'BlogPosting', - url: "#{site['url']}#{site['baseurl']}#{page['url']}", - name: page['title'], - headline: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags. - keywords: page['tags'] || [], - description: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags - articleBody: page.content, # TODO: remove html tags - datePublished: page.date, - dateModified: Gtn::ModificationTimes.obtain_time(page.path), - author: authors, - publisher: GTN, - mainEntityOfPage: { - '@type': 'WebPage', - '@id': "#{site['url']}#{page['url']}" - }, - image: { - '@type': 'ImageObject', - width: 60, - height: 60, - url: "#{site['baseurl']}/assets/images/GTN-60px.png" + ## + # Generate the JSON-LD metadata for a grant + # Parameters: + # +id+:: The id of the grant. + # +contributor+:: The contributor object from GRANTS.yaml. + # +site+:: The site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + def generate_grant_jsonld(id, contributor, site) + organization = { + '@context': 'https://schema.org', + '@type': 'Grant', + identifier: contributor['funding_id'], + url: Gtn::Contributors.fetch_funding_url(contributor) || contributor['url'], + funder: generate_funder_jsonld(id, contributor, site) } - } - data.update(A11Y) - JSON.pretty_generate(data) - end + organization['startDate'] = contributor['start_date'] if contributor.key?('start_date') + organization['endDate'] = contributor['end_date'] if contributor.key?('end_date') - ## - # Generate the JSON-LD metadata for an event - # Parameters: - # +page+:: The page object. - # +site+:: The +Jekyll::Site+ site object. - # Returns: - # +Hash+:: The JSON-LD metadata. - def generate_event_jsonld(page, site) - organisers = Gtn::Contributors.get_organisers(page.to_h).map do |x| - to_pfo_jsonld(x, site, json: false) + organization end - instructors = Gtn::Contributors.get_instructors(page.to_h).map do |x| - to_pfo_jsonld(x, site, json: false) + + ## + # Generate the JSON-LD metadata for a person, funder, or organisation as JSON. + # Parameters: + # +id+:: The id of the person. + # +site+:: The site object. + # +json+:: Should the output be rendered as JSON (only really used in contributor page.) + # Returns: + # +String+:: The JSON-LD metadata. + def to_pfo_jsonld(id, site, json: true) + contributor = Gtn::Contributors.fetch_contributor(site, id) + d = if Gtn::Contributors.person?(site, id) + generate_person_jsonld(id, contributor, site) + elsif Gtn::Contributors.grant?(site, id) + generate_grant_jsonld(id, contributor, site) + else + generate_org_jsonld(id, contributor, site) + end + + if json + JSON.pretty_generate(d) + else + d + end end - funders = Gtn::Contributors.get_funders(site, page.to_h).map do |x| - to_pfo_jsonld(x, site, json: false) + + ## + # Generate the JSON-LD metadata for a news article (blog) + # Parameters: + # +page+:: The page object. + # +site+:: The +Jekyll::Site+ site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + def generate_news_jsonld(page, site) + authors = Gtn::Contributors.get_authors(page.to_h).map do |x| + to_pfo_jsonld(x, site, json: false) + end + + data = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + url: "#{site['url']}#{site['baseurl']}#{page['url']}", + name: page['title'], + headline: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags. + keywords: page['tags'] || [], + description: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags + articleBody: page.content, # TODO: remove html tags + datePublished: page.date, + dateModified: Gtn::ModificationTimes.obtain_time(page.path), + author: authors, + publisher: GTN, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': "#{site['url']}#{page['url']}" + }, + image: { + '@type': 'ImageObject', + width: 60, + height: 60, + url: "#{site['baseurl']}/assets/images/GTN-60px.png" + } + } + data.update(A11Y) + + JSON.pretty_generate(data) end - funding = Gtn::Contributors.get_grants(site, page.to_h).map do |x| - to_pfo_jsonld(x, site, json: false) + + ## + # Generate the JSON-LD metadata for an event + # Parameters: + # +page+:: The page object. + # +site+:: The +Jekyll::Site+ site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + # + # Examples: + # {{ page | generate_event_jsonld: site }} + def generate_event_jsonld(page, site) + organisers = Gtn::Contributors.get_organisers(page.to_h).map do |x| + to_pfo_jsonld(x, site, json: false) + end + instructors = Gtn::Contributors.get_instructors(page.to_h).map do |x| + to_pfo_jsonld(x, site, json: false) + end + funders = Gtn::Contributors.get_funders(site, page.to_h).map do |x| + to_pfo_jsonld(x, site, json: false) + end + funding = Gtn::Contributors.get_grants(site, page.to_h).map do |x| + to_pfo_jsonld(x, site, json: false) + end + + materials = [] + if page['program'] + page['program'].each do |section| + if !section.key? 'tutorials' + next + end + + section['tutorials'].each do |tutorial| + if tutorial.key?('custom') + next + end + + material = Gtn::TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name']) + materials.push(material) + end + end + end + materials.compact! + + # Extract EDAM terms from all materials + edam_terms = materials.map do |material| + material.fetch('edam_ontology', []).map do |term| + { + '@type': 'DefinedTerm', + '@id': "http://edamontology.org/#{term}", + inDefinedTermSet: 'http://edamontology.org', + termCode: term, + } + end + end.flatten.uniq + + learning_objectives = materials.map do |material| + material.fetch('objectives', []) + end.flatten.compact + + # TODO: add topic edam terms too? Not sure. + parts = [] + materials.each do |material| + mat = generate_material_jsonld(material, site['data'][material['topic_name']], site) + if !mat.nil? && !mat.empty? + parts.push(mat) + end + end + + if page['program'] + syllab = page['program'].reject { |s| s['section'].nil? }.map do |section| + { + '@type': 'Syllabus', + name: section['section'], + description: section.fetch('description', nil), + } + end + end + + data = { + '@context': 'https://schema.org', + '@type': 'Course', + url: "#{site['url']}#{site['baseurl']}#{page['url']}", + name: page['title'], + keywords: page['tags'] || [], + description: page['description'], + + about: edam_terms, # TeSS, "scientific topics". + audience: page['audience'], # TeSS: target audience + # If 'online' is present in the mode, the course is online. + # Will fail on "this is NOT an online course" + # Acceptable. + courseMode: page['mode'], + startDate: page['date_start'], + endDate: page['date_end'], + organizer: organisers, # TeSS only, US spelling, non-standard + + location: page['location'], # TODO, TeSS location + teaches: learning_objectives, # TeSS, "learning objectives" + # timeRequired: 'P1D', # TeSS, "duration", TODO: calculate from start/end date, not implemented in scraper currently. + + availableLanguage: ['en'], # TODO: support other languages + inLanguage: ['en'], # TODO: support other languages + # courseCode + # coursePrerequisites + # educationalCredentialAwarded + # financialAidEligible + # hasCourseInstance + # numberOfCredits + # occupationalCredentialAwarded + # syllabusSections + # totalHistoricalEnrollment + + # assesses + # competencyRequired + # educationalAlignment + # educationalLevel + # educationalUse + # learningResourceType + # teaches + + funder: funders, # Org or person + funding: funding, # Grant + publisher: GTN, + provider: GTN, + syllabusSections: syllab, + # Session materials + # TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out. + # hasPart: parts, + } + + begin + data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path) + data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path) + rescue StandardError + data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path']) + data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path']) + end + + if page['cover'] + data['image'] = if page['cover'] =~ /^http/ + [page['cover']] + else + ["#{site['url']}#{site['baseurl']}#{page['cover']}"] + end + end + + # We CANNOT guarantee A11Y + # data.update(A11Y) + if page['cost'] and page['cost'].downcase == 'free' + data['isAccessibleForFree'] = true + offer = { + '@type': 'Offer', + price: 0, + priceCurrency: 'EUR', + category: 'Free', + isAccessibleForFree: true, + } + elsif page['cost'] + data['isAccessibleForFree'] = false + offer = { + '@type': 'Offer', + price: page['cost'].split[0], + priceCurrency: page['cost'].split[1], + isAccessibleForFree: false, + category: 'Paid', + # TODO: this can be more advanced but we need to collect start/end times, and timezone. + } + end + + # TODO: this is wrong in a whole host of scenarios like incl weekends. + course_days = (page.fetch('date_end', page['date_start']) - page['date_start']).to_i + if course_days < 1 + course_days = 1 + end + data['hasCourseInstance'] = [ + { + '@type': 'CourseInstance', + courseMode: page['mode'], + # courseWorkload: "A daily course running from #{page['date_start']} to #{page['date_end']}", + offers: offer, + instructor: instructors, + isAccessibleForFree: data['isAccessibleForFree'], + courseSchedule: { + '@type': 'Schedule', + startDate: page['date_start'], + endDate: page.fetch('date_end', page['date_start']), + repeatCount: course_days, + repeatFrequency: 'daily', # Contrary to schema.org spec, this is what Google wants. + }, + courseWorkload: "P#{course_days}D", + } + ] + + data['offers'] = [offer] + + if page.key?('location') && page['location'].keys.length > 1 + data['location'] = { + '@type': 'Place', + name: page['location']['name'], + address: { + '@type': 'PostalAddress', + streetAddress: page['location'].fetch('address', nil), + addressLocality: page['location'].fetch('city', nil), + addressRegion: page['location'].fetch('region', nil), + postalCode: page['location'].fetch('postcode', nil), + addressCountry: page['location'].fetch('country', nil) + } + } + end + + JSON.pretty_generate(data) end - materials = [] - if page['program'] - page['program'].each do |section| + ## + # Generate the JSON-LD metadata for a learning pathway + # Parameters: + # +page+:: The page object. + # +site+:: The +Jekyll::Site+ site object. + # Returns: + # +Hash+:: The JSON-LD metadata. + # + # Examples: + # {{ page | generate_learning_pathway_jsonld: site }} + def generate_learning_pathway_jsonld(page, site) + materials = [] + page['pathway'].each do |section| if !section.key? 'tutorials' next end @@ -293,119 +521,115 @@ def generate_event_jsonld(page, site) next end - material = TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name']) + material = Gtn::TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name']) materials.push(material) end end - end - materials.compact! + materials.compact! + + # Extract EDAM terms from all materials + edam_terms = materials.map do |material| + material.fetch('edam_ontology', []).map do |term| + { + '@type': 'DefinedTerm', + '@id': "http://edamontology.org/#{term}", + inDefinedTermSet: 'http://edamontology.org', + termCode: term, + } + end + end.flatten.uniq - # Extract EDAM terms from all materials - edam_terms = materials.map do |material| - material.fetch('edam_ontology', []).map do |term| - { - '@type': 'DefinedTerm', - '@id': "http://edamontology.org/#{term}", - inDefinedTermSet: 'http://edamontology.org', - termCode: term, - } - end - end.flatten.uniq - - learning_objectives = materials.map do |material| - material.fetch('objectives', []) - end.flatten.compact - - # TODO: add topic edam terms too? Not sure. - parts = [] - materials.each do |material| - mat = generate_material_jsonld(material, site['data'][material['topic_name']], site) - if !mat.nil? && !mat.empty? - parts.push(mat) + learning_objectives = materials.map do |material| + material.fetch('objectives', []) + end.flatten.compact + + funders = materials.map do |material| + Gtn::Contributors.get_funders(site, material).map do |x| + to_pfo_jsonld(x, site, json: false) + end + end.flatten.uniq.compact + + funding = materials.map do |material| + Gtn::Contributors.get_grants(site, material).map do |x| + to_pfo_jsonld(x, site, json: false) + end + end.flatten.uniq.compact + + # TODO: add topic edam terms too? Not sure. + parts = [] + materials.each do |material| + mat = generate_material_jsonld(material, site['data'][material['topic_name']], site) + if !mat.nil? && !mat.empty? + parts.push(mat) + end end - end - if page['program'] - syllab = page['program'].reject { |s| s['section'].nil? }.map do |section| + syllab = page['pathway'].reject { |s| s['section'].nil? }.map do |section| { '@type': 'Syllabus', name: section['section'], description: section.fetch('description', nil), } end - end - data = { - '@context': 'https://schema.org', - '@type': 'Course', - url: "#{site['url']}#{site['baseurl']}#{page['url']}", - name: page['title'], - keywords: page['tags'] || [], - description: page['description'], - - about: edam_terms, # TeSS, "scientific topics". - audience: page['audience'], # TeSS: target audience - # If 'online' is present in the mode, the course is online. - # Will fail on "this is NOT an online course" - # Acceptable. - courseMode: page['mode'], - startDate: page['date_start'], - endDate: page['date_end'], - organizer: organisers, # TeSS only, US spelling, non-standard - - location: page['location'], # TODO, TeSS location - teaches: learning_objectives, # TeSS, "learning objectives" - # timeRequired: 'P1D', # TeSS, "duration", TODO: calculate from start/end date, not implemented in scraper currently. - - availableLanguage: ['en'], # TODO: support other languages - inLanguage: ['en'], # TODO: support other languages - # courseCode - # coursePrerequisites - # educationalCredentialAwarded - # financialAidEligible - # hasCourseInstance - # numberOfCredits - # occupationalCredentialAwarded - # syllabusSections - # totalHistoricalEnrollment - - # assesses - # competencyRequired - # educationalAlignment - # educationalLevel - # educationalUse - # learningResourceType - # teaches - - funder: funders, # Org or person - funding: funding, # Grant - publisher: GTN, - provider: GTN, - syllabusSections: syllab, - # Session materials - # TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out. - # hasPart: parts, - } + data = { + '@context': 'https://schema.org', + '@type': 'Course', + url: "#{site['url']}#{site['baseurl']}#{page['url']}", + name: "Learning Pathway #{page['title']}", + keywords: page['tags'] || [], + description: page['description'], + about: edam_terms, # TeSS, "scientific topics". + audience: page['audience'], # TeSS: target audience + teaches: learning_objectives, # TeSS, "learning objectives" + availableLanguage: ['en'], # TODO: support other languages + inLanguage: ['en'], # TODO: support other languages + # courseCode + # coursePrerequisites + # educationalCredentialAwarded + # financialAidEligible + # hasCourseInstance + # numberOfCredits + # occupationalCredentialAwarded + # syllabusSections + # totalHistoricalEnrollment + + # assesses + # competencyRequired + # educationalAlignment + # educationalLevel + # educationalUse + # learningResourceType + # teaches + + funder: funders, # Org or person + funding: funding, # Grant + publisher: GTN, + provider: GTN, + syllabusSections: syllab, + # Session materials + # TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out. + # hasPart: parts, + } - begin - data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path) - data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path) - rescue StandardError - data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path']) - data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path']) - end + begin + data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path) + data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path) + rescue StandardError + data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path']) + data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path']) + end - if page['cover'] - data['image'] = if page['cover'] =~ /^http/ - [page['cover']] - else - ["#{site['url']}#{site['baseurl']}#{page['cover']}"] - end - end + if page['cover'] + data['image'] = if page['cover'] =~ /^http/ + [page['cover']] + else + ["#{site['url']}#{site['baseurl']}#{page['cover']}"] + end + end - # We CANNOT guarantee A11Y - # data.update(A11Y) - if page['cost'] and page['cost'].downcase == 'free' + # We CANNOT guarantee A11Y + # data.update(A11Y) data['isAccessibleForFree'] = true offer = { '@type': 'Offer', @@ -414,683 +638,525 @@ def generate_event_jsonld(page, site) category: 'Free', isAccessibleForFree: true, } - elsif page['cost'] - data['isAccessibleForFree'] = false - offer = { - '@type': 'Offer', - price: page['cost'].split[0], - priceCurrency: page['cost'].split[1], - isAccessibleForFree: false, - category: 'Paid', - # TODO: this can be more advanced but we need to collect start/end times, and timezone. - } + data['offers'] = [offer] + + # TODO: this is basically just wrong. + data['hasCourseInstance'] = [ + { + '@type': 'CourseInstance', + courseMode: 'online', + offers: offer, + isAccessibleForFree: data['isAccessibleForFree'], + } + ] + + JSON.pretty_generate(data) end - # TODO: this is wrong in a whole host of scenarios like incl weekends. - course_days = (page.fetch('date_end', page['date_start']) - page['date_start']).to_i - if course_days < 1 - course_days = 1 + ## + # Convert a material to JSON-LD, intended to be used in Jekyll Liquid templates. + # Parameters: + # +material+:: The material object. + # +topic+:: The topic object. + # +site+:: The +Jekyll::Site+ site object. + # + # Returns: + # +String+:: The JSON-LD metadata. + # + # Examples: + # {{ material | to_jsonld: topic, site }} + def to_jsonld(material, topic, site) + JSON.pretty_generate(generate_material_jsonld(material, topic, site)) end - data['hasCourseInstance'] = [ - { - '@type': 'CourseInstance', - courseMode: page['mode'], - # courseWorkload: "A daily course running from #{page['date_start']} to #{page['date_end']}", - offers: offer, - instructor: instructors, - isAccessibleForFree: data['isAccessibleForFree'], - courseSchedule: { - '@type': 'Schedule', - startDate: page['date_start'], - endDate: page.fetch('date_end', page['date_start']), - repeatCount: course_days, - repeatFrequency: 'daily', # Contrary to schema.org spec, this is what Google wants. - }, - courseWorkload: "P#{course_days}D", + + ## + # Convert a material to JSON-LD. Intended to be used by the filters which you should call in templates. + # + # Parameters: + # +material+:: The material object. + # +topic+:: The topic object. + # +site+:: The +Jekyll::Site+ site object. + # + # Returns: + # +Hash+:: The JSON-LD metadata. + def generate_material_jsonld(material, topic, site) + langCodeMap = { + "en" => 'English', + "es" => 'EspaĂąol', + "fr" => 'Français', } - ] - - data['offers'] = [offer] - - if page.key?('location') && page['location'].keys.length > 1 - data['location'] = { - '@type': 'Place', - name: page['location']['name'], - address: { - '@type': 'PostalAddress', - streetAddress: page['location'].fetch('address', nil), - addressLocality: page['location'].fetch('city', nil), - addressRegion: page['location'].fetch('region', nil), - postalCode: page['location'].fetch('postcode', nil), - addressCountry: page['location'].fetch('country', nil) - } + + eduLevel = { + 'Introductory' => 'Beginner', + 'Intermediate' => 'Intermediate', + 'Advanced' => 'Advanced' } - end + return '{}' if !topic - JSON.pretty_generate(data) - end + topic_desc = { + '@type': 'CreativeWork', + name: (topic['title']).to_s, + description: (topic['summary']).to_s, + url: "#{site['url']}#{site['baseurl']}/topics/#{topic['name']}/" + } - ## - # Generate the JSON-LD metadata for a learning pathway - # Parameters: - # +page+:: The page object. - # +site+:: The +Jekyll::Site+ site object. - # Returns: - # +Hash+:: The JSON-LD metadata. - def generate_learning_pathway_jsonld(page, site) - materials = [] - page['pathway'].each do |section| - if !section.key? 'tutorials' - next - end + # aggregate everything + data = { + # Properties from Course + '@context': 'http://schema.org', + '@type': 'LearningResource', - section['tutorials'].each do |tutorial| - if tutorial.key?('custom') - next - end + # Required for BioSchemas + 'http://purl.org/dc/terms/conformsTo': { + '@id': 'https://bioschemas.org/profiles/TrainingMaterial/1.0-RELEASE', + '@type': 'CreativeWork' + }, - material = TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name']) - materials.push(material) - end - end - materials.compact! + # Properties from CreativeWork + # "about" described below + # + # "accountablePerson":, + # "aggregateRating":, + # "alternativeHeadline":, + # "associatedMedia":, + audience: { + '@type': 'EducationalAudience', + educationalRole: EDU_ROLES[topic['type']] + }, + # "audio":, + # "award":, + # "author" described below + # "character":, + citation: [ + { + '@type': 'CreativeWork', + name: 'Galaxy Training: A Powerful Framework for Teaching!', + url: 'https://doi.org/10.1371/journal.pcbi.1010752' + }, + { + '@type': 'CreativeWork', + name: 'Community-Driven Data Analysis Training for Biology', + url: 'https://doi.org/10.1016/j.cels.2018.05.012' + } + ], + # "comment":, + # "commentCount":, + # "contentLocation":, + # "contentRating":, + # "contentReferenceTime":, + # "contributor" described below + # copyrightHolder: GTN, + # copyrightNotice: m + # "copyrightYear":, + # "correction":, + # "creator":, + # "dateCreated":, + # "datePublished":, + discussionUrl: site['gitter_url'], + # "editor":, + # "educationalAlignment":, + # "educationalUse":, + # "encoding":, + # "encodingFormat":, + # "exampleOfWork":, + # "expires":, + # "funder": funding, + # "genre":, + # "hasPart" described below + headline: (material['title']).to_s, + # "interactionStatistic":, + interactivityType: 'mixed', + isAccessibleForFree: true, + # "isBasedOn":, + isFamilyFriendly: true, + # "isPartOf" described below + # "keywords": described below + # "learningResourceType" described below + license: 'https://spdx.org/licenses/CC-BY-4.0.html', + # "locationCreated":, + # "mainEntity":, + # "material":, + # "mentions" described below + # "offers":, + # "position":, + producer: GTN, + provider: GTN, + # "publication":, + # "publisher":, + # "publisherImprint":, + # "publishingPrinciples":, + # "recordedAt":, + # "releasedEvent":, + # "review":, + # "schemaVersion":, + # "sdDatePublished":, + # "sdLicense":, + # "sdPublisher":, + sourceOrganization: GTN, + # "spatialCoverage":, + # "sponsor":, + # "temporalCoverage":, + # "text":, + # "thumbnailUrl":, + # "timeRequired" described below + # "translationOfWork":, + # "translator": Google Translate???, + # "typicalAgeRange":, + # "version":, + # "video":, + # "workExample":, + # "workTranslation":, + + # Properties from Thing + # "additionalType":, + # "alternateName":, + # "description" described below + # "disambiguatingDescription":, + # "image":, + # "mainEntityOfPage":, + # "name" described below + # "potentialAction":, + # "sameAs":, + # "subjectOf":, + # "url" described below + workTranslation: [], + creativeWorkStatus: material['draft'] ? 'Draft' : 'Active', + } - # Extract EDAM terms from all materials - edam_terms = materials.map do |material| - material.fetch('edam_ontology', []).map do |term| - { - '@type': 'DefinedTerm', - '@id': "http://edamontology.org/#{term}", - inDefinedTermSet: 'http://edamontology.org', - termCode: term, - } + if material.key?('pub_date') + data['dateModified'] = material['mod_date'] + data['datePublished'] = material['pub_date'] + else + begin + data['dateModified'] = Gtn::ModificationTimes.obtain_time(material.path) + data['datePublished'] = Gtn::PublicationTimes.obtain_time(material.path) + rescue StandardError + data['dateModified'] = Gtn::ModificationTimes.obtain_time(material['path']) + data['datePublished'] = Gtn::PublicationTimes.obtain_time(material['path']) + end end - end.flatten.uniq - - learning_objectives = materials.map do |material| - material.fetch('objectives', []) - end.flatten.compact - funders = materials.map do |material| - Gtn::Contributors.get_funders(site, material).map do |x| - to_pfo_jsonld(x, site, json: false) + if material.key?('copyright') + # copyrightHolder: GTN, + data['copyrightNotice'] = material['copyright'] + else + # I'm not sure this is accurate. + data['copyrightHolder'] = GTN end - end.flatten.uniq.compact - funding = materials.map do |material| - Gtn::Contributors.get_grants(site, material).map do |x| + funders = Gtn::Contributors.get_funders(site, material).map do |x| to_pfo_jsonld(x, site, json: false) end - end.flatten.uniq.compact - - # TODO: add topic edam terms too? Not sure. - parts = [] - materials.each do |material| - mat = generate_material_jsonld(material, site['data'][material['topic_name']], site) - if !mat.nil? && !mat.empty? - parts.push(mat) + grants = Gtn::Contributors.get_grants(site, material).map do |x| + to_pfo_jsonld(x, site, json: false) end - end - - syllab = page['pathway'].reject { |s| s['section'].nil? }.map do |section| - { - '@type': 'Syllabus', - name: section['section'], - description: section.fetch('description', nil), - } - end - - data = { - '@context': 'https://schema.org', - '@type': 'Course', - url: "#{site['url']}#{site['baseurl']}#{page['url']}", - name: "Learning Pathway #{page['title']}", - keywords: page['tags'] || [], - description: page['description'], - about: edam_terms, # TeSS, "scientific topics". - audience: page['audience'], # TeSS: target audience - teaches: learning_objectives, # TeSS, "learning objectives" - availableLanguage: ['en'], # TODO: support other languages - inLanguage: ['en'], # TODO: support other languages - # courseCode - # coursePrerequisites - # educationalCredentialAwarded - # financialAidEligible - # hasCourseInstance - # numberOfCredits - # occupationalCredentialAwarded - # syllabusSections - # totalHistoricalEnrollment - - # assesses - # competencyRequired - # educationalAlignment - # educationalLevel - # educationalUse - # learningResourceType - # teaches - - funder: funders, # Org or person - funding: funding, # Grant - publisher: GTN, - provider: GTN, - syllabusSections: syllab, - # Session materials - # TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out. - # hasPart: parts, - } - - begin - data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path) - data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path) - rescue StandardError - data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path']) - data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path']) - end - - if page['cover'] - data['image'] = if page['cover'] =~ /^http/ - [page['cover']] - else - ["#{site['url']}#{site['baseurl']}#{page['cover']}"] - end - end - # We CANNOT guarantee A11Y - # data.update(A11Y) - data['isAccessibleForFree'] = true - offer = { - '@type': 'Offer', - price: 0, - priceCurrency: 'EUR', - category: 'Free', - isAccessibleForFree: true, - } - data['offers'] = [offer] + data['funder'] = funders + data['funding'] = grants - # TODO: this is basically just wrong. - data['hasCourseInstance'] = [ - { - '@type': 'CourseInstance', - courseMode: 'online', - offers: offer, - isAccessibleForFree: data['isAccessibleForFree'], - } - ] + data['identifier'] = "https://gxy.io/GTN:#{material['short_id']}" if material.key?('short_id') - JSON.pretty_generate(data) - end + data.update(A11Y) - ## - # Convert a material to JSON-LD, intended to be used in Jekyll Liquid templates. - # Parameters: - # +material+:: The material object. - # +topic+:: The topic object. - # +site+:: The +Jekyll::Site+ site object. - # - # Returns: - # +String+:: The JSON-LD metadata. - def to_jsonld(material, topic, site) - JSON.pretty_generate(generate_material_jsonld(material, topic, site)) - end + actual_material = Gtn::TopicFilter.fetch_tutorial_material(site, material['topic_name'], material['tutorial_name']) - ## - # Convert a material to JSON-LD. - # Parameters: - # +material+:: The material object. - # +topic+:: The topic object. - # +site+:: The +Jekyll::Site+ site object. - # - # Returns: - # +Hash+:: The JSON-LD metadata. - def generate_material_jsonld(material, topic, site) - langCodeMap = { - "en" => 'English', - "es" => 'EspaĂąol', - "fr" => 'Français', - } + # info depending if tutorial, hands-on or slide level + # parts = [] + # data['hasPart'] = parts - eduLevel = { - 'Introductory' => 'Beginner', - 'Intermediate' => 'Intermediate', - 'Advanced' => 'Advanced' - } - return '{}' if !topic + mentions = [] + description = [] - topic_desc = { - '@type': 'CreativeWork', - name: (topic['title']).to_s, - description: (topic['summary']).to_s, - url: "#{site['url']}#{site['baseurl']}/topics/#{topic['name']}/" - } + data['isPartOf'] = topic_desc - # aggregate everything - data = { - # Properties from Course - '@context': 'http://schema.org', - '@type': 'LearningResource', - - # Required for BioSchemas - 'http://purl.org/dc/terms/conformsTo': { - '@id': 'https://bioschemas.org/profiles/TrainingMaterial/1.0-RELEASE', - '@type': 'CreativeWork' - }, - - # Properties from CreativeWork - # "about" described below - # - # "accountablePerson":, - # "aggregateRating":, - # "alternativeHeadline":, - # "associatedMedia":, - audience: { - '@type': 'EducationalAudience', - educationalRole: EDU_ROLES[topic['type']] - }, - # "audio":, - # "award":, - # "author" described below - # "character":, - citation: [ - { - '@type': 'CreativeWork', - name: 'Galaxy Training: A Powerful Framework for Teaching!', - url: 'https://doi.org/10.1371/journal.pcbi.1010752' - }, - { - '@type': 'CreativeWork', - name: 'Community-Driven Data Analysis Training for Biology', - url: 'https://doi.org/10.1016/j.cels.2018.05.012' - } - ], - # "comment":, - # "commentCount":, - # "contentLocation":, - # "contentRating":, - # "contentReferenceTime":, - # "contributor" described below - # copyrightHolder: GTN, - # copyrightNotice: m - # "copyrightYear":, - # "correction":, - # "creator":, - # "dateCreated":, - # "datePublished":, - discussionUrl: site['gitter_url'], - # "editor":, - # "educationalAlignment":, - # "educationalUse":, - # "encoding":, - # "encodingFormat":, - # "exampleOfWork":, - # "expires":, - # "funder": funding, - # "genre":, - # "hasPart" described below - headline: (material['title']).to_s, - # "interactionStatistic":, - interactivityType: 'mixed', - isAccessibleForFree: true, - # "isBasedOn":, - isFamilyFriendly: true, - # "isPartOf" described below - # "keywords": described below - # "learningResourceType" described below - license: 'https://spdx.org/licenses/CC-BY-4.0.html', - # "locationCreated":, - # "mainEntity":, - # "material":, - # "mentions" described below - # "offers":, - # "position":, - producer: GTN, - provider: GTN, - # "publication":, - # "publisher":, - # "publisherImprint":, - # "publishingPrinciples":, - # "recordedAt":, - # "releasedEvent":, - # "review":, - # "schemaVersion":, - # "sdDatePublished":, - # "sdLicense":, - # "sdPublisher":, - sourceOrganization: GTN, - # "spatialCoverage":, - # "sponsor":, - # "temporalCoverage":, - # "text":, - # "thumbnailUrl":, - # "timeRequired" described below - # "translationOfWork":, - # "translator": Google Translate???, - # "typicalAgeRange":, - # "version":, - # "video":, - # "workExample":, - # "workTranslation":, - - # Properties from Thing - # "additionalType":, - # "alternateName":, - # "description" described below - # "disambiguatingDescription":, - # "image":, - # "mainEntityOfPage":, - # "name" described below - # "potentialAction":, - # "sameAs":, - # "subjectOf":, - # "url" described below - workTranslation: [], - creativeWorkStatus: material['draft'] ? 'Draft' : 'Active', - } + data['abstract'] = material + .fetch('content', '') + .strip + .split("\n") + .first - if material.key?('pub_date') - data['dateModified'] = material['mod_date'] - data['datePublished'] = material['pub_date'] - else - begin - data['dateModified'] = Gtn::ModificationTimes.obtain_time(material.path) - data['datePublished'] = Gtn::PublicationTimes.obtain_time(material.path) - rescue StandardError - data['dateModified'] = Gtn::ModificationTimes.obtain_time(material['path']) - data['datePublished'] = Gtn::PublicationTimes.obtain_time(material['path']) + if ! data['abstract'].nil? + data['abstract'] = data['abstract'] + .gsub(/\{\{\s*site.baseurl\s*\}\}/, url_prefix(site)) + .gsub(/\[{{\s*site.url\s*}}/, '[' + url_prefix(site)) + .gsub(/{% link (topics[^%]*).md %}/, url_prefix(site) + '\1.html') + .gsub(/{% link (topics[^%]*).html %}/, url_prefix(site) + '\1.html') + .gsub(/\s*\(?{%\s*cite [^}]+\s*%}\)?/, '') + .gsub('{{ site.github_repository }}', safe_site_config(site, 'github_repository', 'https://example.com')) + .gsub(/{% snippet ([^%]*) %}/, '') + .gsub(/{% include ([^%]*) %}/, '') end - end - if material.key?('copyright') - # copyrightHolder: GTN, - data['copyrightNotice'] = material['copyright'] - else - # I'm not sure this is accurate. - data['copyrightHolder'] = GTN - end - - funders = Gtn::Contributors.get_funders(site, material).map do |x| - to_pfo_jsonld(x, site, json: false) - end - grants = Gtn::Contributors.get_grants(site, material).map do |x| - to_pfo_jsonld(x, site, json: false) - end + description.push("## Abstract\n\n#{data['abstract']}\n\n") - data['funder'] = funders - data['funding'] = grants + if (material['name'] == 'tutorial.md') || (material['name'] == 'slides.html') - data['identifier'] = "https://gxy.io/GTN:#{material['short_id']}" if material.key?('short_id') - - data.update(A11Y) - - actual_material = TopicFilter.fetch_tutorial_material(site, material['topic_name'], material['tutorial_name']) - - # info depending if tutorial, hands-on or slide level - # parts = [] - # data['hasPart'] = parts - - mentions = [] - description = [] - - data['isPartOf'] = topic_desc + if material['name'] == 'tutorial.md' + data['learningResourceType'] = 'e-learning' + 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") + else + data['learningResourceType'] = 'slides' + end - data['abstract'] = material - .fetch('content', '') - .strip - .split("\n") - .first + data['name'] = material['title'] + data['url'] = "#{site['url']}#{site['baseurl']}#{material['url']}" - if ! data['abstract'].nil? - data['abstract'] = data['abstract'] - .gsub(/\{\{\s*site.baseurl\s*\}\}/, url_prefix(site)) - .gsub(/\[{{\s*site.url\s*}}/, '[' + url_prefix(site)) - .gsub(/{% link (topics[^%]*).md %}/, url_prefix(site) + '\1.html') - .gsub(/{% link (topics[^%]*).html %}/, url_prefix(site) + '\1.html') - .gsub(/\s*\(?{%\s*cite [^}]+\s*%}\)?/, '') - .gsub('{{ site.github_repository }}', safe_site_config(site, 'github_repository', 'https://example.com')) - .gsub(/{% snippet ([^%]*) %}/, '') - .gsub(/{% include ([^%]*) %}/, '') - end + # Requires https://github.com/galaxyproject/training-material/pull/4271 + data['version'] = Gtn::ModificationTimes.obtain_modification_count(material['path']) - description.push("## Abstract\n\n#{data['abstract']}\n\n") + # Time required + if material.key?('time_estimation') && !material['time_estimation'].nil? + data['timeRequired'] = "PT#{material['time_estimation'].upcase}" + end - if (material['name'] == 'tutorial.md') || (material['name'] == 'slides.html') + # Description with questions, objectives and keypoints + if material.key?('questions') && !material['questions'].nil? && material['questions'].length.positive? + questions = material['questions'].join("\n - ") + description.push("## Questions this #{material['type']} will address\n\n - #{questions}\n\n") + end + if material.key?('objectives') && !material['objectives'].nil? && material['objectives'].length.positive? + objectives = material['objectives'].map{|x| "- #{x}"}.join("\n") + description.push("## Learning Objectives\n\n#{objectives}\n\n") + data['teaches'] = objectives + end + if material.key?('keypoints') && !material['keypoints'].nil? && material['keypoints'].length.positive? + keypoints = material['keypoints'].join("\n - ") + description.push("## Key Points\n\n - #{keypoints}\n\n") + end - if material['name'] == 'tutorial.md' - data['learningResourceType'] = 'e-learning' - 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") - else - data['learningResourceType'] = 'slides' + # Keywords + data['keywords'] = [topic['title']] + (material['tags'] || []) + # Zenodo links end - data['name'] = material['title'] - data['url'] = "#{site['url']}#{site['baseurl']}#{material['url']}" - - # Requires https://github.com/galaxyproject/training-material/pull/4271 - data['version'] = Gtn::ModificationTimes.obtain_modification_count(material['path']) - - # Time required - if material.key?('time_estimation') && !material['time_estimation'].nil? - data['timeRequired'] = "PT#{material['time_estimation'].upcase}" + # Mentions are 'external resources' in TeSS. + # This could be expanded with + # - supported servers + # - tools and resources used (e.g. Galaxy) or tools linked to the TS. + # - slides (if tutorial) and tutorial (if slides) + # - other materials in the same topic? + if actual_material.key?('workflows') + mentions.push({ + '@type': 'Thing', + url: "#{site['url']}#{site['baseurl']}#{material['dir']}workflows/", + name: "Associated Workflows" + }) end - # Description with questions, objectives and keypoints - if material.key?('questions') && !material['questions'].nil? && material['questions'].length.positive? - questions = material['questions'].join("\n - ") - description.push("## Questions this #{material['type']} will address\n\n - #{questions}\n\n") - end - if material.key?('objectives') && !material['objectives'].nil? && material['objectives'].length.positive? - objectives = material['objectives'].map{|x| "- #{x}"}.join("\n") - description.push("## Learning Objectives\n\n#{objectives}\n\n") - data['teaches'] = objectives - end - if material.key?('keypoints') && !material['keypoints'].nil? && material['keypoints'].length.positive? - keypoints = material['keypoints'].join("\n - ") - description.push("## Key Points\n\n - #{keypoints}\n\n") + # Notebooks + if actual_material.key?('notebook') + if actual_material['notebook']['language'] != 'r' + # Python, Bash, SQL (all via jupyter) + url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.ipynb" + mentions.push({ + '@type': 'Thing', + url: url, + name: "Jupyter Notebook (with Solutions)" + }) + mentions.push({ + '@type': 'Thing', + url: url.gsub(/\.ipynb$/, '-course.ipynb'), + name: "Jupyter Notebook (without Solutions)" + }) + elsif actual_material['notebook']['language'] == 'r' # Actual R + url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.Rmd" + mentions.push({ + '@type': 'Thing', + url: url, + name: "Quarto/RMarkdown Notebook" + }) + mentions.push({ + '@type': 'Thing', + url: "https://bio.tools/tool/rstudio", + name: "RStudio" + }) + end end - # Keywords - data['keywords'] = [topic['title']] + (material['tags'] || []) - # Zenodo links - end + # Tools + uses_tools = false + (actual_material['tools'] || []).each do |tool| + if site.data['tool-meta'].nil? + next + end - # Mentions are 'external resources' in TeSS. - # This could be expanded with - # - supported servers - # - tools and resources used (e.g. Galaxy) or tools linked to the TS. - # - slides (if tutorial) and tutorial (if slides) - # - other materials in the same topic? - if actual_material.key?('workflows') - mentions.push({ - '@type': 'Thing', - url: "#{site['url']}#{site['baseurl']}#{material['dir']}workflows/", - name: "Associated Workflows" - }) - end + toolmeta = site.data['tool-meta'][tool] + if toolmeta.nil? + next + end - # Notebooks - if actual_material.key?('notebook') - if actual_material['notebook']['language'] != 'r' - # Python, Bash, SQL (all via jupyter) - url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.ipynb" - mentions.push({ - '@type': 'Thing', - url: url, - name: "Jupyter Notebook (with Solutions)" - }) - mentions.push({ - '@type': 'Thing', - url: url.gsub(/\.ipynb$/, '-course.ipynb'), - name: "Jupyter Notebook (without Solutions)" - }) - elsif actual_material['notebook']['language'] == 'r' # Actual R - url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.Rmd" - mentions.push({ - '@type': 'Thing', - url: url, - name: "Quarto/RMarkdown Notebook" - }) + if toolmeta['bio.tools'].length.positive? + mentions.push({ + '@type': 'Thing', + url: "https://bio.tools/tool/#{toolmeta['bio.tools']}", + name: toolmeta.fetch('bio.tools_name', toolmeta['name']) + }) + end + uses_tools = true + end + if uses_tools mentions.push({ '@type': 'Thing', - url: "https://bio.tools/tool/rstudio", - name: "RStudio" + url: "https://bio.tools/tool/galaxy", + name: "Galaxy" }) end - end - # Zenodo link out - if actual_material.key?('zenodo_link') && ! actual_material['zenodo_link'].nil? - if actual_material['zenodo_link'].length.positive? - mentions.push({ - '@type': 'Thing', - url: (actual_material['zenodo_link']).to_s, - name: "Associated Training Datasets" - }) + # Zenodo link out + if actual_material.key?('zenodo_link') && ! actual_material['zenodo_link'].nil? + if actual_material['zenodo_link'].length.positive? + mentions.push({ + '@type': 'Thing', + url: (actual_material['zenodo_link']).to_s, + name: "Associated Training Datasets" + }) + end end - end - if description.empty? - description.push(material.fetch('content', '').strip.split("\n").first) - end - data['description'] = description.join("\n") - - data['inLanguage'] = if material.key?('lang') - { - '@type': 'Language', - name: langCodeMap[material['lang']], - alternateName: material['lang'] - } - else - { - '@type': 'Language', - name: 'English', - alternateName: 'en' - } - end - - # Course requirements (material + topic) - reqs = [] - reqs.push(*topic['requirements']) if topic.key?('requirements') - reqs.push(*material['requirements']) if material.key?('requirements') - if !reqs.empty? - coursePrerequisites = [] - reqs.each do |req| - if req['type'] == 'internal' - if req.key?('tutorials') - (req['tutorials']).each do |tuto| - (site['pages']).each do |page| - if ((page['name'] == 'tutorial.md') || (page['name'] == 'slides.html')) && - ((page['topic_name'] == req['topic_name']) && (page['tutorial_name'] == tuto)) - # slides - if page['name'] == 'slides.html' - coursePrerequisites.push( - { - '@context': 'http://schema.org', - '@type': 'LearningResource', - url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/" \ - "tutorials/#{tuto}/slides.html", - name: (page['title']).to_s, - description: "Slides for '#{page['title']}' tutorial", - learningResourceType: 'slides', - interactivityType: 'expositive', - provider: GTN - } - ) - if page['hands_on_url'] + if description.empty? + description.push(material.fetch('content', '').strip.split("\n").first) + end + data['description'] = description.join("\n") + + data['inLanguage'] = if material.key?('lang') + { + '@type': 'Language', + name: langCodeMap[material['lang']], + alternateName: material['lang'] + } + else + { + '@type': 'Language', + name: 'English', + alternateName: 'en' + } + end + + # Course requirements (material + topic) + reqs = [] + reqs.push(*topic['requirements']) if topic.key?('requirements') + reqs.push(*material['requirements']) if material.key?('requirements') + if !reqs.empty? + coursePrerequisites = [] + reqs.each do |req| + if req['type'] == 'internal' + if req.key?('tutorials') + (req['tutorials']).each do |tuto| + (site['pages']).each do |page| + if ((page['name'] == 'tutorial.md') || (page['name'] == 'slides.html')) && + ((page['topic_name'] == req['topic_name']) && (page['tutorial_name'] == tuto)) + # slides + if page['name'] == 'slides.html' coursePrerequisites.push( { '@context': 'http://schema.org', '@type': 'LearningResource', - url: (page['hands_on_url']).to_s, + url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/" \ + "tutorials/#{tuto}/slides.html", + name: (page['title']).to_s, + description: "Slides for '#{page['title']}' tutorial", + learningResourceType: 'slides', + interactivityType: 'expositive', + provider: GTN + } + ) + if page['hands_on_url'] + coursePrerequisites.push( + { + '@context': 'http://schema.org', + '@type': 'LearningResource', + url: (page['hands_on_url']).to_s, + learningResourceType: 'e-learning', + interactivityType: 'expositive', + } + ) + end + end + # hands-on + if page['name'] == 'tutorial.md' + coursePrerequisites.push( + { + '@context': 'http://schema.org', + '@type': 'LearningResource', + url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/tutorials" \ + "/#{tuto}/tutorial.html", + name: (page['title']).to_s, + description: "Hands-on for '#{page['title']}' tutorial", learningResourceType: 'e-learning', interactivityType: 'expositive', + provider: GTN } ) end end - # hands-on - if page['name'] == 'tutorial.md' - coursePrerequisites.push( - { - '@context': 'http://schema.org', - '@type': 'LearningResource', - url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/tutorials" \ - "/#{tuto}/tutorial.html", - name: (page['title']).to_s, - description: "Hands-on for '#{page['title']}' tutorial", - learningResourceType: 'e-learning', - interactivityType: 'expositive', - provider: GTN - } - ) - end end end + else + coursePrerequisites.push( + { + '@context': 'http://schema.org', + '@type': 'LearningResource', + url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/", + name: (site['data'][req['topic_name']]['title']).to_s, + description: (site['data'][req['topic_name']]['title']).to_s, + provider: GTN + } + ) end + elsif req['type'] == 'external' + coursePrerequisites.push({ + '@type': 'CreativeWork', + url: (req['link']).to_s, + name: (req['title']).to_s + }) else - coursePrerequisites.push( - { - '@context': 'http://schema.org', - '@type': 'LearningResource', - url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/", - name: (site['data'][req['topic_name']]['title']).to_s, - description: (site['data'][req['topic_name']]['title']).to_s, - provider: GTN - } - ) + coursePrerequisites.push((req['title']).to_s) end - elsif req['type'] == 'external' - coursePrerequisites.push({ - '@type': 'CreativeWork', - url: (req['link']).to_s, - name: (req['title']).to_s - }) - else - coursePrerequisites.push((req['title']).to_s) end + data['competencyRequired'] = coursePrerequisites.uniq end - data['competencyRequired'] = coursePrerequisites.uniq - end - # Add contributors/authors - if material.key?('contributors') || material.key?('contributions') - authors = Gtn::Contributors.get_authors(material).map do |x| - generate_person_jsonld(x, Gtn::Contributors.fetch_contributor(site, x), site) - end + # Add contributors/authors + if material.key?('contributors') || material.key?('contributions') + authors = Gtn::Contributors.get_authors(material).map do |x| + generate_person_jsonld(x, Gtn::Contributors.fetch_contributor(site, x), site) + end - data['author'] = authors - end + data['author'] = authors + end - # Add non-author contributors - if material.key?('contributions') - data['contributor'] = Gtn::Contributors.get_non_authors(material).map do |x| - generate_person_jsonld(x, site['data']['contributors'][x], site) + # Add non-author contributors + if material.key?('contributions') + data['contributor'] = Gtn::Contributors.get_non_authors(material).map do |x| + generate_person_jsonld(x, site['data']['contributors'][x], site) + end end - end - about = [] - about.push(topic_desc) - edam_terms = topic.fetch('edam_ontology', []) | material.fetch('edam_ontology', []) + about = [] + about.push(topic_desc) + edam_terms = topic.fetch('edam_ontology', []) | material.fetch('edam_ontology', []) - about += edam_terms.map do |term| - { - '@type': 'DefinedTerm', - '@id': "http://edamontology.org/#{term}", - inDefinedTermSet: 'http://edamontology.org', - termCode: term, - # "name": , - url: 'https://bioportal.bioontology.org/ontologies/EDAM/?p=classes&conceptid=' \ - "http%3A%2F%2Fedamontology.org%2F#{term}" - } - end + about += edam_terms.map do |term| + { + '@type': 'DefinedTerm', + '@id': "http://edamontology.org/#{term}", + inDefinedTermSet: 'http://edamontology.org', + termCode: term, + # "name": , + url: 'https://bioportal.bioontology.org/ontologies/EDAM/?p=classes&conceptid=' \ + "http%3A%2F%2Fedamontology.org%2F#{term}" + } + end - data['about'] = about + data['about'] = about - data['educationalLevel'] = material.key?('level') ? eduLevel[material['level']] : 'Beginner' - data['mentions'] = mentions + data['educationalLevel'] = material.key?('level') ? eduLevel[material['level']] : 'Beginner' + data['mentions'] = mentions - data + data + end end end end -Liquid::Template.register_filter(Jekyll::JsonldFilter) +Liquid::Template.register_filter(Jekyll::Filters::JsonldFilter) diff --git a/_plugins/jekyll-scholar.rb b/_plugins/jekyll-scholar.rb index 92e381452de712..eed6759d3dec86 100644 --- a/_plugins/jekyll-scholar.rb +++ b/_plugins/jekyll-scholar.rb @@ -3,158 +3,174 @@ require './_plugins/gtn/scholar' module Jekyll - # {% cite X %} which generates the link to the bib + text - class CiteTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end - - def render(context) - page = context.registers[:page] - site = context.registers[:site] - Gtn::Scholar.load_bib(site) - - # Mark this page as having citations - page['cited'] = true - - return "@#{@text}" if page['citation_target'] == 'R' - - # Which page is rendering this tag? - source_page = page['path'] - - # Citation Frequency - site.config['citation_count'] = Hash.new(0) if !site.config.key?('citation_count') - site.config['citation_count'][@text] += 1 - - # If the overall cache is nil, create it - site.config['citation_cache'] = {} if site.config['citation_cache'].nil? - # If the individual page in the chace is nil, create it. - site.config['citation_cache'][source_page] = [] if site.config['citation_cache'][source_page].nil? - - # Push it to our cache. - site.config['citation_cache'][source_page].push(@text) + module Tags + + # Citation Tag which generates the link to the bib + text + # + # Example: + # + # {% cite X %} + class CiteTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end - begin - citation_text = site.config['cached_citeproc'].render(:citation, id: @text) - layout = page.fetch('layout', nil) - if %w[tutorial_slides base_slides introduction_slides].include? layout - doi = site.config['cached_citeproc'].items[@text].doi - url = site.config['cached_citeproc'].items[@text].url - furl = if !doi.nil? - "https://doi.org/#{doi}" - elsif !url.nil? - url - end - - res = if furl.nil? - %(<span class="citation">#{citation_text}</span>) - else - %(<span class="citation"><a href="#{furl}">#{citation_text}</a></span>) - end - else - res = %(<span class="citation"><a href="##{@text}">#{citation_text}</a></span>) + def render(context) + page = context.registers[:page] + site = context.registers[:site] + Gtn::Scholar.load_bib(site) + + # Mark this page as having citations + page['cited'] = true + + return "@#{@text}" if page['citation_target'] == 'R' + + # Which page is rendering this tag? + source_page = page['path'] + + # Citation Frequency + site.config['citation_count'] = Hash.new(0) if !site.config.key?('citation_count') + site.config['citation_count'][@text] += 1 + + # If the overall cache is nil, create it + site.config['citation_cache'] = {} if site.config['citation_cache'].nil? + # If the individual page in the chace is nil, create it. + site.config['citation_cache'][source_page] = [] if site.config['citation_cache'][source_page].nil? + + # Push it to our cache. + site.config['citation_cache'][source_page].push(@text) + + begin + citation_text = site.config['cached_citeproc'].render(:citation, id: @text) + layout = page.fetch('layout', nil) + if %w[tutorial_slides base_slides introduction_slides].include? layout + doi = site.config['cached_citeproc'].items[@text].doi + url = site.config['cached_citeproc'].items[@text].url + furl = if !doi.nil? + "https://doi.org/#{doi}" + elsif !url.nil? + url + end + + res = if furl.nil? + %(<span class="citation">#{citation_text}</span>) + else + %(<span class="citation"><a href="#{furl}">#{citation_text}</a></span>) + end + else + res = %(<span class="citation"><a href="##{@text}">#{citation_text}</a></span>) + end + rescue StandardError => e + Jekyll.logger.warn "[GTN/scholar] Could not render #{@text} from #{source_page} (#{e})" + res = %(<span>ERROR INVALID CITATION #{@text}</span>) end - rescue StandardError => e - Jekyll.logger.warn "[GTN/scholar] Could not render #{@text} from #{source_page} (#{e})" - res = %(<span>ERROR INVALID CITATION #{@text}</span>) - end - res.gsub!(/"/, '\"') if page['citation_target'] == 'jupyter' + res.gsub!(/"/, '\"') if page['citation_target'] == 'jupyter' - res + res + end end - end - # {% cite_url X %} which generates URL for the article - class CiteUrlTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end + # Citation URL tag which just pulls out the URL for the article, most useful in quote citations. + # + # Example: + # + # > Some Quote + # {: .quote cite="{% cite_url Ramalingam_2004 %}"} + class CiteUrlTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end - def render(context) - page = context.registers[:page] - site = context.registers[:site] - Gtn::Scholar.load_bib(site) + def render(context) + page = context.registers[:page] + site = context.registers[:site] + Gtn::Scholar.load_bib(site) - # Mark this page as having citations - page['cited'] = true + # Mark this page as having citations + page['cited'] = true - return "@#{@text}" if page['citation_target'] == 'R' + return "@#{@text}" if page['citation_target'] == 'R' - # Which page is rendering this tag? - source_page = page['path'] + # Which page is rendering this tag? + source_page = page['path'] - # Citation Frequency - site.config['citation_count'] = Hash.new(0) if !site.config.key?('citation_count') - site.config['citation_count'][@text] += 1 + # Citation Frequency + site.config['citation_count'] = Hash.new(0) if !site.config.key?('citation_count') + site.config['citation_count'][@text] += 1 - # If the overall cache is nil, create it - site.config['citation_cache'] = {} if site.config['citation_cache'].nil? - # If the individual page in the chace is nil, create it. - site.config['citation_cache'][source_page] = [] if site.config['citation_cache'][source_page].nil? + # If the overall cache is nil, create it + site.config['citation_cache'] = {} if site.config['citation_cache'].nil? + # If the individual page in the chace is nil, create it. + site.config['citation_cache'][source_page] = [] if site.config['citation_cache'][source_page].nil? - # Push it to our cache. - site.config['citation_cache'][source_page].push(@text) + # Push it to our cache. + site.config['citation_cache'][source_page].push(@text) - begin - doi = site.config['cached_citeproc'].items[@text].doi - url = site.config['cached_citeproc'].items[@text].url - if !doi.nil? - "https://doi.org/#{doi}" - elsif !url.nil? - url + begin + doi = site.config['cached_citeproc'].items[@text].doi + url = site.config['cached_citeproc'].items[@text].url + if !doi.nil? + "https://doi.org/#{doi}" + elsif !url.nil? + url + end + res = url + rescue StandardError => e + Jekyll.logger.warn "[GTN/scholar] Could not render #{@text} from #{source_page} (#{e})" + res = %(<span>https://example.com/ERROR+INVALID+CITATION+#{@text}</span>) end - res = url - rescue StandardError => e - Jekyll.logger.warn "[GTN/scholar] Could not render #{@text} from #{source_page} (#{e})" - res = %(<span>https://example.com/ERROR+INVALID+CITATION+#{@text}</span>) + res end - res - end - end - - # {% bibliography %} which generates the bibliography - class BibTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip end - def render(context) - site = context.registers[:site] - Gtn::Scholar.load_bib(site) - # Which page is rendering this tag? - source_page = context.registers[:page]['path'] - global_bib = site.config['cached_global_bib'] - # citeproc = site.config['cached_citeproc'] - # We have our page's citations - citations = site.config['citation_cache'][source_page] || [] - # For each of these citation IDs, we need to get the formatted version + pull out - # year, month for sorting. - unique_citations = citations.each_with_object(Hash.new(0)) do |b, a| - a[b] += 1 - end.keys - # Remove nil citations - unique_citations = unique_citations.reject { |c| global_bib[c].nil? } - # And now sort them by date + names - sorted_citations = unique_citations.sort do |a, b| - global_bib[a].date.to_s + global_bib[a].names.join(' ') <=> - global_bib[b].date.to_s + global_bib[b].names.join(' ') + # Generate Bibliography for all citations used on that page. + # + # Example: + # + # {% bibliography %} + class BibTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip end - out = '<ol class="bibliography">' - out += sorted_citations.map do |c| - r = Gtn::Scholar.render_citation(c) - %(<li id="#{c}">#{r}</li>) - end.join("\n") - out += '</ol>' - out + def render(context) + site = context.registers[:site] + Gtn::Scholar.load_bib(site) + # Which page is rendering this tag? + source_page = context.registers[:page]['path'] + global_bib = site.config['cached_global_bib'] + # citeproc = site.config['cached_citeproc'] + # We have our page's citations + citations = site.config['citation_cache'][source_page] || [] + # For each of these citation IDs, we need to get the formatted version + pull out + # year, month for sorting. + unique_citations = citations.each_with_object(Hash.new(0)) do |b, a| + a[b] += 1 + end.keys + # Remove nil citations + unique_citations = unique_citations.reject { |c| global_bib[c].nil? } + # And now sort them by date + names + sorted_citations = unique_citations.sort do |a, b| + global_bib[a].date.to_s + global_bib[a].names.join(' ') <=> + global_bib[b].date.to_s + global_bib[b].names.join(' ') + end + + out = '<ol class="bibliography">' + out += sorted_citations.map do |c| + r = Gtn::Scholar.render_citation(c) + %(<li id="#{c}">#{r}</li>) + end.join("\n") + out += '</ol>' + out + end end end end -Liquid::Template.register_tag('cite', Jekyll::CiteTag) -Liquid::Template.register_tag('cite_url', Jekyll::CiteUrlTag) -Liquid::Template.register_tag('bibliography', Jekyll::BibTag) +Liquid::Template.register_tag('cite', Jekyll::Tags::CiteTag) +Liquid::Template.register_tag('cite_url', Jekyll::Tags::CiteUrlTag) +Liquid::Template.register_tag('bibliography', Jekyll::Tags::BibTag) diff --git a/_plugins/jekyll-tool-tag.rb b/_plugins/jekyll-tool-tag.rb index a2cfdef61e56cb..6073e13bb7141e 100644 --- a/_plugins/jekyll-tool-tag.rb +++ b/_plugins/jekyll-tool-tag.rb @@ -1,69 +1,96 @@ # frozen_string_literal: true module Jekyll - # The tool tag which allows us to do fancy tool links - class ToolTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end + ## + # Tags are useful in liquid to process data and access internal functions. Ruby functions are *always* faster than liquid templates, so, when possible, consider writing a custom Ruby filter or tag. + # + # + # - Jekyll::Tags::BibTag - {% bibliography %} + # - Jekyll::Tags::CiteTag - {% cite hiltemann2023galaxy, %} + # - Jekyll::Tags::CiteUrlTag - {% cite_url Batut2018 %} + # - Jekyll::Tags::ColorPickerTag - {% color_picker #ff0000 %} + # - Jekyll::Tags::CustomLinkTag - {% link file.md %} + # - Jekyll::Tags::DumpSearchDataTag - {% dump_search_view testing %} + # - Jekyll::Tags::FileExistsTag - {% file_exists path.md %} + # - Jekyll::Tags::IconTag - {% icon email %} + # - Jekyll::Tags::IconTagVar - {% icon var1 %} + # - Jekyll::Tags::SnippetIncludeTag - {% snippet %} + # - Jekyll::Tags::ToolTag - {% tool [My Tool](Grouping1) %} + # - Jekyll::Tags::WorkflowTag - unused? + module Tags - def render(context) - format = /\[(.*)\]\((.*)\)/ + # The tool tag which allows us to do fancy tool links + class ToolTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end + ## + # {% tool %} - the Tool rendering tag + # # The first part should be any text, the last should be the tool_id/tool_version - # {% tool Group on Column %} - # {% tool [Group on Column](Grouping1) %} - # {% tool [Group on Column](Grouping1/1.0.0) %} - # {% tool [Group on Column](toolshed.g2.bx.psu.edu/repos/devteam/fastqc/fastqc/0.72+galaxy1) %} + # + # Examples: + # + # {% tool Group on Column %} + # {% tool [Group on Column](Grouping1) %} + # {% tool [Group on Column](Grouping1/1.0.0) %} + # {% tool [Group on Column](toolshed.g2.bx.psu.edu/repos/devteam/fastqc/fastqc/0.72+galaxy1) %} + def render(context) + format = /\[(.*)\]\((.*)\)/ + - m = @text.match(format) + m = @text.match(format) - if m - # check if a variable was provided for the tool id - tool = context[m[2].tr('{}', '')] || m[2] - version = tool.split('/').last + if m + # check if a variable was provided for the tool id + tool = context[m[2].tr('{}', '')] || m[2] + version = tool.split('/').last - if tool.count('/').zero? - "<span class=\"tool\" data-tool=\"#{tool}\" title=\"#{m[1]} tool\" aria-role=\"button\">" \ - '<i class="fas fa-wrench" aria-hidden="true"></i> ' \ - "<strong>#{m[1]}</strong>" \ - '</span>' + if tool.count('/').zero? + "<span class=\"tool\" data-tool=\"#{tool}\" title=\"#{m[1]} tool\" aria-role=\"button\">" \ + '<i class="fas fa-wrench" aria-hidden="true"></i> ' \ + "<strong>#{m[1]}</strong>" \ + '</span>' + else + "<span class=\"tool\" data-tool=\"#{tool}\" title=\"#{m[1]} tool\" aria-role=\"button\">" \ + '<i class="fas fa-wrench" aria-hidden="true"></i> ' \ + "<strong>#{m[1]}</strong> " \ + '(' \ + '<i class="fas fa-cubes" aria-hidden="true"></i> ' \ + "Galaxy version #{version}" \ + ')' \ + '</span>' + end else - "<span class=\"tool\" data-tool=\"#{tool}\" title=\"#{m[1]} tool\" aria-role=\"button\">" \ - '<i class="fas fa-wrench" aria-hidden="true"></i> ' \ - "<strong>#{m[1]}</strong> " \ - '(' \ - '<i class="fas fa-cubes" aria-hidden="true"></i> ' \ - "Galaxy version #{version}" \ - ')' \ - '</span>' + %(<span><strong>#{@text}</strong> <i class="fas fa-wrench" aria-hidden="true"></i></span>) end - else - %(<span><strong>#{@text}</strong> <i class="fas fa-wrench" aria-hidden="true"></i></span>) end end - end - # Same for the workflow tags - class WorkflowTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end - - def render(_context) - format = /\[(?<title>.*)\]\((?<url>.*)\)/ - m = @text.match(format) - # puts "Found #{@text} => #{m[:title]}, #{m[:url]}" + ## + # The (unused?) workflow rendering tag. + class WorkflowTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end + # Call the tag. # It MUST be this format: # {% workflow [Main Workflow](topics/x/tutorials/y/material/workflows/main.ga) %} - "<span class=\"workflow\" data-workflow=\"#{m[:url]}\"><strong>#{m[:title]}</strong> " \ - '<i class="fas fa-share-alt" aria-hidden="true"></i></span>' + def render(_context) + format = /\[(?<title>.*)\]\((?<url>.*)\)/ + m = @text.match(format) + # puts "Found #{@text} => #{m[:title]}, #{m[:url]}" + + "<span class=\"workflow\" data-workflow=\"#{m[:url]}\"><strong>#{m[:title]}</strong> " \ + '<i class="fas fa-share-alt" aria-hidden="true"></i></span>' + end end end end -Liquid::Template.register_tag('tool', Jekyll::ToolTag) -Liquid::Template.register_tag('workflow', Jekyll::WorkflowTag) +Liquid::Template.register_tag('tool', Jekyll::Tags::ToolTag) +Liquid::Template.register_tag('workflow', Jekyll::Tags::WorkflowTag) diff --git a/_plugins/jekyll-topic-filter.rb b/_plugins/jekyll-topic-filter.rb index 25a6da2c2420b7..04565bdb70bcc1 100644 --- a/_plugins/jekyll-topic-filter.rb +++ b/_plugins/jekyll-topic-filter.rb @@ -3,92 +3,109 @@ require 'json' require 'yaml' require './_plugins/gtn' +require './_plugins/util' require 'securerandom' -# The main GTN module to parse tutorials and topics into useful lists of things that can bes shown on topic pages -module TopicFilter - ## - # This function returns a list of all the topics that are available. - # Params: - # +site+:: The +Jekyll::Site+ object - # Returns: - # +Array+:: The list of topics - def self.list_topics(site) - list_topics_h(site).keys +class Array + def cumulative_sum + sum = 0 + self.map{|x| sum += x} end +end - def self.list_topics_h(site) - site.data.select { |_k, v| v.is_a?(Hash) && v.key?('editorial_board') } - end +module Gtn + # The main GTN module to parse tutorial.md and slides.html and topics into useful lists of things that can be shown on topic pages, i.e. "materials" (a possible combination of tutorial + slides) + # + # This is by far the most complicated module and the least + # disaggregated/modular part of the GTN infrastructure. + # TopicFilter.resolve_material is probably the single most important function + # in the entire suite. + module TopicFilter - ## - # This function returns a list of all the topics that are available. - # Params: - # +site+:: The +Jekyll::Site+ object - # Returns: - # +Array+:: The topic objects themselves - def self.enumerate_topics(site) - list_topics_h(site).values - end - def self.cache - @@cache ||= Jekyll::Cache.new('JekyllTopicFilter') - end + ## + # This function returns a list of all the topics that are available. + # Params: + # +site+:: The +Jekyll::Site+ object + # Returns: + # +Array+:: The list of topics + def self.list_topics(site) + list_topics_h(site).keys + end - ## - # Fill the cache with all the topics - # Params: - # +site+:: The +Jekyll::Site+ object - # Returns: - # +nil+ - def self.fill_cache(site) - return if site.data.key?('cache_topic_filter') - - Jekyll.logger.debug '[GTN/TopicFilter] Begin Cache Prefill' - site.data['cache_topic_filter'] = {} - - # For each topic - list_topics(site).each do |topic| - site.data['cache_topic_filter'][topic] = filter_by_topic(site, topic) + def self.list_topics_h(site) + site.data.select { |_k, v| v.is_a?(Hash) && v.key?('editorial_board') } end - Jekyll.logger.debug '[GTN/TopicFilter] End Cache Prefill' - end - ## - # This function returns a list of all the materials that are available for a specific topic. - # Params: - # +site+:: The +Jekyll::Site+ object - # +topic_name+:: The name of the topic - # Returns: - # +Array+:: The list of materials - def self.topic_filter(site, topic_name) - fill_cache(site) - site.data['cache_topic_filter'][topic_name] - end + ## + # This function returns a list of all the topics that are available. + # Params: + # +site+:: The +Jekyll::Site+ object + # Returns: + # +Array+:: The topic objects themselves + def self.enumerate_topics(site) + list_topics_h(site).values + end - ## - # This function returns a list of all the materials that are available for a - # specific topic, but this time in a structured manner - # Params: - # +site+:: The +Jekyll::Site+ object - # +topic_name+:: The name of the topic - # Returns: - # +Hash+:: The subtopics and their materials - # - # Example: - # { - # "intro" => { - # "subtopic" => {"title" => "Introduction", "description" => "Introduction to the topic", "id" => "intro"}, - # "materials" => [ - # ... - # ] - # }, - # "__OTHER__" => { - # "subtopic" => {"title" => "Other", "description" => "Other materials", "id" => "__OTHER__"}, - # "materials" => [.. ] - # } - # ] - def self.list_materials_structured(site, topic_name) + ## + # Setup the local cache via +Jekyll::Cache+ + def self.cache + @@cache ||= Jekyll::Cache.new('GtnTopicFilter') + end + + ## + # Fill the cache with all the topics if it hasn't been done already. Safe to be called multiple times. + # Params: + # +site+:: The +Jekyll::Site+ object + # Returns: + # +nil+ + def self.fill_cache(site) + return if site.data.key?('cache_topic_filter') + + Jekyll.logger.debug '[GTN/TopicFilter] Begin Cache Prefill' + site.data['cache_topic_filter'] = {} + + # For each topic + list_topics(site).each do |topic| + site.data['cache_topic_filter'][topic] = filter_by_topic(site, topic) + end + Jekyll.logger.debug '[GTN/TopicFilter] End Cache Prefill' + end + + ## + # This function returns a list of all the materials that are available for a specific topic. + # Params: + # +site+:: The +Jekyll::Site+ object + # +topic_name+:: The name of the topic + # Returns: + # +Array+:: The list of materials + def self.topic_filter(site, topic_name) + fill_cache(site) + site.data['cache_topic_filter'][topic_name] + end + + ## + # This function returns a list of all the materials that are available for a + # specific topic, but this time in a structured manner + # Params: + # +site+:: The +Jekyll::Site+ object + # +topic_name+:: The name of the topic + # Returns: + # +Hash+:: The subtopics and their materials + # + # Example: + # { + # "intro" => { + # "subtopic" => {"title" => "Introduction", "description" => "Introduction to the topic", "id" => "intro"}, + # "materials" => [ + # ... + # ] + # }, + # "__OTHER__" => { + # "subtopic" => {"title" => "Other", "description" => "Other materials", "id" => "__OTHER__"}, + # "materials" => [.. ] + # } + # ] # This method is built with the idea to replace the "topic_filter" command, # and instead of returning semi-structured data, we will immediately return # fully structured data for a specific "topic_name" query, like, "admin" @@ -99,1113 +116,1549 @@ def self.list_materials_structured(site, topic_name) # # This will let us generate new "views" into the tutorial lists, having # them arranged in new and exciting ways. + def self.list_materials_structured(site, topic_name) + + fill_cache(site) + + # Here we want to either return data structured around subtopics + + if site.data[topic_name]['tag_based'].nil? && site.data[topic_name].key?('subtopics') + # We'll construct a new hash of subtopic => tutorials + out = {} + seen_ids = [] + site.data[topic_name]['subtopics'].each do |subtopic, _v| + specific_resources = filter_by_topic_subtopic(site, topic_name, subtopic['id']) + out[subtopic['id']] = { + 'subtopic' => subtopic, + 'materials' => specific_resources + } + seen_ids += specific_resources.map { |x| x['id'] } + end - fill_cache(site) - - # Here we want to either return data structured around subtopics + # And we'll have this __OTHER__ subtopic for any tutorials that weren't + # in a subtopic. + all_topics_for_tutorial = filter_by_topic(site, topic_name) + out['__OTHER__'] = { + 'subtopic' => { 'title' => 'Other', 'description' => 'Assorted Tutorials', 'id' => 'other' }, + 'materials' => all_topics_for_tutorial.reject { |x| seen_ids.include?(x['id']) } + } + elsif site.data[topic_name]['tag_based'] && site.data[topic_name].key?('subtopics') + out = {} + seen_ids = [] + tn = topic_name.gsub('by_tag_', '') + materials = filter_by_tag(site, tn) + + # For each subtopics + site.data[topic_name]['subtopics'].each do |subtopic| + # Find matching tag-based tutorials in our filtered-by-tag materials + specific_resources = materials.select { |x| (x['tags'] || []).include?(subtopic['id']) } + out[subtopic['id']] = { + 'subtopic' => subtopic, + 'materials' => specific_resources + } + seen_ids += specific_resources.map { |x| x['id'] } + end - if site.data[topic_name]['tag_based'].nil? && site.data[topic_name].key?('subtopics') - # We'll construct a new hash of subtopic => tutorials - out = {} - seen_ids = [] - site.data[topic_name]['subtopics'].each do |subtopic, _v| - specific_resources = filter_by_topic_subtopic(site, topic_name, subtopic['id']) - out[subtopic['id']] = { - 'subtopic' => subtopic, - 'materials' => specific_resources + filter_by_tag(site, tn) + out['__OTHER__'] = { + 'subtopic' => { 'title' => 'Other', 'description' => 'Assorted Tutorials', 'id' => 'other' }, + 'materials' => materials.reject { |x| seen_ids.include?(x['id']) } } - seen_ids += specific_resources.map { |x| x['id'] } - end + elsif site.data[topic_name]['tag_based'] # Tag based Topic + # We'll construct a new hash of subtopic(parent topic) => tutorials + out = {} + seen_ids = [] + tn = topic_name.gsub('by_tag_', '') + materials = filter_by_tag(site, tn) + + # Which topics are represented in those materials? + seen_topics = materials.map { |x| x['topic_name'] }.sort + + # Treat them like subtopics, but fake subtopics. + seen_topics.each do |parent_topic, _v| + specific_resources = materials.select { |x| x['topic_name'] == parent_topic } + out[parent_topic] = { + 'subtopic' => { 'id' => parent_topic, 'title' => site.data[parent_topic]['title'], 'description' => nil }, + 'materials' => specific_resources + } + seen_ids += specific_resources.map { |x| x['id'] } + end - # And we'll have this __OTHER__ subtopic for any tutorials that weren't - # in a subtopic. - all_topics_for_tutorial = filter_by_topic(site, topic_name) - out['__OTHER__'] = { - 'subtopic' => { 'title' => 'Other', 'description' => 'Assorted Tutorials', 'id' => 'other' }, - 'materials' => all_topics_for_tutorial.reject { |x| seen_ids.include?(x['id']) } - } - elsif site.data[topic_name]['tag_based'] && site.data[topic_name].key?('subtopics') - out = {} - seen_ids = [] - tn = topic_name.gsub('by_tag_', '') - materials = filter_by_tag(site, tn) - - # For each subtopics - site.data[topic_name]['subtopics'].each do |subtopic| - # Find matching tag-based tutorials in our filtered-by-tag materials - specific_resources = materials.select { |x| (x['tags'] || []).include?(subtopic['id']) } - out[subtopic['id']] = { - 'subtopic' => subtopic, - 'materials' => specific_resources + # And we'll have this __OTHER__ subtopic for any tutorials that weren't + # in a subtopic. + all_topics_for_tutorial = filter_by_tag(site, tn) + out['__OTHER__'] = { + 'subtopic' => { 'title' => 'Other', 'description' => 'Assorted Tutorials', 'id' => 'other' }, + 'materials' => all_topics_for_tutorial.reject { |x| seen_ids.include?(x['id']) } + } + else + # Or just the list (Jury is still out on this one, should it really be a + # flat list? Or in this identical structure.) + out = { + '__FLAT__' => { + 'subtopic' => nil, + 'materials' => filter_by_topic(site, topic_name) + } } - seen_ids += specific_resources.map { |x| x['id'] } end - filter_by_tag(site, tn) - out['__OTHER__'] = { - 'subtopic' => { 'title' => 'Other', 'description' => 'Assorted Tutorials', 'id' => 'other' }, - 'materials' => materials.reject { |x| seen_ids.include?(x['id']) } - } - elsif site.data[topic_name]['tag_based'] # Tag based Topic - # We'll construct a new hash of subtopic(parent topic) => tutorials - out = {} - seen_ids = [] - tn = topic_name.gsub('by_tag_', '') - materials = filter_by_tag(site, tn) - - # Which topics are represented in those materials? - seen_topics = materials.map { |x| x['topic_name'] }.sort - - # Treat them like subtopics, but fake subtopics. - seen_topics.each do |parent_topic, _v| - specific_resources = materials.select { |x| x['topic_name'] == parent_topic } - out[parent_topic] = { - 'subtopic' => { 'id' => parent_topic, 'title' => site.data[parent_topic]['title'], 'description' => nil }, - 'materials' => specific_resources - } - seen_ids += specific_resources.map { |x| x['id'] } + # Cleanup empty sections + out.delete('__OTHER__') if out.key?('__OTHER__') && out['__OTHER__']['materials'].empty? + + out.each do |_k, v| + v['materials'].sort_by! { |m| [m.fetch('priority', 1), m['title']] } end - # And we'll have this __OTHER__ subtopic for any tutorials that weren't - # in a subtopic. - all_topics_for_tutorial = filter_by_tag(site, tn) - out['__OTHER__'] = { - 'subtopic' => { 'title' => 'Other', 'description' => 'Assorted Tutorials', 'id' => 'other' }, - 'materials' => all_topics_for_tutorial.reject { |x| seen_ids.include?(x['id']) } - } - else - # Or just the list (Jury is still out on this one, should it really be a - # flat list? Or in this identical structure.) - out = { - '__FLAT__' => { - 'subtopic' => nil, - 'materials' => filter_by_topic(site, topic_name) - } - } + out end - # Cleanup empty sections - out.delete('__OTHER__') if out.key?('__OTHER__') && out['__OTHER__']['materials'].empty? + ## + # Fetch a specific tutorial material by topic and tutorial name + # Params: + # +site+:: The +Jekyll::Site+ object + # +topic_name+:: The name of the topic + # +tutorial_name+:: The name of the tutorial + # Returns: + # +Hash+:: The tutorial material + def self.fetch_tutorial_material(site, topic_name, tutorial_name) + if topic_name.nil? + return nil + end + fill_cache(site) + if site.data['cache_topic_filter'][topic_name].nil? + Jekyll.logger.warn "Cannot fetch tutorial material for #{topic_name}" + nil + else + site.data['cache_topic_filter'][topic_name].select { |p| p['tutorial_name'] == tutorial_name }[0] + end + end - out.each do |_k, v| - v['materials'].sort_by! { |m| [m.fetch('priority', 1), m['title']] } + ## + # Extract the list of tools used in a workflow + # Params: + # +data+:: The Galaxy Workflow JSON data, parsed + # Returns: + # +Array+:: The list of tool IDs + def self.extract_workflow_tool_list(data) + out = data['steps'].select { |_k, v| v['type'] == 'tool' }.map { |_k, v| v['tool_id'] }.compact + out += data['steps'].select do |_k, v| + v['type'] == 'subworkflow' + end.map { |_k, v| extract_workflow_tool_list(v['subworkflow']) } + out end - out - end + ## + # Annotation of a path with topic and tutorial information + # Params: + # +path+:: The path to annotate + # +layout+:: The page layout if known + # Returns: + # +Hash+:: The annotation + # + # Example: + # + # h = Gtn::TopicFilter.annotate_path("topics/assembly/tutorials/velvet-assembly/tutorial.md", nil) + # h # => { + # # "topic"=>"assembly", + # # "topic_name"=>"assembly", + # # "material"=>"assembly/velvet-assembly", + # # "tutorial_name"=>"velvet-assembly", + # # "dir"=>"topics/assembly/tutorials/velvet-assembly", + # # "type"=>"tutorial" + # # } + + def self.annotate_path(path, layout) + parts = path.split('/') + parts.shift if parts[0] == '.' + + return nil if parts[0] != 'topics' + + return nil if parts[2] != 'tutorials' + + return nil if parts.length < 4 + + material = { + 'topic' => parts[1], # Duplicate + 'topic_name' => parts[1], + 'material' => "#{parts[1]}/#{parts[3]}", + 'tutorial_name' => parts[3], + 'dir' => parts[0..3].join('/'), + } + + return nil if path =~ %r{/faqs/} + + return nil if parts[-1] =~ /data[_-]library.yaml/ || parts[-1] =~ /data[_-]manager.yaml/ + + # Check if it's a symlink + material['symlink'] = true if File.symlink?(material['dir']) + + if parts[4] =~ /tutorial.*\.md/ || layout == 'tutorial_hands_on' + material['type'] = 'tutorial' + elsif parts[4] =~ /slides.*\.html/ || %w[tutorial_slides base_slides introduction_slides].include?(layout) + material['type'] = 'slides' + elsif parts[4] =~ /ipynb$/ + material['type'] = 'ipynb' + elsif parts[4] =~ /Rmd$/ + material['type'] = 'rmd' + elsif parts[4] == 'workflows' + material['type'] = 'workflow' + elsif parts[4] == 'recordings' + material['type'] = 'recordings' + elsif parts[4] == 'tours' + material['type'] = 'tour' + elsif parts[-1] == 'index.md' + return nil + else + return nil + # material['type'] = 'unknown' + end - ## - # Fetch a specific tutorial material by topic and tutorial name - # Params: - # +site+:: The +Jekyll::Site+ object - # +topic_name+:: The name of the topic - # +tutorial_name+:: The name of the tutorial - # Returns: - # +Hash+:: The tutorial material - def self.fetch_tutorial_material(site, topic_name, tutorial_name) - if topic_name.nil? - return nil + material end - fill_cache(site) - if site.data['cache_topic_filter'][topic_name].nil? - Jekyll.logger.warn "Cannot fetch tutorial material for #{topic_name}" - nil - else - site.data['cache_topic_filter'][topic_name].select { |p| p['tutorial_name'] == tutorial_name }[0] + + ## + # Get the list of posts from the site + # Params: + # +site+:: The +Jekyll::Site+ object + # Returns: + # +Array+:: The list of posts + # + # This is a transition period function that can later be removed. It is added + # because with the jekyll version we're using, site.posts is an iterable in + # prod+dev (_config-dev.yml) modes, however! If we access site.posts.docs in + # prod it's fine, while in dev mode, site.posts claims to be an Array (rather + # than I guess a 'posts' object with a docs method). So we check if it has + # docs and use that, otherwise just site.posts should be iterable. + def self.get_posts(site) + # Handle the transition period + if site.posts.respond_to?(:docs) + site.posts.docs + else + site.posts + end end - end - ## - # Extract the list of tools used in a workflow - # Params: - # +data+:: The workflow data - # Returns: - # +Array+:: The list of tool IDs - def self.extract_workflow_tool_list(data) - out = data['steps'].select { |_k, v| v['type'] == 'tool' }.map { |_k, v| v['tool_id'] }.compact - out += data['steps'].select do |_k, v| - v['type'] == 'subworkflow' - end.map { |_k, v| extract_workflow_tool_list(v['subworkflow']) } - out - end + ## + # Collate the materials into a large hash + # Params: + # +site+:: The +Jekyll::Site+ object + # +pages+:: The list of pages to collate + # Returns: + # +Hash+:: The collated materials + # + # Example: + # h = collate_materials(site, pages) + # h # => { + # # "assembly/velvet-assembly" => { + # # "topic" => "assembly", + # # "topic_name" => "assembly", + # # "material" => "assembly/velvet-assembly", + # # "tutorial_name" => "velvet-assembly", + # # "dir" => "topics/assembly/tutorials/velvet-assembly", + # # "resources" => [ + # # { + # # "type" => "slides", + # # "url" => "/topics/assembly/tutorials/velvet-assembly/slides.html", + # # "title" => "Slides", + # # "priority" => 1 + # # }, + # # { + # # "type" => "tutorial", + # # "url" => "/topics/assembly/tutorials/velvet-assembly/tutorial.html", + # # "title" => "Tutorial", + # # "priority" => 2 + # # } + # # ] + # # } + def self.collate_materials(site, pages) + # In order to speed up queries later, we'll store a set of "interesting" + # pages (i.e. things that are under `topic_name`) + shortlinks = site.data['shortlinks'] + shortlinks_reversed = shortlinks['id'].invert + + interesting = {} + pages.each do |page| + # Skip anything outside of topics. + next if !page.url.include?('/topics/') + + # Extract the material metadata based on the path + page.data['url'] = page.url + material_meta = annotate_path(page.path, page.data['layout']) + + # If unannotated then we want to skip this material. + next if material_meta.nil? + + mk = material_meta['material'] + + if !interesting.key? mk + interesting[mk] = material_meta.dup + interesting[mk].delete('type') # Remove the type since it's specific, not generic + interesting[mk]['resources'] = [] + end - ## - # Annotation of a path with topic and tutorial information - # Params: - # +path+:: The path to annotate - # Returns: - # +Hash+:: The annotation - # - # Example: - # /topics/assembly/tutorials/velvet-assembly/tutorial.md - # => { - # "topic" => "assembly", - # "topic_name" => "assembly", - # "material" => "assembly/velvet-assembly", - # "tutorial_name" => "velvet-assembly", - # "dir" => "topics/assembly/tutorials/velvet-assembly" - # "type" => "tutorial" - # } - def self.annotate_path(path, layout) - parts = path.split('/') - parts.shift if parts[0] == '.' - - return nil if parts[0] != 'topics' - - return nil if parts[2] != 'tutorials' - - return nil if parts.length < 4 - - material = { - 'topic' => parts[1], # Duplicate - 'topic_name' => parts[1], - 'material' => "#{parts[1]}/#{parts[3]}", - 'tutorial_name' => parts[3], - 'dir' => parts[0..3].join('/'), - } - - return nil if path =~ %r{/faqs/} - - return nil if parts[-1] =~ /data[_-]library.yaml/ || parts[-1] =~ /data[_-]manager.yaml/ - - # Check if it's a symlink - material['symlink'] = true if File.symlink?(material['dir']) - - if parts[4] =~ /tutorial.*\.md/ || layout == 'tutorial_hands_on' - material['type'] = 'tutorial' - elsif parts[4] =~ /slides.*\.html/ || %w[tutorial_slides base_slides introduction_slides].include?(layout) - material['type'] = 'slides' - elsif parts[4] =~ /ipynb$/ - material['type'] = 'ipynb' - elsif parts[4] =~ /Rmd$/ - material['type'] = 'rmd' - elsif parts[4] == 'workflows' - material['type'] = 'workflow' - elsif parts[4] == 'recordings' - material['type'] = 'recordings' - elsif parts[4] == 'tours' - material['type'] = 'tour' - elsif parts[-1] == 'index.md' - return nil - else - return nil - # material['type'] = 'unknown' - end + page.data['topic_name'] = material_meta['topic_name'] + page.data['tutorial_name'] = material_meta['tutorial_name'] + page.data['dir'] = material_meta['dir'] + page.data['short_id'] = shortlinks_reversed[page.data['url']] + page.data['symlink'] = material_meta['symlink'] - material - end + interesting[mk]['resources'].push([material_meta['type'], page]) + end - ## - # Get the list of posts from the site - # Params: - # +site+:: The +Jekyll::Site+ object - # Returns: - # +Array+:: The list of posts - # - # This is a transition period function that can later be removed. It is added - # because with the jekyll version we're using, site.posts is an iterable in - # prod+dev (_config-dev.yml) modes, however! If we access site.posts.docs in - # prod it's fine, while in dev mode, site.posts claims to be an Array (rather - # than I guess a 'posts' object with a docs method). So we check if it has - # docs and use that, otherwise just site.posts should be iterable. - def self.get_posts(site) - # Handle the transition period - if site.posts.respond_to?(:docs) - site.posts.docs - else - site.posts + interesting end - end - ## - # Collate the materials into a large hash - # Params: - # +site+:: The +Jekyll::Site+ object - # +pages+:: The list of pages to collate - # Returns: - # +Hash+:: The collated materials - # - # Example: - # collate_materials(site, pages) - # => { - # "assembly/velvet-assembly" => { - # "topic" => "assembly", - # "topic_name" => "assembly", - # "material" => "assembly/velvet-assembly", - # "tutorial_name" => "velvet-assembly", - # "dir" => "topics/assembly/tutorials/velvet-assembly", - # "resources" => [ - # { - # "type" => "slides", - # "url" => "/topics/assembly/tutorials/velvet-assembly/slides.html", - # "title" => "Slides", - # "priority" => 1 - # }, - # { - # "type" => "tutorial", - # "url" => "/topics/assembly/tutorials/velvet-assembly/tutorial.html", - # "title" => "Tutorial", - # "priority" => 2 - # } - # ] - # } - def self.collate_materials(site, pages) - # In order to speed up queries later, we'll store a set of "interesting" - # pages (i.e. things that are under `topic_name`) - shortlinks = site.data['shortlinks'] - shortlinks_reversed = shortlinks['id'].invert - - interesting = {} - pages.each do |page| - # Skip anything outside of topics. - next if !page.url.include?('/topics/') - - # Extract the material metadata based on the path - page.data['url'] = page.url - material_meta = annotate_path(page.path, page.data['layout']) - - # If unannotated then we want to skip this material. - next if material_meta.nil? - - mk = material_meta['material'] - - if !interesting.key? mk - interesting[mk] = material_meta.dup - interesting[mk].delete('type') # Remove the type since it's specific, not generic - interesting[mk]['resources'] = [] - end - - page.data['topic_name'] = material_meta['topic_name'] - page.data['tutorial_name'] = material_meta['tutorial_name'] - page.data['dir'] = material_meta['dir'] - page.data['short_id'] = shortlinks_reversed[page.data['url']] - page.data['symlink'] = material_meta['symlink'] - - interesting[mk]['resources'].push([material_meta['type'], page]) + ## + # Make a label safe for use in mermaid (without ()[]"') + def self.mermaid_safe_label(label) + (label || '') + .gsub('(', '').gsub(')', '') + .gsub('[', '').gsub(']', '') + .gsub('"', '”') # We accept that this is not perfectly correct. + .gsub("'", '’') end - interesting - end - - def self.mermaid_safe_label(label) - (label || '') - .gsub('(', '').gsub(')', '') - .gsub('[', '').gsub(']', '') - .gsub('"', '”') # We accept that this is not perfectly correct. - .gsub("'", '’') - end - - def self.mermaid(wf) - # We're converting it to Mermaid.js - # flowchart TD - # A[Start] --> B{Is it?} - # B -- Yes --> C[OK] - # C --> D[Rethink] - # D --> B - # B -- No ----> E[End] - - statements = [] - wf['steps'].each_key do |id| - step = wf['steps'][id] - chosen_label = mermaid_safe_label(step['label'] || step['name']) - - case step['type'] - when 'data_collection_input' - statements.append "#{id}[\"ℹ️ Input Collection\\n#{chosen_label}\"];" - when 'data_input' - statements.append "#{id}[\"ℹ️ Input Dataset\\n#{chosen_label}\"];" - when 'parameter_input' - statements.append "#{id}[\"ℹ️ Input Parameter\\n#{chosen_label}\"];" - when 'subworkflow' - statements.append "#{id}[\"🛠️ Subworkflow\\n#{chosen_label}\"];" - else - statements.append "#{id}[\"#{chosen_label}\"];" - end + ## + # Build a Mermaid.js compatible graph of a given Galaxy Workflow + # + # TODO: extract into own module along with DOT> + # + # Params: + # +wf+:: The Galaxy Workflow JSON representation + # Returns: + # +String+:: A Mermaid.js compatible graph of the workflow. + def self.mermaid(wf) + # We're converting it to Mermaid.js + # flowchart TD + # A[Start] --> B{Is it?} + # B -- Yes --> C[OK] + # C --> D[Rethink] + # D --> B + # B -- No ----> E[End] + + statements = [] + wf['steps'].each_key do |id| + step = wf['steps'][id] + chosen_label = mermaid_safe_label(step['label'] || step['name']) + + case step['type'] + when 'data_collection_input' + statements.append "#{id}[\"ℹ️ Input Collection\\n#{chosen_label}\"];" + when 'data_input' + statements.append "#{id}[\"ℹ️ Input Dataset\\n#{chosen_label}\"];" + when 'parameter_input' + statements.append "#{id}[\"ℹ️ Input Parameter\\n#{chosen_label}\"];" + when 'subworkflow' + statements.append "#{id}[\"🛠️ Subworkflow\\n#{chosen_label}\"];" + else + statements.append "#{id}[\"#{chosen_label}\"];" + end - case step['type'] - when 'data_collection_input', 'data_input' - statements.append "style #{id} stroke:#2c3143,stroke-width:4px;" - when 'parameter_input' - statements.append "style #{id} fill:#ded,stroke:#393,stroke-width:4px;" - when 'subworkflow' - statements.append "style #{id} fill:#edd,stroke:#900,stroke-width:4px;" - end + case step['type'] + when 'data_collection_input', 'data_input' + statements.append "style #{id} stroke:#2c3143,stroke-width:4px;" + when 'parameter_input' + statements.append "style #{id} fill:#ded,stroke:#393,stroke-width:4px;" + when 'subworkflow' + statements.append "style #{id} fill:#edd,stroke:#900,stroke-width:4px;" + end - step = wf['steps'][id] - step['input_connections'].each do |_, v| - # if v is a list - if v.is_a?(Array) - v.each do |v2| - statements.append "#{v2['id']} -->|#{mermaid_safe_label(v2['output_name'])}| #{id};" + step = wf['steps'][id] + step['input_connections'].each do |_, v| + # if v is a list + if v.is_a?(Array) + v.each do |v2| + statements.append "#{v2['id']} -->|#{mermaid_safe_label(v2['output_name'])}| #{id};" + end + else + statements.append "#{v['id']} -->|#{mermaid_safe_label(v['output_name'])}| #{id};" end - else - statements.append "#{v['id']} -->|#{mermaid_safe_label(v['output_name'])}| #{id};" end - end - (step['workflow_outputs'] || []) - .reject { |wo| wo['label'].nil? } - .map do |wo| - wo['uuid'] = SecureRandom.uuid.to_s if wo['uuid'].nil? - wo + (step['workflow_outputs'] || []) + .reject { |wo| wo['label'].nil? } + .map do |wo| + wo['uuid'] = SecureRandom.uuid.to_s if wo['uuid'].nil? + wo + end + .each do |wo| + statements.append "#{wo['uuid']}[\"Output\\n#{wo['label']}\"];" + statements.append "#{id} --> #{wo['uuid']};" + statements.append "style #{wo['uuid']} stroke:#2c3143,stroke-width:4px;" end - .each do |wo| - statements.append "#{wo['uuid']}[\"Output\\n#{wo['label']}\"];" - statements.append "#{id} --> #{wo['uuid']};" - statements.append "style #{wo['uuid']} stroke:#2c3143,stroke-width:4px;" end - end - "flowchart TD\n" + statements.map { |q| " #{q}" }.join("\n") - end + "flowchart TD\n" + statements.map { |q| " #{q}" }.join("\n") + end - def self.graph_dot(wf) - # We're converting it to Mermaid - # flowchart TD - # A[Start] --> B{Is it?} - # B -- Yes --> C[OK] - # C --> D[Rethink] - # D --> B - # B -- No ----> E[End] - # digraph test { + ## + # Build a DOT graph for a given tutorial file. # - # 0[shape=box,style=filled,color=lightblue,label="ℹ️ Input Dataset\nBionano_dataset"] - # 1[shape=box,style=filled,color=lightblue,label="ℹ️ Input Dataset\nHi-C_dataset_R"] - # 3 -> 6 [label="output"] - # 7[shape=box,label="Busco"] - # 4 -> 7 [label="out_fa"] - # 8[shape=box,label="Busco"] - # 5 -> 8 [label="out_fa"] - - statements = [ - 'node [fontname="Atkinson Hyperlegible", shape=box, color=white,style=filled,color=peachpuff,margin="0.2,0.2"];', - 'edge [fontname="Atkinson Hyperlegible"];', - ] - wf['steps'].each_key do |id| - step = wf['steps'][id] - chosen_label = mermaid_safe_label(step['label'] || step['name']) - - case step['type'] - when 'data_collection_input' - statements.append "#{id}[color=lightblue,label=\"ℹ️ Input Collection\\n#{chosen_label}\"]" - when 'data_input' - statements.append "#{id}[color=lightblue,label=\"ℹ️ Input Dataset\\n#{chosen_label}\"]" - when 'parameter_input' - statements.append "#{id}[color=lightgreen,label=\"ℹ️ Input Parameter\\n#{chosen_label}\"]" - when 'subworkflow' - statements.append "#{id}[color=lightcoral,label=\"🛠️ Subworkflow\\n#{chosen_label}\"]" - else - statements.append "#{id}[label=\"#{chosen_label}\"]" - end - - step = wf['steps'][id] - step['input_connections'].each do |_, v| - # if v is a list - if v.is_a?(Array) - v.each do |v2| - statements.append "#{v2['id']} -> #{id} [label=\"#{mermaid_safe_label(v2['output_name'])}\"]" - end + # TODO: extract into own module along with mermaid. + # + # Params: + # +wf+:: The Galaxy Workflow JSON representation + # Returns: + # +String+:: A DOT graph of the workflow. + def self.graph_dot(wf) + # digraph test { + # 0[shape=box,style=filled,color=lightblue,label="ℹ️ Input Dataset\nBionano_dataset"] + # 1[shape=box,style=filled,color=lightblue,label="ℹ️ Input Dataset\nHi-C_dataset_R"] + # 3 -> 6 [label="output"] + # 7[shape=box,label="Busco"] + # 4 -> 7 [label="out_fa"] + # 8[shape=box,label="Busco"] + # 5 -> 8 [label="out_fa"] + + statements = [ + 'node [fontname="Atkinson Hyperlegible", shape=box, color=white,style=filled,color=peachpuff,margin="0.2,0.2"];', + 'edge [fontname="Atkinson Hyperlegible"];', + ] + wf['steps'].each_key do |id| + step = wf['steps'][id] + chosen_label = mermaid_safe_label(step['label'] || step['name']) + + case step['type'] + when 'data_collection_input' + statements.append "#{id}[color=lightblue,label=\"ℹ️ Input Collection\\n#{chosen_label}\"]" + when 'data_input' + statements.append "#{id}[color=lightblue,label=\"ℹ️ Input Dataset\\n#{chosen_label}\"]" + when 'parameter_input' + statements.append "#{id}[color=lightgreen,label=\"ℹ️ Input Parameter\\n#{chosen_label}\"]" + when 'subworkflow' + statements.append "#{id}[color=lightcoral,label=\"🛠️ Subworkflow\\n#{chosen_label}\"]" else - statements.append "#{v['id']} -> #{id} [label=\"#{mermaid_safe_label(v['output_name'])}\"]" + statements.append "#{id}[label=\"#{chosen_label}\"]" end - end - (step['workflow_outputs'] || []) - .reject { |wo| wo['label'].nil? } - .map do |wo| - wo['uuid'] = SecureRandom.uuid.to_s if wo['uuid'].nil? - wo - end - .each do |wo| - statements.append "k#{wo['uuid'].gsub('-', '')}[color=lightseagreen,label=\"Output\\n#{wo['label']}\"]" - statements.append "#{id} -> k#{wo['uuid'].gsub('-', '')}" + step = wf['steps'][id] + step['input_connections'].each do |_, v| + # if v is a list + if v.is_a?(Array) + v.each do |v2| + statements.append "#{v2['id']} -> #{id} [label=\"#{mermaid_safe_label(v2['output_name'])}\"]" + end + else + statements.append "#{v['id']} -> #{id} [label=\"#{mermaid_safe_label(v['output_name'])}\"]" + end end - end - "digraph main {\n" + statements.map { |q| " #{q}" }.join("\n") + "\n}" - end + (step['workflow_outputs'] || []) + .reject { |wo| wo['label'].nil? } + .map do |wo| + wo['uuid'] = SecureRandom.uuid.to_s if wo['uuid'].nil? + wo + end + .each do |wo| + statements.append "k#{wo['uuid'].gsub('-', '')}[color=lightseagreen,label=\"Output\\n#{wo['label']}\"]" + statements.append "#{id} -> k#{wo['uuid'].gsub('-', '')}" + end + end - def self.git_log(wf_path) - if Jekyll.env != 'production' - return [] + "digraph main {\n" + statements.map { |q| " #{q}" }.join("\n") + "\n}" end - cache.getset(wf_path) do - require 'shellwords' + ## + # (PRODUCTION ONLY) Extract a log of commits (hash, timestamp, message) for commits to a specific path + # + # Params: + # +wf_path+:: Path to a file + # Returns: + # +Array+:: An array of {'hash' => ..., 'unix' => 1230, 'message' => 'I did something', 'short_hash' => ... } + def self.git_log(wf_path) + if Jekyll.env != 'production' + return [] + end + + cache.getset(wf_path) do + require 'shellwords' - commits = %x[git log --format="%H %at %s" #{Shellwords.escape(wf_path)}] - .split("\n") - .map { |x| x.split(' ', 3) } - .map { |x| { 'hash' => x[0], 'unix' => x[1], 'message' => x[2], 'short_hash' => x[0][0..8] } } + commits = %x[git log --format="%H %at %s" #{Shellwords.escape(wf_path)}] + .split("\n") + .map { |x| x.split(' ', 3) } + .map { |x| { 'hash' => x[0], 'unix' => x[1], 'message' => x[2], 'short_hash' => x[0][0..8] } } - commits.map.with_index do |c, i| - c['num'] = commits.length - i - c + commits.map.with_index do |c, i| + c['num'] = commits.length - i + c + end end end - end - def self.resolve_material(site, material) - # We've already - # looked in every /topic/*/tutorials/* folder, and turn these disparate - # resources into a page_obj as well. Most variables are copied directly, - # either from a tutorial, or a slides (if no tutorial is available.) This - # means we do not (cannot) support external_slides AND external_handson. - # This is probably a sub-optimal situation we'll end up fixing someday. + ## + # Resolve a material from a given collated material. What does that entail? A LOT. # - tutorials = material['resources'].select { |a| a[0] == 'tutorial' } - slides = material['resources'].select { |a| a[0] == 'slides' } - tours = material['resources'].select { |a| a[0] == 'tours' } - - # Our final "page" object (a "material") - page = nil - - slide_has_video = false - slide_has_recordings = false - slide_translations = [] - page_ref = nil - - if slides.length.positive? - page = slides.min { |a, b| a[1].path <=> b[1].path }[1] - slide_has_video = page.data.fetch('video', false) - slide_has_recordings = page.data.fetch('recordings', false) - slide_translations = page.data.fetch('translations', []) - page_ref = page - end + # Given a collated material, e.g. + # + # material = Gtn::TopicFilter.collate_materials(site, site.pages)['proteomics/database-handling'] + # material # => + # # {"topic"=>"proteomics", + # # "topic_name"=>"proteomics", + # # "material"=>"proteomics/database-handling", + # # "tutorial_name"=>"database-handling", + # # "dir"=>"topics/proteomics/tutorials/database-handling", + # # "resources"=> + # # [["workflow", #<Jekyll::Page @relative_path="topics/proteomics/tutorials/database-handling/workflows/index.md">], + # # ["tour", #<Jekyll::Page @relative_path="topics/proteomics/tutorials/database-handling/tours/proteomics-database-handling-mycroplasma.yaml">], + # # ["tour", #<Jekyll::Page @relative_path="topics/proteomics/tutorials/database-handling/tours/proteomics-database-handling.yaml">], + # # ["tutorial", #<Jekyll::Page @relative_path="topics/proteomics/tutorials/database-handling/tutorial.md">], + # # ["recordings", #<Jekyll::PageWithoutAFile @relative_path="topics/proteomics/tutorials/database-handling/recordings/index.html">], + # # ["workflow", #<Jekyll::PageWithoutAFile @relative_path="topics/proteomics/tutorials/database-handling/workflows/wf_database-handling.html">], + # # ["workflow", #<Jekyll::PageWithoutAFile @relative_path="topics/proteomics/tutorials/database-handling/workflows/wf_database-handling_mycoplasma.html">]]} + # + # We can then choose to 'resolve' that material, i.e. collect all of the + # relevant information that is needed for it to really be useful. This + # includes things like tools, workflows, etc. Everything is packed into a + # highly annotated 'material' Hash. + # + # You might look below and say "Wow that is ridiculously unnecessarily + # complicated", or, maybe not. But either way, this is what is required to display a full 'learning material' + # on the GTN, and all of the metadata that goes into it. + # + # Some of the highlights are: + # - learning resource metadata (taken from tutorial if it exists, otherwise, from the slides) + # - short ID + # - topic information (topic name/ topic_id) + # - any javascript requirements + # - All associated workflows, and metadata about those workflows (tests, features used, associated test results, mermaid and dot graphs, associated tools, inputs and outputs.) + # - +ref+, +ref_tutorials+, +ref_slides+ that point to the actual Jekyll pages, in case you need those. + # - api URL + # - tools (discovered from the tutorial text + workflows) + # - a list of supported servers for easy display (exact and inexact matches) + # - a matrix of which servers support which versions of those tools, for a full compatibility table (used on maintainer page.) + # - requisite metdata for an admin to install these tools + # + # resource = Gtn::TopicFilter.collate_materials(site, site.pages)['proteomics/database-handling'] + # material = Gtn::TopicFilter.resolve_material(site, resource) + # material # => + # {"layout"=>"tutorial_hands_on", + # "title"=>"Protein FASTA Database Handling", + # "edam_ontology"=>["topic_0121"], + # "zenodo_link"=>"", + # "level"=>"Introductory", + # "questions"=>["How to download protein FASTA databases of a certain organism?", "How to download a contaminant database?", "How to create a decoy database?", "How to combine databases?"], + # "objectives"=>["Creation of a protein FASTA database ready for use with database search algorithms."], + # "time_estimation"=>"30m", + # "key_points"=> + # ["There are several types of Uniprot databases.", + # "Search databases should always include possible contaminants.", + # "For analyzing cell culture or organic samples, search databases should include mycoplasma databases.", + # "Some peptide search engines depend on decoys to calculate the FDR."], + # "contributors"=>["stortebecker", "bgruening"], + # "subtopic"=>"id-quant", + # "tags"=>["DDA"], + # "js_requirements"=>{"mathjax"=>nil, "mermaid"=>false}, + # "short_id"=>"T00214", + # "symlink"=>nil, + # "url"=>"/topics/proteomics/tutorials/database-handling/tutorial.html", + # "topic_name"=>"proteomics", + # "tutorial_name"=>"database-handling", + # "dir"=>"topics/proteomics/tutorials/database-handling", + # "redirect_from"=>["/short/proteomics/database-handling", "/short/T00214"], + # "id"=>"proteomics/database-handling", + # "ref"=>#<Jekyll::Page @relative_path="topics/proteomics/tutorials/database-handling/tutorial.md">, + # "ref_tutorials"=>[#<Jekyll::Page @relative_path="topics/proteomics/tutorials/database-handling/tutorial.md">], "ref_slides"=>[], "hands_on"=>true, "slides"=>false, "mod_date"=>2023-11-09 09:55:09 +0100, + # "pub_date"=>2017-02-14 13:20:30 +0100, + # "version"=>29, + # "workflows"=> + # "workflows"=> + # [{"workflow"=>"wf_database-handling.ga", + # "tests"=>false, + # "url"=>"https://training.galaxyproject.org/training-material/topics/proteomics/tutorials/database-handling/workflows/wf_database-handling.ga", + # "url_html"=>"https://training.galaxyproject.org/training-material/topics/proteomics/tutorials/database-handling/workflows/wf_database-handling.html", + # "path"=>"topics/proteomics/tutorials/database-handling/workflows/wf_database-handling.ga", + # "wfid"=>"proteomics-database-handling", + # "wfname"=>"wf-database-handling", + # "trs_endpoint"=>"https://training.galaxyproject.org/training-material/api/ga4gh/trs/v2/tools/proteomics-database-handling/versions/wf-database-handling", + # "license"=>nil, + # "parent_id"=>"proteomics/database-handling", + # "topic_id"=>"proteomics", + # "tutorial_id"=>"database-handling", + # "creators"=>[], + # "name"=>"Proteomics: database handling", + # "title"=>"Proteomics: database handling", + # "version"=>5, + # "description"=>"Protein FASTA Database Handling", + # "tags"=>["proteomics"], + # "features"=>{"report"=>nil, "subworkflows"=>false, "comments"=>false, "parameters"=>false}, + # "workflowhub_id"=>"1204", + # "history"=>[], + # "test_results"=>nil, + # "modified"=>2024-03-18 12:38:44.394831189 +0100, + # "mermaid"=> + # "flowchart TD\n 0[\"Protein Database Downloader\"];\n 1[\"Protein Database Downloader\"];\n 2[\"FASTA-to-Tabular\"];\n 0 -->|output_database| 2;\n 3[\"Add column\"];\n 2 -->|output| 3;\n 4[\"Tabular + # -to-FASTA\"];\n 3 -->|out_file1| 4;\n 5[\"FASTA Merge Files and Filter Unique Sequences\"];\n 4 -->|output| 5;\n 1 -->|output_database| 5;\n 6[\"DecoyDatabase\"];\n 5 -->|output| 6;", + # "graph_dot"=> + # "digraph main {\n node [fontname=\"Atkinson Hyperlegible\", shape=box, color=white,style=filled,color=peachpuff,margin=\"0.2,0.2\"];\n edge [fontname=\"Atkinson Hyperlegible\"];\n 0[label=\"Protein Data + # base Downloader\"]\n 1[label=\"Protein Database Downloader\"]\n 2[label=\"FASTA-to-Tabular\"]\n 0 -> 2 [label=\"output_database\"]\n 3[label=\"Add column\"]\n 2 -> 3 [label=\"output\"]\n 4[label=\"Tabular + # -to-FASTA\"]\n 3 -> 4 [label=\"out_file1\"]\n 5[label=\"FASTA Merge Files and Filter Unique Sequences\"]\n 4 -> 5 [label=\"output\"]\n 1 -> 5 [label=\"output_database\"]\n 6[label=\"DecoyDatabase\"]\n 5 - + # > 6 [label=\"output\"]\n}", + # "workflow_tools"=> + # ["addValue", + # "toolshed.g2.bx.psu.edu/repos/devteam/fasta_to_tabular/fasta2tab/1.1.1", + # "toolshed.g2.bx.psu.edu/repos/devteam/tabular_to_fasta/tab2fasta/1.1.1", + # "toolshed.g2.bx.psu.edu/repos/galaxyp/dbbuilder/dbbuilder/0.3.1", + # "toolshed.g2.bx.psu.edu/repos/galaxyp/fasta_merge_files_and_filter_unique_sequences/fasta_merge_files_and_filter_unique_sequences/1.2.0", + # "toolshed.g2.bx.psu.edu/repos/galaxyp/openms_decoydatabase/DecoyDatabase/2.6+galaxy0"], + # "inputs"=>[], + # "outputs"=> + # [{"annotation"=>"", + # "content_id"=>"toolshed.g2.bx.psu.edu/repos/galaxyp/dbbuilder/dbbuilder/0.3.1", + # "errors"=>nil, + # "id"=>0, + # "input_connections"=>{}, + # "inputs"=>[], + # "label"=>nil, + # "name"=>"Protein Database Downloader", + # "outputs"=>[{"name"=>"output_database", "type"=>"fasta"}], + # "position"=>{"bottom"=>380.6000061035156, "height"=>102.60000610351562, "left"=>-110, "right"=>90, "top"=>278, "width"=>200, "x"=>-110, "y"=>278}, + # "post_job_actions"=>{}, + # "tool_id"=>"toolshed.g2.bx.psu.edu/repos/galaxyp/dbbuilder/dbbuilder/0.3.1", + # "tool_shed_repository"=>{"changeset_revision"=>"c1b437242fee", "name"=>"dbbuilder", "owner"=>"galaxyp", "tool_shed"=>"toolshed.g2.bx.psu.edu"}, + # "tool_state"=> + # "{\"__input_ext\": \"data\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"source\": {\"from\": \"cRAP\", \"__current_case__\": 1}, \"__page__\": null, \"__rerun_remap_job_id__\": + # null}", + # "tool_version"=>"0.3.1", + # "type"=>"tool", + # "uuid"=>"6613b72c-2bab-423c-88fc-05edfe9ea8ec", + # "workflow_outputs"=>[{"label"=>nil, "output_name"=>"output_database", "uuid"=>"2d289b03-c396-46a2-a725-987b6c75ada9"}]}, + # ... + # "api"=>"https://training.galaxyproject.org/training-material/api/topics/proteomics/tutorials/database-handling/tutorial.json", + # "tools"=> + # ["addValue", + # "toolshed.g2.bx.psu.edu/repos/devteam/fasta_to_tabular/fasta2tab/1.1.1", + # "toolshed.g2.bx.psu.edu/repos/devteam/tabular_to_fasta/tab2fasta/1.1.1", + # "toolshed.g2.bx.psu.edu/repos/galaxyp/dbbuilder/dbbuilder/0.3.1", + # "toolshed.g2.bx.psu.edu/repos/galaxyp/fasta_merge_files_and_filter_unique_sequences/fasta_merge_files_and_filter_unique_sequences/1.2.0", + # "toolshed.g2.bx.psu.edu/repos/galaxyp/openms_decoydatabase/DecoyDatabase/2.6+galaxy0"], + # "supported_servers"=> + # {"exact"=>[{"url"=>"https://usegalaxy.eu", "name"=>"UseGalaxy.eu", "usegalaxy"=>true}, {"url"=>"https://usegalaxy.org.au", "name"=>"UseGalaxy.org.au", "usegalaxy"=>true}], + # "inexact"=>[{"url"=>"https://usegalaxy.no/", "name"=>"UseGalaxy.no", "usegalaxy"=>false}]}, + # "supported_servers_matrix"=> + # {"servers"=> + # [{"url"=>"http://aspendb.uga.edu:8085/", "name"=>"AGEseq @ AspenDB"}, + # {"url"=>"http://motherbox.chemeng.ntua.gr/anastasia_dev/", "name"=>"ANASTASIA"}, + # ... + # "tools"=> + # [{"id"=>"addValue", + # "servers"=> + # [{"state"=>"local", "server"=>"http://aspendb.uga.edu:8085/"}, + # {"state"=>"missing", "server"=>"http://motherbox.chemeng.ntua.gr/anastasia_dev/"}, + # {"state"=>"local", "server"=>"http://apostl.moffitt.org/"}, + # {"state"=>"local", "server"=>"http://smile.hku.hk/SARGs"}, + # {"state"=>"local", "server"=>"http://bf2i-galaxy.insa-lyon.fr:8080/"}, + # {"state"=>"local", "server"=>"http://143.169.238.104/galaxy/"}, + # {"state"=>"missing", "server"=>"https://iris.angers.inra.fr/galaxypub-cfbp"}, + # {"state"=>"local", "server"=>"https://cpt.tamu.edu/galaxy-public/"}, + # {"state"=>"missing", "server"=>"https://vm-chemflow-francegrille.eu/"}, + # {"state"=>"local", "server"=>"https://hyperbrowser.uio.no/coloc-stats"}, + # {"state"=>"local", "server"=>"http://corgat.cloud.ba.infn.it/galaxy"}, + # {"state"=>"local", "server"=>"http://cropgalaxy.excellenceinbreeding.org/"}, + # {"state"=>"local", "server"=>"http://dintor.eurac.edu/"}, + # {"state"=>"missing", "server"=>"http://www.freebioinfo.org/"}, + # {"state"=>"local", "server"=>"http://igg.cloud.ba.infn.it/galaxy"}, + # "topic_name_human"=>"Proteomics", + # "admin_install"=> + # {"install_tool_dependencies"=>true, + # "install_repository_dependencies"=>true, + # "install_resolver_dependencies"=>true, + # "tools"=> + # [{"name"=>"fasta_to_tabular", "owner"=>"devteam", "revisions"=>"e7ed3c310b74", "tool_panel_section_label"=>"FASTA/FASTQ", "tool_shed_url"=>"https://toolshed.g2.bx.psu.edu/"}, + # {"name"=>"tabular_to_fasta", "owner"=>"devteam", "revisions"=>"0a7799698fe5", "tool_panel_section_label"=>"FASTA/FASTQ", "tool_shed_url"=>"https://toolshed.g2.bx.psu.edu/"}, + # {"name"=>"dbbuilder", "owner"=>"galaxyp", "revisions"=>"c1b437242fee", "tool_panel_section_label"=>"Get Data", "tool_shed_url"=>"https://toolshed.g2.bx.psu.edu/"}, + # {"name"=>"fasta_merge_files_and_filter_unique_sequences", "owner"=>"galaxyp", "revisions"=>"f546e7278f04", "tool_panel_section_label"=>"FASTA/FASTQ", "tool_shed_url"=>"https://toolshed.g2.bx.psu.edu/"}, + # {"name"=>"openms_decoydatabase", "owner"=>"galaxyp", "revisions"=>"370141bc0da3", "tool_panel_section_label"=>"Proteomics", "tool_shed_url"=>"https://toolshed.g2.bx.psu.edu/"}]}, + # "admin_install_yaml"=> + # "---\ninstall_tool_dependencies: true\ninstall_repository_dependencies: true\ninstall_resolver_dependencies: true\ntools:\n- name: fasta_to_tabular\n owner: devteam\n revisions: e7ed3c310b74\n tool_panel_s + # ection_label: FASTA/FASTQ\n tool_shed_url: https://toolshed.g2.bx.psu.edu/\n- name: tabular_to_fasta\n owner: devteam\n revisions: 0a7799698fe5\n tool_panel_section_label: FASTA/FASTQ\n tool_shed_url: http + # s://toolshed.g2.bx.psu.edu/\n- name: dbbuilder\n owner: galaxyp\n revisions: c1b437242fee\n tool_panel_section_label: Get Data\n tool_shed_url: https://toolshed.g2.bx.psu.edu/\n- name: fasta_merge_files_and + # _filter_unique_sequences\n owner: galaxyp\n revisions: f546e7278f04\n tool_panel_section_label: FASTA/FASTQ\n tool_shed_url: https://toolshed.g2.bx.psu.edu/\n- name: openms_decoydatabase\n owner: galaxyp\n + # revisions: 370141bc0da3\n tool_panel_section_label: Proteomics\n tool_shed_url: https://toolshed.g2.bx.psu.edu/\n", + # "tours"=>false, + # "video"=>false, + # "slides_recordings"=>false, + # "translations"=>{"tutorial"=>[], "slides"=>[], "video"=>false}, + # "license"=>"CC-BY-4.0", + # "type"=>"tutorial"} + + + + + + def self.resolve_material(site, material) + # We've already + # looked in every /topic/*/tutorials/* folder, and turn these disparate + # resources into a page_obj as well. Most variables are copied directly, + # either from a tutorial, or a slides (if no tutorial is available.) This + # means we do not (cannot) support external_slides AND external_handson. + # This is probably a sub-optimal situation we'll end up fixing someday. + # + tutorials = material['resources'].select { |a| a[0] == 'tutorial' } + slides = material['resources'].select { |a| a[0] == 'slides' } + tours = material['resources'].select { |a| a[0] == 'tours' } + + # Our final "page" object (a "material") + page = nil + + slide_has_video = false + slide_has_recordings = false + slide_translations = [] + page_ref = nil + + if slides.length.positive? + page = slides.min { |a, b| a[1].path <=> b[1].path }[1] + slide_has_video = page.data.fetch('video', false) + slide_has_recordings = page.data.fetch('recordings', false) + slide_translations = page.data.fetch('translations', []) + page_ref = page + end - # No matter if there were slides, we override with tutorials if present. - tutorial_translations = [] - if tutorials.length.positive? - page = tutorials.min { |a, b| a[1].path <=> b[1].path }[1] - tutorial_translations = page.data.fetch('translations', []) - page_ref = page - end + # No matter if there were slides, we override with tutorials if present. + tutorial_translations = [] + if tutorials.length.positive? + page = tutorials.min { |a, b| a[1].path <=> b[1].path }[1] + tutorial_translations = page.data.fetch('translations', []) + page_ref = page + end - if page.nil? - Jekyll.logger.error '[GTN/TopicFilter] Could not process material' - return {} - end + if page.nil? + Jekyll.logger.error '[GTN/TopicFilter] Could not process material' + return {} + end - # Otherwise clone the metadata from it which works well enough. - page_obj = page.data.dup - page_obj['id'] = "#{page['topic_name']}/#{page['tutorial_name']}" - page_obj['ref'] = page_ref - page_obj['ref_tutorials'] = tutorials.map { |a| a[1] } - page_obj['ref_slides'] = slides.map { |a| a[1] } - - id = page_obj['id'] - - # Sometimes `hands_on` is set to something like `external`, in which - # case it is important to not override it. So we only do that if the - # key isn't already set. Then we choose to set it to a test for the - # tutorial being present. We probably don't need to test both, but it - # is hard to follow which keys are which and safer to test for both in - # case someone edits the code later. If either of these exist, we can - # automatically set `hands_on: true` - page_obj['hands_on'] = tutorials.length.positive? if !page_obj.key?('hands_on') - - # Same for slides, if there's a resource by that name, we can - # automatically set `slides: true` - page_obj['slides'] = slides.length.positive? if !page_obj.key?('slides') - - all_resources = slides + tutorials - page_obj['mod_date'] = all_resources - .map { |p| Gtn::ModificationTimes.obtain_time(p[1].path) } - .max - - page_obj['pub_date'] = all_resources - .map { |p| Gtn::PublicationTimes.obtain_time(p[1].path) } - .min - - page_obj['version'] = all_resources - .map { |p| Gtn::ModificationTimes.obtain_modification_count(p[1].path) } - .max - - folder = material['dir'] - - ymls = Dir.glob("#{folder}/quiz/*.yml") + Dir.glob("#{folder}/quiz/*.yaml") - if ymls.length.positive? - quizzes = ymls.map { |a| a.split('/')[-1] } - page_obj['quiz'] = quizzes.map do |q| - quiz_data = YAML.load_file("#{folder}/quiz/#{q}") - { - 'id' => q, - 'path' => "#{folder}/quiz/#{q}", - 'title' => quiz_data['title'], - 'contributors' => quiz_data['contributors'], - } + # Otherwise clone the metadata from it which works well enough. + page_obj = page.data.dup + page_obj['id'] = "#{page['topic_name']}/#{page['tutorial_name']}" + page_obj['ref'] = page_ref + page_obj['ref_tutorials'] = tutorials.map { |a| a[1] } + page_obj['ref_slides'] = slides.map { |a| a[1] } + + id = page_obj['id'] + + # Sometimes `hands_on` is set to something like `external`, in which + # case it is important to not override it. So we only do that if the + # key isn't already set. Then we choose to set it to a test for the + # tutorial being present. We probably don't need to test both, but it + # is hard to follow which keys are which and safer to test for both in + # case someone edits the code later. If either of these exist, we can + # automatically set `hands_on: true` + page_obj['hands_on'] = tutorials.length.positive? if !page_obj.key?('hands_on') + + # Same for slides, if there's a resource by that name, we can + # automatically set `slides: true` + page_obj['slides'] = slides.length.positive? if !page_obj.key?('slides') + + all_resources = slides + tutorials + page_obj['mod_date'] = all_resources + .map { |p| Gtn::ModificationTimes.obtain_time(p[1].path) } + .max + + page_obj['pub_date'] = all_resources + .map { |p| Gtn::PublicationTimes.obtain_time(p[1].path) } + .min + + page_obj['version'] = all_resources + .map { |p| Gtn::ModificationTimes.obtain_modification_count(p[1].path) } + .max + + folder = material['dir'] + + ymls = Dir.glob("#{folder}/quiz/*.yml") + Dir.glob("#{folder}/quiz/*.yaml") + if ymls.length.positive? + quizzes = ymls.map { |a| a.split('/')[-1] } + page_obj['quiz'] = quizzes.map do |q| + quiz_data = YAML.load_file("#{folder}/quiz/#{q}") + quiz_data['id'] = q + quiz_data['path'] = "#{folder}/quiz/#{q}" + quiz_data + end end - end - # In dev configuration, this breaks for me. Not sure why config isn't available. - domain = if !site.config.nil? && site.config.key?('url') - "#{site.config['url']}#{site.config['baseurl']}" - else - 'http://localhost:4000/training-material/' - end - # Similar as above. - workflows = Dir.glob("#{folder}/workflows/*.ga") # TODO: support gxformat2 - if workflows.length.positive? - workflow_names = workflows.map { |a| a.split('/')[-1] } - page_obj['workflows'] = workflow_names.map do |wf| - wfid = "#{page['topic_name']}-#{page['tutorial_name']}" - wfname = wf.gsub(/.ga/, '').downcase.gsub(/[^a-z0-9]/, '-') - trs = "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}" - wf_path = "#{folder}/workflows/#{wf}" - wf_json = JSON.parse(File.read(wf_path)) - license = wf_json['license'] - creators = wf_json['creator'] || [] - wftitle = wf_json['name'] - - # /galaxy-intro-101-workflow.eu.json - workflow_test_results = Dir.glob(wf_path.gsub(/.ga$/, '.*.json')) - workflow_test_outputs = {} - workflow_test_results.each do |test_result| - server = workflow_test_results[0].match(/\.(..)\.json$/)[1] - workflow_test_outputs[server] = JSON.parse(File.read(test_result)) + # In dev configuration, this breaks for me. Not sure why config isn't available. + domain = if !site.config.nil? && site.config.key?('url') + "#{site.config['url']}#{site.config['baseurl']}" + else + 'http://localhost:4000/training-material/' + end + # Similar as above. + workflows = Dir.glob("#{folder}/workflows/*.ga") # TODO: support gxformat2 + if workflows.length.positive? + workflow_names = workflows.map { |a| a.split('/')[-1] } + page_obj['workflows'] = workflow_names.map do |wf| + wfid = "#{page['topic_name']}-#{page['tutorial_name']}" + wfname = wf.gsub(/.ga/, '').downcase.gsub(/[^a-z0-9]/, '-') + trs = "api/ga4gh/trs/v2/tools/#{wfid}/versions/#{wfname}" + wf_path = "#{folder}/workflows/#{wf}" + wf_json = JSON.parse(File.read(wf_path)) + license = wf_json['license'] + creators = wf_json['creator'] || [] + wftitle = wf_json['name'] + + # /galaxy-intro-101-workflow.eu.json + workflow_test_results = Dir.glob(wf_path.gsub(/.ga$/, '.*.json')) + workflow_test_outputs = {} + workflow_test_results.each do |test_result| + server = workflow_test_results[0].match(/\.(..)\.json$/)[1] + workflow_test_outputs[server] = JSON.parse(File.read(test_result)) + end + workflow_test_outputs = nil if workflow_test_outputs.empty? + + wfhkey = [page['topic_name'], page['tutorial_name'], wfname].join('/') + + { + 'workflow' => wf, + 'tests' => Dir.glob("#{folder}/workflows/" + wf.gsub(/.ga/, '-test*')).length.positive?, + 'url' => "#{domain}/#{folder}/workflows/#{wf}", + 'url_html' => "#{domain}/#{folder}/workflows/#{wf.gsub(/.ga$/, '.html')}", + 'path' => wf_path, + 'wfid' => wfid, + 'wfname' => wfname, + 'trs_endpoint' => "#{domain}/#{trs}", + 'license' => license, + 'parent_id' => page_obj['id'], + 'topic_id' => page['topic_name'], + 'tutorial_id' => page['tutorial_name'], + 'creators' => creators, + 'name' => wf_json['name'], + 'title' => wftitle, + 'version' => Gtn::ModificationTimes.obtain_modification_count(wf_path), + 'description' => wf_json['annotation'], + 'tags' => wf_json['tags'], + 'features' => { + 'report' => wf_json['report'], + 'subworkflows' => wf_json['steps'].map{|_, x| x['type']}.any?{|x| x == "subworkflow"}, + 'comments' => (wf_json['comments'] || []).length.positive?, + 'parameters' => wf_json['steps'].map{|_, x| x['type']}.any?{|x| x == "parameter_input"}, + }, + 'workflowhub_id' => (site.data['workflowhub'] || {}).fetch(wfhkey, nil), + 'history' => git_log(wf_path), + 'test_results' => workflow_test_outputs, + 'modified' => File.mtime(wf_path), + 'mermaid' => mermaid(wf_json), + 'graph_dot' => graph_dot(wf_json), + 'workflow_tools' => extract_workflow_tool_list(wf_json).flatten.uniq.sort, + 'inputs' => wf_json['steps'].select { |_k, v| ['data_input', 'data_collection_input', 'parameter_input'].include? v['type'] }.map{|_, v| v}, + 'outputs' => wf_json['steps'].select { |_k, v| v['workflow_outputs'] && v['workflow_outputs'].length.positive? }.map{|_, v| v}, + } end - workflow_test_outputs = nil if workflow_test_outputs.empty? - - wfhkey = [page['topic_name'], page['tutorial_name'], wfname].join('/') - - { - 'workflow' => wf, - 'tests' => Dir.glob("#{folder}/workflows/" + wf.gsub(/.ga/, '-test*')).length.positive?, - 'url' => "#{domain}/#{folder}/workflows/#{wf}", - 'url_html' => "#{domain}/#{folder}/workflows/#{wf.gsub(/.ga$/, '.html')}", - 'path' => wf_path, - 'wfid' => wfid, - 'wfname' => wfname, - 'trs_endpoint' => "#{domain}/#{trs}", - 'license' => license, - 'parent_id' => page_obj['id'], - 'topic_id' => page['topic_name'], - 'tutorial_id' => page['tutorial_name'], - 'creators' => creators, - 'name' => wf_json['name'], - 'title' => wftitle, - 'version' => Gtn::ModificationTimes.obtain_modification_count(wf_path), - 'description' => wf_json['annotation'], - 'tags' => wf_json['tags'], - 'features' => { - 'report' => wf_json['report'], - 'subworkflows' => wf_json['steps'].map{|_, x| x['type']}.any?{|x| x == "subworkflow"}, - 'comments' => (wf_json['comments'] || []).length.positive?, - 'parameters' => wf_json['steps'].map{|_, x| x['type']}.any?{|x| x == "parameter_input"}, - }, - 'workflowhub_id' => (site.data['workflowhub'] || {}).fetch(wfhkey, nil), - 'history' => git_log(wf_path), - 'test_results' => workflow_test_outputs, - 'modified' => File.mtime(wf_path), - 'mermaid' => mermaid(wf_json), - 'graph_dot' => graph_dot(wf_json), - 'workflow_tools' => extract_workflow_tool_list(wf_json).flatten.uniq.sort, - 'inputs' => wf_json['steps'].select { |_k, v| ['data_input', 'data_collection_input', 'parameter_input'].include? v['type'] }.map{|_, v| v}, - 'outputs' => wf_json['steps'].select { |_k, v| v['workflow_outputs'] && v['workflow_outputs'].length.positive? }.map{|_, v| v}, - } end - end - # Really only used for tool list install for ephemeris, not general. - page_obj['api'] = "#{domain}/api/topics/#{page['topic_name']}/tutorials/#{page['tutorial_name']}/tutorial.json" + # Really only used for tool list install for ephemeris, not general. + page_obj['api'] = "#{domain}/api/topics/#{page['topic_name']}/tutorials/#{page['tutorial_name']}/tutorial.json" - # Tool List - # - # This is exposed in the GTN API to help admins/devs easily get the tool - # list for installation. - page_obj['tools'] = [] - page_obj['tools'] += page.content.scan(/{% tool \[[^\]]*\]\(([^)]*)\)\s*%}/) if page_obj['hands_on'] + # Tool List + # + # This is exposed in the GTN API to help admins/devs easily get the tool + # list for installation. + page_obj['tools'] = [] + page_obj['tools'] += page.content.scan(/{% tool \[[^\]]*\]\(([^)]*)\)\s*%}/) if page_obj['hands_on'] - page_obj['workflows']&.each do |wf| - wf_path = "#{folder}/workflows/#{wf['workflow']}" + page_obj['workflows']&.each do |wf| + wf_path = "#{folder}/workflows/#{wf['workflow']}" - page_obj['tools'] += wf['workflow_tools'] - end - page_obj['tools'] = page_obj['tools'].flatten.sort.uniq - - topic = site.data[page_obj['topic_name']] - page_obj['supported_servers'] = if topic['type'] == 'use' || topic['type'] == 'basics' - Gtn::Supported.calculate(site.data['public-server-tools'], page_obj['tools']) - else - [] - end - - page_obj['supported_servers_matrix'] = if topic['type'] == 'use' || topic['type'] == 'basics' - Gtn::Supported.calculate_matrix(site.data['public-server-tools'], page_obj['tools']) - else - [] - end + page_obj['tools'] += wf['workflow_tools'] + end + page_obj['tools'] = page_obj['tools'].flatten.sort.uniq + + topic = site.data[page_obj['topic_name']] + page_obj['supported_servers'] = if topic['type'] == 'use' || topic['type'] == 'basics' + Gtn::Supported.calculate(site.data['public-server-tools'], page_obj['tools']) + else + [] + end + + page_obj['supported_servers_matrix'] = if topic['type'] == 'use' || topic['type'] == 'basics' + Gtn::Supported.calculate_matrix(site.data['public-server-tools'], page_obj['tools']) + else + [] + end - topic_name_human = site.data[page_obj['topic_name']]['title'] - page_obj['topic_name_human'] = topic_name_human # TODO: rename 'topic_name' and 'topic_name' to 'topic_id' - admin_install = Gtn::Toolshed.format_admin_install(site.data['toolshed-revisions'], page_obj['tools'], - topic_name_human, site.data['toolcats']) - page_obj['admin_install'] = admin_install - page_obj['admin_install_yaml'] = admin_install.to_yaml - - page_obj['tours'] = tours.length.positive? - page_obj['video'] = slide_has_video - page_obj['slides_recordings'] = slide_has_recordings - page_obj['translations'] = {} - page_obj['translations']['tutorial'] = tutorial_translations - page_obj['translations']['slides'] = slide_translations - page_obj['translations']['video'] = slide_has_video # Just demand it? - page_obj['license'] = 'CC-BY-4.0' if page_obj['license'].nil? - # I feel less certain about this override, but it works well enough in - # practice, and I did not find any examples of `type: <anything other - # than tutorial>` in topics/*/tutorials/*/tutorial.md but that doesn't - # make it future proof. - page_obj['type'] = 'tutorial' - - if page_obj.key?('draft') && page_obj['draft'] - page_obj['tags'] = [] if !page_obj.key? 'tags' - page_obj['tags'].push('work-in-progress') + topic_name_human = site.data[page_obj['topic_name']]['title'] + page_obj['topic_name_human'] = topic_name_human # TODO: rename 'topic_name' and 'topic_name' to 'topic_id' + admin_install = Gtn::Toolshed.format_admin_install(site.data['toolshed-revisions'], page_obj['tools'], + topic_name_human, site.data['toolcats']) + page_obj['admin_install'] = admin_install + page_obj['admin_install_yaml'] = admin_install.to_yaml + + page_obj['tours'] = tours.length.positive? + page_obj['video'] = slide_has_video + page_obj['slides_recordings'] = slide_has_recordings + page_obj['translations'] = {} + page_obj['translations']['tutorial'] = tutorial_translations + page_obj['translations']['slides'] = slide_translations + page_obj['translations']['video'] = slide_has_video # Just demand it? + page_obj['license'] = 'CC-BY-4.0' if page_obj['license'].nil? + # I feel less certain about this override, but it works well enough in + # practice, and I did not find any examples of `type: <anything other + # than tutorial>` in topics/*/tutorials/*/tutorial.md but that doesn't + # make it future proof. + page_obj['type'] = 'tutorial' + + if page_obj.key?('draft') && page_obj['draft'] + page_obj['tags'] = [] if !page_obj.key? 'tags' + page_obj['tags'].push('work-in-progress') + end + + page_obj end - page_obj - end + def self.process_pages(site, pages) + # eww. + return site.data['cache_processed_pages'] if site.data.key?('cache_processed_pages') - def self.process_pages(site, pages) - # eww. - return site.data['cache_processed_pages'] if site.data.key?('cache_processed_pages') + materials = collate_materials(site, pages).map { |_k, v| resolve_material(site, v) } + Jekyll.logger.info '[GTN/TopicFilter] Filling Materials Cache' + site.data['cache_processed_pages'] = materials - materials = collate_materials(site, pages).map { |_k, v| resolve_material(site, v) } - Jekyll.logger.info '[GTN/TopicFilter] Filling Materials Cache' - site.data['cache_processed_pages'] = materials + # Prepare short URLs + shortlinks = site.data['shortlinks'] + mappings = Hash.new { |h, k| h[k] = [] } - # Prepare short URLs - shortlinks = site.data['shortlinks'] - mappings = Hash.new { |h, k| h[k] = [] } + shortlinks.each_key do |kp| + shortlinks[kp].each do |k, v| + mappings[v].push("/short/#{k}") + end + end + # Update the materials with their short IDs + redirects + pages.select { |p| mappings.keys.include? p.url }.each do |p| + # Set the short id on the material + if p['ref'] + # Initialise redirects if it wasn't set + p['ref'].data['redirect_from'] = [] if !p['ref'].data.key?('redirect_from') + p['ref'].data['redirect_from'].push(*mappings[p.url]) + p['ref'].data['redirect_from'].uniq! + else + p.data['redirect_from'] = [] if !p.data.key?('redirect_from') - shortlinks.each_key do |kp| - shortlinks[kp].each do |k, v| - mappings[v].push("/short/#{k}") + p.data['redirect_from'].push(*mappings[p.url]) + p.data['redirect_from'].uniq! + end end - end - # Update the materials with their short IDs + redirects - pages.select { |p| mappings.keys.include? p.url }.each do |p| - # Set the short id on the material - if p['ref'] - # Initialise redirects if it wasn't set - p['ref'].data['redirect_from'] = [] if !p['ref'].data.key?('redirect_from') - p['ref'].data['redirect_from'].push(*mappings[p.url]) - p['ref'].data['redirect_from'].uniq! - else + # Same for news + get_posts(site).select { |p| mappings.keys.include? p.url }.each do |p| + # Set the short id on the material p.data['redirect_from'] = [] if !p.data.key?('redirect_from') - p.data['redirect_from'].push(*mappings[p.url]) p.data['redirect_from'].uniq! end - end - # Same for news - get_posts(site).select { |p| mappings.keys.include? p.url }.each do |p| - # Set the short id on the material - p.data['redirect_from'] = [] if !p.data.key?('redirect_from') - p.data['redirect_from'].push(*mappings[p.url]) - p.data['redirect_from'].uniq! - end - - materials - end - ## - # This is a helper function to get all the materials in a site. - def self.list_all_materials(site) - process_pages(site, site.pages) - end - - ## - # This is a helper function to get all the materials in a site. - def self.list_videos(site) - materials = process_pages(site, site.pages) - materials.select { |x| x['video'] == true } - end - - ## - # List every tag used across all materials. - # This is used to generate the tag cloud. - # - # Parameters: - # +site+:: The +Jekyll::Site+ object, used to get the list of pages. - # Returns: - # +Array+:: An array of strings, each string is a tag. (sorted and unique) - # - def self.list_all_tags(site) - materials = process_pages(site, site.pages) - (materials.map { |x| x['tags'] || [] }.flatten + list_topics(site)).sort.uniq - end + materials + end - def self.filter_by_topic(site, topic_name) - # Here we make a (cached) call to load materials into memory and sort them - # properly. - materials = process_pages(site, site.pages) + ## + # This is a helper function to get all the materials in a site. + def self.list_all_materials(site) + process_pages(site, site.pages) + end - # Select out the materials by topic: - resource_pages = materials.select { |x| x['topic_name'] == topic_name } + ## + # This is a helper function to get materials with automated videos. + def self.list_videos(site) + materials = process_pages(site, site.pages) + materials.select { |x| x['video'] == true } + end - # If there is nothing with that topic name, try generating it by tags. - resource_pages = materials.select { |x| (x['tags'] || []).include?(topic_name) } if resource_pages.empty? + ## + # List every tag used across all materials. + # This is used to generate the tag cloud. + # + # Parameters: + # +site+:: The +Jekyll::Site+ object, used to get the list of pages. + # Returns: + # +Array+:: An array of strings, each string is a tag. (sorted and unique) + # + def self.list_all_tags(site) + materials = process_pages(site, site.pages) + (materials.map { |x| x['tags'] || [] }.flatten + list_topics(site)).sort.uniq + end - # The complete resources we'll return is the introduction slides first - # (EDIT: not anymore, we rely on prioritisation!) - # and then the rest of the pages. - resource_pages = resource_pages.sort_by { |k| k.fetch('priority', 1) } + def self.filter_by_topic(site, topic_name) + # Here we make a (cached) call to load materials into memory and sort them + # properly. + materials = process_pages(site, site.pages) - Jekyll.logger.error "Error? Could not find any relevant pages for #{topic_name}" if resource_pages.empty? + # Select out the materials by topic: + resource_pages = materials.select { |x| x['topic_name'] == topic_name } - resource_pages - end + # If there is nothing with that topic name, try generating it by tags. + resource_pages = materials.select { |x| (x['tags'] || []).include?(topic_name) } if resource_pages.empty? - def self.filter_by_tag(site, topic_name) - # Here we make a (cached) call to load materials into memory and sort them - # properly. - materials = process_pages(site, site.pages) + # The complete resources we'll return is the introduction slides first + # (EDIT: not anymore, we rely on prioritisation!) + # and then the rest of the pages. + resource_pages = resource_pages.sort_by { |k| k.fetch('priority', 1) } - # Select those with that topic ID or that tag - resource_pages = materials.select { |x| x['topic_name'] == topic_name } - resource_pages += materials.select { |x| (x['tags'] || []).include?(topic_name) } + Jekyll.logger.error "Error? Could not find any relevant pages for #{topic_name}" if resource_pages.empty? - # The complete resources we'll return is the introduction slides first - # (EDIT: not anymore, we rely on prioritisation!) - # and then the rest of the pages. - resource_pages = resource_pages.sort_by { |k| k.fetch('priority', 1) } + resource_pages + end - Jekyll.logger.error "Error? Could not find any relevant tagged pages for #{topic_name}" if resource_pages.empty? + def self.filter_by_tag(site, topic_name) + # Here we make a (cached) call to load materials into memory and sort them + # properly. + materials = process_pages(site, site.pages) - resource_pages - end + # Select those with that topic ID or that tag + resource_pages = materials.select { |x| x['topic_name'] == topic_name } + resource_pages += materials.select { |x| (x['tags'] || []).include?(topic_name) } - ## - # Filter a list of materials by topic and subtopic. - def self.filter_by_topic_subtopic(site, topic_name, subtopic_id) - resource_pages = filter_by_topic(site, topic_name) + # The complete resources we'll return is the introduction slides first + # (EDIT: not anymore, we rely on prioritisation!) + # and then the rest of the pages. + resource_pages = resource_pages.sort_by { |k| k.fetch('priority', 1) } - # Select out materials with the correct subtopic - resource_pages = resource_pages.select { |x| x['subtopic'] == subtopic_id } + Jekyll.logger.error "Error? Could not find any relevant tagged pages for #{topic_name}" if resource_pages.empty? - if resource_pages.empty? - Jekyll.logger.error "Error? Could not find any relevant pages for #{topic_name} / #{subtopic_id}" + resource_pages end - resource_pages - end - - ## - # Get a list of contributors for a list of materials - # Parameters: - # +materials+:: An array of materials - # Returns: - # +Array+:: An array of individual contributors as strings. - def self.identify_contributors(materials, site) - materials - .map { |_k, v| v['materials'] }.flatten - # Not 100% sure why this flatten is needed? Probably due to the map over hash - .map { |mat| Gtn::Contributors.get_contributors(mat) } - .flatten - .select { |c| Gtn::Contributors.person?(site, c) } - .uniq - .shuffle - end + ## + # Filter a list of materials by topic and subtopic. + def self.filter_by_topic_subtopic(site, topic_name, subtopic_id) + resource_pages = filter_by_topic(site, topic_name) - ## - # Get a list of funders for a list of materials - # Parameters: - # +materials+:: An array of materials - # Returns: - # +Array+:: An array of funder (organisations that provided support) IDs as strings. - def self.identify_funders_and_grants(materials, site) - materials - .map { |_k, v| v['materials'] }.flatten - # Not 100% sure why this flatten is needed? Probably due to the map over hash - .map { |mat| Gtn::Contributors.get_all_funding(site, mat) } - .flatten - .uniq - .shuffle - end + # Select out materials with the correct subtopic + resource_pages = resource_pages.select { |x| x['subtopic'] == subtopic_id } - ## - # Get the version of a tool. - # Parameters: - # +tool+:: A tool string - # Returns: - # +String+:: The version of the tool. - # - # Examples: - # get_version("toolshed.g2.bx.psu.edu/repos/galaxyp/regex_find_replace/regex1/1.0.0") => "1.0.0" - def self.get_version(tool) - if tool.count('/') > 4 - tool.split('/')[-1] - else - tool - end - end + if resource_pages.empty? + Jekyll.logger.error "Error? Could not find any relevant pages for #{topic_name} / #{subtopic_id}" + end - ## - # Get a short version of a tool. - # Parameters: - # +tool+:: A tool string - # Returns: - # +String+:: The short version of the tool. - # - # Examples: - # short_tool("toolshed.g2.bx.psu.edu/repos/galaxyp/regex_find_replace/regex1/1.0.0") => "galaxyp/regex1" - def self.short_tool(tool) - if tool.count('/') > 4 - "#{tool.split('/')[2]}/#{tool.split('/')[4]}" - else - tool + resource_pages end - end - ## - # List materials by tool - # Parameters: - # +site+:: The +Jekyll::Site+ object, used to get the list of pages. - # Returns: - # +Hash+:: A hash of tool_id => { - # "tool_id" => [tool_id, version], - # "tutorials" => [tutorial_id, tutorial_title, topic_title, tutorial_url] - # } - def self.list_materials_by_tool(site) - tool_map = {} - - list_all_materials(site).each do |m| - m.fetch('tools', []).each do |tool| - sid = short_tool(tool) - tool_map[sid] = { 'tool_id' => [], 'tutorials' => [] } if !tool_map.key?(sid) - - tool_map[sid]['tool_id'].push([tool, get_version(tool)]) - tool_map[sid]['tutorials'].push([ - m['id'], m['title'], site.data[m['topic_name']]['title'], m['url'] - ]) - end + ## + # Get a list of contributors for a list of materials + # Parameters: + # +materials+:: An array of materials + # Returns: + # +Array+:: An array of individual contributors as strings. + def self.identify_contributors(materials, site) + materials + .map { |_k, v| v['materials'] }.flatten + # Not 100% sure why this flatten is needed? Probably due to the map over hash + .map { |mat| Gtn::Contributors.get_contributors(mat) } + .flatten + .select { |c| Gtn::Contributors.person?(site, c) } + .uniq + .shuffle end - # Uniqueify/sort - t = tool_map.to_h do |k, v| - v['tool_id'].uniq! - v['tool_id'].sort_by! { |k2| k2[1] } - v['tool_id'].reverse! - - v['tutorials'].uniq! - v['tutorials'].sort! - [k, v] + ## + # Get a list of funders for a list of materials + # Parameters: + # +materials+:: An array of materials + # Returns: + # +Array+:: An array of funder (organisations that provided support) IDs as strings. + def self.identify_funders_and_grants(materials, site) + materials + .map { |_k, v| v['materials'] }.flatten + # Not 100% sure why this flatten is needed? Probably due to the map over hash + .map { |mat| Gtn::Contributors.get_all_funding(site, mat) } + .flatten + .uniq + .shuffle end - # Order by most popular tool - t.sort_by { |_k, v| v['tutorials'].length }.reverse.to_h - end -end - -module Jekyll - # The "implementation" of the topic filter as liquid accessible filters - module ImplTopicFilter ## - # List the most recent contributors to the GTN. + # Get the version of a tool. # Parameters: - # +contributors+:: A hash of contributors - # +count+:: The number of contributors to return + # +tool+:: A tool string # Returns: - # +Hash+:: A hash of contributors + # +String+:: The version of the tool. # - # Example: - # most_recent_contributors(contributors, 5) - # => { - # "hexylena" => { - # "name" => "Hexylena", - # "avatar" => "https://avatars.githubusercontent.com/u/458683?v=3", - # ... - # } - # } - def most_recent_contributors(contributors, count) - # Remove non-hof - hof = contributors.reject { |_k, v| v.fetch('halloffame', 'yes') == 'no' } - # Get keys + sort by joined date - hof_k = hof.keys.sort do |x, y| - hof[y].fetch('joined', '2016-01') <=> hof[x].fetch('joined', '2016-01') - end - - # Transform back into hash - hof_k.slice(0, count).to_h { |k| [k, hof[k]] } + # Examples: + # get_version("toolshed.g2.bx.psu.edu/repos/galaxyp/regex_find_replace/regex1/1.0.0") => "1.0.0" + def self.get_version(tool) + if tool.count('/') > 4 + tool.split('/')[-1] + else + tool + end end ## - # Find the most recently modified tutorials + # Get a short version of a tool. # Parameters: - # +site+:: The +Jekyll::Site+ object, used to get the list of pages. - # +exclude_recently_published+:: Do not include ones that were recently - # published in the slice, to make it look a bit nicer. + # +tool+:: A tool string # Returns: - # +Array+:: An array of the 10 most recently modified pages - # Example: - # {% assign latest_tutorials = site | recently_modified_tutorials %} - def recently_modified_tutorials(site, exclude_recently_published: true) - tutorials = site.pages.select { |page| page.data['layout'] == 'tutorial_hands_on' } - - latest = tutorials.sort do |x, y| - Gtn::ModificationTimes.obtain_time(y.path) <=> Gtn::ModificationTimes.obtain_time(x.path) + # +String+:: The short version of the tool. + # + # Examples: + # short_tool("toolshed.g2.bx.psu.edu/repos/galaxyp/regex_find_replace/regex1/1.0.0") => "galaxyp/regex1" + def self.short_tool(tool) + if tool.count('/') > 4 + "#{tool.split('/')[2]}/#{tool.split('/')[3]}/#{tool.split('/')[4]}" + else + tool end - - latest_published = recently_published_tutorials(site) - latest = latest.reject { |x| latest_published.include?(x) } if exclude_recently_published - - latest.slice(0, 10) end ## - # Find the most recently published tutorials + # List materials by tool + # # Parameters: # +site+:: The +Jekyll::Site+ object, used to get the list of pages. # Returns: - # +Array+:: An array of the 10 most recently published modified pages - # Example: - # {% assign latest_tutorials = site | recently_modified_tutorials %} - def recently_published_tutorials(site) - tutorials = site.pages.select { |page| page.data['layout'] == 'tutorial_hands_on' } + # +Hash+:: A hash as below: + # + # { + # tool_id => { + # "tool_id" => [tool_id, version], + # "tutorials" => [tutorial_id, tutorial_title, topic_title, tutorial_url] + # }, ... + # } + # + # *Nota Bene!!!*: Galaxy depends on the structure of this response, please + # do not change it, add a new API instead if you need to modify it + # significantly. + # + def self.list_materials_by_tool(site) + tool_map = {} + + list_all_materials(site).each do |m| + m.fetch('tools', []).each do |tool| + sid = short_tool(tool) + tool_map[sid] = { 'tool_id' => [], 'tutorials' => [] } if !tool_map.key?(sid) + + tool_map[sid]['tool_id'].push([tool, get_version(tool)]) + tool_map[sid]['tutorials'].push([ + m['id'], m['title'], site.data[m['topic_name']]['title'], m['url'] + ]) + end + end + + # Uniqueify/sort + t = tool_map.to_h do |k, v| + v['tool_id'].uniq! + v['tool_id'].sort_by! { |k2| k2[1] } + v['tool_id'].reverse! - latest = tutorials.sort do |x, y| - Gtn::PublicationTimes.obtain_time(y.path) <=> Gtn::PublicationTimes.obtain_time(x.path) + v['tutorials'].uniq! + v['tutorials'].sort! + [k, v] end - latest.slice(0, 10) + # Order by most popular tool + t.sort_by { |_k, v| v['tutorials'].length }.reverse.to_h end - def topic_count(resources) - # Count lines in the table except introduction slides - resources.length - end ## - # Fetch a tutorial material's metadata - # Parameters: - # +site+:: The +Jekyll::Site+ object, used to get the list of pages. - # +topic_name+:: The name of the topic - # +page_name+:: The name of the page - # Returns: - # +Hash+:: The metadata for the tutorial material + # Not materials but resources (including e.g. recordings, slides separate from tutorials, etc.) # - # Example: - # {% assign material = site | fetch_tutorial_material:page.topic_name,page.tutorial_name%} - def fetch_tutorial_material(site, topic_name, page_name) - TopicFilter.fetch_tutorial_material(site, topic_name, page_name) + # The structure is a large array of arrays, with [date, category, page-like object, tags] + # + # [#<DateTime: 2019-02-22T20:53:50+01:00 ((2458537j,71630s,0n),+3600s,2299161j)>, + # "tutorials", + # #<Jekyll::Page @relative_path="topics/single-cell/tutorials/scrna-preprocessing/tutorial.md">, + # ["single-cell"]], + # [#<DateTime: 2019-02-20T19:33:11+01:00 ((2458535j,66791s,0n),+3600s,2299161j)>, + # "tutorials", + # #<Jekyll::Page @relative_path="topics/single-cell/tutorials/scrna-umis/tutorial.md">, + # ["single-cell"]], + # [#<DateTime: 2019-02-16T21:04:07+01:00 ((2458531j,72247s,0n),+3600s,2299161j)>, + # "slides", + # #<Jekyll::Page @relative_path="topics/single-cell/tutorials/scrna-plates-batches-barcodes/slides.html">, + # ["single-cell"]]] + def self.all_date_sorted_resources(site) + cache.getset('all_date_sorted_resources') do + self._all_date_sorted_resources(site) + end end - def fetch_tutorial_material_by_id(site, id) - TopicFilter.fetch_tutorial_material(site, id.split('/')[0], id.split('/')[1]) - end + def self._all_date_sorted_resources(site) + events = site.pages.select { |x| x['layout'] == 'event' || x['layout'] == 'event-external' } + materials = list_all_materials(site).reject { |k, _v| k['draft'] } + news = site.posts.select { |x| x['layout'] == 'news' } + faqs = site.pages.select { |x| x['layout'] == 'faq' } + pathways = site.pages.select { |x| x['layout'] == 'learning-pathway' } + workflows = Dir.glob('topics/**/*.ga') - def list_topics_ids(site) - ['introduction'] + TopicFilter.list_topics(site).filter { |k| k != 'introduction' } - end + bucket = events.map do |e| + [Gtn::PublicationTimes.obtain_time(e.path).to_datetime, 'events', e, ['event'] + e.data.fetch('tags', [])] + end - def list_topics_h(site) - TopicFilter.list_topics(site) - end + materials.each do |m| + tags = [m['topic_name']] + (m['tags'] || []) + m.fetch('ref_tutorials', []).map do |t| + bucket << [Gtn::PublicationTimes.obtain_time(t.path).to_datetime, 'tutorials', t, tags] + + (t['recordings'] || []).map do |r| + url = '/' + t.path.gsub(/tutorial(_[A_Z_]*)?.(html|md)$/, 'recordings/') + url += "#tutorial-recording-#{Date.parse(r['date']).strftime('%-d-%B-%Y').downcase}" + attr = {'title' => "Recording of " + t['title'], + 'contributors' => r['speakers'] + (r['captions'] || []), + 'content' => "A #{r['length']} long recording is now available."} + + obj = objectify(attr, url, t.path) + bucket << [DateTime.parse(r['date'].to_s), 'recordings', obj, tags] + end + end - def list_topics_by_category(site, category) - q = TopicFilter.list_topics(site).map do |k| - [k, site.data[k]] + m.fetch('ref_slides', []).reject { |s| s.url =~ /-plain.html/ }.map do |s| + bucket << [Gtn::PublicationTimes.obtain_time(s.path).to_datetime, 'slides', s, tags] + + (s['recordings'] || []).map do |r| + url = '/' + s.path.gsub(/slides(_[A_Z_]*)?.(html|md)$/, 'recordings/') + url += "#tutorial-recording-#{Date.parse(r['date']).strftime('%-d-%B-%Y').downcase}" + attr = {'title' => "Recording of " + s['title'], + 'contributors' => r['speakers'] + (r['captions'] || []), + 'content' => "A #{r['length']} long recording is now available."} + obj = objectify(attr, url, s.path) + bucket << [DateTime.parse(r['date'].to_s), 'recordings', obj, tags] + end + end end - # Alllow filtering by a category, or return "all" otherwise. - if category == 'non-tag' - q = q.select { |_k, v| v['tag_based'].nil? } - elsif category == 'science' - q = q.select { |_k, v| %w[use basics].include? v['type'] } - elsif category == 'technical' - q = q.select { |_k, v| %w[admin-dev data-science instructors].include? v['type'] } - elsif category == 'science-technical' - q = q.select { |_k, v| %w[use basics admin-dev data-science instructors].include? v['type'] } - elsif category != 'all' - q = q.select { |_k, v| v['type'] == category } + bucket += news.map do |n| + [n.date.to_datetime, 'news', n, ['news'] + n.data.fetch('tags', [])] end - # Sort alphabetically by titles - q.sort { |a, b| a[1]['title'] <=> b[1]['title'] } - end + bucket += faqs.map do |n| + tag = Gtn::PublicationTimes.clean_path(n.path).split('/')[1] + [Gtn::PublicationTimes.obtain_time(n.path).to_datetime, 'faqs', n, ['faqs', tag]] + end - def to_keys(arr) - arr.map { |k| k[0] } - end + bucket += pathways.map do |n| + tags = ['learning-pathway'] + (n['tags'] || []) + [Gtn::PublicationTimes.obtain_time(n.path).to_datetime, 'learning-pathways', n, tags] + end - def to_vals(arr) - arr.map { |k| k[1] } - end + bucket += workflows.map do |n| + tag = Gtn::PublicationTimes.clean_path(n).split('/')[1] + wf_data = JSON.parse(File.read(n)) + + attrs = { + 'title' => wf_data['name'], + 'description' => wf_data['annotation'], + 'tags' => wf_data['tags'], + 'contributors' => wf_data.fetch('creator', []).map do |c| + matched = site.data['contributors'].select{|k, v| + v.fetch('orcid', "does-not-exist") == c.fetch('identifier', "").gsub('https://orcid.org/', '') + }.first + if matched + matched[0] + else + c['name'] + end + end + } + # These aren't truly stable. I'm not sure what to do about that. + obj = objectify(attrs, '/' + n.gsub(/\.ga$/, '.html'), n) + # obj = objectify(attrs, '/' + n.path[0..n.path.rindex('/')], n) + [Gtn::PublicationTimes.obtain_time(n).to_datetime, 'workflows', obj, ['workflows', tag] + obj['tags']] + end - def list_materials_by_tool(site) - TopicFilter.list_materials_by_tool(site) - end + # Remove symlinks from bucket. + bucket = bucket.reject { |date, type, page, tags| + File.symlink?(page.path) || File.symlink?(File.dirname(page.path)) || File.symlink?(File.dirname(File.dirname(page.path))) + } - def list_materials_structured(site, topic_name) - TopicFilter.list_materials_structured(site, topic_name) - end + bucket += site.data['contributors'].map do |k, v| + a = {'title' => "@#{k}", + 'content' => "GTN Contributions from #{k}"} + obj = objectify(a, "/hall-of-fame/#{k}/", k) - def list_materials_flat(site, topic_name) - TopicFilter - .list_materials_structured(site, topic_name) - .map { |k, v| v['materials'] } - .flatten - .uniq { |x| x['id'] } - end + [DateTime.parse("#{v['joined']}-01T12:00:00", 'content' => "GTN Contributions from #{k}"), 'contributors', obj, ['contributor']] + end - def list_all_tags(site) - TopicFilter.list_all_tags(site) - end + bucket += site.data['grants'].map do |k, v| + a = {'title' => "@#{k}", + 'content' => "GTN Contributions from #{k}"} + obj = objectify(a, "/hall-of-fame/#{k}/", k) - def topic_filter(site, topic_name) - TopicFilter.topic_filter(site, topic_name) - end + # TODO: backdate grants, organisations + if v['joined'] + [DateTime.parse("#{v['joined']}-01T12:00:00"), 'grants', obj, ['grant']] + end + end.compact - def topic_filter_tutorial_count(site, topic_name) - TopicFilter.topic_filter(site, topic_name).length - end + bucket += site.data['organisations'].map do |k, v| + a = {'title' => "@#{k}", + 'content' => "GTN Contributions from #{k}"} + obj = objectify(a, "/hall-of-fame/#{k}/", k) - def identify_contributors(materials, site) - TopicFilter.identify_contributors(materials, site) - end + if v['joined'] + [DateTime.parse("#{v['joined']}-01T12:00:00"), 'organisations', obj, ['organisation']] + end + end.compact - def identify_funders(materials, site) - TopicFilter.identify_funders_and_grants(materials, site) + bucket + .reject{|x| x[0] > DateTime.now } # Remove future-dated materials + .reject{|x| x[2]['draft'] == true } # Remove drafts + .sort_by {|x| x[0] } # Date-sorted, not strictly necessary since will be grouped. + .reverse end + end +end - def list_videos(site) - TopicFilter.list_all_materials(site) - .select { |k, _v| k['recordings'] || k['slides_recordings'] } - .map { |k, _v| (k['recordings'] || []) + (k['slides_recordings'] || []) } - .flatten - end +module Jekyll + # The "implementation" of the topic filter as liquid accessible filters + module Filters + module TopicFilter + ## + # List the most recent contributors to the GTN. + # Parameters: + # +contributors+:: A hash of contributors + # +count+:: The number of contributors to return + # Returns: + # +Hash+:: A hash of contributors + # + # Example: + # most_recent_contributors(contributors, 5) + # => { + # "hexylena" => { + # "name" => "Hexylena", + # "avatar" => "https://avatars.githubusercontent.com/u/458683?v=3", + # ... + # } + # } + def most_recent_contributors(contributors, count) + # Remove non-hof + hof = contributors.reject { |_k, v| v.fetch('halloffame', 'yes') == 'no' } + # Get keys + sort by joined date + hof_k = hof.keys.sort do |x, y| + hof[y].fetch('joined', '2016-01') <=> hof[x].fetch('joined', '2016-01') + end - def findDuration(duration) - if ! duration.nil? - eval(duration.gsub(/H/, ' * 3600 + ').gsub(/M/, ' * 60 + ').gsub(/S/, ' + ') + " 0") - else - 0 + # Transform back into hash + hof_k.slice(0, count).to_h { |k| [k, hof[k]] } end - end - def list_videos_total_time(site) - vids = list_videos(site) - vids.map { |v| findDuration(v['length']) }.sum / 3600.0 - end + ## + # Find the most recently modified tutorials + # Parameters: + # +site+:: The +Jekyll::Site+ object, used to get the list of pages. + # +exclude_recently_published+:: Do not include ones that were recently + # published in the slice, to make it look a bit nicer. + # Returns: + # +Array+:: An array of the 10 most recently modified pages + # Example: + # {% assign latest_tutorials = site | recently_modified_tutorials %} + def recently_modified_tutorials(site, exclude_recently_published: true) + tutorials = site.pages.select { |page| page.data['layout'] == 'tutorial_hands_on' } + + latest = tutorials.sort do |x, y| + Gtn::ModificationTimes.obtain_time(y.path) <=> Gtn::ModificationTimes.obtain_time(x.path) + end - def list_draft_materials(site) - TopicFilter.list_all_materials(site).select { |k, _v| k['draft'] } - end + latest_published = recently_published_tutorials(site) + latest = latest.reject { |x| latest_published.include?(x) } if exclude_recently_published - def to_material(site, page) - topic = page['path'].split('/')[1] - material = page['path'].split('/')[3] - ret = TopicFilter.fetch_tutorial_material(site, topic, material) - Jekyll.logger.warn "Could not find material #{topic} #{material}" if ret.nil? - ret - end + latest.slice(0, 10) + end + + ## + # Find the most recently published tutorials + # Parameters: + # +site+:: The +Jekyll::Site+ object, used to get the list of pages. + # Returns: + # +Array+:: An array of the 10 most recently published modified pages + # Example: + # {% assign latest_tutorials = site | recently_modified_tutorials %} + def recently_published_tutorials(site) + tutorials = site.pages.select { |page| page.data['layout'] == 'tutorial_hands_on' } + + latest = tutorials.sort do |x, y| + Gtn::PublicationTimes.obtain_time(y.path) <=> Gtn::PublicationTimes.obtain_time(x.path) + end + + latest.slice(0, 10) + end + + def topic_count(resources) + # Count lines in the table except introduction slides + resources.length + end + + ## + # Fetch a tutorial material's metadata + # Parameters: + # +site+:: The +Jekyll::Site+ object, used to get the list of pages. + # +topic_name+:: The name of the topic + # +page_name+:: The name of the page + # Returns: + # +Hash+:: The metadata for the tutorial material + # + # Example: + # {% assign material = site | fetch_tutorial_material:page.topic_name,page.tutorial_name%} + def fetch_tutorial_material(site, topic_name, page_name) + Gtn::TopicFilter.fetch_tutorial_material(site, topic_name, page_name) + end + + def fetch_tutorial_material_by_id(site, id) + Gtn::TopicFilter.fetch_tutorial_material(site, id.split('/')[0], id.split('/')[1]) + end + + def list_topics_ids(site) + ['introduction'] + Gtn::TopicFilter.list_topics(site).filter { |k| k != 'introduction' } + end + + def list_topics_h(site) + Gtn::TopicFilter.list_topics(site) + end + + def list_topics_by_category(site, category) + q = Gtn::TopicFilter.list_topics(site).map do |k| + [k, site.data[k]] + end + + # Alllow filtering by a category, or return "all" otherwise. + if category == 'non-tag' + q = q.select { |_k, v| v['tag_based'].nil? } + elsif category == 'science' + q = q.select { |_k, v| %w[use basics].include? v['type'] } + elsif category == 'technical' + q = q.select { |_k, v| %w[admin-dev data-science instructors].include? v['type'] } + elsif category == 'science-technical' + q = q.select { |_k, v| %w[use basics admin-dev data-science instructors].include? v['type'] } + elsif category != 'all' + q = q.select { |_k, v| v['type'] == category } + end + + # Sort alphabetically by titles + q.sort { |a, b| a[1]['title'] <=> b[1]['title'] } + end + + def to_keys(arr) + arr.map { |k| k[0] } + end + + def to_vals(arr) + arr.map { |k| k[1] } + end + + ## + # Galaxy depends on the structure of this response, please do not change + # it, add a new API instead if you need to modify it significantly. + def list_materials_by_tool(site) + Gtn::TopicFilter.list_materials_by_tool(site) + end + + def list_materials_structured(site, topic_name) + Gtn::TopicFilter.list_materials_structured(site, topic_name) + end + + def list_materials_flat(site, topic_name) + Gtn::TopicFilter + .list_materials_structured(site, topic_name) + .map { |k, v| v['materials'] } + .flatten + .uniq { |x| x['id'] } + end + + def list_topic_materials_yearly(site, topic_name) + flat_mats = list_materials_flat(site, topic_name) + years = flat_mats.map{|x| x['pub_date'].year} + flat_mats.map{|x| x['mod_date'].year} + # doesn't use identify_contributors because that excludes grants/orgs. + topic_contribs = flat_mats.map{|x| x['contributions'] || {"all" => x['contributors']}}.map{|x| x.values.flatten}.flatten.uniq.sort + pfo = ['contributors', 'grants', 'organisations'] + + Gtn::TopicFilter.all_date_sorted_resources(site) + .select{|x| (x[3].include? topic_name) || (pfo.include?(x[1]) && topic_contribs.include?(x[2].title[1..]))} + .group_by{|x| x[0].year} + .map{|k, v| [k, v.group_by{|z| z[1]}]} + .to_h + end + + def count_topic_materials_yearly(site, topic_name) + flat_mats = list_materials_flat(site, topic_name) + years = flat_mats.map{|x| x['pub_date'].year} + flat_mats.map{|x| x['mod_date'].year} + # doesn't use identify_contributors because that excludes grants/orgs. + topic_contribs = flat_mats.map{|x| x['contributions'] || {"all" => x['contributors']}}.map{|x| x.values.flatten}.flatten.uniq.sort + pfo = ['contributors', 'grants', 'organisations'] + + r = Gtn::TopicFilter.all_date_sorted_resources(site) + .select{|x| (x[3].include? topic_name) || (pfo.include?(x[1]) && topic_contribs.include?(x[2].title[1..]))} + .map{|x| [x[0].year, x[1]]} # Only need year + type + .group_by{|x| x[1]} # Group by type. + .map{|k, v| [k, v.map{|vv| vv[0]}.tally]} + .to_h + + years = (2015..Date.today.year).to_a + # Fill in zeros for missing years + r.map{|k, v| [k, years.map{|y| v[y] || 0} + .cumulative_sum + .map.with_index{|value, i| {"y" => value, "x" => "#{years[i]}-01-01"}}] + }.to_h + end + + def list_all_tags(site) + Gtn::TopicFilter.list_all_tags(site) + end + + def topic_filter(site, topic_name) + Gtn::TopicFilter.topic_filter(site, topic_name) + end + + def topic_filter_tutorial_count(site, topic_name) + Gtn::TopicFilter.topic_filter(site, topic_name).length + end + + def identify_contributors(materials, site) + Gtn::TopicFilter.identify_contributors(materials, site) + end + + def identify_funders(materials, site) + Gtn::TopicFilter.identify_funders_and_grants(materials, site) + end + + ## + # Just used for stats page. + def list_videos(site) + Gtn::TopicFilter.list_all_materials(site) + .select { |k, _v| k['recordings'] || k['slides_recordings'] } + .map { |k, _v| (k['recordings'] || []) + (k['slides_recordings'] || []) } + .flatten + end + + def findDuration(duration) + if ! duration.nil? + eval(duration.gsub(/H/, ' * 3600 + ').gsub(/M/, ' * 60 + ').gsub(/S/, ' + ') + " 0") + else + 0 + end + end + + ## + # Just used for stats page. + def list_videos_total_time(site) + vids = list_videos(site) + vids.map { |v| findDuration(v['length']) }.sum / 3600.0 + end - def get_workflow(site, page, workflow) - mat = to_material(site, page) - mat['workflows'].select { |w| w['workflow'] == workflow }[0] + def list_draft_materials(site) + Gtn::TopicFilter.list_all_materials(site).select { |k, _v| k['draft'] } + end + + def to_material(site, page) + topic = page['path'].split('/')[1] + material = page['path'].split('/')[3] + ret = Gtn::TopicFilter.fetch_tutorial_material(site, topic, material) + Jekyll.logger.warn "Could not find material #{topic} #{material}" if ret.nil? + ret + end + + def get_workflow(site, page, workflow) + mat = to_material(site, page) + mat['workflows'].select { |w| w['workflow'] == workflow }[0] + end + + def tool_version_support(site, tool) + Gtn::Supported.calculate(site.data['public-server-tools'], [tool]) + end + + def edamify(term, site) + site.data['EDAM'].select{|row| row['Class ID'] == "http://edamontology.org/#{term}"}.first.to_h + end + + def titlecase(term) + term.split(' ').map(&:capitalize).join(' ') + end end end end -Liquid::Template.register_filter(Jekyll::ImplTopicFilter) +Liquid::Template.register_filter(Jekyll::Filters::TopicFilter) diff --git a/_plugins/link.rb b/_plugins/link.rb index 3d0371c7d58d60..6f907e7eee3f61 100644 --- a/_plugins/link.rb +++ b/_plugins/link.rb @@ -1,21 +1,24 @@ # frozen_string_literal: true module Jekyll - # Replaces the built in link tag temporarily - class CustomLinkTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end + module Tags + + # Replaces the built in link tag temporarily + class CustomLinkTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end - def render(_context) - # This is a workaround for https://github.com/jekyll/jekyll/issues/9179 - # We should remove it when 9179 is solved. - # - # Note that this does NOT support news posts with a date in the URL. - "/training-material/#{@text.gsub(/\.md/, '.html')}" + def render(_context) + # This is a workaround for https://github.com/jekyll/jekyll/issues/9179 + # We should remove it when 9179 is solved. + # + # Note that this does NOT support news posts with a date in the URL. + "/training-material/#{@text.gsub(/\.md/, '.html')}" + end end end end -Liquid::Template.register_tag('link', Jekyll::CustomLinkTag) +Liquid::Template.register_tag('link', Jekyll::Tags::CustomLinkTag) diff --git a/_plugins/notebook-jupyter.rb b/_plugins/notebook-jupyter.rb index c5abc1399a416d..218e40163cce18 100644 --- a/_plugins/notebook-jupyter.rb +++ b/_plugins/notebook-jupyter.rb @@ -41,7 +41,7 @@ def jupyter_pre_render(site) site.config['__rendered_notebook_cache'] = {} # For every tutorial with the 'notebook' key in the page data - site.pages.select { |page| GTNNotebooks.notebook_filter(page.data) }.each do |page| + site.pages.select { |page| Gtn::Notebooks.notebook_filter(page.data) }.each do |page| # We get the path to the tutorial source dir = File.dirname(File.join('.', page.url)) fn = File.join('.', page.url).sub(/html$/, 'md') @@ -53,7 +53,7 @@ def jupyter_pre_render(site) Jekyll.logger.info "[GTN/Notebooks] Rendering #{notebook_language} #{fn}" last_modified = Gtn::ModificationTimes.obtain_time(page.path) - notebook = GTNNotebooks.render_jupyter_notebook(page.data, page.content, page.url, last_modified, + notebook = Gtn::Notebooks.render_jupyter_notebook(page.data, page.content, page.url, last_modified, notebook_language, site, dir) topic_id = dir.split('/')[-3] diff --git a/_plugins/notebook-rmarkdown.rb b/_plugins/notebook-rmarkdown.rb index 7cbf285f065daa..52ca6f82d8521c 100644 --- a/_plugins/notebook-rmarkdown.rb +++ b/_plugins/notebook-rmarkdown.rb @@ -5,40 +5,42 @@ require './_plugins/notebook' module Jekyll - # Generate RMarkdown documents from GTN markdown - class RmarkdownGenerator < Generator - safe true - - def generate(site) - # For every tutorial with the 'notebook' key in the page data - site.pages.select { |page| GTNNotebooks.notebook_filter(page.data, 'r') }.each do |page| - # We get the path to the tutorial source - dir = File.dirname(File.join('.', page.url)) - fn = File.join('.', page.url).sub(/html$/, 'Rmd') - - # Tag our source page - page.data['tags'] = page.data['tags'] || [] - page.data['tags'].push('rmarkdown-notebook') - - Jekyll.logger.info "[GTN/Notebooks/R] Rendering RMarkdown #{fn}" - last_modified = Gtn::ModificationTimes.obtain_time(page.path) - notebook = GTNNotebooks.render_rmarkdown(site, page.data, page.content, page.url, last_modified, fn) - - topic_id = dir.split('/')[-3] - tutorial_id = dir.split('/')[-1] - - # Write it out! - page2 = PageWithoutAFile.new(site, '', dir, "#{topic_id}-#{tutorial_id}.Rmd") - page2.content = notebook - page2.data['layout'] = nil - page2.data['citation_target'] = 'R' - site.pages << page2 + module Generators + # Generate RMarkdown documents from GTN markdown + class RmarkdownGenerator < Generator + safe true + + def generate(site) + # For every tutorial with the 'notebook' key in the page data + site.pages.select { |page| Gtn::Notebooks.notebook_filter(page.data, 'r') }.each do |page| + # We get the path to the tutorial source + dir = File.dirname(File.join('.', page.url)) + fn = File.join('.', page.url).sub(/html$/, 'Rmd') + + # Tag our source page + page.data['tags'] = page.data['tags'] || [] + page.data['tags'].push('rmarkdown-notebook') + + Jekyll.logger.info "[GTN/Notebooks/R] Rendering RMarkdown #{fn}" + last_modified = Gtn::ModificationTimes.obtain_time(page.path) + notebook = Gtn::Notebooks.render_rmarkdown(site, page.data, page.content, page.url, last_modified, fn) + + topic_id = dir.split('/')[-3] + tutorial_id = dir.split('/')[-1] + + # Write it out! + page2 = PageWithoutAFile.new(site, '', dir, "#{topic_id}-#{tutorial_id}.Rmd") + page2.content = notebook + page2.data['layout'] = nil + page2.data['citation_target'] = 'R' + site.pages << page2 + end + + page3 = PageWithoutAFile.new(site, '', File.join('assets', 'css'), 'r-notebook.css') + page3.content = Gtn::Notebooks.generate_css + page3.data['layout'] = nil + site.pages << page3 end - - page3 = PageWithoutAFile.new(site, '', File.join('assets', 'css'), 'r-notebook.css') - page3.content = GTNNotebooks.generate_css - page3.data['layout'] = nil - site.pages << page3 end end end diff --git a/_plugins/notebook.rb b/_plugins/notebook.rb index 42f6715f66952b..99a0445fedd904 100644 --- a/_plugins/notebook.rb +++ b/_plugins/notebook.rb @@ -12,545 +12,630 @@ def fetch2(key, default) end # Generate Notebooks from Markdown -module GTNNotebooks - COLORS = { - 'overview' => '#8A9AD0', - 'agenda' => '#86D486', - 'keypoints' => '#FFA1A1', - 'tip' => '#FFE19E', - 'warning' => '#de8875', - 'comment' => '#ffecc1', - 'hands_on' => '#dfe5f9', - 'question' => '#8A9AD0', - 'solution' => '#B8C3EA', - 'details' => '#ddd', - 'feedback' => '#86D486', - 'code-in' => '#86D486', - 'code-out' => '#fb99d0', - }.freeze - COLORS_EXTRA = { - 'agenda' => 'display: none', - }.freeze - - ICONS = { - 'tip' => '💡', - 'code-in' => '⌨️', - 'code-out' => '🖥', - 'question' => '❓', - 'solution' => '👁', - 'warning' => '⚠️', - 'comment' => '💬', - 'feedback' => '⁉️', - 'details' => '💬', - 'hands_on' => '✏️', - }.freeze - - ICONS_FA = { - 'far fa-keyboard' => 'code-in', - 'fas fa-laptop-code' => 'code-out', - 'far fa-comment-dots' => 'comment', - 'fas fa-info-circle' => 'details', - 'far fa-comments' => 'feedback', - 'fas fa-pencil-alt' => 'hands_on', - 'far fa-question-circle' => 'question', - 'far fa-eye' => 'solution', - 'far fa-lightbulb' => 'tip', - 'fas fa-exclamation-triangle' => 'warning', - }.freeze - - def self.generate_css - COLORS.map do |key, val| - ".#{key} { padding: 0 1em; margin: 1em 0.2em; border: 2px solid #{val} }" - end.join("\n") - end +module Gtn + ## + # Notebook generation module, this converts markdown into Jupyter and RMarkdown/Quarto notebooks + module Notebooks + + # Colors for the various boxes, based on our 2024 CSS + COLORS = { + 'overview' => '#8A9AD0', + 'agenda' => '#86D486', + 'keypoints' => '#FFA1A1', + 'tip' => '#FFE19E', + 'warning' => '#de8875', + 'comment' => '#ffecc1', + 'hands_on' => '#dfe5f9', + 'question' => '#8A9AD0', + 'solution' => '#B8C3EA', + 'details' => '#ddd', + 'feedback' => '#86D486', + 'code-in' => '#86D486', + 'code-out' => '#fb99d0', + }.freeze + + # +COLORS+ but hide the agenda box. + COLORS_EXTRA = { + 'agenda' => 'display: none', + }.freeze + + # Emoji icons for the various boxes + ICONS = { + 'tip' => '💡', + 'code-in' => '⌨️', + 'code-out' => '🖥', + 'question' => '❓', + 'solution' => '👁', + 'warning' => '⚠️', + 'comment' => '💬', + 'feedback' => '⁉️', + 'details' => '💬', + 'hands_on' => '✏️', + }.freeze + + # Font-awesome equivalents of the icons we use for our boxes + ICONS_FA = { + 'far fa-keyboard' => 'code-in', + 'fas fa-laptop-code' => 'code-out', + 'far fa-comment-dots' => 'comment', + 'fas fa-info-circle' => 'details', + 'far fa-comments' => 'feedback', + 'fas fa-pencil-alt' => 'hands_on', + 'far fa-question-circle' => 'question', + 'far fa-eye' => 'solution', + 'far fa-lightbulb' => 'tip', + 'fas fa-exclamation-triangle' => 'warning', + }.freeze + + # Generate the CSS to be included, by mapping our colors to appropriate classes. + def self.generate_css + COLORS.map do |key, val| + ".#{key} { padding: 0 1em; margin: 1em 0.2em; border: 2px solid #{val} }" + end.join("\n") + end - def self.convert_notebook_markdown(content, accepted_languages) - out = [] - inside_block = false - cur_lang = nil - val = [] - data = content.split("\n") - data.each.with_index do |line, i| - m = line.match(/^```(#{accepted_languages.join('|')})\s*$/) - if m - if inside_block - puts data[i - 2..i + 2] - raise "[GTN/Notebook] L#{i} Error! we're already in a block:" + ## + # Convert a markdown file into a Jupyter notebook JSON structure. + # + # Params: + # +content+:: The markdown content to convert + # +accepted_languages+:: The languages to accept as code blocks. Code blocks that do not match will not be accepted. + # + # Returns: + # +Hash+:: A JSON structure representing the Jupyter notebook. + def self.convert_notebook_markdown(content, accepted_languages) + out = [] + inside_block = false + cur_lang = nil + val = [] + data = content.split("\n") + data.each.with_index do |line, i| + m = line.match(/^```(#{accepted_languages.join('|')})\s*$/) + if m + if inside_block + puts data[i - 2..i + 2] + raise "[GTN/Notebook] L#{i} Error! we're already in a block:" + end + # End the previous block + out.push([val, inside_block, cur_lang]) + val = [] + + inside_block = true + cur_lang = m[1] + elsif inside_block && line == '```' + # End of code block + out.push([val, inside_block, cur_lang]) + val = [] + inside_block = false + else + val.push(line) end - # End the previous block - out.push([val, inside_block, cur_lang]) - val = [] - - inside_block = true - cur_lang = m[1] - elsif inside_block && line == '```' - # End of code block - out.push([val, inside_block, cur_lang]) - val = [] - inside_block = false - else - val.push(line) end - end - # final flush - out.push([val, inside_block, cur_lang]) if !val.nil? - - notebook = { - 'metadata' => {}, - 'nbformat' => 4, - 'nbformat_minor' => 5, - } - - notebook['cells'] = out.map.with_index do |data2, index| - res = { - 'id' => "cell-#{index}", - 'source' => data2[0].map { |x| "#{x.rstrip}\n" } + # final flush + out.push([val, inside_block, cur_lang]) if !val.nil? + + notebook = { + 'metadata' => {}, + 'nbformat' => 4, + 'nbformat_minor' => 5, } - # Strip the trailing newline in the last cell. - res['source'][-1] = res['source'][-1].rstrip if res['source'].length.positive? - - # Remove any remaining language tagged code blocks, e.g. in - # tip/solution/etc boxes. These do not render well. - res['source'] = res['source'].map { |x| x.gsub(/```(#{accepted_languages.join('|')})/, '```') } - - if data2[1] - res.update({ - 'cell_type' => 'code', - 'execution_count' => nil, - 'outputs' => [], - 'metadata' => { - 'attributes' => { - 'classes' => [ - data[2] - ], - 'id' => '', + + notebook['cells'] = out.map.with_index do |data2, index| + res = { + 'id' => "cell-#{index}", + 'source' => data2[0].map { |x| "#{x.rstrip}\n" } + } + # Strip the trailing newline in the last cell. + res['source'][-1] = res['source'][-1].rstrip if res['source'].length.positive? + + # Remove any remaining language tagged code blocks, e.g. in + # tip/solution/etc boxes. These do not render well. + res['source'] = res['source'].map { |x| x.gsub(/```(#{accepted_languages.join('|')})/, '```') } + + if data2[1] + res.update({ + 'cell_type' => 'code', + 'execution_count' => nil, + 'outputs' => [], + 'metadata' => { + 'attributes' => { + 'classes' => [ + data[2] + ], + 'id' => '', + } } - } - }) - else - res['cell_type'] = 'markdown' + }) + else + res['cell_type'] = 'markdown' + end + res end - res + notebook end - notebook - end - def self.group_doc_by_first_char(data) - out = [] - first_char = nil - val = [] - data = data.split("\n") - - # Here we collapse running groups of `>` into single blocks. - data.each do |line| - if first_char.nil? - first_char = line[0] - val = [line] - elsif line[0] == first_char - val.push(line) - elsif line[0..1] == '{:' && first_char == '>' - val.push(line) - else - # flush - out.push(val) - first_char = if line.size.positive? - line[0] - else - '' - end - val = [line] + ## + # Group a document by the first character seen, which extracts blockquotes mostly. + def self.group_doc_by_first_char(data) + out = [] + first_char = nil + val = [] + data = data.split("\n") + + # Here we collapse running groups of `>` into single blocks. + data.each do |line| + if first_char.nil? + first_char = line[0] + val = [line] + elsif line[0] == first_char + val.push(line) + elsif line[0..1] == '{:' && first_char == '>' + val.push(line) + else + # flush + out.push(val) + first_char = if line.size.positive? + line[0] + else + '' + end + val = [line] + end end - end - # final flush - out.push(val) + # final flush + out.push(val) - out.reject! do |v| - (v[0][0] == '>' && v[-1][0..1] == '{:' && v[-1].match(/.agenda/)) - end - out.map! do |v| - if v[0][0] == '>' && v[-1][0..1] == '{:' - cls = v[-1][2..-2].strip - res = [":::{#{cls}}"] - res += v[0..-2].map { |c| c.sub(/^>\s*/, '') } - res += [':::'] - res - else - v + out.reject! do |v| + (v[0][0] == '>' && v[-1][0..1] == '{:' && v[-1].match(/.agenda/)) + end + out.map! do |v| + if v[0][0] == '>' && v[-1][0..1] == '{:' + cls = v[-1][2..-2].strip + res = [":::{#{cls}}"] + res += v[0..-2].map { |c| c.sub(/^>\s*/, '') } + res += [':::'] + res + else + v + end end - end - out.flatten(1).join("\n") - end + out.flatten(1).join("\n") + end - def self.construct_byline(site, metadata) - folks = Gtn::Contributors.get_authors(metadata) - folks.map do |c| - name = Gtn::Contributors.fetch_name(site, c) - "[#{name}](https://training.galaxyproject.org/hall-of-fame/#{c}/)" - end.join(', ') - end + ## + # Construct a byline from the metadata + # + # Params: + # +site+:: The Jekyll site object + # +metadata+:: The metadata to construct the byline from, including a contributions or contributors key + # + # Returns: + # +String+:: The byline with markdown hyperlinks to the contributors + def self.construct_byline(site, metadata) + folks = Gtn::Contributors.get_authors(metadata) + folks.map do |c| + name = Gtn::Contributors.fetch_name(site, c) + "[#{name}](https://training.galaxyproject.org/hall-of-fame/#{c}/)" + end.join(', ') + end - def self.add_metadata_cell(site, notebook, metadata) - by_line = construct_byline(site, metadata) - - meta_header = [ - "<div style=\"border: 2px solid #8A9AD0; margin: 1em 0.2em; padding: 0.5em;\">\n\n", - "# #{metadata['title']}\n", - "\n", - "by #{by_line}\n", - "\n", - "#{metadata.fetch('license', 'CC-BY')} licensed content from the [Galaxy Training Network]" \ - "(https://training.galaxyproject.org/)\n", - "\n", - "**Objectives**\n", - "\n" - ] + metadata.fetch2('questions', []).map { |q| "- #{q}\n" } + [ - "\n", - "**Objectives**\n", - "\n" - ] + metadata.fetch2('objectives', []).map { |q| "- #{q}\n" } + [ - "\n", - "**Time Estimation: #{metadata['time_estimation']}**\n", - "\n", - "</div>\n" - ] - metadata_cell = { - 'id' => 'metadata', - 'cell_type' => 'markdown', - 'source' => meta_header - } - notebook['cells'].unshift(metadata_cell) - notebook - end + ## + # Given a notebook, add the metadata cell to the top of the notebook with the agenda, license, LOs, etc. + # + # Params: + # +site+:: The Jekyll site object + # +notebook+:: The notebook to add the metadata cell to + # +metadata+:: The page.data to construct use for metadata. + # + # Returns: + # +Hash+:: The updated notebook with the metadata cell added to the top. + def self.add_metadata_cell(site, notebook, metadata) + by_line = construct_byline(site, metadata) + + meta_header = [ + "<div style=\"border: 2px solid #8A9AD0; margin: 1em 0.2em; padding: 0.5em;\">\n\n", + "# #{metadata['title']}\n", + "\n", + "by #{by_line}\n", + "\n", + "#{metadata.fetch('license', 'CC-BY')} licensed content from the [Galaxy Training Network]" \ + "(https://training.galaxyproject.org/)\n", + "\n", + "**Objectives**\n", + "\n" + ] + metadata.fetch2('questions', []).map { |q| "- #{q}\n" } + [ + "\n", + "**Objectives**\n", + "\n" + ] + metadata.fetch2('objectives', []).map { |q| "- #{q}\n" } + [ + "\n", + "**Time Estimation: #{metadata['time_estimation']}**\n", + "\n", + "</div>\n" + ] + metadata_cell = { + 'id' => 'metadata', + 'cell_type' => 'markdown', + 'source' => meta_header + } + notebook['cells'].unshift(metadata_cell) + notebook + end - def self.fixRNotebook(notebook) - # Set the bash kernel - notebook['metadata'] = { - 'kernelspec' => { - 'display_name' => 'R', - 'language' => 'R', - 'name' => 'r' - }, - 'language_info' => { - 'codemirror_mode' => 'r', - 'file_extension' => '.r', - 'mimetype' => 'text/x-r-source', - 'name' => 'R', - 'pygments_lexer' => 'r', - 'version' => '4.1.0' + ## + # Fix an R based Jupyter notebook by setting the kernel to R and stripping out the %%R magic commands. + def self.fixRNotebook(notebook) + # Set the bash kernel + notebook['etadata'] = { + 'kernelspec' => { + 'display_name' => 'R', + 'language' => 'R', + 'name' => 'r' + }, + 'language_info' => { + 'codemirror_mode' => 'r', + 'file_extension' => '.r', + 'mimetype' => 'text/x-r-source', + 'name' => 'R', + 'pygments_lexer' => 'r', + 'version' => '4.1.0' + } } - } - # Strip out %%R since we'll use the bash kernel - notebook['cells'].map do |cell| - if cell.fetch('cell_type') == 'code' && (cell['source'][0] == "%%R\n") - cell['source'] = cell['source'].slice(1..-1) + # Strip out %%R since we'll use the bash kernel + notebook['cells'].map do |cell| + if cell.fetch('cell_type') == 'code' && (cell['source'][0] == "%%R\n") + cell['source'] = cell['source'].slice(1..-1) + end + cell end - cell + notebook end - notebook - end - def self.fixBashNotebook(notebook) - # Set the bash kernel - notebook['metadata'] = { - 'kernelspec' => { - 'display_name' => 'Bash', - 'language' => 'bash', - 'name' => 'bash' - }, - 'language_info' => { - 'codemirror_mode' => 'shell', - 'file_extension' => '.sh', - 'mimetype' => 'text/x-sh', - 'name' => 'bash' + ## + # Similar to +fixRNotebook+ but for bash. + def self.fixBashNotebook(notebook) + # Set the bash kernel + notebook['metadata'] = { + 'kernelspec' => { + 'display_name' => 'Bash', + 'language' => 'bash', + 'name' => 'bash' + }, + 'language_info' => { + 'codemirror_mode' => 'shell', + 'file_extension' => '.sh', + 'mimetype' => 'text/x-sh', + 'name' => 'bash' + } } - } - # Strip out %%bash since we'll use the bash kernel - notebook['cells'].map do |cell| - if cell.fetch('cell_type') == 'code' && (cell['source'][0] == "%%bash\n") - cell['source'] = cell['source'].slice(1..-1) + # Strip out %%bash since we'll use the bash kernel + notebook['cells'].map do |cell| + if cell.fetch('cell_type') == 'code' && (cell['source'][0] == "%%bash\n") + cell['source'] = cell['source'].slice(1..-1) + end + cell end - cell + notebook end - notebook - end - def self.fixPythonNotebook(notebook) - # TODO - # prefix bash cells with `!` - notebook['cells'].map do |cell| - if cell.fetch('metadata', {}).fetch('attributes', {}).fetch('classes', [])[0] == 'bash' - cell['source'] = cell['source'].map { |line| "!#{line}" } + ## + # Similar to +fixRNotebook+ but for Python, bash cells are accepted but must be prefixed with ! + def self.fixPythonNotebook(notebook) + # TODO + # prefix bash cells with `!` + notebook['cells'].map do |cell| + if cell.fetch('metadata', {}).fetch('attributes', {}).fetch('classes', [])[0] == 'bash' + cell['source'] = cell['source'].map { |line| "!#{line}" } + end + cell end - cell + notebook end - notebook - end - def self.fixSqlNotebook(notebook) - # Add in a %%sql at the top of each cell - notebook['cells'].map do |cell| - if cell.fetch('cell_type') == 'code' && cell['source'].join.index('load_ext').nil? - cell['source'] = ["%%sql\n"] + cell['source'] + ## + # Ibid, +fixRNotebook+ but for SQL. + def self.fixSqlNotebook(notebook) + # Add in a %%sql at the top of each cell + notebook['cells'].map do |cell| + if cell.fetch('cell_type') == 'code' && cell['source'].join.index('load_ext').nil? + cell['source'] = ["%%sql\n"] + cell['source'] + end + cell end - cell + notebook end - notebook - end - def self.markdownify(site, text) - site.find_converter_instance( - Jekyll::Converters::Markdown - ).convert(text.to_s) - rescue StandardError - require 'kramdown' - Kramdown::Document.new(text).to_html - end + ## + # Call Jekyll's markdown plugin or failover to Kramdown + # + # I have no idea why that failure mode is supported, that's kinda wild. + # + # Params: + # +site+:: The Jekyll site object + # +text+:: The text to convert to html + # + # Returns: + # +String+:: The HTML representation + def self.markdownify(site, text) + site.find_converter_instance( + Jekyll::Converters::Markdown + ).convert(text.to_s) + rescue StandardError + require 'kramdown' + Kramdown::Document.new(text).to_html + end - def self.notebook_filter(data, language = nil) - data['layout'] == 'tutorial_hands_on' \ - and data.key?('notebook') \ - and (language.nil? or data['notebook']['language'].downcase == language) - end + ## + # Return true if it's a notebook and the language is correct + # + # TODO: convert to `notebook?` which is more ruby-esque. + # + # +data+:: The page data to check + # +language+:: The language to check for + # + # Returns: + # +Boolean+:: True if it's a notebook (i.e hands on tutorial, has a notebook key, and the language is correct) + def self.notebook_filter(data, language = nil) + data['layout'] == 'tutorial_hands_on' \ + and data.key?('notebook') \ + and (language.nil? or data['notebook']['language'].downcase == language) + end - def self.render_rmarkdown(site, page_data, page_content, page_url, page_last_modified, fn) - by_line = construct_byline(site, page_data) + ## + # Massage a page into RMarkdown preferred formatting. + # + # Params: + # +site+:: The Jekyll site object + # +page_data+:: The page metadata (page.data) + # +page_content+:: The page content (page.content) + # +page_url+:: The page URL + # +page_last_modified+:: The last modified time of the page + # +fn+:: The source filename of the page + # + # Returns: + # +String+:: The RMarkdown formatted content + # + def self.render_rmarkdown(site, page_data, page_content, page_url, page_last_modified, fn) + by_line = construct_byline(site, page_data) + + # Replace top level `>` blocks with fenced `:::` + content = group_doc_by_first_char(page_content) + + # Re-run a second time to catch singly-nested Q&A? + content = group_doc_by_first_char(content) + + # Replace zenodo links, the only replacement we do + if !page_data['zenodo_link'].nil? + Jekyll.logger.debug "Replacing zenodo links in #{page_url}, #{page_data['zenodo_link']}" + content.gsub!(/{{\s*page.zenodo_link\s*}}/, page_data['zenodo_link']) + end - # Replace top level `>` blocks with fenced `:::` - content = group_doc_by_first_char(page_content) + ICONS.each do |key, val| + content.gsub!(/{% icon #{key} %}/, val) + end + ICONS_FA.each do |key, val| + content.gsub!(%r{<i class="#{key}" aria-hidden="true"></i>}, ICONS[val]) + end - # Re-run a second time to catch singly-nested Q&A? - content = group_doc_by_first_char(content) + content += %(\n\n# References\n\n<div id="refs"></div>\n) + + # https://raw.githubusercontent.com/rstudio/cheatsheets/master/rmarkdown-2.0.pdf + # https://bookdown.org/yihui/rmarkdown/ + + fnparts = fn.split('/') + rmddata = { + 'title' => page_data['title'], + 'author' => "#{by_line}, #{page_data.fetch('license', + 'CC-BY')} licensed content from the [Galaxy Training Network](https://training.galaxyproject.org/)", + 'bibliography' => "#{fnparts[2]}-#{fnparts[4]}.bib", + 'output' => { + 'html_notebook' => { + 'toc' => true, + 'toc_depth' => 2, + 'css' => 'gtn.css', + 'toc_float' => { + 'collapsed' => false, + 'smooth_scroll' => false, + }, + # 'theme' => {'bootswatch' => 'journal'} + }, + 'word_document' => { + 'toc' => true, + 'toc_depth' => 2, + 'latex_engine' => 'xelatex', + }, + 'pdf_document' => { + 'toc' => true, + 'toc_depth' => 2, + 'latex_engine' => 'xelatex', + }, + }, + 'date' => page_last_modified.to_s, + 'link-citations' => true, + 'anchor_sections' => true, + 'code_download' => true, + } + rmddata['output']['html_document'] = JSON.parse(JSON.generate(rmddata['output']['html_notebook'])) + + final_content = [ + "# Introduction\n", + content.gsub(/```[Rr]/, '```{r}'), + "# Key Points\n" + ] + page_data.fetch2('key_points', []).map { |k| "- #{k}" } + [ + "\n# Congratulations on successfully completing this tutorial!\n", + 'Please [fill out the feedback on the GTN website](https://training.galaxyproject.org/' \ + "training-material#{page_url}#feedback) and check there for further resources!\n" + ] - # Replace zenodo links, the only replacement we do - if !page_data['zenodo_link'].nil? - Jekyll.logger.debug "Replacing zenodo links in #{page_url}, #{page_data['zenodo_link']}" - content.gsub!(/{{\s*page.zenodo_link\s*}}/, page_data['zenodo_link']) + "#{rmddata.to_yaml(line_width: rmddata['author'].size + 10)}---\n#{final_content.join("\n")}" end - ICONS.each do |key, val| - content.gsub!(/{% icon #{key} %}/, val) - end - ICONS_FA.each do |key, val| - content.gsub!(%r{<i class="#{key}" aria-hidden="true"></i>}, ICONS[val]) - end - content += %(\n\n# References\n\n<div id="refs"></div>\n) - - # https://raw.githubusercontent.com/rstudio/cheatsheets/master/rmarkdown-2.0.pdf - # https://bookdown.org/yihui/rmarkdown/ - - fnparts = fn.split('/') - rmddata = { - 'title' => page_data['title'], - 'author' => "#{by_line}, #{page_data.fetch('license', - 'CC-BY')} licensed content from the [Galaxy Training Network](https://training.galaxyproject.org/)", - 'bibliography' => "#{fnparts[2]}-#{fnparts[4]}.bib", - 'output' => { - 'html_notebook' => { - 'toc' => true, - 'toc_depth' => 2, - 'css' => 'gtn.css', - 'toc_float' => { - 'collapsed' => false, - 'smooth_scroll' => false, - }, - # 'theme' => {'bootswatch' => 'journal'} - }, - 'word_document' => { - 'toc' => true, - 'toc_depth' => 2, - 'latex_engine' => 'xelatex', - }, - 'pdf_document' => { - 'toc' => true, - 'toc_depth' => 2, - 'latex_engine' => 'xelatex', - }, - }, - 'date' => page_last_modified.to_s, - 'link-citations' => true, - 'anchor_sections' => true, - 'code_download' => true, - } - rmddata['output']['html_document'] = JSON.parse(JSON.generate(rmddata['output']['html_notebook'])) - - final_content = [ - "# Introduction\n", - content.gsub(/```[Rr]/, '```{r}'), - "# Key Points\n" - ] + page_data.fetch2('key_points', []).map { |k| "- #{k}" } + [ - "\n# Congratulations on successfully completing this tutorial!\n", - 'Please [fill out the feedback on the GTN website](https://training.galaxyproject.org/' \ - "training-material#{page_url}#feedback) and check there for further resources!\n" - ] - - "#{rmddata.to_yaml(line_width: rmddata['author'].size + 10)}---\n#{final_content.join("\n")}" - end + def self.render_jupyter_notebook(data, content, url, _last_modified, notebook_language, site, dir) + # Here we read use internal methods to convert the tutorial to a Hash + # representing the notebook + accepted_languages = [notebook_language] + accepted_languages << 'bash' if notebook_language == 'python' - def self.render_jupyter_notebook(data, content, url, _last_modified, notebook_language, site, dir) - # Here we read use internal methods to convert the tutorial to a Hash - # representing the notebook - accepted_languages = [notebook_language] - accepted_languages << 'bash' if notebook_language == 'python' + if !data['zenodo_link'].nil? + Jekyll.logger.debug "Replacing zenodo links in #{url}, #{data['zenodo_link']}" + content.gsub!(/{{\s*page.zenodo_link\s*}}/, data['zenodo_link']) + end + notebook = convert_notebook_markdown(content, accepted_languages) + # This extracts the metadata yaml header and does manual formatting of + # the header data to make for a nicer notebook. + notebook = add_metadata_cell(site, notebook, data) + + # Apply language specific conventions + case notebook_language + when 'bash' + notebook = fixBashNotebook(notebook) + when 'sql' + notebook = fixSqlNotebook(notebook) + when 'r' + notebook = fixRNotebook(notebook) + when 'python' + notebook = fixPythonNotebook(notebook) + end - if !data['zenodo_link'].nil? - Jekyll.logger.debug "Replacing zenodo links in #{url}, #{data['zenodo_link']}" - content.gsub!(/{{\s*page.zenodo_link\s*}}/, data['zenodo_link']) - end - notebook = convert_notebook_markdown(content, accepted_languages) - # This extracts the metadata yaml header and does manual formatting of - # the header data to make for a nicer notebook. - notebook = add_metadata_cell(site, notebook, data) - - # Apply language specific conventions - case notebook_language - when 'bash' - notebook = fixBashNotebook(notebook) - when 'sql' - notebook = fixSqlNotebook(notebook) - when 'r' - notebook = fixRNotebook(notebook) - when 'python' - notebook = fixPythonNotebook(notebook) + # Here we loop over the markdown cells and render them to HTML. This + # allows us to get rid of classes like {: .tip} that would be left in + # the output by Jupyter's markdown renderer, and additionally do any + # custom CSS which only seems to work when inline on a cell, i.e. we + # can't setup a style block, so we really need to render the markdown + # to html. + notebook = renderMarkdownCells(site, notebook, data, url, dir) + + # Here we add a close to the notebook + notebook['cells'] = notebook['cells'] + [{ + 'cell_type' => 'markdown', + 'id' => 'final-ending-cell', + 'metadata' => { 'editable' => false, 'collapsed' => false }, + 'source' => [ + "# Key Points\n\n" + ] + data.fetch2('key_points', []).map { |k| "- #{k}\n" } + [ + "\n# Congratulations on successfully completing this tutorial!\n\n", + 'Please [fill out the feedback on the GTN website](https://training.galaxyproject.org/training-material' \ + "#{url}#feedback) and check there for further resources!\n" + ] + }] + notebook end - # Here we loop over the markdown cells and render them to HTML. This - # allows us to get rid of classes like {: .tip} that would be left in - # the output by Jupyter's markdown renderer, and additionally do any - # custom CSS which only seems to work when inline on a cell, i.e. we - # can't setup a style block, so we really need to render the markdown - # to html. - notebook = renderMarkdownCells(site, notebook, data, url, dir) - - # Here we add a close to the notebook - notebook['cells'] = notebook['cells'] + [{ - 'cell_type' => 'markdown', - 'id' => 'final-ending-cell', - 'metadata' => { 'editable' => false, 'collapsed' => false }, - 'source' => [ - "# Key Points\n\n" - ] + data.fetch2('key_points', []).map { |k| "- #{k}\n" } + [ - "\n# Congratulations on successfully completing this tutorial!\n\n", - 'Please [fill out the feedback on the GTN website](https://training.galaxyproject.org/training-material' \ - "#{url}#feedback) and check there for further resources!\n" - ] - }] - notebook - end - - def self.renderMarkdownCells(site, notebook, metadata, _page_url, dir) - seen_abbreviations = {} - notebook['cells'].map do |cell| - if cell.fetch('cell_type') == 'markdown' + def self.renderMarkdownCells(site, notebook, metadata, _page_url, dir) + seen_abbreviations = {} + notebook['cells'].map do |cell| + if cell.fetch('cell_type') == 'markdown' - # The source is initially a list of strings, we'll merge it together - # to make it easier to work with. - source = cell['source'].join.strip + # The source is initially a list of strings, we'll merge it together + # to make it easier to work with. + source = cell['source'].join.strip - # Here we replace individual `s with codeblocks, they screw up - # rendering otherwise by going through rouge - source = source.gsub(/ `([^`]*)`([^`])/, ' <code>\1</code>\2') - .gsub(/([^`])`([^`]*)` /, '\1<code>\2</code> ') + # Here we replace individual `s with codeblocks, they screw up + # rendering otherwise by going through rouge + source = source.gsub(/ `([^`]*)`([^`])/, ' <code>\1</code>\2') + .gsub(/([^`])`([^`]*)` /, '\1<code>\2</code> ') - # Strip out includes, snippets - source.gsub!(/{% include .* %}/, '') - source.gsub!(/{% snippet .* %}/, '') + # Strip out includes, snippets + source.gsub!(/{% include .* %}/, '') + source.gsub!(/{% snippet .* %}/, '') - # Replace all the broken icons that can't render, because we don't - # have access to the full render pipeline. - cell['source'] = markdownify(site, source) + # Replace all the broken icons that can't render, because we don't + # have access to the full render pipeline. + cell['source'] = markdownify(site, source) - ICONS.each do |key, val| - # Replace the new box titles with h3s. - cell['source'].gsub!(%r{<div class="box-title #{key}-title".*?</span>(.*?)</div>}, - "<div style=\"font-weight:900;font-size: 125%\">#{val} \\1</div>") + ICONS.each do |key, val| + # Replace the new box titles with h3s. + cell['source'].gsub!(%r{<div class="box-title #{key}-title".*?</span>(.*?)</div>}, + "<div style=\"font-weight:900;font-size: 125%\">#{val} \\1</div>") - # Remove the fa-icon spans - cell['source'].gsub!(%r{<span role="button" class="fold-unfold fa fa-minus-square"></span>}, '') + # Remove the fa-icon spans + cell['source'].gsub!(%r{<span role="button" class="fold-unfold fa fa-minus-square"></span>}, '') - # just removing the buttons from solutions since they'll be changed - # into summary/details in the parent notebook-jupyter. - cell['source'].gsub!(%r{<button class="gtn-boxify-button solution".*?</button>}, '') - end - - if metadata.key?('abbreviations') - metadata['abbreviations'].each do |abbr, defn| - cell['source'].gsub(/\{#{abbr}\}/) do - if seen_abbreviations.key?(abbr) - firstdef = false - else - firstdef = true - seen_abbreviations[abbr] = true - end + # just removing the buttons from solutions since they'll be changed + # into summary/details in the parent notebook-jupyter. + cell['source'].gsub!(%r{<button class="gtn-boxify-button solution".*?</button>}, '') + end - if firstdef - "#{defn} (#{abbr})" - else - "<abbr title=\"#{defn}\">#{abbr}</abbr>" + if metadata.key?('abbreviations') + metadata['abbreviations'].each do |abbr, defn| + cell['source'].gsub(/\{#{abbr}\}/) do + if seen_abbreviations.key?(abbr) + firstdef = false + else + firstdef = true + seen_abbreviations[abbr] = true + end + + if firstdef + "#{defn} (#{abbr})" + else + "<abbr title=\"#{defn}\">#{abbr}</abbr>" + end end end end - end - # Here we give a GTN-ish styling that doesn't try to be too faithful, - # so we aren't spending time keeping up with changes to GTN css, - # we're making it 'our own' a bit. + # Here we give a GTN-ish styling that doesn't try to be too faithful, + # so we aren't spending time keeping up with changes to GTN css, + # we're making it 'our own' a bit. - COLORS.each do |key, val| - val = "#{val};#{COLORS_EXTRA[key]}" if COLORS_EXTRA.key? key + COLORS.each do |key, val| + val = "#{val};#{COLORS_EXTRA[key]}" if COLORS_EXTRA.key? key - cell['source'].gsub!(/<blockquote class="#{key}">/, - "<blockquote class=\"#{key}\" style=\"border: 2px solid #{val}; margin: 1em 0.2em\">") - end + cell['source'].gsub!(/<blockquote class="#{key}">/, + "<blockquote class=\"#{key}\" style=\"border: 2px solid #{val}; margin: 1em 0.2em\">") + end - # Images are referenced in the through relative URLs which is - # fab, but in a notebook this doesn't make sense as it will live - # outside of the GTN. We need real URLs. - # - # So either we'll embed the images directly via base64 encoding (cool, - # love it) or we'll link to the production images and folks can live - # without their images for a bit until it's merged. - - if cell['source'].match(/<img src="\.\./) - cell['source'].gsub!(/<img src="(\.\.[^"]*)/) do |img| - path = img[10..] - image_path = File.join(dir, path) - - if img[-3..].downcase == 'png' - data = Base64.encode64(File.binread(image_path)) - %(<img src="data:image/png;base64,#{data}") - elsif (img[-3..].downcase == 'jpg') || (img[-4..].downcase == 'jpeg') - data = Base64.encode64(File.binread(image_path)) - %(<img src="data:image/jpeg;base64,#{data}") - elsif img[-3..].downcase == 'svg' - data = Base64.encode64(File.binread(image_path)) - %(<img src="data:image/svg+xml;base64,#{data}") - else - # Falling back to non-embedded images - "<img src=\"https://training.galaxyproject.org/training-material/#{page_url.split('/')[0..-2].join('/')}/.." + # Images are referenced in the through relative URLs which is + # fab, but in a notebook this doesn't make sense as it will live + # outside of the GTN. We need real URLs. + # + # So either we'll embed the images directly via base64 encoding (cool, + # love it) or we'll link to the production images and folks can live + # without their images for a bit until it's merged. + + if cell['source'].match(/<img src="\.\./) + cell['source'].gsub!(/<img src="(\.\.[^"]*)/) do |img| + path = img[10..] + image_path = File.join(dir, path) + + if img[-3..].downcase == 'png' + data = Base64.encode64(File.binread(image_path)) + %(<img src="data:image/png;base64,#{data}") + elsif (img[-3..].downcase == 'jpg') || (img[-4..].downcase == 'jpeg') + data = Base64.encode64(File.binread(image_path)) + %(<img src="data:image/jpeg;base64,#{data}") + elsif img[-3..].downcase == 'svg' + data = Base64.encode64(File.binread(image_path)) + %(<img src="data:image/svg+xml;base64,#{data}") + else + # Falling back to non-embedded images + "<img src=\"https://training.galaxyproject.org/training-material/#{page_url.split('/')[0..-2].join('/')}/.." + end end end - end - # Strip out the highlighting as it is bad on some platforms. - cell['source'].gsub!(/<pre class="highlight">/, '<pre style="color: inherit; background: transparent">') - cell['source'].gsub!(/<div class="highlight">/, '<div>') - cell['source'].gsub!(/<code>/, '<code style="color: inherit">') - - # There is some weirdness in the processing of $s in Jupyter. After a - # certain number of them, it will give up, and just render everything - # like with a '<pre>'. We remove this to prevent that result. - cell['source'].gsub!(/^\s*</, '<') - # Additionally leading spaces are sometimes interpreted as <pre>s and - # end up causing paragraphs to be rendered as code. So we wipe out - # all leading space. - # 'editable' is actually CoCalc specific but oh well. - cell['metadata'] = { 'editable' => false, 'collapsed' => false } - cell['source'].gsub!(/\$/, '$') + # Strip out the highlighting as it is bad on some platforms. + cell['source'].gsub!(/<pre class="highlight">/, '<pre style="color: inherit; background: transparent">') + cell['source'].gsub!(/<div class="highlight">/, '<div>') + cell['source'].gsub!(/<code>/, '<code style="color: inherit">') + + # There is some weirdness in the processing of $s in Jupyter. After a + # certain number of them, it will give up, and just render everything + # like with a '<pre>'. We remove this to prevent that result. + cell['source'].gsub!(/^\s*</, '<') + # Additionally leading spaces are sometimes interpreted as <pre>s and + # end up causing paragraphs to be rendered as code. So we wipe out + # all leading space. + # 'editable' is actually CoCalc specific but oh well. + cell['metadata'] = { 'editable' => false, 'collapsed' => false } + cell['source'].gsub!(/\$/, '$') + end + cell end - cell + notebook end - notebook end end diff --git a/_plugins/plaintext-slides.rb b/_plugins/plaintext-slides.rb index a622a110dd6930..e8da8d54f389b2 100644 --- a/_plugins/plaintext-slides.rb +++ b/_plugins/plaintext-slides.rb @@ -1,44 +1,46 @@ # frozen_string_literal: true module Jekyll - # Convert our slides to plaintext - # It's not a great convesion, the CSS classes are retained which are ugly - # But there's no good way to parse those out since they use a wildly nonstandard syntax - class PlaintextSlidesGenerator < Generator - SLIDE_LAYOUTS = %w[ - tutorial_slides - base_slides - introduction_slides - tutorial_slides_ai4life - ].freeze + module Generators + # Convert our slides to plaintext + # It's not a great convesion, the CSS classes are retained which are ugly + # But there's no good way to parse those out since they use a wildly nonstandard syntax + class PlaintextSlidesGenerator < Generator + SLIDE_LAYOUTS = %w[ + tutorial_slides + base_slides + introduction_slides + tutorial_slides_ai4life + ].freeze - ## - # Generate a plaintext version of the slides - # Params: - # +site+:: The +Jekyll::Site+ object - def generate(site) - # layout: tutorial_slides - # layout: base_slides + ## + # Generate a plaintext version of the slides + # Params: + # +site+:: The +Jekyll::Site+ object + def generate(site) + # layout: tutorial_slides + # layout: base_slides - site.pages.select { |page| SLIDE_LAYOUTS.include? page.data['layout'] }.each do |page| - dir = File.dirname(File.join('.', page.url)) - page2 = Jekyll::Page.new(site, site.source, dir, page.name) - page2.data['layout'] = 'slides-plain' - page2.basename = if page2.data.key?('lang') - "slides-plain_#{page2.data['lang'].upcase}" - else - 'slides-plain' - end - page2.content = page2.content.gsub(/^name:\s*([^ ]+)\s*$/) do - anchor = ::Regexp.last_match(1) + site.pages.select { |page| SLIDE_LAYOUTS.include? page.data['layout'] }.each do |page| + dir = File.dirname(File.join('.', page.url)) + page2 = Jekyll::Page.new(site, site.source, dir, page.name) + page2.data['layout'] = 'slides-plain' + page2.basename = if page2.data.key?('lang') + "slides-plain_#{page2.data['lang'].upcase}" + else + 'slides-plain' + end + page2.content = page2.content.gsub(/^name:\s*([^ ]+)\s*$/) do + anchor = ::Regexp.last_match(1) - "<span id=\"#{anchor.strip}\"><i class=\"fas fa-link\" aria-hidden=\"true\"></i> #{anchor}</span>" - end - if page2.data.key?('redirect_from') - page2.data['redirect_from'].map { |x| x.gsub!(%r{/slides}, '/slides-plain') } - end + "<span id=\"#{anchor.strip}\"><i class=\"fas fa-link\" aria-hidden=\"true\"></i> #{anchor}</span>" + end + if page2.data.key?('redirect_from') + page2.data['redirect_from'].map { |x| x.gsub!(%r{/slides}, '/slides-plain') } + end - site.pages << page2 + site.pages << page2 + end end end end diff --git a/_plugins/search.rb b/_plugins/search.rb index d671258e190677..6b2a2de46a2377 100644 --- a/_plugins/search.rb +++ b/_plugins/search.rb @@ -6,66 +6,76 @@ require './_plugins/jekyll-topic-filter' module Jekyll - # Export search data as JSON - class DumpSearchDataTag < Liquid::Tag - def initialize(tag_name, text, tokens) - super - @text = text.strip - end + module Tags - def getlist(tutorial, attr) - tutorial[attr] || [] - end + # Class to support exporting search data as JSON + class DumpSearchDataTag < Liquid::Tag + def initialize(tag_name, text, tokens) + super + @text = text.strip + end - def render(context) - if Jekyll.env != 'production' - Jekyll.logger.info '[GTN/Search] Skipping search generation in development' - return + def getlist(tutorial, attr) + tutorial[attr] || [] end - Jekyll.logger.info '[GTN/Search]' - site = context.registers[:site] - topics = TopicFilter.list_topics_h(site) + ## + # (PRODUCTION ONLY) Export a large JSON blob with records like + # + # [{type:Tutorial, topic: ..., title: ..., contributors: [..], tags: [<a href="...">tag</a>...], url: ...} + # + # Example + # {{ dump_search_view testing }} + def render(context) + if Jekyll.env != 'production' + Jekyll.logger.info '[GTN/Search] Skipping search generation in development' + return + end + Jekyll.logger.info '[GTN/Search]' + + site = context.registers[:site] + topics = Gtn::TopicFilter.list_topics_h(site) - results = {} - topics.each do |k, topic| - tutorials = site.data['cache_topic_filter'][k] - tutorials.each do |tutorial| - results[tutorial['url']] = { - 'type' => 'Tutorial', - 'topic' => topic['title'], - 'title' => tutorial['title'], - 'contributors' => getlist(tutorial, 'contributors').map do |c| + results = {} + topics.each do |k, topic| + tutorials = site.data['cache_topic_filter'][k] + tutorials.each do |tutorial| + results[tutorial['url']] = { + 'type' => 'Tutorial', + 'topic' => topic['title'], + 'title' => tutorial['title'], + 'contributors' => getlist(tutorial, 'contributors').map do |c| + site.data['contributors'].fetch(c, {}).fetch('name', c) + end.join(', '), + 'tags' => getlist(tutorial, 'tags').map do |tag| + href = "#{site.baseurl}/search?query=#{tag}" + title = "Show all tutorials tagged #{tag}" + style = Gtn::HashedColours.colour_tag tag + %(<a class="label label-default" title="#{title}" href="#{href}" style="#{style}">#{tag}</a>) + end, + 'url' => site.baseurl + tutorial['url'], + } + end + end + + faqs = site.pages.select { |p| p.data['layout'] == 'faq' } + faqs.each do |resource| + results[resource['url']] = { + 'type' => 'FAQ', + 'topic' => 'FAQ', + 'title' => resource['title'], + 'contributors' => getlist(resource.data, 'contributors').map do |c| site.data['contributors'].fetch(c, {}).fetch('name', c) end.join(', '), - 'tags' => getlist(tutorial, 'tags').map do |tag| - href = "#{site.baseurl}/search?query=#{tag}" - title = "Show all tutorials tagged #{tag}" - style = ColourTag.colour_tag tag - %(<a class="label label-default" title="#{title}" href="#{href}" style="#{style}">#{tag}</a>) - end, - 'url' => site.baseurl + tutorial['url'], + 'tags' => [], + 'url' => site.baseurl + resource['url'], } end - end - faqs = site.pages.select { |p| p.data['layout'] == 'faq' } - faqs.each do |resource| - results[resource['url']] = { - 'type' => 'FAQ', - 'topic' => 'FAQ', - 'title' => resource['title'], - 'contributors' => getlist(resource.data, 'contributors').map do |c| - site.data['contributors'].fetch(c, {}).fetch('name', c) - end.join(', '), - 'tags' => [], - 'url' => site.baseurl + resource['url'], - } + JSON.pretty_generate(results) end - - JSON.pretty_generate(results) end end end -Liquid::Template.register_tag('dump_search_view', Jekyll::DumpSearchDataTag) +Liquid::Template.register_tag('dump_search_view', Jekyll::Tags::DumpSearchDataTag) diff --git a/_plugins/sitemap.rb b/_plugins/sitemap.rb index 010cfa28a50e39..f1c382475c51e4 100644 --- a/_plugins/sitemap.rb +++ b/_plugins/sitemap.rb @@ -1,61 +1,63 @@ # frozen_string_literal: true module Jekyll - # Generate a sitemap like Jekyll::Sitemap - class SitemapGenerator2 < Generator - safe true - - ## - # Generate a sitemap.xml file - # We reimplement the default Jekyll sitemap generator, because we want to - # leverage the GTN::ModificationTimes class to obtain the last modification - # date of a page, in a more efficient way than the default Jekyll sitemap - # - # Params: - # +site+:: The +Jekyll::Site+ object - def generate(site) - if Jekyll.env == 'production' - _build(site) - else - Jekyll.logger.info '[GTN/Sitemap] Skipping in development mode' + module Generators + # Generate a sitemap like Jekyll::Sitemap + class SitemapGenerator2 < Generator + safe true + + ## + # Generate a sitemap.xml file + # We reimplement the default Jekyll sitemap generator, because we want to + # leverage the GTN::ModificationTimes class to obtain the last modification + # date of a page, in a more efficient way than the default Jekyll sitemap + # + # Params: + # +site+:: The +Jekyll::Site+ object + def generate(site) + if Jekyll.env == 'production' + _build(site) + else + Jekyll.logger.info '[GTN/Sitemap] Skipping in development mode' + end end - end - def _build(site) - # We import later in case we don't need to bother importing in the first place. - require 'date' - require './_plugins/gtn' - - Jekyll.logger.info '[GTN/Sitemap] Generating' - result = '<?xml version="1.0" encoding="UTF-8"?>' - result += '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' \ - 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 ' \ - 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" ' \ - 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' - - subset_pages = site.pages - .reject { |t| t.path =~ /ipynb$/ || t.path =~ /api\/ga4gh\/trs\/v2/} - .reject { |t| t.data.fetch('layout', 'page') =~ /external/} - .reject { |t| t.data.fetch('hands_on', '') == 'external'} - - subset_pages.each do |t| - begin - d = Gtn::ModificationTimes.obtain_time(t.path) - d.format = '%FT%T%:z' - formatted_date = d.to_s - rescue StandardError - d = Time.new - formatted_date = d.strftime('%FT%T%:z') + def _build(site) + # We import later in case we don't need to bother importing in the first place. + require 'date' + require './_plugins/gtn' + + Jekyll.logger.info '[GTN/Sitemap] Generating' + result = '<?xml version="1.0" encoding="UTF-8"?>' + result += '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' \ + 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 ' \ + 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" ' \ + 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + + subset_pages = site.pages + .reject { |t| t.path =~ /ipynb$/ || t.path =~ /api\/ga4gh\/trs\/v2/} + .reject { |t| t.data.fetch('layout', 'page') =~ /external/} + .reject { |t| t.data.fetch('hands_on', '') == 'external'} + + subset_pages.each do |t| + begin + d = Gtn::ModificationTimes.obtain_time(t.path) + d.format = '%FT%T%:z' + formatted_date = d.to_s + rescue StandardError + d = Time.new + formatted_date = d.strftime('%FT%T%:z') + end + + result += "<url><loc>#{site.config['url'] + site.config['baseurl'] + t.url}</loc>" \ + "<lastmod>#{formatted_date}</lastmod></url>" end + result += '</urlset>' - result += "<url><loc>#{site.config['url'] + site.config['baseurl'] + t.url}</loc>" \ - "<lastmod>#{formatted_date}</lastmod></url>" + page2 = PageWithoutAFile.new(site, '', '.', 'sitemap.xml') + page2.content = result + site.pages << page2 end - result += '</urlset>' - - page2 = PageWithoutAFile.new(site, '', '.', 'sitemap.xml') - page2.content = result - site.pages << page2 end end end diff --git a/_plugins/util.rb b/_plugins/util.rb index df5cb2cef38808..5732a200cdb72b 100644 --- a/_plugins/util.rb +++ b/_plugins/util.rb @@ -1,5 +1,41 @@ require 'yaml' +# This module specifically avoids having any dependencies. If you have a method +# that needs to be used in many places, even those without initialisation, it +# should go here. + +ALLOWED_SHORT_IDS = [ + 'ChangeCase', + 'Convert characters1', + 'Count1', + 'Cut1', + 'Extract_features1', + 'Filter1', + 'Grep1', + 'Grouping1', + 'Paste1', + 'Remove beginning1', + 'Show beginning1', + 'Summary_Statistics1', + 'addValue', + 'cat1', + 'comp1', + 'gene2exon1', + 'gff2bed1', + 'intermine', + 'join1', + 'param_value_from_file', + 'random_lines1', + 'sort1', + 'csv_to_tabular', + # 'ucsc_table_direct1', # This does not work, surprisingly. + 'upload1', + 'wc_gnu', + 'wig_to_bigWig' +].freeze + +ALLOWED_LOWER_SHORT_IDS = ALLOWED_SHORT_IDS.map(&:downcase) + def safe_load_yaml(file) YAML.load_file(file) rescue StandardError @@ -56,3 +92,112 @@ def markdownify(site, text) def unsafe_slugify(text) text.gsub(%r{["'\\/;:,.!@#$%^&*()]}, '').gsub(/\s/, '-').gsub(/-+/, '-') end + +def fix_version(version) + version + .gsub('_beta+galaxy', '+galaxy') + .gsub(/^([0-9]+)_([0-9]+)_([0-9]+)\+galaxy(.+)/, '\1.\2.\3galaxy\4') + .gsub(/^([0-9]+)\+galaxy(.+)/, '\1.0.0galaxy\2') + .gsub(/^([0-9.]+)_([0-9]+)/, '\1galaxy\2') + .gsub(/_rc(.*)galaxy/, 'rc\1galaxy') + .gsub('+', '') + .gsub(/^v/, '') +end + +def acceptable_tool?(tool_id) + if ! tool_id.is_a?(String) + return false + end + + # Public TS links are fine + if tool_id.start_with?('toolshed.g2.bx.psu.edu') + return true + end + + # These are always allowed (mostly built-ins) + if ALLOWED_LOWER_SHORT_IDS.include?(tool_id.downcase) || tool_id =~ /^__.*__$/ + return true + end + + if tool_id.start_with?('interactive_tool_') + return true + end + + if tool_id.start_with?('CONVERTER_') + return true + end + + # Templated tool IDs are hopefully fine! + if tool_id.start_with?('{{') + return true + end + + # Just the tutorial + if tool_id.start_with?('Toolshed ID') + return true + end + + # Unacceptable + return false +end + + +def tool_id_extractor(wf, path: []) + res = [] + wf['steps'].each do |step_id, step| + if step.key?('subworkflow') + res += tool_id_extractor(step['subworkflow'], path: path + [step_id]) + elsif step.key?('tool_id') && ! step['tool_id'].nil? + res.push(["#{path.join('/')}/#{step_id}", step['tool_id']]) + end + end + res +end + +def objectify(attrs, url, path) + obj = attrs.clone + obj['__path'] = path + obj['__url'] = url + + def obj.data + self + end + + def obj.path + self['__path'] + end + + def obj.url + self['__url'] + end + + def obj.content + self.fetch('content', 'NO CONTENT AVAILABLE') + end + + def obj.title + self['title'] + end + + obj +end + +if __FILE__ == $PROGRAM_NAME + require 'test/unit' + # Testing for the class + class Gtn::Test::IntersectionTest < Test::Unit::TestCase + def test_bad_versions + # toolshed.g2.bx.psu.edu/repos/wolma/mimodd_main/mimodd_info/0.1.8_1 + assert_equal(fix_version("0.1.8_1"), "0.1.8galaxy1") + + # toolshed.g2.bx.psu.edu/repos/iuc/snap_training/snap_training/2013_11_29+galaxy1 + assert_equal(fix_version("2013_11_29+galaxy1"), "2013.11.29galaxy1") + + # toolshed.g2.bx.psu.edu/repos/devteam/vcffilter/vcffilter2/1.0.0_rc1+galaxy3 + assert_equal(fix_version("1.0.0_rc1+galaxy3"), "1.0.0rc1galaxy3") + + # + assert_equal(fix_version("3+galaxy0"), "3.0.0galaxy0") + end + end +end diff --git a/_plugins_dev/api-fake.rb b/_plugins_dev/api-fake.rb deleted file mode 100644 index 2419d9c6e325ac..00000000000000 --- a/_plugins_dev/api-fake.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'json' - -module Jekyll - # API Generation Disabled - class APIGenerator < Generator - def generate(_site) - puts '[GTN/API] Disabled' - end - end -end diff --git a/_plugins_dev/author-page.rb b/_plugins_dev/author-page.rb deleted file mode 100644 index 4d7b0f8cf95b3c..00000000000000 --- a/_plugins_dev/author-page.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'json' - -module Jekyll - # A generator that creates author pages for each author in the site - # normally. But this one just disables it. - class AuthorPageGenerator < Generator - def generate(_site) - puts '[GTN/AuthorPages] Disabled' - end - end -end diff --git a/_plugins_dev/notebook.rb b/_plugins_dev/notebook.rb index e1f72a93634587..3bb6d1b8f1bb30 100644 --- a/_plugins_dev/notebook.rb +++ b/_plugins_dev/notebook.rb @@ -7,17 +7,19 @@ def jupyter_post_write(_site) end module Jekyll - # Notebook Generation Disabled - class RmarkdownGenerator < Generator - def generate(_site) - Jekyll.logger.info 'Notebooks disabled' + module Generators + # Notebook Generation Disabled + class RmarkdownGenerator < Generator + def generate(_site) + Jekyll.logger.info 'Notebooks disabled' + end end - end - # Notebook Generation Disabled - class JupyterNotebookGenerator < Generator - def generate(_site) - Jekyll.logger.info 'Notebooks disabled' + # Notebook Generation Disabled + class JupyterNotebookGenerator < Generator + def generate(_site) + Jekyll.logger.info 'Notebooks disabled' + end end end end diff --git a/_plugins_dev/sitemap-fake.rb b/_plugins_dev/sitemap-fake.rb index 063e3edfb5042e..52cd17a30044fd 100644 --- a/_plugins_dev/sitemap-fake.rb +++ b/_plugins_dev/sitemap-fake.rb @@ -1,31 +1,33 @@ module Jekyll - # Fake sitemap generator. - class SitemapGenerator2 < Generator - safe true + module Generators + # Fake sitemap generator. + class SitemapGenerator2 < Generator + safe true - def generate(site) - result = '<?xml version="1.0" encoding="UTF-8"?>' - result += '<!-- This sitemap has FAKE modification dates intentionally, that step is slow. -->' - result += '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' \ - 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 ' \ - 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" ' \ - 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + def generate(site) + result = '<?xml version="1.0" encoding="UTF-8"?>' + result += '<!-- This sitemap has FAKE modification dates intentionally, that step is slow. -->' + result += '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' \ + 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 ' \ + 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" ' \ + 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' - subset_pages = site.pages - .reject { |t| t.path =~ /ipynb$/ || t.path =~ /api\/ga4gh\/trs\/v2/} - .reject { |t| t.data.fetch('layout', 'page') =~ /external/} - .reject { |t| t.data.fetch('hands_on', '') == 'external'} + subset_pages = site.pages + .reject { |t| t.path =~ /ipynb$/ || t.path =~ /api\/ga4gh\/trs\/v2/} + .reject { |t| t.data.fetch('layout', 'page') =~ /external/} + .reject { |t| t.data.fetch('hands_on', '') == 'external'} - subset_pages.each do |t| - result += "<url><loc>#{site.config['url'] + site.config['baseurl'] + t.url}</loc>" \ - '<lastmod>2016-06-30T18:00:00-07:00</lastmod></url>' - end - result += '</urlset>' + subset_pages.each do |t| + result += "<url><loc>#{site.config['url'] + site.config['baseurl'] + t.url}</loc>" \ + '<lastmod>2016-06-30T18:00:00-07:00</lastmod></url>' + end + result += '</urlset>' - page2 = PageWithoutAFile.new(site, '', '.', 'sitemap.xml') - page2.content = result - site.pages << page2 + page2 = PageWithoutAFile.new(site, '', '.', 'sitemap.xml') + page2.content = result + site.pages << page2 + end end end end diff --git a/assets/css/main.scss b/assets/css/main.scss index 7be7c37fd12a94..e414dab75ee7de 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -33,7 +33,7 @@ $tutorial-box-spacing: 1rem; --navbar-item-hover: transparent; // Basically only for the breadcrumbs. - --secondary-color: #e9ecef; + --secondary-color: #f2d57c; --secondary-color-text: #000; // A11y @@ -774,7 +774,7 @@ blockquote { vertical-align: center; } .card-title{ - height: 5rem; + height: 6rem; } .card-header { background: var(--brand-color); @@ -1220,6 +1220,11 @@ nav[data-toggle='toc'] { } } +.contributor-badge-small{ + border: none; + padding-right: 0em; +} + .contributor-badge-inline { /* prevent breaking across lines */ white-space: nowrap; @@ -1424,16 +1429,20 @@ video::cue { color: white; } -#playlist { +.playlist { display: flex; flex-direction: row; - overflow: auto; + flex-wrap: wrap; + &.vertical { flex-direction: column; } - .pl-item a { + .pl-item { + width: 16em; + padding: 1em; + display: flex; flex-direction: column; @@ -1639,6 +1648,11 @@ $btn-pink: var(--keypoints-color); border-color: var(--brand-color); color: var(--brand-color-contrast); } +.btn-secondary { + background-color: var(--secondary-color); + border-color: var(--secondary-color); + color: var(--secondary-color-text); +} .btn-success { background-color: #1d7531; // AA border-color: #1d7531; // AA @@ -1928,7 +1942,7 @@ body[data-brightness="dark"] { .recording-video { width: 75%; - iframe { + iframe, video { width:100%; height: 100%; aspect-ratio: 16 / 9; diff --git a/bin/fetch-categories.rb b/bin/fetch-categories.rb index 2cb3af2886467b..7e6221ed1bb37c 100755 --- a/bin/fetch-categories.rb +++ b/bin/fetch-categories.rb @@ -26,10 +26,9 @@ def fetch_toolcats(server) # Parse the response toolcats_eu = fetch_toolcats('https://usegalaxy-eu.github.io/usegalaxy-eu-tools/api/labels.json') -# toolcats_eu = File.open('/tmp/tmp.ccFYsrbAa5/usegalaxy-eu-tools/api/labels.json') { |f| YAML.safe_load(f) } toolcats_org = fetch_toolcats('https://galaxyproject.github.io/usegalaxy-tools/api/labels.json') toolcats_aus = fetch_toolcats('https://usegalaxy-au.github.io/usegalaxy-au-tools/api/labels.json') -# toolcats_aus = File.open('/tmp/tmp.ccFYsrbAa5/usegalaxy-au-tools/api/labels.json') { |f| YAML.safe_load(f) } + tool_ids = (toolcats_org.keys + toolcats_eu.keys + toolcats_aus.keys).uniq # tool_ids = toolcats_org.keys tool_ids.sort! diff --git a/bin/find-duplicate-workflows.sh b/bin/find-duplicate-workflows.sh new file mode 100644 index 00000000000000..830a8cdd8a494c --- /dev/null +++ b/bin/find-duplicate-workflows.sh @@ -0,0 +1,2 @@ +#!/bin/bash +md5sum $(find topics -name '*.ga') | grep $(md5sum $(find topics -name '*.ga') | sort | cut -f 1 -d' ' | sort | uniq -c | sort -n | grep -v '^\s*1 ' | awk '{print $2}') diff --git a/bin/lint-deploy.rb b/bin/lint-deploy.rb new file mode 100644 index 00000000000000..ddd036f92d1785 --- /dev/null +++ b/bin/lint-deploy.rb @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby +require 'json' +# Ensure that some mandatory files are here before we deploy. Mostly to catch +# people moving/renaming files that aren't as visibly used. + +# _site/training-material/api/top-tools.json must exist + have correct structure. +raise 'top-tools.json missing' unless File.exist?('_site/training-material/api/top-tools.json') +top_tools = JSON.parse(File.read('_site/training-material/api/top-tools.json')) +raise 'top-tools.json is not an array' unless top_tools.is_a?(Hash) +raise 'Missing important tools' unless top_tools.key? 'Grep1' +raise 'Wrong structure: missing tool_id' unless top_tools['iuc/circos/circos'].key? 'tool_id' +raise 'Wrong structure: missing tutorials' unless top_tools['iuc/circos/circos'].key? 'tutorials' and top_tools['iuc/circos/circos']['tutorials'].length.positive? diff --git a/bin/lint-test.rb b/bin/lint-test.rb index 6c209e4ab77106..34a2b9ef59a0a1 100644 --- a/bin/lint-test.rb +++ b/bin/lint-test.rb @@ -4,12 +4,12 @@ require './bin/lint' # Test case for the GTN linter -class GtnLinterTest < Test::Unit::TestCase - include GtnLinter +class Gtn::Linter::Test < Test::Unit::TestCase + include Gtn::Linter def test_fix_notoc text = "a\n{: .no_toc}\nasdf".split "\n" - result = GtnLinter.fix_notoc(text) + result = Gtn::Linter.fix_notoc(text) assert_equal(result[0]['location']['range']['start']['line'], 2) assert_equal(result[0]['location']['range']['start']['column'], 1) @@ -21,7 +21,7 @@ def test_fix_notoc def test_fix_broken_link text = "a\n{% link does-not-exist.md %}\nasdf".split "\n" # 123456789 - result = GtnLinter.check_bad_link(text) + result = Gtn::Linter.check_bad_link(text) assert_equal(result[0]['message'], 'The linked file (`does-not-exist.md`) could not be found.') @@ -34,7 +34,7 @@ def test_fix_broken_link def test_youtube text = "a\n<iframe .. youtube.com ... </iframe>x\nasdf".split "\n" - result = GtnLinter.youtube_bad(text) + result = Gtn::Linter.youtube_bad(text) assert_equal(result[0]['location']['range']['start']['line'], 2) assert_equal(result[0]['location']['range']['start']['column'], 1) @@ -47,7 +47,7 @@ def test_external_gtn_link url = 'https://training.galaxyproject.org/training-material/topics/admin/tutorials/ansible-galaxy/tutorial.html' text = "a\na[test](#{url})b\nasdf".split "\n" # 1234567890 - result = GtnLinter.link_gtn_tutorial_external(text) + result = Gtn::Linter.link_gtn_tutorial_external(text) assert_equal(result[0]['location']['range']['start']['line'], 2) assert_equal(result[0]['location']['range']['start']['column'], 9) @@ -67,7 +67,7 @@ def test_external_gtn_link_slides url = 'https://training.galaxyproject.org/training-material/topics/admin/tutorials/ansible-galaxy/slides.html' text = "a\na[test](#{url})b\nasdf".split "\n" # 1234567890 - result = GtnLinter.link_gtn_slides_external(text) + result = Gtn::Linter.link_gtn_slides_external(text) assert_equal(result[0]['location']['range']['start']['line'], 2) assert_equal(result[0]['location']['range']['start']['column'], 9) @@ -86,7 +86,7 @@ def test_external_gtn_link_slides def test_doi text = "a\nfrom [Pedro LarraĂąaga, 2006](https://doi.org/10.1093/bib/bbk007).\nasdf".split "\n" # 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567 - result = GtnLinter.check_dois(text) + result = Gtn::Linter.check_dois(text) assert_equal(result[0]['location']['range']['start']['line'], 2) assert_equal(result[0]['location']['range']['start']['column'], 6) @@ -102,7 +102,7 @@ def test_doi text = "a\nfrom [Pedro LarraĂąaga, 2006](https://doi.org/10.5281/zenodo.10238184212).\nasdf".split "\n" # 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012 - result = GtnLinter.check_dois(text) + result = Gtn::Linter.check_dois(text) assert_equal(result.length, 0) end @@ -114,7 +114,7 @@ def testnew_title_ ].each do |key, key_text| text = "a\n> ### {% icon #{key} %} #{key_text}: Blah\n> #{key_text} text\n{: .#{key}}".split "\n" # 12345678901234567890123456789012345678901234567890 - result = GtnLinter.new_more_accessible_boxes(text) + result = Gtn::Linter.new_more_accessible_boxes(text) assert_equal(result[0]['location']['range']['start']['line'], 2) assert_equal(result[0]['location']['range']['start']['column'], 3) diff --git a/bin/lint.rb b/bin/lint.rb index 27dfbb3251e3cc..458d99ad3333ca 100755 --- a/bin/lint.rb +++ b/bin/lint.rb @@ -6,1422 +6,1632 @@ require 'find' require 'bibtex' require 'json' +require 'kramdown' +require 'kramdown-parser-gfm' require 'citeproc/ruby' require 'csl/styles' require './_plugins/util' GTN_HOME = Pathname.new(__dir__).parent.to_s -# This is our ONE central linting script that handles EVERYTHING. - -# A custom module to properly format reviewdog json output -module ReviewDogEmitter - @CODE_URL = 'https://github.com/galaxyproject/training-material/wiki/Error-Codes' - def self.delete_text(path: '', idx: 0, text: '', message: 'No message', code: 'GTN000', full_line: '') - error( - path: path, - idx: idx, - match_start: 0, - match_end: text.length, - replacement: '', - message: message, - code: code, - full_line: full_line - ) - end - def self.file_error(path: '', message: 'None', code: 'GTN:000') - error( - path: path, - idx: 0, - match_start: 0, - match_end: 1, - replacement: nil, - message: message, - code: code, - full_line: '' - ) - end - def self.warning(path: '', idx: 0, match_start: 0, match_end: 1, - replacement: nil, message: 'No message', code: 'GTN000', full_line: '') - self.message( - path: path, - idx: idx, - match_start: match_start, - match_end: match_end, - replacement: replacement, - message: message, - level: 'WARNING', - code: code, - full_line: full_line - ) - end - - def self.error(path: '', idx: 0, match_start: 0, match_end: 1, replacement: nil, message: 'No message', - code: 'GTN000', full_line: '') - self.message( - path: path, - idx: idx, - match_start: match_start, - match_end: match_end, - replacement: replacement, - message: message, - level: 'ERROR', - code: code, - full_line: full_line - ) - end +module Gtn - def self.message(path: '', idx: 0, match_start: 0, match_end: 1, replacement: nil, message: 'No message', - level: 'WARNING', code: 'GTN000', full_line: '') - end_area = { 'line' => idx + 1, 'column' => match_end } - end_area = { 'line' => idx + 2, 'column' => 1 } if match_end == full_line.length - - res = { - 'message' => message, - 'location' => { - 'path' => path, - 'range' => { - 'start' => { 'line' => idx + 1, 'column' => match_start + 1 }, - 'end' => end_area - } - }, - 'severity' => level - } - if !code.nil? - res['code'] = { - 'value' => code, - 'url' => "#{@CODE_URL}##{code.gsub(/:/, '').downcase}", - } - end - if !replacement.nil? - res['suggestions'] = [{ - 'text' => replacement, - 'range' => { - 'start' => { 'line' => idx + 1, 'column' => match_start + 1 }, - 'end' => end_area - } - }] - end - res - end -end - -# Linting functions for the GTN -module GtnLinter - @BAD_TOOL_LINK = /{% tool (\[[^\]]*\])\(https?.*tool_id=([^)]*)\)\s*%}/i - @BAD_TOOL_LINK2 = %r{{% tool (\[[^\]]*\])\(https://toolshed.g2([^)]*)\)\s*%}}i - - def self.find_matching_texts(contents, query) - contents.map.with_index do |text, idx| - [idx, text, text.match(query)] - end.select { |_idx, _text, selected| selected } - end - - def self.fix_notoc(contents) - find_matching_texts(contents, /{:\s*.no_toc\s*}/) - .map do |idx, text, _selected| - ReviewDogEmitter.delete_text( - path: @path, - idx: idx, - text: text, - message: 'Setting no_toc is discouraged, these headings provide useful places for readers to jump to.', - code: 'GTN:001', - full_line: text - ) - end - end + # A custom module to properly format reviewdog json output + module ReviewDogEmitter + @CODE_URL = 'https://training.galaxyproject.org/training-material/gtn_rdoc/Gtn/Linter.html' - # GTN:002 youtube discouraged - def self.youtube_bad(contents) - find_matching_texts(contents, %r{<iframe.*youtu.?be.*</iframe>}) - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, + def self.delete_text(path: '', idx: 0, text: '', message: 'No message', code: 'GTN000', full_line: '', fn: '') + error( + path: path, idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0) + 1, + match_start: 0, + match_end: text.length, replacement: '', - message: 'Instead of embedding IFrames to YouTube contents, consider adding this video to the ' \ - 'GTN tutorial "recordings" metadata where it will ' \ - 'be more visible for others.', - code: 'GTN:002' + message: message, + code: code, + full_line: full_line, + fn: fn, ) end - end - def self.link_gtn_tutorial_external(contents) - find_matching_texts( - contents, - %r{\(https?://(training.galaxyproject.org|galaxyproject.github.io)/training-material/([^)]*)\)} - ) - .map do |idx, _text, selected| - # puts "#{idx} 0 #{selected[0]} 1 #{selected[1]} 2 #{selected[2]} 3 #{selected[3]}" - ReviewDogEmitter.error( - path: @path, - idx: idx, - # We wrap the entire URL (inside the explicit () in a matching group to make it easy to select/replace) - match_start: selected.begin(0) + 1, - match_end: selected.end(0), - replacement: "{% link #{selected[2].gsub('.html', '.md')} %}", - message: 'Please use the link function to link to other pages within the GTN. ' \ - 'It helps us ensure that all links are correct', - code: 'GTN:003' + def self.file_error(path: '', message: 'None', code: 'GTN:000', fn: '') + error( + path: path, + idx: 0, + match_start: 0, + match_end: 1, + replacement: nil, + message: message, + code: code, + full_line: '', + fn: fn ) end - end - def self.link_gtn_slides_external(contents) - find_matching_texts( - contents, - %r{\((https?://(training.galaxyproject.org|galaxyproject.github.io)/training-material/(.*slides.html))\)} - ) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, + def self.warning(path: '', idx: 0, match_start: 0, match_end: 1, + replacement: nil, message: 'No message', code: 'GTN000', full_line: '', fn: '') + self.message( + path: path, idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: "{% link #{selected[3]} %}", - message: 'Please use the link function to link to other pages within the GTN. ' \ - 'It helps us ensure that all links are correct', - code: 'GTN:003' + match_start: match_start, + match_end: match_end, + replacement: replacement, + message: message, + level: 'WARNING', + code: code, + full_line: full_line, + fn: fn, ) end - end - def self.check_dois(contents) - find_matching_texts(contents, %r{(\[[^\]]*\]\(https?://doi.org/[^)]*\))}) - .reject { |_idx, _text, selected| selected[0].match(%r{10.5281/zenodo}) } # Ignoring zenodo - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, + def self.error(path: '', idx: 0, match_start: 0, match_end: 1, replacement: nil, message: 'No message', + code: 'GTN000', full_line: '', fn: '') + self.message( + path: path, idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0) + 2, - replacement: '{% cite ... %}', - message: 'This looks like a DOI which could be better served by using the built-in Citations mechanism. ' \ - 'You can use https://doi2bib.org to convert your DOI into a .bib formatted entry, ' \ - 'and add to your tutorial.md', - code: 'GTN:004' + match_start: match_start, + match_end: match_end, + replacement: replacement, + message: message, + level: 'ERROR', + code: code, + full_line: full_line, + fn: fn, ) end - end - def self.check_pmids(contents) - # https://www.ncbi.nlm.nih.gov/pubmed/24678044 - find_matching_texts(contents, - %r{(\[[^\]]*\]\(https?://www.ncbi.nlm.nih.gov/pubmed//[0-9]*\))}).map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0) + 2, - replacement: '{% cite ... %}', - message: 'This looks like a PMID which could be better served by using the built-in Citations mechanism. ' \ - 'You can use https://doi2bib.org to convert your PMID/PMCID into a .bib formatted entry, ' \ - 'and add to your tutorial.md', - code: 'GTN:004' - ) + def self.message(path: '', idx: 0, match_start: 0, match_end: 1, replacement: nil, message: 'No message',level: 'WARNING', code: 'GTN000', full_line: '', fn: '') + end_area = { 'line' => idx + 1, 'column' => match_end } + end_area = { 'line' => idx + 2, 'column' => 1 } if match_end == full_line.length + + res = { + 'message' => message, + 'location' => { + 'path' => path, + 'range' => { + 'start' => { 'line' => idx + 1, 'column' => match_start + 1 }, + 'end' => end_area + } + }, + 'severity' => level + } + if !code.nil? + res['code'] = { + 'value' => code + } + if !fn.nil? + res['code']['url'] = "#{@CODE_URL}#method-c-#{fn}" + end + end + if !replacement.nil? + res['suggestions'] = [{ + 'text' => replacement, + 'range' => { + 'start' => { 'line' => idx + 1, 'column' => match_start + 1 }, + 'end' => end_area + } + }] + end + res end end - def self.check_bad_link_text(contents) - find_matching_texts(contents, /\[\s*(here|link)\s*\]/i) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0) + 1, - replacement: '[Something better here]', - message: "Please do not use 'here' as your link title, it is " \ - '[bad for accessibility](https://usability.yale.edu/web-accessibility/articles/links#link-text). ' \ - 'Instead try restructuring your sentence to have useful descriptive text in the link.', - code: 'GTN:005' - ) - end - end + # This is our ONE central linting script that handles EVERYTHING. + module Linter + @BAD_TOOL_LINK = /{% tool (\[[^\]]*\])\(\s*https?.*tool_id=([^)]*)\)\s*%}/i + @BAD_TOOL_LINK2 = %r{{% tool (\[[^\]]*\])\(\s*https://toolshed.g2([^)]*)\)\s*%}}i + @MAYBE_OK_TOOL_LINK = /{% tool (\[[^\]]*\])\(([^)]*)\)\s*%}/i - def self.incorrect_calls(contents) - a = find_matching_texts(contents, /([^{]|^)(%\s*[^%]*%})/i) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(2), - match_end: selected.end(2) + 1, - replacement: "{#{selected[2]}", - message: 'It looks like you might be missing the opening { of a jekyll function', - code: 'GTN:006' - ) - end - b = find_matching_texts(contents, /{([^%]\s*[^%]* %})/i) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: "%#{selected[1]}", - message: 'It looks like you might be missing the opening % of a jekyll function', - code: 'GTN:006' - ) + def self.find_matching_texts(contents, query) + contents.map.with_index do |text, idx| + [idx, text, text.match(query)] + end.select { |_idx, _text, selected| selected } end - c = find_matching_texts(contents, /({%\s*[^%]*%)([^}]|$)/i) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 2, - replacement: "#{selected[1]}}#{selected[2]}", - message: 'It looks like you might be missing the closing } of a jekyll function', - code: 'GTN:006' - ) + ## + # GTN:001 - Setting no_toc is discouraged as headers are useful for learners to link to and to jump to. Setting no_toc removes it from the table of contents which is generally inadvisable. + # + # Remediation: remove {: .no_toc} + def self.fix_notoc(contents) + find_matching_texts(contents, /{:\s*.no_toc\s*}/) + .map do |idx, text, _selected| + ReviewDogEmitter.delete_text( + path: @path, + idx: idx, + text: text, + message: 'Setting no_toc is discouraged, these headings provide useful places for readers to jump to.', + code: 'GTN:001', + full_line: text, + fn: __method__.to_s, + ) + end end - d = find_matching_texts(contents, /({%\s*[^}]*[^%])}/i) + ## + # GTN:002 - YouTube links are discouraged. Please consider using our include for it: + # + # E.g, instead of + # + # <iframe ... youtube.../> + # + # Consider: + # + # {% include _includes/youtube.html id="e0vj-0imOLw" title="Difference between climate and weather" %} + def self.youtube_bad(contents) + find_matching_texts(contents, %r{<iframe.*youtu.?be.*</iframe>}) .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: "#{selected[1]}%", - message: 'It looks like you might be missing the closing % of a jekyll function', - code: 'GTN:006' - ) + ReviewDogEmitter.warning( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0) + 1, + replacement: '', + message: 'Instead of embedding IFrames to YouTube contents, consider adding this video to the ' \ + 'GTN tutorial "recordings" metadata where it will ' \ + 'be more visible for others.', + code: 'GTN:002', + fn: __method__.to_s, + ) + end end - a + b + c + d - end - @CITATION_LIBRARY = nil - - def self.citation_library - if @CITATION_LIBRARY.nil? - lib = BibTeX::Bibliography.new - (enumerate_type(/bib$/) + enumerate_type(/bib$/, root_dir: 'faqs')).each do |path| - b = BibTeX.open(path) - b.each do |x| - # Record the bib path. - x._path = path - lib << x - end + ## + # GTN:003 - We discourage linking to training.galaxyproject.org or + # galaxyproject.github.io/training-material as those are "external" links, + # which are slower for us to validate. Every build we run tests to be sure + # that every link is valid, but we cannot do that for every external site to + # avoid putting unnecessary pressure on them. + # + # Instead of + # + # [see this other tutorial(https://training.galaxyproject.org/training-material/topics/admin/tutorials/ansible/tutorial.html) + # + # Consider: + # + # [see this other tutorial({% link topics/admin/tutorials/ansible/tutorial.md %}) + def self.link_gtn_tutorial_external(contents) + find_matching_texts( + contents, + %r{\(https?://(training.galaxyproject.org|galaxyproject.github.io)/training-material/([^)]*)\)} + ) + .map do |idx, _text, selected| + # puts "#{idx} 0 #{selected[0]} 1 #{selected[1]} 2 #{selected[2]} 3 #{selected[3]}" + ReviewDogEmitter.error( + path: @path, + idx: idx, + # We wrap the entire URL (inside the explicit () in a matching group to make it easy to select/replace) + match_start: selected.begin(0) + 1, + match_end: selected.end(0), + replacement: "{% link #{selected[2].gsub('.html', '.md')} %}", + message: 'Please use the link function to link to other pages within the GTN. ' \ + 'It helps us ensure that all links are correct', + code: 'GTN:003', + fn: __method__.to_s, + ) end - @CITATION_LIBRARY = lib end - @CITATION_LIBRARY - end - - @JEKYLL_CONFIG = nil - def self.jekyll_config - if @JEKYLL_CONFIG.nil? - # Load - @JEKYLL_CONFIG = YAML.load_file('_config.yml') - end - @JEKYLL_CONFIG - end - - def self.check_bad_cite(contents) - find_matching_texts(contents, /{%\s*cite\s+([^%]*)\s*%}/i) - .map do |idx, _text, selected| - citation_key = selected[1].strip - if citation_library[citation_key].nil? + ## + # GTN:003 - We discourage linking to training.galaxyproject.org or + # galaxyproject.github.io/training-material as those are "external" links, + # which are slower for us to validate. Every build we run tests to be sure + # that every link is valid, but we cannot do that for every external site to + # avoid putting unnecessary pressure on them. + # + # Instead of + # + # [see this other tutorial(https://training.galaxyproject.org/training-material/topics/admin/tutorials/ansible/slides.html) + # + # Consider: + # + # [see this other tutorial({% link topics/admin/tutorials/ansible/slides.html %}) + def self.link_gtn_slides_external(contents) + find_matching_texts( + contents, + %r{\((https?://(training.galaxyproject.org|galaxyproject.github.io)/training-material/(.*slides.html))\)} + ) + .map do |idx, _text, selected| ReviewDogEmitter.error( path: @path, idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0), - replacement: nil, - message: "The citation (#{citation_key}) could not be found.", - code: 'GTN:007' + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: "{% link #{selected[3]} %}", + message: 'Please use the link function to link to other pages within the GTN. ' \ + 'It helps us ensure that all links are correct', + code: 'GTN:003', + fn: __method__.to_s, ) end end - end - def self.check_bad_icon(contents) - find_matching_texts(contents, /{%\s*icon\s+([^%]*)\s*%}/i) - .map do |idx, _text, selected| - icon_key = selected[1].strip.split[0] - if jekyll_config['icon-tag'][icon_key].nil? - ReviewDogEmitter.error( + ## + # GTN:004 - Instead of linking directly to a DOI and citing it yourself, consider obtaining the BibTeX formatted citation and adding it to a tutorial.bib (or slides.bib) file. Then we can generate a full set of references for the citations and give proper credit. + # + # Companion function to Gtn::Linter.check_pmids + def self.check_dois(contents) + find_matching_texts(contents, %r{(\[[^\]]*\]\(https?://doi.org/[^)]*\))}) + .reject { |_idx, _text, selected| selected[0].match(%r{10.5281/zenodo}) } # Ignoring zenodo + .map do |idx, _text, selected| + ReviewDogEmitter.warning( path: @path, idx: idx, match_start: selected.begin(0), - match_end: selected.end(0), - replacement: nil, - message: "The icon (#{icon_key}) could not be found, please add it to _config.yml.", - code: 'GTN:033' + match_end: selected.end(0) + 2, + replacement: '{% cite ... %}', + message: 'This looks like a DOI which could be better served by using the built-in Citations mechanism. ' \ + 'You can use https://doi2bib.org to convert your DOI into a .bib formatted entry, ' \ + 'and add to your tutorial.md', + code: 'GTN:004', + fn: __method__.to_s, ) end end - end - def self.non_existent_snippet(contents) - find_matching_texts(contents, /{%\s*snippet\s+([^ ]*)/i) - .reject do |_idx, _text, selected| - File.exist?(selected[1]) - end - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0), - replacement: nil, - message: "This snippet (`#{selected[1]}`) does not seem to exist", - code: 'GTN:008' - ) + ## + # GTN:004 - Instead of linking directly to a PMID URL and citing it yourself, consider obtaining the BibTeX formatted citation and adding it to a tutorial.bib (or slides.bib) file. Then we can generate a full set of references for the citations and give proper credit. + # + # Companion function to Gtn::Linter.check_dois + def self.check_pmids(contents) + # https://www.ncbi.nlm.nih.gov/pubmed/24678044 + find_matching_texts(contents, + %r{(\[[^\]]*\]\(https?://www.ncbi.nlm.nih.gov/pubmed//[0-9]*\))}).map do |idx, _text, selected| + ReviewDogEmitter.warning( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0) + 2, + replacement: '{% cite ... %}', + message: 'This looks like a PMID which could be better served by using the built-in Citations mechanism. ' \ + 'You can use https://doi2bib.org to convert your PMID/PMCID into a .bib formatted entry, ' \ + 'and add to your tutorial.md', + code: 'GTN:004', + fn: __method__.to_s, + ) + end end - end - def self.bad_tool_links(contents) - find_matching_texts(contents, @BAD_TOOL_LINK) + \ - find_matching_texts(contents, @BAD_TOOL_LINK2) - .map do |idx, _text, selected| + ## + # GTN:005 - Using link names like 'here' are unhelpful for learners who are progressing through the material with a screenreader. Please use a more descriptive text for your linke + # + # Instead of + # + # see the documentation [here](https://example.com) + # + # Consider + # + # see [edgeR's documentation](https://example.com) + def self.check_bad_link_text(contents) + find_matching_texts(contents, /\[\s*(here|link)\s*\]/i) + .map do |idx, _text, selected| ReviewDogEmitter.error( path: @path, idx: idx, match_start: selected.begin(0), match_end: selected.end(0) + 1, - replacement: "{% tool #{selected[1]}(#{selected[2]}) %}", - message: 'You have used the full tool URL to a specific server, here we only need the tool ID portion.', - code: 'GTN:009' + replacement: '[Something better here]', + message: "Please do not use 'here' as your link title, it is " \ + '[bad for accessibility](https://usability.yale.edu/web-accessibility/articles/links#link-text). ' \ + 'Instead try restructuring your sentence to have useful descriptive text in the link.', + code: 'GTN:005', + fn: __method__.to_s, ) end - end + end - def self.bad_zenodo_links(contents) - find_matching_texts(contents, /https:\/\/zenodo.org\/api\//) - .reject { |_idx, _text, selected| _text =~ /files-archive/ } - .map do |idx, _text, selected| + ## + # GTN:006 - This is a potentially incorrect Jekyll/Liquid template function/variable access. + # + # Variables can be placed into your template like so: + # + # {{ page.title }} + # + # And functions can be called like so: + # + # {% if page.title %} + # + # So please be sure {{ }} and {% %} are matching. + def self.incorrect_calls(contents) + a = find_matching_texts(contents, /([^{]|^)(%\s*[^%]*%})/i) + .map do |idx, _text, selected| ReviewDogEmitter.error( path: @path, idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0) + 1, - replacement: nil, - message: 'Please do not use zenodo.org/api/ links, instead it should look like zenodo.org/records/id/files/<filename>', - code: 'GTN:040' + match_start: selected.begin(2), + match_end: selected.end(2) + 1, + replacement: "{#{selected[2]}", + message: 'It looks like you might be missing the opening { of a jekyll function', + code: 'GTN:006', + fn: __method__.to_s, ) end - end - - def self.snippets_too_close_together(contents) - prev_line = -2 - res = [] - find_matching_texts(contents, /^[> ]*{% snippet/) - .each do |idx, _text, selected| - if idx == prev_line + 1 - res.push(ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0) + 1, - replacement: nil, - message: 'Snippets too close together', - code: 'GTN:032' - )) + b = find_matching_texts(contents, /{([^%]\s*[^%]* %})/i) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: "%#{selected[1]}", + message: 'It looks like you might be missing the opening % of a jekyll function', + code: 'GTN:006', + fn: __method__.to_s, + ) end - prev_line = idx - end - res - end - ALLOWED_SHORT_IDS = [ - 'ChangeCase', - 'Convert characters1', - 'Count1', - 'Cut1', - 'Extract_features1', - 'Filter1', - 'Grep1', - 'Grouping1', - 'Paste1', - 'Remove beginning1', - 'Show beginning1', - 'Summary_Statistics1', - 'addValue', - 'cat1', - 'comp1', - 'gene2exon1', - 'gff2bed1', - 'intermine', - 'join1', - 'param_value_from_file', - 'random_lines1', - 'sort1', - # 'ucsc_table_direct1', # This does not work, surprisingly. - 'upload1', - 'wc_gnu', - 'wig_to_bigWig' - ].freeze - - def self.check_tool_link(contents) - find_matching_texts(contents, /{%\s*tool \[([^\]]*)\]\(([^)]*)\)\s*%}/) - .map do |idx, _text, selected| - # text = selected[1] - link = selected[2] - - errs = [] - if link.match(%r{/}) - if link.count('/') < 5 - errs.push(ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(2), - match_end: selected.end(2) + 1, - replacement: nil, - message: "This tool identifier looks incorrect, it doesn't have the right number of segments.", - code: 'GTN:009' - )) - end + c = find_matching_texts(contents, /({%\s*[^%]*%)([^}]|$)/i) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 2, + replacement: "#{selected[1]}}#{selected[2]}", + message: 'It looks like you might be missing the closing } of a jekyll function', + code: 'GTN:006', + fn: __method__.to_s, + ) + end - if link.match(/testtoolshed/) - errs.push(ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(2), - match_end: selected.end(2) + 1, - replacement: nil, - message: 'The GTN strongly avoids using testtoolshed tools in your tutorials or workflows', - code: 'GTN:009' - )) - end - else - if link.match(/\+/) - errs.push(ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(2), - match_end: selected.end(2) + 1, - replacement: nil, - message: 'Broken tool link, unnecessary +', - code: 'GTN:009' - )) - end + d = find_matching_texts(contents, /({%\s*[^}]*[^%])}/i) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: "#{selected[1]}%", + message: 'It looks like you might be missing the closing % of a jekyll function', + code: 'GTN:006', + fn: __method__.to_s, + ) + end + a + b + c + d + end - if !ALLOWED_SHORT_IDS.include?(link) && - !link.match(/^interactive_tool_/) && - !link.match(/__[A-Z_]+__/) && - !link.match(/^{{.*}}$/) && - !link.match(/^CONVERTER_/) - errs.push(ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(2), - match_end: selected.end(2) + 1, - replacement: nil, - message: 'Unknown short tool ID. Please use the full tool ID, or check bin/lint.rb ' \ - 'if you believe this is correct.', - code: 'GTN:009' - )) + @CITATION_LIBRARY = nil + + def self.citation_library + if @CITATION_LIBRARY.nil? + lib = BibTeX::Bibliography.new + (enumerate_type(/bib$/) + enumerate_type(/bib$/, root_dir: 'faqs')).each do |path| + b = BibTeX.open(path) + b.each do |x| + # Record the bib path. + x._path = path + lib << x + end end + @CITATION_LIBRARY = lib end - errs + @CITATION_LIBRARY end - end - def self.new_more_accessible_boxes(contents) - # \#\#\# - find_matching_texts(contents, /> (### {%\s*icon ([^%]*)\s*%}[^:]*:?(.*))/) - .map do |idx, _text, selected| - key = selected[2].strip.gsub(/_/, '-') - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: "<#{key}-title>#{selected[3].strip}</#{key}-title>", - message: 'We have developed a new syntax for box titles, please consider using this instead.', - code: 'GTN:010' - ) + @JEKYLL_CONFIG = nil + + def self.jekyll_config + if @JEKYLL_CONFIG.nil? + # Load + @JEKYLL_CONFIG = YAML.load_file('_config.yml') + end + @JEKYLL_CONFIG end - end - def self.new_more_accessible_boxes_agenda(contents) - # \#\#\# - find_matching_texts(contents, /> (###\s+Agenda\s*)/) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: '<agenda-title></agenda-title>', - message: 'We have developed a new syntax for box titles, please consider using this instead.', - code: 'GTN:010' - ) + ## + # GTN:007 - We could not find a citation key, please be sure it is used in a bibliography somewhere. + def self.check_bad_cite(contents) + find_matching_texts(contents, /{%\s*cite\s+([^%]*)\s*%}/i) + .map do |idx, _text, selected| + citation_key = selected[1].strip + if citation_library[citation_key].nil? + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0), + replacement: nil, + message: "The citation (#{citation_key}) could not be found.", + code: 'GTN:007', + fn: __method__.to_s, + ) + end + end end - end - def self.no_target_blank(contents) - find_matching_texts(contents, /target=("_blank"|'_blank')/) - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0), - replacement: nil, - message: 'Please do not use `target="_blank"`, [it is bad for accessibility.]' \ - '(https://www.a11yproject.com/checklist/#identify-links-that-open-in-a-new-tab-or-window)', - code: 'GTN:011' - ) + ## + # GTN:033 - This icon is not known to use. If it is new, please add it to {our configuration.}[https://training.galaxyproject.org/training-material/topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.html] + def self.check_bad_icon(contents) + find_matching_texts(contents, /{%\s*icon\s+([^%]*)\s*%}/i) + .map do |idx, _text, selected| + icon_key = selected[1].strip.split[0] + if jekyll_config['icon-tag'][icon_key].nil? + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0), + replacement: nil, + message: "The icon (#{icon_key}) could not be found, please add it to _config.yml.", + code: 'GTN:033', + fn: __method__.to_s, + ) + end + end end - end - def self.empty_alt_text(contents) - find_matching_texts(contents, /!\[\]\(/i) - .map do |idx, _text, selected| - path = selected[1].to_s.strip - if !File.exist?(path.gsub(%r{^/}, '')) + ## + # GTN:008 - This snippet is not known to us, please check that it exists somewhere in the snippets/ folder. + def self.non_existent_snippet(contents) + find_matching_texts(contents, /{%\s*snippet\s+([^ ]*)/i) + .reject do |_idx, _text, selected| + File.exist?(selected[1]) + end + .map do |idx, _text, selected| ReviewDogEmitter.error( path: @path, idx: idx, match_start: selected.begin(0), match_end: selected.end(0), replacement: nil, - message: 'The alt text for this image seems to be empty', - code: 'GTN:034' + message: "This snippet (`#{selected[1]}`) does not seem to exist", + code: 'GTN:008', + fn: __method__.to_s, ) end end - end - def self.check_bad_link(contents) - find_matching_texts(contents, /{%\s*link\s+([^%]*)\s*%}/i) - .map do |idx, _text, selected| - path = selected[1].to_s.strip - if !File.exist?(path.gsub(%r{^/}, '')) + ## + # GTN:009 - This looks like an invalid tool link. There are several ways that tool links can be invalid, and only one correct way to reference a tool + # + # Correct + # + # {% tool [JBrowse genome browser](toolshed.g2.bx.psu.edu/repos/iuc/jbrowse/jbrowse/1.16.4+galaxy3) %} + # + # Incorrect + # + # {% tool [JBrowse genome browser](https://toolshed.g2.bx.psu.edu/repos/iuc/jbrowse/jbrowse/1.16.4+galaxy3) %} + # {% tool [JBrowse genome browser](https://toolshed.g2.bx.psu.edu/repos/iuc/jbrowse/jbrowse) %} + # {% tool [JBrowse genome browser](jbrowse/1.16.4+galaxy3) %} + # {% tool [JBrowse genome browser](https://toolshed.g2.bx.psu.edu/repos/iuc/jbrowse/jbrowse/deadbeefcafe) %} + def self.bad_tool_links(contents) + find_matching_texts(contents, @BAD_TOOL_LINK) + \ + find_matching_texts(contents, @BAD_TOOL_LINK2) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0) + 1, + replacement: "{% tool #{selected[1]}(#{selected[2]}) %}", + message: 'You have used the full tool URL to a specific server, here we only need the tool ID portion.', + code: 'GTN:009', + fn: __method__.to_s, + ) + end + + find_matching_texts(contents, @MAYBE_OK_TOOL_LINK) + .map do |idx, _text, selected| + + if acceptable_tool?(selected[2]) + next + end + + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0) + 1, + replacement: "{% tool #{selected[1]}(#{selected[2]}) %}", + message: 'You have used an invalid tool URL, it should be of the form "toolshed.g2.bx.psu.edu/repos/{owner}/{repo}/{tool}/{version}" (or an internal tool ID) so, please double check.', + code: 'GTN:009' + ) + end + end + + ## + # GTN:040 - zenodo.org/api links are invalid in the GTN, please use the zenodo.org/records/id/files/<filename> format instead. This ensures that when users download files from zenodo into Galaxy, they appear correctly, with a useful filename. + def self.bad_zenodo_links(contents) + find_matching_texts(contents, /https:\/\/zenodo.org\/api\//) + .reject { |_idx, _text, selected| _text =~ /files-archive/ } + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0) + 1, + replacement: nil, + message: 'Please do not use zenodo.org/api/ links, instead it should look like zenodo.org/records/id/files/<filename>', + code: 'GTN:040', + fn: __method__.to_s, + ) + end + end + + ## + # GTN:032 - Snippets are too close together which sometimes breaks snippet rendering. Please ensure snippets are separated by one line. + def self.snippets_too_close_together(contents) + prev_line = -2 + res = [] + find_matching_texts(contents, /^[> ]*{% snippet/) + .each do |idx, _text, selected| + if idx == prev_line + 1 + res.push(ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0) + 1, + replacement: nil, + message: 'Snippets too close together', + code: 'GTN:032', + fn: __method__.to_s, + )) + end + prev_line = idx + end + res + end + + ## + # GTN:009 - See Gtn::Linter.bad_tool_links + def self.check_tool_link(contents) + find_matching_texts(contents, /{%\s*tool \[([^\]]*)\]\(([^)]*)\)\s*%}/) + .map do |idx, _text, selected| + # text = selected[1] + link = selected[2] + + errs = [] + if link.match(%r{/}) + if link.count('/') < 5 + errs.push(ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(2), + match_end: selected.end(2) + 1, + replacement: nil, + message: "This tool identifier looks incorrect, it doesn't have the right number of segments.", + code: 'GTN:009' + )) + end + + if link.match(/testtoolshed/) + errs.push(ReviewDogEmitter.warning( + path: @path, + idx: idx, + match_start: selected.begin(2), + match_end: selected.end(2) + 1, + replacement: nil, + message: 'The GTN strongly avoids using testtoolshed tools in your tutorials or workflows', + code: 'GTN:009' + )) + end + else + if link.match(/\+/) + errs.push(ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(2), + match_end: selected.end(2) + 1, + replacement: nil, + message: 'Broken tool link, unnecessary +', + code: 'GTN:009' + )) + end + + + if !acceptable_tool?(link) + errs.push(ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(2), + match_end: selected.end(2) + 1, + replacement: nil, + message: 'Unknown short tool ID. Please use the full tool ID, or check bin/lint.rb ' \ + 'if you believe this is correct.', + code: 'GTN:009' + )) + end + end + + errs + end + end + + ## + # GTN:010 - We have a new, more accessible syntax for box titles. Please use this instead: + # + # > <box-title>Some Title</box-title> + # > ... + # {: .box} + def self.new_more_accessible_boxes(contents) + # \#\#\# + find_matching_texts(contents, /> (### {%\s*icon ([^%]*)\s*%}[^:]*:?(.*))/) + .map do |idx, _text, selected| + key = selected[2].strip.gsub(/_/, '-') ReviewDogEmitter.error( path: @path, idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0), - replacement: nil, - message: "The linked file (`#{selected[1].strip}`) could not be found.", - code: 'GTN:018' + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: "<#{key}-title>#{selected[3].strip}</#{key}-title>", + message: 'We have developed a new syntax for box titles, please consider using this instead.', + code: 'GTN:010', + fn: __method__.to_s, ) end end - find_matching_texts(contents, /\]\(\)/i) - .map do |idx, _text, selected| - path = selected[1].to_s.strip - if !File.exist?(path.gsub(%r{^/}, '')) + ## + # GTN:010 - See Gtn::Linter.new_more_accessible_boxes_agenda + def self.new_more_accessible_boxes_agenda(contents) + # \#\#\# + find_matching_texts(contents, /> (###\s+Agenda\s*)/) + .map do |idx, _text, selected| ReviewDogEmitter.error( path: @path, idx: idx, - match_start: selected.begin(0), - match_end: selected.end(0), - replacement: nil, - message: 'The link does not seem to have a target.', - code: 'GTN:018' + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: '<agenda-title></agenda-title>', + message: 'We have developed a new syntax for box titles, please consider using this instead.', + code: 'GTN:010', + fn: __method__.to_s, ) end end - end - def self.check_bad_trs_link(contents) - find_matching_texts(contents, %r{snippet faqs/galaxy/workflows_run_trs.md path="([^"]*)"}i) - .map do |idx, _text, selected| - path = selected[1].to_s.strip - if !File.exist?(path) - ReviewDogEmitter.error( + ## + # GTN:011 - Do not use target="_blank" it is bad for accessibility. + def self.no_target_blank(contents) + find_matching_texts(contents, /target=("_blank"|'_blank')/) + .map do |idx, _text, selected| + ReviewDogEmitter.warning( path: @path, idx: idx, match_start: selected.begin(0), match_end: selected.end(0), replacement: nil, - message: "The linked file (`#{path}`) could not be found.", - code: 'GTN:036' + message: 'Please do not use `target="_blank"`, [it is bad for accessibility.]' \ + '(https://www.a11yproject.com/checklist/#identify-links-that-open-in-a-new-tab-or-window)', + code: 'GTN:011', + fn: __method__.to_s, ) end end - end - def self.check_looks_like_heading(contents) - # TODO: we should remove this someday, but, we need to have a good solution - # and we're still a ways from that. + ## + # GTN:034 - Alternative text or alt-text is mandatory for every image in the GTN. + def self.empty_alt_text(contents) + find_matching_texts(contents, /!\[\]\(/i) + .map do |idx, _text, selected| + path = selected[1].to_s.strip + if !File.exist?(path.gsub(%r{^/}, '')) + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0), + replacement: nil, + message: 'The alt text for this image seems to be empty', + code: 'GTN:034', + fn: __method__.to_s, + ) + end + end + end + + ## + # GTN:018 - You have linked to a file but this file could not be found. Check your link to make sure the path exists. # - # There's no clear way to say "this subsection of the content has its own hierarchy" - return if @path.match(/faq/) + # Note that we use a customised version of Jekyll's link function which will not work correctly for _posts/news items, which should be corrected at some point. We should remove our custom link function and go back to the official one. + def self.check_bad_link(contents) + find_matching_texts(contents, /{%\s*link\s+([^%]*)\s*%}/i) + .map do |idx, _text, selected| + path = selected[1].to_s.strip + if !File.exist?(path.gsub(%r{^/}, '')) + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0), + replacement: nil, + message: "The linked file (`#{selected[1].strip}`) could not be found.", + code: 'GTN:018', + fn: __method__.to_s, + ) + end + end - find_matching_texts(contents, /^\*\*(.*)\*\*$/) - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: "### #{selected[1]}", - message: "This looks like a heading, but isn't. Please use proper semantic headings where possible. " \ - 'You should check the heading level of this suggestion, rather than accepting the change as-is.', - code: 'GTN:020' - ) + find_matching_texts(contents, /\]\(\)/i) + .map do |idx, _text, selected| + path = selected[1].to_s.strip + if !File.exist?(path.gsub(%r{^/}, '')) + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0), + replacement: nil, + message: 'The link does not seem to have a target.', + code: 'GTN:018', + fn: __method__.to_s, + ) + end + end end - end - @KNOWN_TAGS = [ - # GTN - 'cite', - 'snippet', - 'link', - 'icon', - 'tool', - 'color', - - 'set', # This isn't strictly GTN, it's seen inside a raw in a tool tutorial. - # Jekyll - 'if', 'else', 'elsif', 'endif', - 'capture', 'assign', 'include', - 'comment', 'endcomment', - 'for', 'endfor', - 'unless', 'endunless', - 'raw', 'endraw' - ].freeze - - def self.check_bad_tag(contents) - find_matching_texts(contents, /{%\s*(?<tag>[a-z]+)/) - .reject { |_idx, _text, selected| @KNOWN_TAGS.include? selected[:tag] } - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: nil, - message: "We're not sure this tag is correct (#{selected[:tag]}), it isn't one of the known tags.", - code: 'GTN:021' - ) + ## + # GTN:036 - You have used the TRS snippet to link to a TRS ID but the link does not seem to be correct. + def self.check_bad_trs_link(contents) + find_matching_texts(contents, %r{snippet faqs/galaxy/workflows_run_trs.md path="([^"]*)"}i) + .map do |idx, _text, selected| + path = selected[1].to_s.strip + if !File.exist?(path) + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(0), + match_end: selected.end(0), + replacement: nil, + message: "The linked file (`#{path}`) could not be found.", + code: 'GTN:036', + fn: __method__.to_s, + ) + end + end + end + + ## + # GTN:020 - Please do not bold random lines, use a heading properly. + def self.check_looks_like_heading(contents) + # TODO: we should remove this someday, but, we need to have a good solution + # and we're still a ways from that. + # + # There's no clear way to say "this subsection of the content has its own hierarchy" + return if @path.match(/faq/) + + find_matching_texts(contents, /^\*\*(.*)\*\*$/) + .map do |idx, _text, selected| + ReviewDogEmitter.warning( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: "### #{selected[1]}", + message: "This looks like a heading, but isn't. Please use proper semantic headings where possible. " \ + 'You should check the heading level of this suggestion, rather than accepting the change as-is.', + code: 'GTN:020', + fn: __method__.to_s, + ) + end + end + + @KNOWN_TAGS = [ + # GTN + 'cite', + 'snippet', + 'link', + 'icon', + 'tool', + 'color', + + 'set', # This isn't strictly GTN, it's seen inside a raw in a tool tutorial. + # Jekyll + 'if', 'else', 'elsif', 'endif', + 'capture', 'assign', 'include', + 'comment', 'endcomment', + 'for', 'endfor', + 'unless', 'endunless', + 'raw', 'endraw' + ].freeze + + ## + # GTN:021 - We are not sure this tag is correct, there is a very limited set of Jekyll/liquid tags that are used in GTN tutorials, and this checks for surprises. + def self.check_bad_tag(contents) + find_matching_texts(contents, /{%\s*(?<tag>[a-z]+)/) + .reject { |_idx, _text, selected| @KNOWN_TAGS.include? selected[:tag] } + .map do |idx, _text, selected| + ReviewDogEmitter.warning( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: nil, + message: "We're not sure this tag is correct (#{selected[:tag]}), it isn't one of the known tags.", + code: 'GTN:021', + fn: __method__.to_s, + ) + end + end + + @BOX_CLASSES = %w[ + agenda + code-in + code-out + comment + details + feedback + hands-on + hands_on + question + solution + tip + warning + ].freeze + + ## + # GTN:022 - Please do not prefix your boxes of a type with the box name. + # + # Do not do: + # + # > <question-title>Question: Some question!</question-title> + # + # Instead: + # + # > <question-title>Some question!</question-title> + # + # As the Question: prefix will be added automatically when necessary. This goes also for tip/comment/etc. + def self.check_useless_box_prefix(contents) + find_matching_texts(contents, /<(?<tag>[a-z_-]+)-title>(?<fw>[a-zA-Z_-]+:?\s*)/) + .select do |_idx, _text, selected| + @BOX_CLASSES.include?(selected[:tag]) and selected[:tag] == selected[:fw].gsub(/:\s*$/, '').downcase + end + .map do |idx, _text, selected| + ReviewDogEmitter.warning( + path: @path, + idx: idx, + match_start: selected.begin(2), + match_end: selected.end(2) + 1, + replacement: '', + message: "It is no longer necessary to prefix your #{selected[:tag]} box titles with " \ + "#{selected[:tag].capitalize}, this is done automatically.", + code: 'GTN:022', + fn: __method__.to_s, + ) + end end - end - @BOX_CLASSES = %w[ - agenda - code-in - code-out - comment - details - feedback - hands-on - hands_on - question - solution - tip - warning - ].freeze - - def self.check_useless_box_prefix(contents) - find_matching_texts(contents, /<(?<tag>[a-z_-]+)-title>(?<fw>[a-zA-Z_-]+:?\s*)/) - .select do |_idx, _text, selected| - @BOX_CLASSES.include?(selected[:tag]) and selected[:tag] == selected[:fw].gsub(/:\s*$/, '').downcase - end - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(2), - match_end: selected.end(2) + 1, - replacement: '', - message: "It is no longer necessary to prefix your #{selected[:tag]} box titles with " \ - "#{selected[:tag].capitalize}, this is done automatically.", - code: 'GTN:022' - ) - end - end + ## + # GTN:028 - Your headings are out of order. Please check it properly, that you do not skip levels. + def self.check_bad_heading_order(contents) + doc = Kramdown::Document.new(contents.join("\n"), input: 'GFM') + headers = doc.root.children.select{|k| k.type == :header} - def self.check_bad_heading_order(contents) - depth = 1 - headings = find_matching_texts(contents, /^(?<level>#+)\s?(?<title>.*)/) - .map do |idx, text, selected| - new_depth = selected[:level].length - depth_change = new_depth - depth - depth = new_depth - [idx, text, selected, depth_change, new_depth] - end + bad_depth = headers + .each_cons(2) # Two items at a time + .select{|k1, k2| k2.options[:level] - k1.options[:level] > 1} # All that have a >1 shift in heading depth + .map{|_,b | b} # Only the second, failing one. - all_headings = headings.map do |_idx, _text, selected, _depth_change, _new_depth| - "#{selected[:level]} #{selected[:title]}" - end + all_headings = headers + .map{|k| "#" * k.options[:level] + " "+ k.options[:raw_text] } - headings.select do |_idx, _text, _selected, depth_change, _new_depth| - depth_change > 1 - end.map do |idx, _text, selected, depth_change, new_depth| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: '#' * (new_depth - depth_change + 1), - message: "You have skipped a heading level, please correct this.\n<details>" \ - "<summary>Listing of Heading Levels</summary>\n\n```\n#{all_headings.join("\n")}\n```\n</details>", - code: 'GTN:028' - ) + bad_depth.map{|k| + ReviewDogEmitter.error( + path: @path, + idx: k.options[:location] - 1, + match_start: 0, + match_end: k.options[:raw_text].length + k.options[:level] + 1, + replacement: '#' * (k.options[:level] - 1), + message: "You have skipped a heading level, please correct this.\n<details>" \ + "<summary>Listing of Heading Levels</summary>\n\n```\n#{all_headings.join("\n")}\n```\n</details>", + code: 'GTN:028', + fn: __method__.to_s, + ) + } end - end - def self.check_bolded_heading(contents) - find_matching_texts(contents, /^#+ (?<title>\*\*.*\*\*)$/) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: selected[:title][2..-3], - message: 'Please do not bold headings, it is unncessary ' \ - 'and will potentially cause screen readers to shout them.', - code: 'GTN:029' - ) + ## + # GTN:029 - Please do not bold headings + def self.check_bolded_heading(contents) + find_matching_texts(contents, /^#+ (?<title>\*\*.*\*\*)$/) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: selected[:title][2..-3], + message: 'Please do not bold headings, it is unncessary ' \ + 'and will potentially cause screen readers to shout them.', + code: 'GTN:029', + fn: __method__.to_s, + ) + end end - end - def self.zenodo_api(contents) - find_matching_texts(contents, %r{(zenodo\.org/api/files/)}) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: nil, - message: 'The Zenodo.org/api URLs are not stable, you must use a URL of the format zenodo.org/record/...', - code: 'GTN:032' - ) + ## + # GTN:032 - zenodo.org/api links are invalid in the GTN, please use the zenodo.org/records/id/files/<filename> format instead. This ensures that when users download files from zenodo into Galaxy, they appear correctly, with a useful filename. + # + # Seems to be a duplicate of Gtn::Linter.bad_zenodo_links + def self.zenodo_api(contents) + find_matching_texts(contents, %r{(zenodo\.org/api/files/)}) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: nil, + message: 'The Zenodo.org/api URLs are not stable, you must use a URL of the format zenodo.org/record/...', + code: 'GTN:032' + ) + end end - end - def self.nonsemantic_list(contents) - find_matching_texts(contents, />\s*(\*\*\s*[Ss]tep)/) - .map do |idx, _text, selected| - ReviewDogEmitter.error( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: nil, - message: 'This is a non-semantic list which is bad for accessibility and bad for screenreaders. ' \ - 'It results in poorly structured HTML and as a result is not allowed.', - code: 'GTN:035' - ) + ## + # GTN:035 - This is a non-semantic list which is bad for accessibility and screenreaders. + # + # Do not do: + # + # * Step 1. Some text + # * Step 2. some other thing + # + # Do not do: + # + # Step 1. Some text + # Step 2. some other thing + # + # Instead: + # + # 1. some text + # 2. some other + # + # That is a proper semantic list. + def self.nonsemantic_list(contents) + find_matching_texts(contents, />\s*(\*\*\s*[Ss]tep)/) + .map do |idx, _text, selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: nil, + message: 'This is a non-semantic list which is bad for accessibility and bad for screenreaders. ' \ + 'It results in poorly structured HTML and as a result is not allowed.', + code: 'GTN:035', + fn: __method__.to_s, + ) + end end - end - def self.cyoa_branches(contents) - joined_contents = contents.join("\n") - cyoa_branches = joined_contents.scan(/_includes\/cyoa-choices[^%]*%}/m) - .map{|cyoa_line| - cyoa_line.gsub(/\n/, ' ') # Remove newlines, want it all one one line. - .gsub(/\s+/, ' ') # Collapse multiple whitespace for simplicity - .gsub(/_includes\/cyoa-choices.html/, '').gsub(/%}$/, '') # Strip start/end - .strip - .split('" ') # Split on the end of an option to get the individual option groups - .map{|p| p.gsub(/="/, '=').split('=')}.to_h} # convert it into a convenient hash - # NOTE: Errors on this line usually mean that folks have used ' instead of " in their CYOA. - - - # cyoa_branches = - # [{"option1"=>"Quick one tool method", - # "option2"=>"Convert to AnnData object compatible with Filter, Plot, Explore workflow", - # "default"=>"Quick one tool method", - # "text"=>"Choose below if you just want to convert your object quickly or see how it all happens behind the scenes!", - # "disambiguation"=>"seurat2anndata\""}, - - # We use slugify_unsafe to convert it to a slug, now we should check: - # 1. Is it unique in the file? No duplicate options? - # 2. Is every branch used? - - # Uniqueness: - options = cyoa_branches.map{|o| o.select{|k, v| k =~ /option/}.values}.flatten - slugified = options.map{|o| [o, unsafe_slugify(o)]} - slugified_grouped = slugified.group_by{|before, after| after} - .map{|k, pairs| [k, pairs.map{|p| p[0]}]}.to_h - - errors = [] - if slugified_grouped.values.any?{|v| v.length > 1} - dupes = slugified_grouped.select{|k, v| v.length > 1} - msg = "We identified the following duplicate options in your CYOA: " - msg += dupes.map do |slug, options| - "Options #{options.join(', ')} became the key: #{slug}" - end.join("; ") - - errors << ReviewDogEmitter.error( - path: @path, - idx: 0, - match_start: 0, - match_end: 1, - replacement: nil, - message: 'You have non-unique options in your Choose Your Own Adventure. Please ensure that each option is unique in its text. Unfortunately we do not currently support re-using the same option text across differently disambiguated CYOA branches, so, please inform us if this is a requirement for you.' + msg, - code: 'GTN:041' - ) - end + ## + # GTN:041, GTN:042, GTN:043, GTN:044, GTN:045 - This checks for a myriad variety of CYOA issues. Please see the error message for help resolving them. + def self.cyoa_branches(contents) + joined_contents = contents.join("\n") + cyoa_branches = joined_contents.scan(/_includes\/cyoa-choices[^%]*%}/m) + .map{|cyoa_line| + cyoa_line.gsub(/\n/, ' ') # Remove newlines, want it all one one line. + .gsub(/\s+/, ' ') # Collapse multiple whitespace for simplicity + .gsub(/_includes\/cyoa-choices.html/, '').gsub(/%}$/, '') # Strip start/end + .strip + .split('" ') # Split on the end of an option to get the individual option groups + .map{|p| p.gsub(/="/, '=').split('=')}.to_h} # convert it into a convenient hash + # NOTE: Errors on this line usually mean that folks have used ' instead of " in their CYOA. + + + # cyoa_branches = + # [{"option1"=>"Quick one tool method", + # "option2"=>"Convert to AnnData object compatible with Filter, Plot, Explore workflow", + # "default"=>"Quick one tool method", + # "text"=>"Choose below if you just want to convert your object quickly or see how it all happens behind the scenes!", + # "disambiguation"=>"seurat2anndata\""}, + + # We use slugify_unsafe to convert it to a slug, now we should check: + # 1. Is it unique in the file? No duplicate options? + # 2. Is every branch used? + + # Uniqueness: + options = cyoa_branches.map{|o| o.select{|k, v| k =~ /option/}.values}.flatten + slugified = options.map{|o| [o, unsafe_slugify(o)]} + slugified_grouped = slugified.group_by{|before, after| after} + .map{|k, pairs| [k, pairs.map{|p| p[0]}]}.to_h + + errors = [] + if slugified_grouped.values.any?{|v| v.length > 1} + dupes = slugified_grouped.select{|k, v| v.length > 1} + msg = "We identified the following duplicate options in your CYOA: " + msg += dupes.map do |slug, options| + "Options #{options.join(', ')} became the key: #{slug}" + end.join("; ") - # Missing default - cyoa_branches.each do |branch| - if branch['default'].nil? errors << ReviewDogEmitter.error( path: @path, idx: 0, match_start: 0, match_end: 1, replacement: nil, - message: 'We recommend specifying a default for every branch', - code: 'GTN:042' + message: 'You have non-unique options in your Choose Your Own Adventure. Please ensure that each option is unique in its text. Unfortunately we do not currently support re-using the same option text across differently disambiguated CYOA branches, so, please inform us if this is a requirement for you.' + msg, + code: 'GTN:041', + fn: __method__.to_s, ) end - # Checking default/options correspondence. - options = branch.select{|k, v| k =~ /option/}.values - if branch.key?("default") && ! options.include?(branch['default']) - if options.any?{|o| unsafe_slugify(o) == unsafe_slugify(branch['default'])} - errors << ReviewDogEmitter.warning( + # Missing default + cyoa_branches.each do |branch| + if branch['default'].nil? + errors << ReviewDogEmitter.error( path: @path, idx: 0, match_start: 0, match_end: 1, replacement: nil, - message: "We did not see a corresponding option# for the default: ÂŤ#{branch['default']}Âť, but this could have been written before we automatically slugified the options. If you like, please consider making your default option match the option text exactly.", - code: 'GTN:043' - ) - else - errors << ReviewDogEmitter.warning( - path: @path, - idx: 0, - match_start: 0, - match_end: 1, - replacement: nil, - message: "We did not see a corresponding option# for the default: ÂŤ#{branch['default']}Âť, please ensure the text matches one of the branches.", - code: 'GTN:044' + message: 'We recommend specifying a default for every branch', + code: 'GTN:042', + fn: __method__.to_s, ) end + + # Checking default/options correspondence. + options = branch.select{|k, v| k =~ /option/}.values + if branch.key?("default") && ! options.include?(branch['default']) + if options.any?{|o| unsafe_slugify(o) == unsafe_slugify(branch['default'])} + errors << ReviewDogEmitter.warning( + path: @path, + idx: 0, + match_start: 0, + match_end: 1, + replacement: nil, + message: "We did not see a corresponding option# for the default: ÂŤ#{branch['default']}Âť, but this could have been written before we automatically slugified the options. If you like, please consider making your default option match the option text exactly.", + code: 'GTN:043', + fn: __method__.to_s, + ) + else + errors << ReviewDogEmitter.warning( + path: @path, + idx: 0, + match_start: 0, + match_end: 1, + replacement: nil, + message: "We did not see a corresponding option# for the default: ÂŤ#{branch['default']}Âť, please ensure the text matches one of the branches.", + code: 'GTN:044', + fn: __method__.to_s, + ) + end + end end - end - # Branch testing. - cyoa_branches.each do |branch| - options = branch - .select{|k, v| k =~ /option/} - .values - - # Check for matching lines in the file. - options.each do |option| - slug_option = unsafe_slugify(option) - if !joined_contents.match(/#{slug_option}/) - errors << ReviewDogEmitter.warning( - path: @path, - idx: 0, - match_start: 0, - match_end: 1, - replacement: nil, - message: "We did not see a branch for #{option} (#{slug_option}) in the file. Please consider ensuring that all options are used.", - code: 'GTN:045' - ) + # Branch testing. + cyoa_branches.each do |branch| + options = branch + .select{|k, v| k =~ /option/} + .values + + # Check for matching lines in the file. + options.each do |option| + slug_option = unsafe_slugify(option) + if !joined_contents.match(/#{slug_option}/) + errors << ReviewDogEmitter.warning( + path: @path, + idx: 0, + match_start: 0, + match_end: 1, + replacement: nil, + message: "We did not see a branch for #{option} (#{slug_option}) in the file. Please consider ensuring that all options are used.", + code: 'GTN:045', + fn: __method__.to_s, + ) + end end end + + # find_matching_texts(contents, />\s*(\*\*\s*[Ss]tep)/) .map do |idx, _text, selected| + # ReviewDogEmitter.error( + # path: @path, + # idx: idx, + # match_start: selected.begin(1), + # match_end: selected.end(1) + 1, + # replacement: nil, + # message: 'This is a non-semantic list which is bad for accessibility and bad for screenreaders. ' \ + # 'It results in poorly structured HTML and as a result is not allowed.', + # code: 'GTN:035' + # ) + # end + errors end - - - - # find_matching_texts(contents, />\s*(\*\*\s*[Ss]tep)/) .map do |idx, _text, selected| - # ReviewDogEmitter.error( - # path: @path, - # idx: idx, - # match_start: selected.begin(1), - # match_end: selected.end(1) + 1, - # replacement: nil, - # message: 'This is a non-semantic list which is bad for accessibility and bad for screenreaders. ' \ - # 'It results in poorly structured HTML and as a result is not allowed.', - # code: 'GTN:035' - # ) - # end - errors - end - def self.useless_intro(contents) - joined_contents = contents.join("\n") - joined_contents.scan(/\n---\n+# Introduction/m) - .map do |line| - ReviewDogEmitter.error( - path: @path, - idx: 0, - match_start: 0, - match_end: 0, - replacement: '', - message: 'Please do not include an # Introduction section, it is unnecessary here, just start directly into your text. The first paragraph that is seen by our infrastructure will automatically be shown in a few places as an abstract.', - code: 'GTN:046' - ) + ## + # GTN:046 - Please do not add an # Introduction section, as it is unnecessary, please start directly into an abstract or hook for your tutorial that will get the learner interested in the material. + def self.useless_intro(contents) + joined_contents = contents.join("\n") + joined_contents.scan(/\n---\n+# Introduction/m) + .map do |line| + ReviewDogEmitter.error( + path: @path, + idx: 0, + match_start: 0, + match_end: 0, + replacement: '', + message: 'Please do not include an # Introduction section, it is unnecessary here, just start directly into your text. The first paragraph that is seen by our infrastructure will automatically be shown in a few places as an abstract.', + code: 'GTN:046', + fn: __method__.to_s, + ) + end end - end - def self.fix_md(contents) - [ - *fix_notoc(contents), - *youtube_bad(contents), - *link_gtn_slides_external(contents), - *link_gtn_tutorial_external(contents), - *check_dois(contents), - *check_pmids(contents), - *check_bad_link_text(contents), - *incorrect_calls(contents), - *check_bad_cite(contents), - *non_existent_snippet(contents), - *bad_tool_links(contents), - *check_tool_link(contents), - *new_more_accessible_boxes(contents), - *new_more_accessible_boxes_agenda(contents), - *no_target_blank(contents), - *check_bad_link(contents), - *check_bad_icon(contents), - *check_looks_like_heading(contents), - *check_bad_tag(contents), - *check_useless_box_prefix(contents), - *check_bad_heading_order(contents), - *check_bolded_heading(contents), - *snippets_too_close_together(contents), - *bad_zenodo_links(contents), - *zenodo_api(contents), - *empty_alt_text(contents), - *check_bad_trs_link(contents), - *nonsemantic_list(contents), - *cyoa_branches(contents), - *useless_intro(contents) - ] - end + def self.fix_md(contents) + [ + *fix_notoc(contents), + *youtube_bad(contents), + *link_gtn_slides_external(contents), + *link_gtn_tutorial_external(contents), + *check_dois(contents), + *check_pmids(contents), + *check_bad_link_text(contents), + *incorrect_calls(contents), + *check_bad_cite(contents), + *non_existent_snippet(contents), + *bad_tool_links(contents), + *check_tool_link(contents), + *new_more_accessible_boxes(contents), + *new_more_accessible_boxes_agenda(contents), + *no_target_blank(contents), + *check_bad_link(contents), + *check_bad_icon(contents), + *check_looks_like_heading(contents), + *check_bad_tag(contents), + *check_useless_box_prefix(contents), + *check_bad_heading_order(contents), + *check_bolded_heading(contents), + *snippets_too_close_together(contents), + *bad_zenodo_links(contents), + *zenodo_api(contents), + *empty_alt_text(contents), + *check_bad_trs_link(contents), + *nonsemantic_list(contents), + *cyoa_branches(contents), + *useless_intro(contents) + ] + end - def self.bib_missing_mandatory_fields(bib) - results = [] - bib.each do |x| - begin - doi = x.doi - rescue StandardError - doi = nil + def self.bib_missing_mandatory_fields(bib) + results = [] + bib.each do |x| + begin + doi = x.doi + rescue StandardError + doi = nil + end + + begin + url = x.url + rescue StandardError + url = nil + end + + results.push([x.key, 'Missing both a DOI and a URL. Please add one of the two.']) if doi.nil? && url.nil? + + begin + x.title + results.push([x.key, 'This entry is missing a title attribute. Please add it.']) if !x.title + rescue StandardError + results.push([x.key, 'This entry is missing a title attribute. Please add it.']) + end end + results + end - begin - url = x.url - rescue StandardError - url = nil + ## + # GTN:015, GTN:016, GTN:025, GTN:026, others. + # These error messages indicate something is amiss with your workflow. Please consult the error message to correct it. + def self.fix_ga_wf(contents) + results = [] + if !contents.key?('tags') or contents['tags'].empty? + path_parts = @path.split('/') + topic = path_parts[path_parts.index('topics') + 1] + + results.push(ReviewDogEmitter.file_error( + path: @path, message: "This workflow is missing required tags. Please add `\"tags\": [\"#{topic}\"]`", + code: 'GTN:015' + )) end - results.push([x.key, 'Missing both a DOI and a URL. Please add one of the two.']) if doi.nil? && url.nil? + if !contents.key?('annotation') + results.push(ReviewDogEmitter.file_error( + path: @path, + message: 'This workflow is missing an annotation. Please add `"annotation": "title of tutorial"`', + code: 'GTN:016' + )) + end - begin - x.title - results.push([x.key, 'This entry is missing a title attribute. Please add it.']) if !x.title - rescue StandardError - results.push([x.key, 'This entry is missing a title attribute. Please add it.']) + if !contents.key?('license') + results.push(ReviewDogEmitter.file_error( + path: @path, + message: 'This workflow is missing a license. Please select a valid OSI license. ' \ + 'You can correct this in the Galaxy workflow editor.', + code: 'GTN:026' + )) end - end - results - end - def self.fix_ga_wf(contents) - results = [] - if !contents.key?('tags') or contents['tags'].empty? - path_parts = @path.split('/') - topic = path_parts[path_parts.index('topics') + 1] + tool_ids = tool_id_extractor(contents) - results.push(ReviewDogEmitter.file_error( - path: @path, message: "This workflow is missing required tags. Please add `\"tags\": [\"#{topic}\"]`", - code: 'GTN:015' - )) - end + # Check if they use TS tools, we do this here because it's easier to look at the plain text. + tool_ids.each do |step_id, id| + if ! acceptable_tool?(id) + results += [ + ReviewDogEmitter.error( + path: @path, + idx: 0, + match_start: 0, + match_end: 0, + replacement: nil, + message: "A step in your workflow (#{step_id}) uses an invalid tool ID (#{id}) or a tool ID from the testtoolshed. These are not permitted in GTN tutorials. If this is in error, you can add it to the top of _plugins/utils.rb", + code: 'GTN:017' + ) + ] + end + end - if !contents.key?('annotation') - results.push(ReviewDogEmitter.file_error( - path: @path, - message: 'This workflow is missing an annotation. Please add `"annotation": "title of tutorial"`', - code: 'GTN:016' - )) - end - if !contents.key?('license') - results.push(ReviewDogEmitter.file_error( - path: @path, - message: 'This workflow is missing a license. Please select a valid OSI license. ' \ - 'You can correct this in the Galaxy workflow editor.', - code: 'GTN:026' - )) - end - if contents.key?('creator') - contents['creator'] - .select { |c| c['class'] == 'Person' } - .each do |p| - if !p.key?('identifier') || (p['identifier'] == '') - results.push(ReviewDogEmitter.file_error( - path: @path, - message: 'This workflow has a creator but is missing an identifier for them. ' \ - 'Please ensure all creators have valid ORCIDs.', - code: 'GTN:025' - )) - end - if !p.key?('name') || (p['name'] == '') - results.push(ReviewDogEmitter.file_error( - path: @path, message: 'This workflow has a creator but is a name, please add it.', - code: 'GTN:025' - )) + if contents.key?('creator') + contents['creator'] + .select { |c| c['class'] == 'Person' } + .each do |p| + if !p.key?('identifier') || (p['identifier'] == '') + results.push(ReviewDogEmitter.file_error( + path: @path, + message: 'This workflow has a creator but is missing an identifier for them. ' \ + 'Please ensure all creators have valid ORCIDs.', + code: 'GTN:025' + )) + end + + if !p.key?('name') || (p['name'] == '') + results.push(ReviewDogEmitter.file_error( + path: @path, message: 'This workflow has a creator but is a name, please add it.', + code: 'GTN:025' + )) + end end - end - else - results.push(ReviewDogEmitter.file_error( - path: @path, - message: 'This workflow is missing a Creator. Please edit this workflow in ' \ - 'Galaxy to add the correct creator entities', - code: 'GTN:024' - )) + else + results.push(ReviewDogEmitter.file_error( + path: @path, + message: 'This workflow is missing a Creator. Please edit this workflow in ' \ + 'Galaxy to add the correct creator entities', + code: 'GTN:024' + )) + end + results end - results - end - def self.fix_bib(contents, bib) - bad_keys = bib_missing_mandatory_fields(bib) - results = [] - bad_keys.each do |key, reason| - results += find_matching_texts(contents, /^\s*@.*{#{key},/) - .map do |idx, text, _selected| - ReviewDogEmitter.error( + ## + # GTN:012 - Your bibliography is missing mandatory fields (either a URL or DOI). + # GTN:031 - Your bibliography unnecessarily fills the DOI field with https://doi.org, you can just directly specify the DOI. + def self.fix_bib(contents, bib) + bad_keys = bib_missing_mandatory_fields(bib) + results = [] + bad_keys.each do |key, reason| + results += find_matching_texts(contents, /^\s*@.*{#{key},/) + .map do |idx, text, _selected| + ReviewDogEmitter.error( + path: @path, + idx: idx, + match_start: 0, + match_end: text.length, + replacement: nil, + message: reason, + code: 'GTN:012', + fn: __method__.to_s, + ) + end + end + + # 13: doi = {https://doi.org/10.1016/j.cmpbup.2021.100007}, + results += find_matching_texts(contents, %r{doi\s*=\s*\{(https?://doi.org/)}) + .map do |idx, _text, selected| + ReviewDogEmitter.warning( path: @path, idx: idx, - match_start: 0, - match_end: text.length, - replacement: nil, - message: reason, - code: 'GTN:012' + match_start: selected.begin(1), + match_end: selected.end(1) + 1, + replacement: '', + message: 'Unnecessary use of URL in DOI-only field, please just use the doi component itself', + code: 'GTN:031' ) end + results end - # 13: doi = {https://doi.org/10.1016/j.cmpbup.2021.100007}, - results += find_matching_texts(contents, %r{doi\s*=\s*\{(https?://doi.org/)}) - .map do |idx, _text, selected| - ReviewDogEmitter.warning( - path: @path, - idx: idx, - match_start: selected.begin(1), - match_end: selected.end(1) + 1, - replacement: '', - message: 'Unnecessary use of URL in DOI-only field, please just use the doi component itself', - code: 'GTN:031' - ) - end - results - end - - @PLAIN_OUTPUT = false + @PLAIN_OUTPUT = false - def self.set_plain_output - @PLAIN_OUTPUT = true - end + def self.set_plain_output + @PLAIN_OUTPUT = true + end - def self.set_rdjson_output - @PLAIN_OUTPUT = false - end + def self.set_rdjson_output + @PLAIN_OUTPUT = false + end - @SHORT_PATH = false - def self.set_short_path - @SHORT_PATH = true - end + @SHORT_PATH = false + def self.set_short_path + @SHORT_PATH = true + end - @LIMIT_EMITTED_CODES = nil - def self.code_limits(codes) - @LIMIT_EMITTED_CODES = codes - end + @LIMIT_EMITTED_CODES = nil + def self.code_limits(codes) + @LIMIT_EMITTED_CODES = codes + end - @AUTO_APPLY_FIXES = false - def self.enable_auto_fix - @AUTO_APPLY_FIXES = true - end + @AUTO_APPLY_FIXES = false + def self.enable_auto_fix + @AUTO_APPLY_FIXES = true + end - def self.format_reviewdog_output(message) - return if message.nil? || message.empty? - return if !@LIMIT_EMITTED_CODES.nil? && !@LIMIT_EMITTED_CODES.include?(message['code']['value']) + def self.format_reviewdog_output(message) + return if message.nil? || message.empty? + return if !@LIMIT_EMITTED_CODES.nil? && !@LIMIT_EMITTED_CODES.include?(message['code']['value']) - if !message.nil? && (message != []) && message.is_a?(Hash) - path = message['location']['path'] - if @SHORT_PATH && path.include?(GTN_HOME + '/') - path = path.gsub(GTN_HOME + '/', '') - end - if @PLAIN_OUTPUT # $stdout.tty? or - parts = [ - path, - message['location']['range']['start']['line'], - message['location']['range']['start']['column'], - message['location']['range']['end']['line'], - message['location']['range']['end']['column'], - "#{message['code']['value'].gsub(/:/, '')} #{message['message'].split("\n")[0]}" - ] - puts parts.join(':') - else - puts JSON.generate(message) + if !message.nil? && (message != []) && message.is_a?(Hash) + path = message['location']['path'] + if @SHORT_PATH && path.include?(GTN_HOME + '/') + path = path.gsub(GTN_HOME + '/', '') + end + if @PLAIN_OUTPUT # $stdout.tty? or + parts = [ + path, + message['location']['range']['start']['line'], + message['location']['range']['start']['column'], + message['location']['range']['end']['line'], + message['location']['range']['end']['column'], + "#{message['code']['value'].gsub(/:/, '')} #{message['message'].split("\n")[0]}" + ] + puts parts.join(':') + else + puts JSON.generate(message) + end end - end - return unless @AUTO_APPLY_FIXES && message['suggestions'].length.positive? + return unless @AUTO_APPLY_FIXES && message['suggestions'].length.positive? - start_line = message['location']['range']['start']['line'] - start_coln = message['location']['range']['start']['column'] - end_line = message['location']['range']['end']['line'] - end_coln = message['location']['range']['end']['column'] + start_line = message['location']['range']['start']['line'] + start_coln = message['location']['range']['start']['column'] + end_line = message['location']['range']['end']['line'] + end_coln = message['location']['range']['end']['column'] - if start_line == end_line - # We only really support single-line changes. This will probs fuck up - lines = File.read(message['location']['path']).split("\n") - original = lines[start_line - 1].dup + if start_line == end_line + # We only really support single-line changes. This will probs fuck up + lines = File.read(message['location']['path']).split("\n") + original = lines[start_line - 1].dup - repl = message['suggestions'][0]['text'] + repl = message['suggestions'][0]['text'] - # puts "orig #{original}" - # puts "before #{original[0..start_coln - 2]}" - # puts "selected '#{original[start_coln-1..end_coln-2]}'" - # puts "after #{original[end_coln-2..-1]}" - # puts "replace: #{repl}" + # puts "orig #{original}" + # puts "before #{original[0..start_coln - 2]}" + # puts "selected '#{original[start_coln-1..end_coln-2]}'" + # puts "after #{original[end_coln-2..-1]}" + # puts "replace: #{repl}" - # puts "#{original[0..start_coln - 2]} + #{repl} + #{original[end_coln-1..-1]}" - fixed = original[0..start_coln - 2] + repl + original[end_coln - 1..] - warn "DIFF\n-#{original}\n+#{fixed}" - lines[start_line - 1] = fixed + # puts "#{original[0..start_coln - 2]} + #{repl} + #{original[end_coln-1..-1]}" + fixed = original[0..start_coln - 2] + repl + original[end_coln - 1..] + warn "DIFF\n-#{original}\n+#{fixed}" + lines[start_line - 1] = fixed - # Save our changes - File.write(message['location']['path'], (lines + ['']).join("\n")) - else - warn 'Cannot apply this suggestion sorry' + # Save our changes + File.write(message['location']['path'], (lines + ['']).join("\n")) + else + warn 'Cannot apply this suggestion sorry' + end end - end - def self.emit_results(results) - return unless !results.nil? && results.length.positive? + def self.emit_results(results) + return unless !results.nil? && results.length.positive? - results.compact.flatten - .select{|r| r.is_a? Hash } - .each { |r| format_reviewdog_output(r) } - end - - def self.should_ignore(contents) - contents.select { |x| x.match(/GTN:IGNORE:(\d\d\d)/) }.map { |x| "GTN:#{x.match(/GTN:IGNORE:(\d\d\d)/)[1]}" }.uniq - end - - def self.filter_results(results, ignores) - if !results.nil? - # Remove any empty lists - results = results.select { |x| !x.nil? && x.length.positive? }.flatten - # Before ignoring anything matching GTN:IGNORE:### - return results if ignores.nil? or ignores.empty? + results.compact.flatten + .select{|r| r.is_a? Hash } + .each { |r| format_reviewdog_output(r) } + end - results = results.select { |x| ignores.index(x['code']['value']).nil? } if results.length.positive? - return results + def self.should_ignore(contents) + contents.select { |x| x.match(/GTN:IGNORE:(\d\d\d)/) }.map { |x| "GTN:#{x.match(/GTN:IGNORE:(\d\d\d)/)[1]}" }.uniq end - nil - end - def self.fix_file(path) - @path = path + def self.filter_results(results, ignores) + if !results.nil? + # Remove any empty lists + results = results.select { |x| !x.nil? && x.length.positive? }.flatten + # Before ignoring anything matching GTN:IGNORE:### + return results if ignores.nil? or ignores.empty? - if path.match(/\s/) - emit_results([ReviewDogEmitter.file_error(path: path, - message: 'There are spaces in this filename, that is forbidden.', - code: 'GTN:014')]) + results = results.select { |x| ignores.index(x['code']['value']).nil? } if results.length.positive? + return results + end + nil end - if path.match(/\?/) - emit_results([ReviewDogEmitter.file_error(path: path, - message: 'There ?s in this filename, that is forbidden.', - code: 'GTN:014')]) - end + def self.fix_file(path) + @path = path - case path - when /md$/ - handle = File.open(path, 'r') - contents = handle.read.split("\n") - ignores = should_ignore(contents) - results = fix_md(contents) - - results = filter_results(results, ignores) - emit_results(results) - when /.bib$/ - handle = File.open(path, 'r') - contents = handle.read.split("\n") - - bib = BibTeX.open(path) - results = fix_bib(contents, bib) - - results = filter_results(results, ignores) - emit_results(results) - when /.ga$/ - handle = File.open(path, 'r') - begin - contents = handle.read - data = JSON.parse(contents) - rescue StandardError => e - warn "Error parsing #{path}: #{e}" - emit_results([ReviewDogEmitter.file_error(path: path, message: 'Unparseable JSON in this workflow file.', - code: 'GTN:019')]) + if path.match(/\s/) + emit_results([ReviewDogEmitter.file_error(path: path, + message: 'There are spaces in this filename, that is forbidden.', + code: 'GTN:014')]) end - results = [] - # Check if there's a missing workflow test - folder = File.dirname(path) - basename = File.basename(path).gsub(/.ga$/, '') - possible_tests = Dir.glob("#{folder}/#{Regexp.escape(basename)}*ym*") - possible_tests = possible_tests.grep(/#{Regexp.escape(basename)}[_-]tests?.ya?ml/) - - contains_interactive_tool = contents.match(/interactive_tool_/) + if path.match(/\?/) + emit_results([ReviewDogEmitter.file_error(path: path, + message: 'There ?s in this filename, that is forbidden.', + code: 'GTN:014')]) + end - if possible_tests.empty? - if !contains_interactive_tool - results += [ - ReviewDogEmitter.file_error(path: path, - message: 'This workflow is missing a test, which is now mandatory. Please ' \ - 'see [the FAQ on how to add tests to your workflows](' \ - 'https://training.galaxyproject.org/training-material/faqs/' \ - 'gtn/gtn_workflow_testing.html).', - code: 'GTN:027') - ] + case path + when /md$/ + handle = File.open(path, 'r') + contents = handle.read.split("\n") + ignores = should_ignore(contents) + results = fix_md(contents) + + results = filter_results(results, ignores) + emit_results(results) + when /.bib$/ + handle = File.open(path, 'r') + contents = handle.read.split("\n") + + bib = BibTeX.open(path) + results = fix_bib(contents, bib) + + results = filter_results(results, ignores) + emit_results(results) + when /.ga$/ + handle = File.open(path, 'r') + begin + contents = handle.read + data = JSON.parse(contents) + rescue StandardError => e + warn "Error parsing #{path}: #{e}" + emit_results([ReviewDogEmitter.file_error(path: path, message: 'Unparseable JSON in this workflow file.', + code: 'GTN:019')]) end - else - # Load tests and run some quick checks: - possible_tests.each do |test_file| - if !test_file.match(/-tests.yml/) + + results = [] + # Check if there's a missing workflow test + folder = File.dirname(path) + basename = File.basename(path).gsub(/.ga$/, '') + possible_tests = Dir.glob("#{folder}/#{Regexp.escape(basename)}*ym*") + possible_tests = possible_tests.grep(/#{Regexp.escape(basename)}[_-]tests?.ya?ml/) + + contains_interactive_tool = contents.match(/interactive_tool_/) + + if possible_tests.empty? + if !contains_interactive_tool results += [ ReviewDogEmitter.file_error(path: path, - message: 'Please use the extension -tests.yml ' \ - 'for this test file.', - code: 'GTN:032') + message: 'This workflow is missing a test, which is now mandatory. Please ' \ + 'see [the FAQ on how to add tests to your workflows](' \ + 'https://training.galaxyproject.org/training-material/faqs/' \ + 'gtn/gtn_workflow_testing.html).', + code: 'GTN:027') ] end - - test = YAML.safe_load(File.open(test_file)) - test_plain = File.read(test_file) - # check that for each test, the outputs is non-empty - test.each do |test_job| - if (test_job['outputs'].nil? || test_job['outputs'].empty?) && !test_plain.match(/GTN_RUN_SKIP_REASON/) + else + # Load tests and run some quick checks: + possible_tests.each do |test_file| + if !test_file.match(/-tests.yml/) results += [ ReviewDogEmitter.file_error(path: path, - message: 'This workflow test does not test the contents of outputs, ' \ - 'which is now mandatory. Please see [the FAQ on how to add ' \ - 'tests to your workflows](' \ - 'https://training.galaxyproject.org/training-material/faqs/' \ - 'gtn/gtn_workflow_testing.html).', - code: 'GTN:030') + message: 'Please use the extension -tests.yml ' \ + 'for this test file.', + code: 'GTN:032') ] end + + test = YAML.safe_load(File.open(test_file)) + test_plain = File.read(test_file) + # check that for each test, the outputs is non-empty + unless test.is_a?(Array) + next + end + test.each do |test_job| + if (test_job['outputs'].nil? || test_job['outputs'].empty?) && !test_plain.match(/GTN_RUN_SKIP_REASON/) + results += [ + ReviewDogEmitter.file_error(path: path, + message: 'This workflow test does not test the contents of outputs, ' \ + 'which is now mandatory. Please see [the FAQ on how to add ' \ + 'tests to your workflows](' \ + 'https://training.galaxyproject.org/training-material/faqs/' \ + 'gtn/gtn_workflow_testing.html).', + code: 'GTN:030') + ] + end + end end + end - end + results += fix_ga_wf(data) - # Check if they use TS tools, we do this here because it's easier to look at the plain text. - contents.split("\n").each.with_index do |text, linenumber| - if text.match(/testtoolshed/) - results += [ - ReviewDogEmitter.error( - path: @path, - idx: linenumber, - match_start: 0, - match_end: text.length, - replacement: nil, - message: 'This step uses a tool from the testtoolshed. These are not permitted in GTN tutorials.', - code: 'GTN:017' - ) - ] - end + results = filter_results(results, ignores) + emit_results(results) end - results += fix_ga_wf(data) - - results = filter_results(results, ignores) - emit_results(results) end - end - def self.enumerate_type(filter, root_dir: 'topics') - paths = [] - Find.find("./#{root_dir}") do |path| - if FileTest.directory?(path) - next unless File.basename(path).start_with?('.') + def self.enumerate_type(filter, root_dir: 'topics') + paths = [] + Find.find("./#{root_dir}") do |path| + if FileTest.directory?(path) + next unless File.basename(path).start_with?('.') - Find.prune # Don't look any further into this directory. + Find.prune # Don't look any further into this directory. - elsif path.match(filter) - paths.push(path) + elsif path.match(filter) + paths.push(path) + end end + paths end - paths - end - def self.enumerate_symlinks - paths = [] - Find.find('./topics') do |path| - if FileTest.directory?(path) - next unless File.basename(path).start_with?('.') + def self.enumerate_symlinks + paths = [] + Find.find('./topics') do |path| + if FileTest.directory?(path) + next unless File.basename(path).start_with?('.') - Find.prune # Don't look any further into this directory. + Find.prune # Don't look any further into this directory. - elsif File.symlink?(path) - paths.push(path) + elsif File.symlink?(path) + paths.push(path) + end end + paths end - paths - end - - def self.enumerate_lintable - enumerate_type(/bib$/) + enumerate_type(/md$/) + enumerate_type(/md$/, - root_dir: 'faqs') + enumerate_type(/md$/, - root_dir: 'news') - end - def self.enumerate_all - enumerate_type(/.*/) - end + def self.enumerate_lintable + enumerate_type(/bib$/) + enumerate_type(/md$/) + enumerate_type(/md$/, + root_dir: 'faqs') + enumerate_type(/md$/, + root_dir: 'news') + end - def self.run_linter_global - enumerate_type(/:/).each do |path| - format_reviewdog_output( - ReviewDogEmitter.file_error(path: path, - message: 'There are colons in this filename, that is forbidden.', code: 'GTN:014') - ) + def self.enumerate_all + enumerate_type(/.*/) end - enumerate_symlinks.each do |path| - if !File.exist?(Pathname.new(path).realpath) + ## + # GTN:014 - please do not use : colon in your filename. + # GTN:013 - Please fix this symlink + # GTN:023 - data libraries must be named data-library.yaml + def self.run_linter_global + enumerate_type(/:/).each do |path| format_reviewdog_output( - ReviewDogEmitter.file_error(path: path, message: 'This is a BAD symlink', code: 'GTN:013') + ReviewDogEmitter.file_error(path: path, + message: 'There are colons in this filename, that is forbidden.', code: 'GTN:014') ) end - rescue StandardError - format_reviewdog_output( - ReviewDogEmitter.file_error(path: path, message: 'This is a BAD symlink', code: 'GTN:013') - ) - end - enumerate_type(/data[_-]library.ya?ml/).each do |path| - if path.split('/')[-1] != 'data-library.yaml' + + enumerate_symlinks.each do |path| + if !File.exist?(Pathname.new(path).realpath) + format_reviewdog_output( + ReviewDogEmitter.file_error(path: path, message: 'This is a BAD symlink', code: 'GTN:013') + ) + end + rescue StandardError format_reviewdog_output( - ReviewDogEmitter.file_error(path: path, - message: 'This file must be named data-library.yaml. Please rename it.', - code: 'GTN:023') + ReviewDogEmitter.file_error(path: path, message: 'This is a BAD symlink', code: 'GTN:013') ) end - end - enumerate_type(/\.ga$/).each do |path| - fix_file(path) - end - enumerate_lintable.each do |path| - fix_file(path) + enumerate_type(/data[_-]library.ya?ml/).each do |path| + if path.split('/')[-1] != 'data-library.yaml' + format_reviewdog_output( + ReviewDogEmitter.file_error(path: path, + message: 'This file must be named data-library.yaml. Please rename it.', + code: 'GTN:023') + ) + end + end + enumerate_type(/\.ga$/).each do |path| + fix_file(path) + end + enumerate_lintable.each do |path| + fix_file(path) + end end end end if $PROGRAM_NAME == __FILE__ - linter = GtnLinter + linter = Gtn::Linter require 'optparse' require 'ostruct' diff --git a/bin/schema-event.yaml b/bin/schema-event.yaml index 52c6f69ca13363..9601ea7e6ee580 100644 --- a/bin/schema-event.yaml +++ b/bin/schema-event.yaml @@ -320,10 +320,6 @@ mapping: type: str description: | Custom markdown formatted text to display. Note that Jekyll things like `{% snippet %}` or `{% include %}` will not work here. - no_galaxy: - type: bool - description: | - Set to `true` to disable the automatic "register for a galaxy account" card. tags: type: seq sequence: diff --git a/bin/schema-grants.yaml b/bin/schema-grants.yaml index 9a0c0619f9aa3d..477d8e0c60349b 100644 --- a/bin/schema-grants.yaml +++ b/bin/schema-grants.yaml @@ -112,6 +112,7 @@ mapping: - erasmusplus - ukri - highergov + - dfg funder_name: type: str description: A name for the agency providing the funding. diff --git a/bin/schema-topic.yaml b/bin/schema-topic.yaml index 2f96b1a51290eb..156168bd08511f 100644 --- a/bin/schema-topic.yaml +++ b/bin/schema-topic.yaml @@ -224,6 +224,43 @@ mapping: type: str description: | The alt text for the logo (MANDATORY). + + learning_path_ctas: + type: map + description: | + The specific learning path you wish to reference as a call-to-action for views who aren't sure where to get started. + + It must be the name of a file in learning-pathways, without the `.md` suffix. This is not validated so, please make sure it's correct :) + mapping: + '=': + type: str + required: true + _examples: | + For Beginners: intro_single_cell + For Intermediate Users: intermediate_single_cell + For Coding Enthusiasts: coding_single_cell + + community_ctas: + type: seq + sequence: + - type: map + mapping: + description: + type: str + required: true + description: "Some text to go along with the call to action" + link: + type: str + required: true + description: "A link target (full qualified please)" + link_text: + type: str + required: true + description: What goes on the button + icon: + type: str + description: One of the icons from _config.yml + learning_path_cta: type: str description: | diff --git a/bin/validate-frontmatter.rb b/bin/validate-frontmatter.rb index f87bf891e659fe..8a6e35b599a4d7 100755 --- a/bin/validate-frontmatter.rb +++ b/bin/validate-frontmatter.rb @@ -8,278 +8,280 @@ require 'kwalify' require './bin/gtn' -# Validates the frontmatter of all files -module SchemaValidator - # Schemas - @TOPIC_SCHEMA_UNSAFE = YAML.load_file('bin/schema-topic.yaml') - begin - @TUTORIAL_SCHEMA_UNSAFE = YAML.load_file('bin/schema-tutorial.yaml', aliases: true) - rescue StandardError - @TUTORIAL_SCHEMA_UNSAFE = YAML.load_file('bin/schema-tutorial.yaml') - end - @SLIDES_SCHEMA_UNSAFE = YAML.load_file('bin/schema-slides.yaml') - @FAQ_SCHEMA_UNSAFE = YAML.load_file('bin/schema-faq.yaml') - @QUIZ_SCHEMA_UNSAFE = YAML.load_file('bin/schema-quiz.yaml') - @NEWS_SCHEMA_UNSAFE = YAML.load_file('bin/schema-news.yaml') - @requirement_external_schema = YAML.load_file('bin/schema-requirement-external.yaml') - @requirement_internal_schema = YAML.load_file('bin/schema-requirement-internal.yaml') - - # Update the existing schemas to have enums with values. Then we get validation *for free*! - @TUTORIAL_SCHEMA = automagic_loading(@TUTORIAL_SCHEMA_UNSAFE) - @SLIDES_SCHEMA = automagic_loading(@SLIDES_SCHEMA_UNSAFE) - @TOPIC_SCHEMA = automagic_loading(@TOPIC_SCHEMA_UNSAFE) - @FAQ_SCHEMA = automagic_loading(@FAQ_SCHEMA_UNSAFE) - @QUIZ_SCHEMA = automagic_loading(@QUIZ_SCHEMA_UNSAFE) - @NEWS_SCHEMA = automagic_loading(@NEWS_SCHEMA_UNSAFE) - - @TUTORIAL_SCHEMA['mapping']['contributions']['required'] = false - @SLIDES_SCHEMA['mapping']['contributions']['required'] = false - - # Build validators now that we've filled out the subtopic enum - @topic_validator = Kwalify::Validator.new(@TOPIC_SCHEMA) - @tutorial_validator = Kwalify::Validator.new(@TUTORIAL_SCHEMA) - @slides_validator = Kwalify::Validator.new(@SLIDES_SCHEMA) - @faq_validator = Kwalify::Validator.new(@FAQ_SCHEMA) - @quiz_validator = Kwalify::Validator.new(@QUIZ_SCHEMA) - @news_validator = Kwalify::Validator.new(@NEWS_SCHEMA) - @requirement_external_validator = Kwalify::Validator.new(@requirement_external_schema) - @requirement_internal_validator = Kwalify::Validator.new(@requirement_internal_schema) - - def self.validate_document(document, validator) - errors = validator.validate(document) - return errors if errors && !errors.empty? - - [] - end +module Gtn + # Validates the frontmatter of all files + module SchemaValidator + # Schemas + @TOPIC_SCHEMA_UNSAFE = YAML.load_file('bin/schema-topic.yaml') + begin + @TUTORIAL_SCHEMA_UNSAFE = YAML.load_file('bin/schema-tutorial.yaml', aliases: true) + rescue StandardError + @TUTORIAL_SCHEMA_UNSAFE = YAML.load_file('bin/schema-tutorial.yaml') + end + @SLIDES_SCHEMA_UNSAFE = YAML.load_file('bin/schema-slides.yaml') + @FAQ_SCHEMA_UNSAFE = YAML.load_file('bin/schema-faq.yaml') + @QUIZ_SCHEMA_UNSAFE = YAML.load_file('bin/schema-quiz.yaml') + @NEWS_SCHEMA_UNSAFE = YAML.load_file('bin/schema-news.yaml') + @requirement_external_schema = YAML.load_file('bin/schema-requirement-external.yaml') + @requirement_internal_schema = YAML.load_file('bin/schema-requirement-internal.yaml') + + # Update the existing schemas to have enums with values. Then we get validation *for free*! + @TUTORIAL_SCHEMA = automagic_loading(@TUTORIAL_SCHEMA_UNSAFE) + @SLIDES_SCHEMA = automagic_loading(@SLIDES_SCHEMA_UNSAFE) + @TOPIC_SCHEMA = automagic_loading(@TOPIC_SCHEMA_UNSAFE) + @FAQ_SCHEMA = automagic_loading(@FAQ_SCHEMA_UNSAFE) + @QUIZ_SCHEMA = automagic_loading(@QUIZ_SCHEMA_UNSAFE) + @NEWS_SCHEMA = automagic_loading(@NEWS_SCHEMA_UNSAFE) + + @TUTORIAL_SCHEMA['mapping']['contributions']['required'] = false + @SLIDES_SCHEMA['mapping']['contributions']['required'] = false + + # Build validators now that we've filled out the subtopic enum + @topic_validator = Kwalify::Validator.new(@TOPIC_SCHEMA) + @tutorial_validator = Kwalify::Validator.new(@TUTORIAL_SCHEMA) + @slides_validator = Kwalify::Validator.new(@SLIDES_SCHEMA) + @faq_validator = Kwalify::Validator.new(@FAQ_SCHEMA) + @quiz_validator = Kwalify::Validator.new(@QUIZ_SCHEMA) + @news_validator = Kwalify::Validator.new(@NEWS_SCHEMA) + @requirement_external_validator = Kwalify::Validator.new(@requirement_external_schema) + @requirement_internal_validator = Kwalify::Validator.new(@requirement_internal_schema) + + def self.validate_document(document, validator) + errors = validator.validate(document) + return errors if errors && !errors.empty? + + [] + end - def self.validate_non_empty_key_value(map, key) - return ["Missing #{key} for requirement"] unless map.key?(key) - return ["Empty #{key} for requirement"] if map[key].empty? + def self.validate_non_empty_key_value(map, key) + return ["Missing #{key} for requirement"] unless map.key?(key) + return ["Empty #{key} for requirement"] if map[key].empty? - [] - end + [] + end - def self.tutorial?(fn) - fn.include?('tutorial.md') || fn =~ /tutorial_[A-Z]{2,}.md/ - end + def self.tutorial?(fn) + fn.include?('tutorial.md') || fn =~ /tutorial_[A-Z]{2,}.md/ + end - def self.slide?(fn) - fn.include?('slides.html') || fn =~ /slides_[A-Z]{2,}.html/ - end + def self.slide?(fn) + fn.include?('slides.html') || fn =~ /slides_[A-Z]{2,}.html/ + end - def self.validate_requirements(requirements) - errs = [] - # Exit early if no requirements - return [] if requirements.nil? || requirements.empty? - - # Otherwise check each - requirements.each do |requirement| - # For external links, they need a link that is non-empty - case requirement['type'] - when 'external' - errs.push(*validate_document(requirement, @requirement_external_validator)) - when 'internal' - errs.push(*validate_document(requirement, @requirement_internal_validator)) - - # For the internal requirements, test that they point at something real. - if requirement.key?('tutorials') - requirement['tutorials'].each do |tutorial| - # For each listed tutorial check that a directory with that name exists - pn = Pathname.new("topics/#{requirement['topic_name']}/tutorials/#{tutorial}") - - if !pn.directory? - errs.push("Internal requirement to topics/#{requirement['topic_name']}/tutorials/#{tutorial} " \ - 'does not exist') + def self.validate_requirements(requirements) + errs = [] + # Exit early if no requirements + return [] if requirements.nil? || requirements.empty? + + # Otherwise check each + requirements.each do |requirement| + # For external links, they need a link that is non-empty + case requirement['type'] + when 'external' + errs.push(*validate_document(requirement, @requirement_external_validator)) + when 'internal' + errs.push(*validate_document(requirement, @requirement_internal_validator)) + + # For the internal requirements, test that they point at something real. + if requirement.key?('tutorials') + requirement['tutorials'].each do |tutorial| + # For each listed tutorial check that a directory with that name exists + pn = Pathname.new("topics/#{requirement['topic_name']}/tutorials/#{tutorial}") + + if !pn.directory? + errs.push("Internal requirement to topics/#{requirement['topic_name']}/tutorials/#{tutorial} " \ + 'does not exist') + end end end - end - when 'none' - errs.push(*validate_non_empty_key_value(requirement, 'title')) + when 'none' + errs.push(*validate_non_empty_key_value(requirement, 'title')) - requirement.each_key do |x| - errs.push("Unknown key #{x}") if !%w[title type].include?(x) + requirement.each_key do |x| + errs.push("Unknown key #{x}") if !%w[title type].include?(x) + end + else + errs.push("Unknown requirement type #{requirement['type']}") end - else - errs.push("Unknown requirement type #{requirement['type']}") end - end - errs - end + errs + end - def self.lintable?(fn) - begin + def self.lintable?(fn) begin - data = YAML.load_file(fn, permitted_classes: [Date]) - rescue StandardError - data = YAML.load_file(fn) + begin + data = YAML.load_file(fn, permitted_classes: [Date]) + rescue StandardError + data = YAML.load_file(fn) + end + rescue StandardError => e + return ["YAML error, failed to parse #{fn}, #{e}"] end - rescue StandardError => e - return ["YAML error, failed to parse #{fn}, #{e}"] - end - # Check this is something we actually want to process - if !data.is_a?(Hash) - puts "Skipping #{fn}" - return nil + # Check this is something we actually want to process + if !data.is_a?(Hash) + puts "Skipping #{fn}" + return nil + end + + data end - data - end + def self.lint_faq_file(fn) + errs = [] + data = lintable?(fn) + return data if data.nil? || data.is_a?(Array) - def self.lint_faq_file(fn) - errs = [] - data = lintable?(fn) - return data if data.nil? || data.is_a?(Array) + errs.push(*validate_document(data, @faq_validator)) + errs + end - errs.push(*validate_document(data, @faq_validator)) - errs - end + def self.lint_topic(fn) + # Any error messages + errs = [] + data = lintable?(fn) + return data if data.nil? || data.is_a?(Array) - def self.lint_topic(fn) - # Any error messages - errs = [] - data = lintable?(fn) - return data if data.nil? || data.is_a?(Array) + errs.push(*validate_document(data, @topic_validator)) + end - errs.push(*validate_document(data, @topic_validator)) - end + def self.lint_material(fn) + # Any error messages + errs = [] + data = lintable?(fn) + return data if data.nil? || data.is_a?(Array) + + # Load topic metadata for this file + topic = fn.split('/')[2] + topic_metadata = YAML.load_file("topics/#{topic}/metadata.yaml") + + # Load subtopic titles + if data.key?('subtopic') + subtopic_ids = [] + topic_metadata['subtopics'].each do |x| + subtopic_ids.push(x['id']) + end - def self.lint_material(fn) - # Any error messages - errs = [] - data = lintable?(fn) - return data if data.nil? || data.is_a?(Array) - - # Load topic metadata for this file - topic = fn.split('/')[2] - topic_metadata = YAML.load_file("topics/#{topic}/metadata.yaml") - - # Load subtopic titles - if data.key?('subtopic') - subtopic_ids = [] - topic_metadata['subtopics'].each do |x| - subtopic_ids.push(x['id']) + @TUTORIAL_SCHEMA['mapping']['subtopic']['enum'] = subtopic_ids + @SLIDES_SCHEMA['mapping']['subtopic']['enum'] = subtopic_ids + @tutorial_validator = Kwalify::Validator.new(@TUTORIAL_SCHEMA) + @slides_validator = Kwalify::Validator.new(@SLIDES_SCHEMA) end - @TUTORIAL_SCHEMA['mapping']['subtopic']['enum'] = subtopic_ids - @SLIDES_SCHEMA['mapping']['subtopic']['enum'] = subtopic_ids - @tutorial_validator = Kwalify::Validator.new(@TUTORIAL_SCHEMA) - @slides_validator = Kwalify::Validator.new(@SLIDES_SCHEMA) - end + # Generic error handling: + ## Check requirements + errs.push(*validate_requirements(data['requirements'])) if data.key?('requirements') - # Generic error handling: - ## Check requirements - errs.push(*validate_requirements(data['requirements'])) if data.key?('requirements') + ## Check follow ups + errs.push(*validate_requirements(data['follow_up_training'])) if data.key?('follow_up_training') - ## Check follow ups - errs.push(*validate_requirements(data['follow_up_training'])) if data.key?('follow_up_training') + # Custom error handling: + if tutorial?(fn) + errs.push(*validate_document(data, @tutorial_validator)) + elsif slide?(fn) + errs.push(*validate_document(data, @slides_validator)) + end - # Custom error handling: - if tutorial?(fn) - errs.push(*validate_document(data, @tutorial_validator)) - elsif slide?(fn) - errs.push(*validate_document(data, @slides_validator)) - end + # Check contributors OR contributions + if (slide?(fn) || tutorial?(fn)) && !(data.key?('contributors') || data.key?('contributions')) + errs.push('Document lacks EITHER contributors OR contributions key') + end - # Check contributors OR contributions - if (slide?(fn) || tutorial?(fn)) && !(data.key?('contributors') || data.key?('contributions')) - errs.push('Document lacks EITHER contributors OR contributions key') + # If we had no errors, validated successfully + errs end - # If we had no errors, validated successfully - errs - end + def self.lint_news_file(fn) + errs = [] + data = lintable?(fn) + return data if data.nil? || data.is_a?(Array) - def self.lint_news_file(fn) - errs = [] - data = lintable?(fn) - return data if data.nil? || data.is_a?(Array) - - if data.key?('cover') - if !data['cover'].start_with?('https://') - if !File.exist?(data['cover']) - errs.push("Cover image #{data['cover']} does not exist") + if data.key?('cover') + if !data['cover'].start_with?('https://') + if !File.exist?(data['cover']) + errs.push("Cover image #{data['cover']} does not exist") + end end end - end - errs.push(*validate_document(data, @news_validator)) - errs - end + errs.push(*validate_document(data, @news_validator)) + errs + end - def self.lint_quiz_file(fn) - errs = [] - data = lintable?(fn) - return data if data.nil? || data.is_a?(Array) + def self.lint_quiz_file(fn) + errs = [] + data = lintable?(fn) + return data if data.nil? || data.is_a?(Array) - data['questions'].select { |q| q.key? 'correct' }.each do |q| - if q['correct'].is_a?(Array) - if q['type'] != 'choose-many' - errs.push("There are multiple answers for this question, but it is not a choose-many #{q['title']}") - end + data['questions'].select { |q| q.key? 'correct' }.each do |q| + if q['correct'].is_a?(Array) + if q['type'] != 'choose-many' + errs.push("There are multiple answers for this question, but it is not a choose-many #{q['title']}") + end - q['correct'].each do |c| - errs.push("Answer #{c} not included in options for question #{q['title']}") if !q['answers'].include?(c) - end - else - if q['type'] != 'choose-1' - errs.push("There is only a single textual answer, it must be a list for a choose-many question #{q['title']}") - end + q['correct'].each do |c| + errs.push("Answer #{c} not included in options for question #{q['title']}") if !q['answers'].include?(c) + end + else + if q['type'] != 'choose-1' + errs.push("There is only a single textual answer, it must be a list for a choose-many question #{q['title']}") + end - if !q['answers'].include?(q['correct']) - errs.push("Answer #{q['correct']} not included in options for question #{q['title']}") + if !q['answers'].include?(q['correct']) + errs.push("Answer #{q['correct']} not included in options for question #{q['title']}") + end end end - end - - errs.push(*validate_document(data, @quiz_validator)) - errs - end - def self.run - errors = [] - # Topics - materials = (Dir.glob('./metadata/*.yaml') + Dir.glob('./metadata/*.yml')) - .grep_v(/schema-*/) - .select do |x| - d = YAML.load_file(x) - # Ignore non-hashes - d.is_a?(Hash) && (d.key? 'editorial_board' or d.key? 'summary' or d.key? 'type') + errs.push(*validate_document(data, @quiz_validator)) + errs end - errors += materials.map { |x| [x, lint_topic(x)] } + def self.run + errors = [] + # Topics + materials = (Dir.glob('./metadata/*.yaml') + Dir.glob('./metadata/*.yml')) + .grep_v(/schema-*/) + .select do |x| + d = YAML.load_file(x) + # Ignore non-hashes + d.is_a?(Hash) && (d.key? 'editorial_board' or d.key? 'summary' or d.key? 'type') + end + + errors += materials.map { |x| [x, lint_topic(x)] } - # Lint tutorials/slides/metadata - materials = Dir.glob('./topics/**/slides.*html') + - Dir.glob('./topics/**/tutorial.*md') - errors += materials.map { |x| [x, lint_material(x)] } + # Lint tutorials/slides/metadata + materials = Dir.glob('./topics/**/slides.*html') + + Dir.glob('./topics/**/tutorial.*md') + errors += materials.map { |x| [x, lint_material(x)] } - # Lint FAQs - errors += Dir.glob('**/faqs/**/*.md') - .grep_v(/aaaa_dontquestionthislinkitisthegluethatholdstogetherthegalaxy/) - .grep_v(/index.md$/) - .grep_v(/README.md$/) - .map { |x| [x, lint_faq_file(x)] } + # Lint FAQs + errors += Dir.glob('**/faqs/**/*.md') + .grep_v(/aaaa_dontquestionthislinkitisthegluethatholdstogetherthegalaxy/) + .grep_v(/index.md$/) + .grep_v(/README.md$/) + .map { |x| [x, lint_faq_file(x)] } - # Lint quizzes - errors += Dir.glob('./topics/**/quiz/*') - .grep(/ya?ml$/) - .map { |x| [x, lint_quiz_file(x)] } + # Lint quizzes + errors += Dir.glob('./topics/**/quiz/*') + .grep(/ya?ml$/) + .map { |x| [x, lint_quiz_file(x)] } - # Lint news - errors += Dir.glob('./news/_posts/*') - .map { |x| [x, lint_news_file(x)] } + # Lint news + errors += Dir.glob('./news/_posts/*') + .map { |x| [x, lint_news_file(x)] } - errors.reject! { |_path, errs| errs.nil? or errs.empty? } + errors.reject! { |_path, errs| errs.nil? or errs.empty? } - errors + errors + end end end if $PROGRAM_NAME == __FILE__ ec = 0 - errors = SchemaValidator.run + errors = Gtn::SchemaValidator.run if errors.length.positive? ec = 1 errors.each do |path, errs| diff --git a/bin/yaml2json.py b/bin/yaml2json.py deleted file mode 100644 index df4982086a8517..00000000000000 --- a/bin/yaml2json.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python -import sys -import yaml -import json - -[sys.stdout.write(json.dumps(doc, indent=2)) for doc in yaml.safe_load_all(sys.stdin)] diff --git a/bin/yaml2json.rb b/bin/yaml2json.rb deleted file mode 100755 index d8b4ae94cc296c..00000000000000 --- a/bin/yaml2json.rb +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'yaml' -require 'json' -# [sys.stdout.write(json.dumps(doc, indent=2)) for doc in yaml.safe_load_all(sys.stdin)] -YAML.load_stream($stdin.read).each { |doc| puts JSON.pretty_generate(doc) } diff --git a/events/2022-07-08-gat.md b/events/2022-07-08-gat.md index b986234f4c5cca..498274e5d1d324 100644 --- a/events/2022-07-08-gat.md +++ b/events/2022-07-08-gat.md @@ -13,6 +13,9 @@ date_start: 2022-07-08 cover-image: /assets/images/gat-small.png cover-image-alt: GTN logo on a spiral galaxy background with text galaxy admin training +tags: +- admin + registration: link: "https://galaxyproject.org/events/gcc2022/" deadline: 2022-06-01 diff --git a/events/2023-04-17-gat-gent.md b/events/2023-04-17-gat-gent.md index bea47e80fb06e3..e1425b1031e656 100644 --- a/events/2023-04-17-gat-gent.md +++ b/events/2023-04-17-gat-gent.md @@ -18,6 +18,9 @@ date_end: 2023-04-21 cover-image: /assets/images/gat-small.png cover-image-alt: GTN logo on a spiral galaxy background with text galaxy admin training +tags: +- admin + registration: link: "https://docs.google.com/forms/d/e/1FAIpQLSc3zgDTfcLZ2-92EdgJvfR4j0KxQeOv0tiFMzGXZ6hdW7JlwQ/viewform" deadline: 2022-03-01 diff --git a/events/2023-10-02-mtb-ngs.md b/events/2023-10-02-mtb-ngs.md index fd66eba63da949..a0d3689412c604 100644 --- a/events/2023-10-02-mtb-ngs.md +++ b/events/2023-10-02-mtb-ngs.md @@ -13,6 +13,9 @@ cover-image-alt: banner for the course date_start: 2023-10-02 date_end: 2023-10-06 # optional, if event is more than one day +tags: +- microbiome + contributions: organisers: # GTN contributors or funders, must be defined in CONTRIBUTORS.yaml - dbrites diff --git a/events/2024-04-01-example-event-external.md b/events/2024-04-01-example-event-external.md index a0689937b40000..aeebf6c39d2c59 100644 --- a/events/2024-04-01-example-event-external.md +++ b/events/2024-04-01-example-event-external.md @@ -15,6 +15,11 @@ description: | date_start: 1970-04-01 date_end: # if multi-day event +# add tags to help users find your event +# tag with a topic to have it show op on the topic community page +tags: +- single-cell + contributions: organisers: # organisers must be defined in CONTRIBUTORS.yaml - shiltemann diff --git a/events/2024-04-01-example-event.md b/events/2024-04-01-example-event.md index a772fdabed49a1..24929b4a7a11d7 100644 --- a/events/2024-04-01-example-event.md +++ b/events/2024-04-01-example-event.md @@ -14,6 +14,9 @@ description: | cover-image: # image for your corse, put in 'events/images' folder cover-image-alt: # supply alt text describing your image +# Tags to help users find your event. Tag with a topic id to have it show op on the topic community page +tags: +- single-cell # Practical Information date_start: 1970-04-01 diff --git a/events/2024-06-10-mtb-ngs.md b/events/2024-06-10-mtb-ngs.md index f9b135a9860da7..19f2a4e8ff6b1b 100644 --- a/events/2024-06-10-mtb-ngs.md +++ b/events/2024-06-10-mtb-ngs.md @@ -12,7 +12,7 @@ cover-image-alt: banner for the course date_start: 2024-06-10 date_end: 2024-06-14 # optional, if event is more than one day -tags: [one-health, tuberculosis, NGS, Galaxy, training] +tags: [one-health, tuberculosis, NGS, Galaxy, training, microbiome] contributions: organisers: # GTN contributors or funders, must be defined in CONTRIBUTORS.yaml diff --git a/events/2024-06-18-FAIR-data-management.md b/events/2024-06-18-FAIR-data-management.md index d1daba43d1aac4..812b2d714a4c91 100644 --- a/events/2024-06-18-FAIR-data-management.md +++ b/events/2024-06-18-FAIR-data-management.md @@ -19,4 +19,7 @@ contributions: location: name: Online ---- \ No newline at end of file +tags: +- fair +- single-cell +--- diff --git a/events/2024-07-22-bioconductor-carpentries-workshop-analysis-and-interpretation-of-bulk-rna-seq-data.md b/events/2024-07-22-bioconductor-carpentries-workshop-analysis-and-interpretation-of-bulk-rna-seq-data.md index b2b066795406d4..4620f91f3f0230 100644 --- a/events/2024-07-22-bioconductor-carpentries-workshop-analysis-and-interpretation-of-bulk-rna-seq-data.md +++ b/events/2024-07-22-bioconductor-carpentries-workshop-analysis-and-interpretation-of-bulk-rna-seq-data.md @@ -17,4 +17,6 @@ location: country: USA date_start: 2024-07-22 date_end: 2024-07-23 +tags: +- transcriptomics --- diff --git a/events/2024-07-22-freiburg-july.md b/events/2024-07-22-freiburg-july.md index 16efe9e26ef43b..6f6479b87fd831 100644 --- a/events/2024-07-22-freiburg-july.md +++ b/events/2024-07-22-freiburg-july.md @@ -42,6 +42,14 @@ infrastructure: - server: https://usegalaxy.eu name: Galaxy EU +tags: +- microbiome +- transcriptomics +- sequence-analysis +- introduction +- epigenetics +- variant-analysis + # Program of your course # Add GTN tutorials by supplying the topic and tutorial name program: diff --git a/events/2024-09-10-biont-rnaseq.md b/events/2024-09-10-biont-rnaseq.md index 41d4b4d8c243e6..6f13edcd93da16 100644 --- a/events/2024-09-10-biont-rnaseq.md +++ b/events/2024-09-10-biont-rnaseq.md @@ -2,7 +2,7 @@ layout: event-external title: A practical introduction to bioinformatics and RNA-seq using Galaxy external: "https://www.cecam.org/workshop-details/a-practical-introduction-to-bioinformatics-and-rna-seq-using-galaxy-1359" -description: | +description: | Join us for an engaging 4-half day online workshop and dive into the captivating world of RNA-seq data analysis using Galaxy! This hands-on workshop will equip you with the skills to effectively analyze RNA-seq data from start to finish. You will learn about: Galaxy, Quality Control, Mapping and Quantification and Downstream Analysis. Don't miss this opportunity to enhance your bioinformatics skills. date_start: 2024-09-10 date_end: 2024-09-13 @@ -16,4 +16,8 @@ contributions: - biont location: name: Online +tags: +- transcriptomics +- sequence-analysis +- introduction --- diff --git a/events/2025-01-28-src.md b/events/2025-01-28-src.md new file mode 100644 index 00000000000000..c5115075340a89 --- /dev/null +++ b/events/2025-01-28-src.md @@ -0,0 +1,110 @@ +--- +layout: event + +# Status of this page, remove these lines when ready +#draft: true +#status: wip + +# Description of your event +title: Galaxy at SURF Research Cloud workshop +description: | + Half-day workshop demonstrating the local Galaxy instance built for users of SURF Research Cloud. + + **Registration**: To sign up for the event, please send an email to [Mirela Minkova](mailto:mirela.minkova@surf.nl?subject=Registering%20SRC%20Galaxy%20Workshop) + + + +# Practical Information +date_start: 2025-01-28 + +cost: free +audience: This event is targeted towards researchers in the Netherlands who already have experience working with Galaxy. +contact_email: mirela.minkova@surf.nl +async: false +mode: onsite + +registration: + message: Please send an email to mirela.minkova@surf.nl to register for the workshop! + deadline: 2025-01-10 + open: true + +# Location of the event +# For online events, just the 'name' is enough +location: + name: SURF Amsterdam + address: Science Park 140 + city: Amsterdam + country: The Netherlands + postcode: 1098 XG + + + +# People involved, organisers, speakers, funders, etc +# Must be defined in CONTRIBUTORS.yaml file +contributions: + organisers: + - mirelaminkova + instructors: + - mirelaminkova + - yuliiaorlova + funding: + - surf + + +# Galaxy and other infrastructure that will be used for your event. +# This will be used to create the setup instructions for participants +infrastructure: + tiaas: false + custom: + description: | + Before joining the course, please make sure to: + - Bring a laptop with at least 8GB of RAM. + - Do the [Intro to Galaxy](http://training.galaxyproject.org/topics/introduction/tutorials/galaxy-intro-short/tutorial.html) tutorial if you are not yet familiar with Galaxy + - Up-to-date browser + - Experience in using SURF Research Cloud  + +# Program of your course +# Add GTN tutorials by supplying the topic and tutorial name +# For non-GTN sessions, add a "type:custom" session and description +program: + - section: "Galaxy & Pulsar on SURF" # section title is optional + description: | + During the workshop, you will see a short demo and get a chance to test the catalog item yourself. + tutorials: + - name: SURF SRC introduction + type: custom + time: "13:00 - 13:40" + + - name: surf-research-cloud-galaxy + topic: admin + time: "13:40 - 14:10" + + - name: surf-research-cloud-pulsar + topic: admin + time: "14:10 - 14:30" + + - type: custom + name: Coffee Break + time: "14:30" + - name: mapping + topic: sequence-analysis + time: "14:40 - 15:40" + + - name: general-introduction + topic: assembly + time: "15:40 - 16:10" + + - type: custom + name: General feedback from participants + time: "16:10" + + - type: custom + name: Borreltje + time: "16:30 - 17:00" +tags: +- admin +- surf +- netherlands +--- + +During the workshop, you will see a short demo and get a chance to test the catalog item yourself. This will be followed by a discussion about your needs as Galaxy researchers, helping us work toward a better future for the Dutch research community. We will conclude the day with drinks and networking with fellow Galaxy NL users. diff --git a/events/2025-02-04-biont-software-best-practice.md b/events/2025-02-04-biont-software-best-practice.md new file mode 100644 index 00000000000000..75b3780205d861 --- /dev/null +++ b/events/2025-02-04-biont-software-best-practice.md @@ -0,0 +1,18 @@ +--- +layout: event-external +title: "Code & Collaborate: The FAIRytale of Software Development" +external: "https://www.cecam.org/workshop-details/code-collaborate-the-fairytale-of-software-development-1447" +description: | + Calling all learners! Join us for our #BioNT workshop on software best practices. This three-day workshop will + cover collaborative software development, version control, code review, robust software testing, and much more. + Register by January 20th. +date_start: 2025-02-04 +date_end: 2025-02-06 +contributions: + organisers: + - teresa-m + funding: + - biont +location: + name: Online +--- \ No newline at end of file diff --git a/events/2025-03-10-hts-workshop-freiburg.md b/events/2025-03-10-hts-workshop-freiburg.md index 8687499f59b6ac..1d0323bc3ae492 100644 --- a/events/2025-03-10-hts-workshop-freiburg.md +++ b/events/2025-03-10-hts-workshop-freiburg.md @@ -42,45 +42,60 @@ infrastructure: - server: https://usegalaxy.eu name: Galaxy EU +tags: +- introduction +- sequence-analysis +- epigenetics +- transcriptomics +- variant-analysis +- microbiome + # Program of your course # Add GTN tutorials by supplying the topic and tutorial name program: - - section: "Galaxy Freiburg workshop" # section title is optional - description: | - Next, you see the schedule for the full week. We will do at least one coffee break in the morning, one in the afternoon, and 1h lunch break around noon. + - section: "Monday: Introduction and Quality Control" # section title is optional + description: We will do at least one coffee break in the morning, one in the afternoon, and 1h lunch break around noon. tutorials: - type: custom - name: "Monday " - time: "09:15 - 16:00" - description: Introduction and Quality control + name: "Welcome" + time: "09:15" - name: galaxy-intro-peaks2genes topic: introduction - name: quality-control topic: sequence-analysis - type: custom - name: "Tuesday " - time: "09:15 - 17:00" - description: ChIP-Sequencing + name: "End" + time: "16:00" + + - section: "Tuesday: ChIP-Sequencing" + description: We will do at least one coffee break in the morning, one in the afternoon, and 1h lunch break around noon. + tutorials: - name: formation_of_super-structures_on_xi topic: epigenetics - - type: custom - name: "Wednesday " - time: "09:15 - 17:00" - description: RNA-Sequencing + time: "09:15-17:00" + + - section: "Wednesday: RNA Sequencing" + description: We will do at least one coffee break in the morning, one in the afternoon, and 1h lunch break around noon. + tutorials: - name: ref-based topic: transcriptomics - - type: custom - name: "Thursday " - time: "09:15 - 17:00" - description: Variant Calling/Exome Sequencing + time: "09:15-17:00" + + + - section: "Thursday: Variant Calling/Exome Sequencing" + description: We will do at least one coffee break in the morning, one in the afternoon, and 1h lunch break around noon. + tutorials: - name: exome-seq topic: variant-analysis - - type: custom - name: "Friday " - time: "09:15 - 16:00" - description: Metagenomics + time: "09:15-17:00" + + + - section: "Friday: Metagenomics" + description: We will do at least one coffee break in the morning, one in the afternoon, and 1h lunch break around noon. + tutorials: - name: pathogen-detection-from-nanopore-foodborne-data topic: microbiome + time: "09:15-16:00" --- # Welcome to the Comprehensive Galaxy Workshop: From Introduction to Advanced Applications diff --git a/events/galaxy-academy-2024.md b/events/galaxy-academy-2024.md index a238aae31fc972..edc70bd6ea76ad 100644 --- a/events/galaxy-academy-2024.md +++ b/events/galaxy-academy-2024.md @@ -25,6 +25,17 @@ contact_email: academy@galaxyproject.org async: true mode: online +tags: +- microbiome +- single-cell +- proteomics +- introduction +- galaxy-interface +- ecology +- assembly +- one-health +- statistics + contributions: organisers: - teresa-m diff --git a/learning-pathways/admin-training.md b/learning-pathways/admin-training.md index 436cc5c9110f3e..8094009054e4f9 100644 --- a/learning-pathways/admin-training.md +++ b/learning-pathways/admin-training.md @@ -14,7 +14,7 @@ editorial_board: - natefoo - slugger70 -tags: [Galaxy administrators, 5-day course] +tags: [Galaxy administrators, 5-day course, admin] pathway: - section: "Monday: Setting up Galaxy with Ansible" diff --git a/learning-pathways/amr-gene-detection.md b/learning-pathways/amr-gene-detection.md index 0fedc63a5ddb76..781be3b5d638cd 100644 --- a/learning-pathways/amr-gene-detection.md +++ b/learning-pathways/amr-gene-detection.md @@ -4,7 +4,7 @@ title: Detection of AMR genes in bacterial genomes description: | This learning path aims to teach you the basic steps to detect and check Antimicrobial resistance (AMR) genes in bacterial genomes using Galaxy. type: use -tags: [amr, bacteria, microgalaxy, one-health] +tags: [amr, bacteria, microgalaxy, one-health, microbiome] editorial_board: - bebatut diff --git a/learning-pathways/building_tutorials.md b/learning-pathways/building_tutorials.md index a9fb1f2bf6818b..2dd6441fb879ca 100644 --- a/learning-pathways/building_tutorials.md +++ b/learning-pathways/building_tutorials.md @@ -6,6 +6,8 @@ type: instructors editorial_board: - nomadscientist +tags: [contributing] + priority: 1 title: Building training material in Galaxy diff --git a/learning-pathways/climate-learning.md b/learning-pathways/climate-learning.md index 83f910e283c18f..99092ec634a02a 100644 --- a/learning-pathways/climate-learning.md +++ b/learning-pathways/climate-learning.md @@ -11,6 +11,8 @@ tags: [Climate, Overview] editorial_board: - Marie59 +tags: [climate] + type: use pathway: diff --git a/learning-pathways/clinical-metaproteomics.md b/learning-pathways/clinical-metaproteomics.md index ad9b78175211f7..89d9c47fb9b811 100644 --- a/learning-pathways/clinical-metaproteomics.md +++ b/learning-pathways/clinical-metaproteomics.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, proteomics] type: use diff --git a/learning-pathways/data-driven-biology.md b/learning-pathways/data-driven-biology.md index 5728045f7919e4..ab5a112117315e 100644 --- a/learning-pathways/data-driven-biology.md +++ b/learning-pathways/data-driven-biology.md @@ -2,7 +2,7 @@ layout: learning-pathway cover-image: /assets/images/genomics_intro.png cover-image-alt: "Genomics intro" -tags: [introduction, real-course] +tags: [introduction, real-course, data-science] type: use editorial_board: - nekrut diff --git a/learning-pathways/dev_tools_training.md b/learning-pathways/dev_tools_training.md index be2c159d933192..93fe43dd116f95 100644 --- a/learning-pathways/dev_tools_training.md +++ b/learning-pathways/dev_tools_training.md @@ -10,7 +10,7 @@ cover-image-alt: Image of a researcher or developer on a computer thinking of bu editorial_board: - Marie59 -tags: [subdomain, community, tool development, 3-day course] +tags: [subdomain, community, tool development, 3-day course, dev] pathway: diff --git a/learning-pathways/genome-annotation-eukaryote.md b/learning-pathways/genome-annotation-eukaryote.md index 31ca3b5a68f338..ffef6dfeff5d4f 100644 --- a/learning-pathways/genome-annotation-eukaryote.md +++ b/learning-pathways/genome-annotation-eukaryote.md @@ -4,7 +4,7 @@ title: Genome annotation for eukaryotes description: | Learn how to annotate an eukaryotic genome sequence: identify repeated regions, find the position and function of genes, and even set up a manual curation environment with Apollo. type: use -tags: [genome annotation, eukaryote] +tags: [genome-annotation, eukaryote] cover-image: assets/images/gga.png cover-image-alt: "Galaxy Genome Annotation logo" diff --git a/learning-pathways/genome-annotation-prokaryote.md b/learning-pathways/genome-annotation-prokaryote.md index d814c752fe8020..a44f8c735980bf 100644 --- a/learning-pathways/genome-annotation-prokaryote.md +++ b/learning-pathways/genome-annotation-prokaryote.md @@ -4,7 +4,7 @@ title: Genome annotation for prokaryotes description: | Learn how to annotate a prokaryotic genome sequence: find the position and function of genes, and even set up a manual curation environment with Apollo. type: use -tags: [genome annotation, prokaryote] +tags: [genome-annotation, prokaryote] cover-image: assets/images/gga.png cover-image-alt: "Galaxy Genome Annotation logo" diff --git a/learning-pathways/intro-to-galaxy-and-ecology.md b/learning-pathways/intro-to-galaxy-and-ecology.md index 6a36a12ec4a981..0393f7200730bd 100644 --- a/learning-pathways/intro-to-galaxy-and-ecology.md +++ b/learning-pathways/intro-to-galaxy-and-ecology.md @@ -9,7 +9,7 @@ description: | steps of biodiversity data analysis: download, check, filter and explore biodiversity data and analyze abundance data through modeling. -tags: [beginner] +tags: [beginner, ecology] cover-image: assets/images/galaxy-e-logo.png cover-image-alt: "Drawing of an Ecological System" diff --git a/learning-pathways/intro-to-galaxy-and-genomics.md b/learning-pathways/intro-to-galaxy-and-genomics.md index dadc6f297c5dd1..fa3e5cbd5f9065 100644 --- a/learning-pathways/intro-to-galaxy-and-genomics.md +++ b/learning-pathways/intro-to-galaxy-and-genomics.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, introcuction, sequence-analysis, assembly] type: use editorial_board: diff --git a/learning-pathways/intro-to-r-and-ml.md b/learning-pathways/intro-to-r-and-ml.md index 26efdfa7dea5f2..07307be14052eb 100644 --- a/learning-pathways/intro-to-r-and-ml.md +++ b/learning-pathways/intro-to-r-and-ml.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, statistics, data-science] type: use editorial_board: diff --git a/learning-pathways/io1.md b/learning-pathways/io1.md index aa54d6bf3c8073..f136ea2ffc72fc 100644 --- a/learning-pathways/io1.md +++ b/learning-pathways/io1.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, data-science, contributing, sequence-analysis, transcriptomics] type: use editorial_board: diff --git a/learning-pathways/io2.md b/learning-pathways/io2.md index 3585a1a00288cd..9eee6c43a82671 100644 --- a/learning-pathways/io2.md +++ b/learning-pathways/io2.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, galaxy-interface, microbiome, visualisation, data-science, variant-analysis ] type: use editorial_board: diff --git a/learning-pathways/io3.md b/learning-pathways/io3.md index 4ddea1067d401c..0be496b2d7b93f 100644 --- a/learning-pathways/io3.md +++ b/learning-pathways/io3.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, genome-annotation] type: use editorial_board: diff --git a/learning-pathways/io4.md b/learning-pathways/io4.md index 59bcf47a8fa718..f6f8101e38529a 100644 --- a/learning-pathways/io4.md +++ b/learning-pathways/io4.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, ecology] type: use editorial_board: diff --git a/learning-pathways/io5.md b/learning-pathways/io5.md index 11a317f24573f7..6c978c2a2cc0ed 100644 --- a/learning-pathways/io5.md +++ b/learning-pathways/io5.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, contributing, teaching] type: use editorial_board: diff --git a/learning-pathways/pathway-example.md b/learning-pathways/pathway-example.md index 3a6962741f4b9d..57c60277bc4f45 100644 --- a/learning-pathways/pathway-example.md +++ b/learning-pathways/pathway-example.md @@ -8,7 +8,7 @@ description: | lists all the learning paths, and at the top of the pathway page type: use # 'use' for science topics, or admin-dev or instructors -tags: [some, keywords, here ] +tags: [some, keywords, here, gtn-topic-id ] editorial_board: - shiltemann diff --git a/learning-pathways/proteogenomics.md b/learning-pathways/proteogenomics.md index b63b6156332b94..694de181dbe73a 100644 --- a/learning-pathways/proteogenomics.md +++ b/learning-pathways/proteogenomics.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [beginner] +tags: [beginner, proteomics] type: use diff --git a/learning-pathways/python.md b/learning-pathways/python.md index 1e7491e8a46961..4dd5a766ad2cc9 100644 --- a/learning-pathways/python.md +++ b/learning-pathways/python.md @@ -12,7 +12,7 @@ funding: - gallantries - avans-atgm -tags: [python, real-course] +tags: [python, real-course, data-science] pathway: - section: "Week 1: Python is a Calculator" diff --git a/learning-pathways/reloaded_single_cell.md b/learning-pathways/reloaded_single_cell.md index fe9b3f65721cbd..5c4773b65a42f8 100644 --- a/learning-pathways/reloaded_single_cell.md +++ b/learning-pathways/reloaded_single_cell.md @@ -1,6 +1,6 @@ --- layout: learning-pathway -tags: [advanced] +tags: [advanced, single-cell] cover-image: assets/images/wab-annotatedcells-2.png cover-image-alt: "Image of cells in different coloured clusters" type: use diff --git a/learning-pathways/sql.md b/learning-pathways/sql.md index cc021b06fd1d00..6f58a6117cf07a 100644 --- a/learning-pathways/sql.md +++ b/learning-pathways/sql.md @@ -12,7 +12,7 @@ funding: - gallantries - avans-atgm -tags: [python, real-course] +tags: [python, real-course, data-science] pathway: - section: "Week 1: SQL Basics" diff --git a/learning-pathways/train-the-trainers.md b/learning-pathways/train-the-trainers.md index a580352c23f247..dbda99ad6ee88f 100644 --- a/learning-pathways/train-the-trainers.md +++ b/learning-pathways/train-the-trainers.md @@ -5,7 +5,7 @@ type: instructors title: Train the Trainers description: | This pathway introduces trainers to learning principles, training techniques, lesson, session, course, and material design as well as assessment and feedback. This is has been developed for by trainers in the bioinformatics but is suitable for all trainers and educators in higher education. -tags: [training, trainers] +tags: [training, trainers, teaching, community] editorial_board: - bebatut diff --git a/metadata/git-mod-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt b/metadata/git-mod-b25abfa33fe2909c7effad8e6cf45e05e9006c02.txt similarity index 99% rename from metadata/git-mod-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt rename to metadata/git-mod-b25abfa33fe2909c7effad8e6cf45e05e9006c02.txt index 19e125e8776c37..c073dd99e155c4 100644 --- a/metadata/git-mod-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt +++ b/metadata/git-mod-b25abfa33fe2909c7effad8e6cf45e05e9006c02.txt @@ -1,3 +1,216 @@ +GTN_GTN:1734116077 + +topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data-tests.yml +topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.ga +GTN_GTN:1734116059 + +topics/single-cell/tutorials/scrna-plant/tutorial.md +topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis-tests.yml +topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.ga +GTN_GTN:1734115964 + +topics/single-cell/tutorials/scrna-raceid/data-library.yaml +topics/single-cell/tutorials/scrna-raceid/tutorial.md +topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow-tests.yml +topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.ga +GTN_GTN:1734105652 + +CONTRIBUTORS.yaml +topics/ecology/tutorials/ENA_Biodiv_submission/images/image.PNG +topics/ecology/tutorials/ENA_Biodiv_submission/images/image1.png +topics/ecology/tutorials/ENA_Biodiv_submission/images/image2.png +topics/ecology/tutorials/ENA_Biodiv_submission/images/image3.png +topics/ecology/tutorials/ENA_Biodiv_submission/images/image4.png +topics/ecology/tutorials/ENA_Biodiv_submission/images/image5.png +topics/ecology/tutorials/ENA_Biodiv_submission/images/image7.PNG +topics/ecology/tutorials/ENA_Biodiv_submission/images/imageI.png +topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx +topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.bib +topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.md +topics/ecology/tutorials/ENA_Biodiv_submission/workflows/index.md +GTN_GTN:1734105104 + +.github/workflows/ci.yml +.github/workflows/deploy.yml +Gemfile +Gemfile.lock +_includes/quiz.html +_layouts/tutorial_hands_on.html +_plugins/abbr.rb +_plugins/api.rb +_plugins/author-page.rb +_plugins/by-tag-pages.rb +_plugins/colour-tags.rb +_plugins/feeds.rb +_plugins/file_exists.rb +_plugins/generator-recordings.rb +_plugins/generator-workflows.rb +_plugins/gtn.rb +_plugins/gtn/boxify.rb +_plugins/gtn/hooks.rb +_plugins/gtn/shortlinks.rb +_plugins/gtn/supported.rb +_plugins/gtn/synthetic.rb +_plugins/jekyll-boxify.rb +_plugins/jekyll-bundler.rb +_plugins/jekyll-color-picker.rb +_plugins/jekyll-duration.rb +_plugins/jekyll-environment_variables.rb +_plugins/jekyll-figurify.rb +_plugins/jekyll-icon-tag.rb +_plugins/jekyll-jsonld.rb +_plugins/jekyll-scholar.rb +_plugins/jekyll-tool-tag.rb +_plugins/jekyll-topic-filter.rb +_plugins/link.rb +_plugins/notebook-jupyter.rb +_plugins/notebook-rmarkdown.rb +_plugins/notebook.rb +_plugins/plaintext-slides.rb +_plugins/search.rb +_plugins/sitemap.rb +_plugins/util.rb +_plugins_dev/api-fake.rb +_plugins_dev/author-page.rb +_plugins_dev/notebook.rb +_plugins_dev/sitemap-fake.rb +assets/css/main.scss +bin/lint-test.rb +bin/lint.rb +bin/validate-frontmatter.rb +metadata/shortlinks.yaml +topics/admin/tutorials/subdomain/tutorial.md +topics/contributing/faqs/gtn-adr-images.md +topics/contributing/faqs/gtn-adr-jekyll.md +topics/contributing/faqs/gtn-adr-template.md +topics/contributing/faqs/gtn-adr.md +topics/contributing/faqs/gtn-slow-incremental.md +topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.md +topics/data-science/tutorials/sql-advanced/tutorial.md +topics/data-science/tutorials/sql-basic/tutorial.md +videos/index.html +videos/index.md +videos/watch.html +videos/watch.md +GTN_GTN:1734096307 + +topics/statistics/images/loris_tutorial/hyperparameters.png +topics/statistics/images/loris_tutorial/model_comparison.png +topics/statistics/images/loris_tutorial/report_tabs.png +topics/statistics/images/loris_tutorial/robustness.png +topics/statistics/images/loris_tutorial/test_metrics_results.png +topics/statistics/images/loris_tutorial/tutorial_schema.png +topics/statistics/tutorials/loris_model/README.md +topics/statistics/tutorials/loris_model/data-library.yaml +topics/statistics/tutorials/loris_model/faqs/index.md +topics/statistics/tutorials/loris_model/tutorial.bib +topics/statistics/tutorials/loris_model/tutorial.md +topics/statistics/tutorials/loris_model/workflows/index.md +topics/statistics/tutorials/loris_model/workflows/main_workflow-tests.yml +topics/statistics/tutorials/loris_model/workflows/main_workflow.ga +GTN_GTN:1733930817 + +GRANTS.yaml +ORGANISATIONS.yaml +_layouts/contributor_index.html +_plugins/gtn/contributors.rb +bin/schema-grants.yaml +shared/images/DFG_grant.png +topics/fair/tutorials/dataplant-arcs/tutorial.md +topics/imaging/tutorials/omero-suite/tutorial.md +GTN_GTN:1733917350 + +_layouts/home.html +_layouts/topic.html +assets/css/main.scss +bin/schema-topic.yaml +topics/single-cell/metadata.yaml +GTN_GTN:1733915934 + +.github/workflows/deploy.yml +_includes/cyoa-choices.html +_layouts/by_tool.html +_plugins/api.rb +_plugins/gtn.rb +_plugins/gtn/hooks.rb +_plugins/gtn/supported.rb +_plugins/jekyll-jsonld.rb +_plugins/jekyll-topic-filter.rb +_plugins/util.rb +bin/fetch-categories.rb +bin/find-duplicate-workflows.sh +bin/lint.rb +stats/top-tools.html +topics/assembly/tutorials/vgp_genome_assembly/tutorial.md +topics/epigenetics/tutorials/cut_and_run/tutorial.md +topics/galaxy-interface/tutorials/upload-data-to-ena/tutorial.md +topics/introduction/tutorials/galaxy-intro-101/tutorial.md +topics/single-cell/tutorials/scrna-case_trajectories/tutorial.md +topics/statistics/tutorials/galaxy-ludwig/tutorial.md +topics/statistics/tutorials/galaxy-ludwig/workflows/main_workflow.ga +GTN_GTN:1733902092 + +topics/variant-analysis/tutorials/dunovo/data-library.yaml +GTN_GTN:1733763216 + +topics/admin/tutorials/beacon/tutorial.md +GTN_GTN:1733763095 + +_layouts/learning-pathway.html +_plugins/jekyll-duration.rb +bin/schema-learning-pathway.yaml +bin/schema-tutorial.yaml +GTN_GTN:1733762976 + +metadata/workflowhub.yml +GTN_GTN:1733762899 + +topics/variant-analysis/images/workflow-dunovo-variant-calling.png +topics/variant-analysis/images/workflow-dunovo.png +topics/variant-analysis/tutorials/dunovo/tutorial.md +GTN_GTN:1733733637 + +assets/images/galaxy_climate.png +assets/images/galaxy_subdomain.png +news/images/2024-12-06-spoc-cofest_outputs.png +shared/images/nfdi4bioimage.jpeg +topics/assembly/images/image10.png +topics/community/images/tool_subdomain/add_interactive_tool.png +topics/community/images/tool_subdomain/add_section.png +topics/community/images/tool_subdomain/ecology_yml_tool.png +topics/contributing/tutorials/running-codespaces/images/codespace-publlish.png +topics/contributing/tutorials/running-codespaces/images/codespaces-branch-change1.png +topics/contributing/tutorials/running-codespaces/images/codespaces-branch-change2.png +topics/contributing/tutorials/running-codespaces/images/codespaces-commit-plus.png +topics/fair/tutorials/earth_system_rocrate/images/all_tree_structure.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate1.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate10.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate11.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate13.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate2.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate4.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate5.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate6.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate7.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate8.png +topics/fair/tutorials/earth_system_rocrate/images/rocrate9.png +topics/fair/tutorials/earth_system_rocrate/images/workflow_marino.png +topics/fair/tutorials/earth_system_rocrate/images/workflow_rocrate.png +topics/imaging/images/omero-suite/omero_credential.png +topics/imaging/images/omero-suite/omero_import.png +topics/imaging/images/omero-suite/omero_metadata.png +topics/imaging/images/omero-suite/omero_rois.png +topics/imaging/images/omero-suite/workflow.png +topics/imaging/images/omero-suite/workflow_invocation.png +topics/introduction/images/101_11.png +GTN_GTN:1733733599 + +metadata/shortlinks.yaml +GTN_GTN:1733733574 + +metadata/git-mod-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt +metadata/git-pub-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt +metadata/github.yml GTN_GTN:1733508635 news/_posts/2024-12-06-spoc_cofest.md diff --git a/metadata/git-pub-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt b/metadata/git-pub-b25abfa33fe2909c7effad8e6cf45e05e9006c02.txt similarity index 99% rename from metadata/git-pub-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt rename to metadata/git-pub-b25abfa33fe2909c7effad8e6cf45e05e9006c02.txt index 96ccc6b777d075..c20c9e5e3dc7b9 100644 --- a/metadata/git-pub-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt +++ b/metadata/git-pub-b25abfa33fe2909c7effad8e6cf45e05e9006c02.txt @@ -1,3 +1,68 @@ +GTN_GTN:1734116077 + +A topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data-tests.yml +R082 topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,_Plot_and_Explore_Single-cell_RNA-seq_Data.ga topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.ga +GTN_GTN:1734116059 + +A topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis-tests.yml +A topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.ga +GTN_GTN:1734115964 + +A topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow-tests.yml +R072 topics/single-cell/tutorials/scrna-raceid/workflows/main_workflow.ga topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.ga +GTN_GTN:1734105652 + +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image.PNG +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image1.png +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image2.png +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image3.png +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image4.png +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image5.png +A topics/ecology/tutorials/ENA_Biodiv_submission/images/image7.PNG +A topics/ecology/tutorials/ENA_Biodiv_submission/images/imageI.png +A topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx +A topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.bib +A topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.md +A topics/ecology/tutorials/ENA_Biodiv_submission/workflows/index.md +GTN_GTN:1734105104 + +A topics/contributing/faqs/gtn-adr-images.md +A topics/contributing/faqs/gtn-adr-jekyll.md +A topics/contributing/faqs/gtn-adr-template.md +A topics/contributing/faqs/gtn-adr.md +A topics/contributing/faqs/gtn-slow-incremental.md +A videos/index.html +A videos/watch.html +GTN_GTN:1734096307 + +A topics/statistics/images/loris_tutorial/hyperparameters.png +A topics/statistics/images/loris_tutorial/model_comparison.png +A topics/statistics/images/loris_tutorial/report_tabs.png +A topics/statistics/images/loris_tutorial/robustness.png +A topics/statistics/images/loris_tutorial/test_metrics_results.png +A topics/statistics/images/loris_tutorial/tutorial_schema.png +A topics/statistics/tutorials/loris_model/README.md +A topics/statistics/tutorials/loris_model/data-library.yaml +A topics/statistics/tutorials/loris_model/faqs/index.md +A topics/statistics/tutorials/loris_model/tutorial.bib +A topics/statistics/tutorials/loris_model/tutorial.md +A topics/statistics/tutorials/loris_model/workflows/index.md +A topics/statistics/tutorials/loris_model/workflows/main_workflow-tests.yml +A topics/statistics/tutorials/loris_model/workflows/main_workflow.ga +GTN_GTN:1733930817 + +A shared/images/DFG_grant.png +GTN_GTN:1733915934 + +A _plugins/gtn/hooks.rb +A bin/find-duplicate-workflows.sh +GTN_GTN:1733902092 + +A topics/variant-analysis/tutorials/dunovo/data-library.yaml +GTN_GTN:1733733574 + +R099 metadata/git-mod-aa284c5d875ec7123a085d0e0bcf0c59bd10c8ce.txt metadata/git-mod-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt +R099 metadata/git-pub-aa284c5d875ec7123a085d0e0bcf0c59bd10c8ce.txt metadata/git-pub-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt GTN_GTN:1733508635 A news/_posts/2024-12-06-spoc_cofest.md diff --git a/metadata/github.yml b/metadata/github.yml index d8841fed88a6d7..79325c473a6748 100644 --- a/metadata/github.yml +++ b/metadata/github.yml @@ -227180,3 +227180,1299 @@ title: Add News Post about Tracking Mitochondria and Capturing mitoflashes updatedAt: '2024-12-02T18:15:32Z' url: https://github.com/galaxyproject/training-material/pull/5571 +1657: + author: + id: MDQ6VXNlcjY0NTc3Mw== + is_bot: false + login: NickSto + name: Nick Stoler + closedAt: '2024-12-09T16:48:19Z' + createdAt: '2019-11-19T21:31:57Z' + files: + - path: topics/variant-analysis/images/workflow-dunovo-variant-calling.png + additions: 0 + deletions: 0 + - path: topics/variant-analysis/images/workflow-dunovo.png + additions: 0 + deletions: 0 + - path: topics/variant-analysis/tutorials/dunovo/tutorial.md + additions: 99 + deletions: 111 + headRefName: dunovo_enable + headRepository: + id: MDEwOlJlcG9zaXRvcnkxNDExMDIxOTc= + name: training-material + labels: + - work-in-progress + - variant-analysis + mergedAt: '2024-12-09T16:48:19Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: hexylena + state: COMMENTED + submittedAt: '2019-11-27T10:34:59Z' + reactionGroups: [] + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T16:48:12Z' + reactionGroups: [] + state: MERGED + title: Enable dunovo (calling rare variants) tutorial. + updatedAt: '2024-12-15T01:55:57Z' + url: https://github.com/galaxyproject/training-material/pull/1657 +5642: + author: + id: MDQ6VXNlcjUyNjgyNzc= + is_bot: false + login: pavanvidem + name: Pavankumar Videm + closedAt: '2024-12-13T18:54:37Z' + createdAt: '2024-12-13T16:53:33Z' + files: + - path: topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data-tests.yml + additions: 27 + deletions: 0 + - path: topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.ga + additions: 95 + deletions: 39 + headRefName: fpe-wf-test + headRepository: + id: MDEwOlJlcG9zaXRvcnk2Njg0NTY3Mw== + name: training-material + labels: + - single-cell + mergedAt: '2024-12-13T18:54:37Z' + mergedBy: + id: MDQ6VXNlcjQ2OTk4Mw== + is_bot: false + login: bgruening + name: BjĂśrn GrĂźning + reactionGroups: [] + reviews: [] + state: MERGED + title: add WF tests for scrna basic pipeline + updatedAt: '2024-12-13T18:54:38Z' + url: https://github.com/galaxyproject/training-material/pull/5642 +5641: + author: + id: MDQ6VXNlcjUyNjgyNzc= + is_bot: false + login: pavanvidem + name: Pavankumar Videm + closedAt: '2024-12-13T18:54:19Z' + createdAt: '2024-12-13T16:06:31Z' + files: + - path: topics/single-cell/tutorials/scrna-plant/tutorial.md + additions: 4 + deletions: 3 + - path: topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis-tests.yml + additions: 29 + deletions: 0 + - path: topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.ga + additions: 1448 + deletions: 0 + headRefName: scrna-plant-wf-test + headRepository: + id: MDEwOlJlcG9zaXRvcnk2Njg0NTY3Mw== + name: training-material + labels: + - single-cell + mergedAt: '2024-12-13T18:54:19Z' + mergedBy: + id: MDQ6VXNlcjQ2OTk4Mw== + is_bot: false + login: bgruening + name: BjĂśrn GrĂźning + reactionGroups: [] + reviews: + - author: + login: bgruening + state: APPROVED + submittedAt: '2024-12-13T18:54:12Z' + reactionGroups: [] + state: MERGED + title: Add scrna-plant WF tests + updatedAt: '2024-12-13T18:54:19Z' + url: https://github.com/galaxyproject/training-material/pull/5641 +5117: + author: + id: U_kgDOCkzJZg + is_bot: false + login: Najatamk + name: '' + closedAt: '2024-12-13T16:00:52Z' + createdAt: '2024-07-03T15:12:10Z' + files: + - path: CONTRIBUTORS.yaml + additions: 5 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image.PNG + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image1.png + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image2.png + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image3.png + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image4.png + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image5.png + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/image7.PNG + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/images/imageI.png + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx + additions: 0 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.bib + additions: 42 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.md + additions: 393 + deletions: 0 + - path: topics/ecology/tutorials/ENA_Biodiv_submission/workflows/index.md + additions: 3 + deletions: 0 + headRefName: Najatamk-patch-1 + headRepository: + id: R_kgDOMRjHag + name: training-material + labels: + - template-and-tools + - ecology + mergedAt: '2024-12-13T16:00:52Z' + mergedBy: + id: MDQ6VXNlcjc5MTA2Nzk= + is_bot: false + login: yvanlebras + name: Yvan Le Bras + reactionGroups: [] + reviews: + - author: + login: yvanlebras + state: DISMISSED + submittedAt: '2024-07-12T15:45:28Z' + reactionGroups: [] + - author: + login: yvanlebras + state: DISMISSED + submittedAt: '2024-09-09T09:22:59Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-05T11:49:38Z' + reactionGroups: + - content: THUMBS_UP + users: + totalCount: 1 + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T12:55:27Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T13:36:51Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T13:46:53Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T13:55:34Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T16:50:45Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T16:55:43Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T17:00:13Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-05T17:07:11Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-06T14:04:26Z' + reactionGroups: [] + - author: + login: yvanlebras + state: DISMISSED + submittedAt: '2024-12-06T14:08:31Z' + reactionGroups: [] + - author: + login: yvanlebras + state: DISMISSED + submittedAt: '2024-12-06T14:27:32Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-06T15:31:19Z' + reactionGroups: + - content: THUMBS_UP + users: + totalCount: 1 + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-10T08:13:56Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-10T08:27:04Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-10T08:34:34Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-10T08:35:46Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-13T10:32:17Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-13T10:32:54Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-13T10:47:46Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-13T10:47:58Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-13T10:53:57Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-13T10:54:12Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-13T11:33:22Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-13T15:19:00Z' + reactionGroups: [] + - author: + login: yvanlebras + state: COMMENTED + submittedAt: '2024-12-13T15:26:45Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-13T15:42:31Z' + reactionGroups: [] + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-13T15:43:46Z' + reactionGroups: [] + state: MERGED + title: Add ENA_Biodiv_submission Tutorial + updatedAt: '2024-12-13T18:53:34Z' + url: https://github.com/galaxyproject/training-material/pull/5117 +5639: + author: + id: MDQ6VXNlcjUyNjgyNzc= + is_bot: false + login: pavanvidem + name: Pavankumar Videm + closedAt: '2024-12-13T18:52:44Z' + createdAt: '2024-12-13T13:51:52Z' + files: + - path: topics/single-cell/tutorials/scrna-raceid/data-library.yaml + additions: 1 + deletions: 1 + - path: topics/single-cell/tutorials/scrna-raceid/tutorial.md + additions: 11 + deletions: 4 + - path: topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow-tests.yml + additions: 19 + deletions: 0 + - path: topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.ga + additions: 140 + deletions: 126 + headRefName: raceid-wf-test + headRepository: + id: MDEwOlJlcG9zaXRvcnk2Njg0NTY3Mw== + name: training-material + labels: + - single-cell + mergedAt: '2024-12-13T18:52:44Z' + mergedBy: + id: MDQ6VXNlcjQ2OTk4Mw== + is_bot: false + login: bgruening + name: BjĂśrn GrĂźning + reactionGroups: + - content: HEART + users: + totalCount: 1 + reviews: [] + state: MERGED + title: add workflow test for single-cell raceid tutorial + updatedAt: '2024-12-13T18:52:45Z' + url: https://github.com/galaxyproject/training-material/pull/5639 +5628: + author: + id: MDQ6VXNlcjQ1ODY4Mw== + is_bot: false + login: hexylena + name: Helena + closedAt: '2024-12-13T15:51:44Z' + createdAt: '2024-12-10T13:36:54Z' + files: + - path: ".github/workflows/ci.yml" + additions: 2 + deletions: 2 + - path: ".github/workflows/deploy.yml" + additions: 1 + deletions: 1 + - path: Gemfile + additions: 2 + deletions: 0 + - path: Gemfile.lock + additions: 2 + deletions: 0 + - path: _includes/quiz.html + additions: 90 + deletions: 7 + - path: _layouts/tutorial_hands_on.html + additions: 2 + deletions: 0 + - path: _plugins/abbr.rb + additions: 46 + deletions: 44 + - path: _plugins/api.rb + additions: 256 + deletions: 250 + - path: _plugins/author-page.rb + additions: 139 + deletions: 137 + - path: _plugins/by-tag-pages.rb + additions: 42 + deletions: 34 + - path: _plugins/colour-tags.rb + additions: 45 + deletions: 35 + - path: _plugins/feeds.rb + additions: 8 + deletions: 2 + - path: _plugins/file_exists.rb + additions: 24 + deletions: 20 + - path: _plugins/generator-recordings.rb + additions: 26 + deletions: 24 + - path: _plugins/generator-workflows.rb + additions: 72 + deletions: 44 + - path: _plugins/gtn.rb + additions: 92 + deletions: 6 + - path: _plugins/gtn/boxify.rb + additions: 1 + deletions: 1 + - path: _plugins/gtn/hooks.rb + additions: 1 + deletions: 1 + - path: _plugins/gtn/shortlinks.rb + additions: 44 + deletions: 1 + - path: _plugins/gtn/supported.rb + additions: 1 + deletions: 1 + - path: _plugins/gtn/synthetic.rb + additions: 19 + deletions: 17 + - path: _plugins/jekyll-boxify.rb + additions: 59 + deletions: 57 + - path: _plugins/jekyll-bundler.rb + additions: 73 + deletions: 58 + - path: _plugins/jekyll-color-picker.rb + additions: 20 + deletions: 17 + - path: _plugins/jekyll-duration.rb + additions: 102 + deletions: 99 + - path: _plugins/jekyll-environment_variables.rb + additions: 24 + deletions: 21 + - path: _plugins/jekyll-figurify.rb + additions: 106 + deletions: 103 + - path: _plugins/jekyll-icon-tag.rb + additions: 80 + deletions: 61 + - path: _plugins/jekyll-jsonld.rb + additions: 1014 + deletions: 977 + - path: _plugins/jekyll-scholar.rb + additions: 147 + deletions: 131 + - path: _plugins/jekyll-tool-tag.rb + additions: 75 + deletions: 48 + - path: _plugins/jekyll-topic-filter.rb + additions: 1277 + deletions: 1041 + - path: _plugins/link.rb + additions: 16 + deletions: 13 + - path: _plugins/notebook-jupyter.rb + additions: 2 + deletions: 2 + - path: _plugins/notebook-rmarkdown.rb + additions: 35 + deletions: 33 + - path: _plugins/notebook.rb + additions: 564 + deletions: 479 + - path: _plugins/plaintext-slides.rb + additions: 36 + deletions: 34 + - path: _plugins/search.rb + additions: 58 + deletions: 48 + - path: _plugins/sitemap.rb + additions: 52 + deletions: 50 + - path: _plugins/util.rb + additions: 5 + deletions: 1 + - path: _plugins_dev/api-fake.rb + additions: 0 + deletions: 10 + - path: _plugins_dev/author-page.rb + additions: 0 + deletions: 13 + - path: _plugins_dev/notebook.rb + additions: 11 + deletions: 9 + - path: _plugins_dev/sitemap-fake.rb + additions: 24 + deletions: 22 + - path: assets/css/main.scss + additions: 8 + deletions: 4 + - path: bin/lint-test.rb + additions: 10 + deletions: 10 + - path: bin/lint.rb + additions: 1375 + deletions: 1161 + - path: bin/validate-frontmatter.rb + additions: 220 + deletions: 218 + - path: metadata/shortlinks.yaml + additions: 0 + deletions: 2 + - path: topics/admin/tutorials/subdomain/tutorial.md + additions: 1 + deletions: 1 + - path: topics/contributing/faqs/gtn-adr-images.md + additions: 47 + deletions: 0 + - path: topics/contributing/faqs/gtn-adr-jekyll.md + additions: 98 + deletions: 0 + - path: topics/contributing/faqs/gtn-adr-template.md + additions: 76 + deletions: 0 + - path: topics/contributing/faqs/gtn-adr.md + additions: 18 + deletions: 0 + - path: topics/contributing/faqs/gtn-slow-incremental.md + additions: 9 + deletions: 0 + - path: topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.md + additions: 2 + deletions: 2 + - path: topics/data-science/tutorials/sql-advanced/tutorial.md + additions: 2 + deletions: 2 + - path: topics/data-science/tutorials/sql-basic/tutorial.md + additions: 2 + deletions: 0 + - path: videos/index.html + additions: 84 + deletions: 0 + - path: videos/index.md + additions: 0 + deletions: 88 + - path: videos/watch.html + additions: 194 + deletions: 0 + - path: videos/watch.md + additions: 0 + deletions: 130 + headRefName: platypus-tailor + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: + - admin + - template-and-tools + - contributing + - data-science + mergedAt: '2024-12-13T15:51:44Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: + - content: HEART + users: + totalCount: 1 + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-13T15:51:31Z' + reactionGroups: [] + state: MERGED + title: code documentation + updatedAt: '2024-12-13T15:51:44Z' + url: https://github.com/galaxyproject/training-material/pull/5628 +5539: + author: + id: MDQ6VXNlcjU4MDA4MjAw + is_bot: false + login: paulocilasjr + name: Paulo Cilas Morais Lyra Junior + closedAt: '2024-12-13T13:25:07Z' + createdAt: '2024-11-14T19:59:16Z' + files: + - path: topics/statistics/images/loris_tutorial/hyperparameters.png + additions: 0 + deletions: 0 + - path: topics/statistics/images/loris_tutorial/model_comparison.png + additions: 0 + deletions: 0 + - path: topics/statistics/images/loris_tutorial/report_tabs.png + additions: 0 + deletions: 0 + - path: topics/statistics/images/loris_tutorial/robustness.png + additions: 0 + deletions: 0 + - path: topics/statistics/images/loris_tutorial/test_metrics_results.png + additions: 0 + deletions: 0 + - path: topics/statistics/images/loris_tutorial/tutorial_schema.png + additions: 0 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/README.md + additions: 2 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/data-library.yaml + additions: 23 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/faqs/index.md + additions: 3 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/tutorial.bib + additions: 36 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/tutorial.md + additions: 287 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/workflows/index.md + additions: 3 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/workflows/main_workflow-tests.yml + additions: 19 + deletions: 0 + - path: topics/statistics/tutorials/loris_model/workflows/main_workflow.ga + additions: 176 + deletions: 0 + headRefName: main + headRepository: + id: R_kgDOMcxfZQ + name: training-material + labels: + - statistics + mergedAt: '2024-12-13T13:25:07Z' + mergedBy: + id: MDQ6VXNlcjMwMjI1MTg= + is_bot: false + login: anuprulez + name: Anup Kumar, PhD + reactionGroups: [] + reviews: + - author: + login: bgruening + state: COMMENTED + submittedAt: '2024-12-02T19:40:15Z' + reactionGroups: + - content: THUMBS_UP + users: + totalCount: 1 + - author: + login: paulocilasjr + state: COMMENTED + submittedAt: '2024-12-02T21:07:16Z' + reactionGroups: [] + - author: + login: bgruening + state: COMMENTED + submittedAt: '2024-12-03T15:40:07Z' + reactionGroups: [] + - author: + login: bgruening + state: COMMENTED + submittedAt: '2024-12-03T15:40:21Z' + reactionGroups: [] + - author: + login: paulocilasjr + state: COMMENTED + submittedAt: '2024-12-03T16:02:59Z' + reactionGroups: [] + - author: + login: bgruening + state: COMMENTED + submittedAt: '2024-12-10T16:35:48Z' + reactionGroups: [] + - author: + login: anuprulez + state: COMMENTED + submittedAt: '2024-12-11T12:22:32Z' + reactionGroups: [] + - author: + login: paulocilasjr + state: COMMENTED + submittedAt: '2024-12-11T16:36:13Z' + reactionGroups: [] + - author: + login: anuprulez + state: COMMENTED + submittedAt: '2024-12-12T17:59:55Z' + reactionGroups: + - content: THUMBS_UP + users: + totalCount: 1 + - author: + login: anuprulez + state: APPROVED + submittedAt: '2024-12-13T13:24:36Z' + reactionGroups: [] + state: MERGED + title: PyCaret - LORIS Tutorial + updatedAt: '2024-12-13T13:25:07Z' + url: https://github.com/galaxyproject/training-material/pull/5539 +5627: + author: + id: U_kgDOBwiTXg + is_bot: false + login: rmassei + name: Riccardo Massei + closedAt: '2024-12-11T15:26:58Z' + createdAt: '2024-12-10T08:10:40Z' + files: + - path: GRANTS.yaml + additions: 24 + deletions: 0 + - path: ORGANISATIONS.yaml + additions: 8 + deletions: 13 + - path: _layouts/contributor_index.html + additions: 14 + deletions: 6 + - path: _plugins/gtn/contributors.rb + additions: 2 + deletions: 0 + - path: bin/schema-grants.yaml + additions: 1 + deletions: 0 + - path: shared/images/DFG_grant.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/dataplant-arcs/tutorial.md + additions: 1 + deletions: 0 + - path: topics/imaging/tutorials/omero-suite/tutorial.md + additions: 1 + deletions: 0 + headRefName: main + headRepository: + id: R_kgDOML73DQ + name: training-material + labels: + - template-and-tools + - imaging + - fair + mergedAt: '2024-12-11T15:26:58Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: bgruening + state: COMMENTED + submittedAt: '2024-12-10T08:23:52Z' + reactionGroups: [] + - author: + login: shiltemann + state: COMMENTED + submittedAt: '2024-12-11T12:07:11Z' + reactionGroups: [] + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-11T15:21:48Z' + reactionGroups: [] + state: MERGED + title: Add the DFG grant to the omero training matherial + updatedAt: '2024-12-11T15:26:58Z' + url: https://github.com/galaxyproject/training-material/pull/5627 +5515: + author: + id: MDQ6VXNlcjQ1ODY4Mw== + is_bot: false + login: hexylena + name: Helena + closedAt: '2024-12-11T11:18:54Z' + createdAt: '2024-11-07T16:27:03Z' + files: + - path: ".github/workflows/deploy.yml" + additions: 1 + deletions: 0 + - path: _includes/cyoa-choices.html + additions: 3 + deletions: 1 + - path: _layouts/by_tool.html + additions: 77 + deletions: 16 + - path: _plugins/api.rb + additions: 7 + deletions: 11 + - path: _plugins/gtn.rb + additions: 7 + deletions: 0 + - path: _plugins/gtn/hooks.rb + additions: 72 + deletions: 0 + - path: _plugins/gtn/supported.rb + additions: 3 + deletions: 3 + - path: _plugins/jekyll-jsonld.rb + additions: 29 + deletions: 0 + - path: _plugins/jekyll-topic-filter.rb + additions: 9 + deletions: 1 + - path: _plugins/util.rb + additions: 113 + deletions: 0 + - path: bin/fetch-categories.rb + additions: 1 + deletions: 2 + - path: bin/find-duplicate-workflows.sh + additions: 2 + deletions: 0 + - path: bin/lint.rb + additions: 48 + deletions: 52 + - path: stats/top-tools.html + additions: 3 + deletions: 0 + - path: topics/assembly/tutorials/vgp_genome_assembly/tutorial.md + additions: 1 + deletions: 1 + - path: topics/epigenetics/tutorials/cut_and_run/tutorial.md + additions: 1 + deletions: 2 + - path: topics/galaxy-interface/tutorials/upload-data-to-ena/tutorial.md + additions: 1 + deletions: 1 + - path: topics/introduction/tutorials/galaxy-intro-101/tutorial.md + additions: 1 + deletions: 1 + - path: topics/single-cell/tutorials/scrna-case_trajectories/tutorial.md + additions: 1 + deletions: 3 + - path: topics/statistics/tutorials/galaxy-ludwig/tutorial.md + additions: 2 + deletions: 2 + - path: topics/statistics/tutorials/galaxy-ludwig/workflows/main_workflow.ga + additions: 1 + deletions: 1 + headRefName: manticore-bowfin + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: + - introduction + - template-and-tools + - assembly + - epigenetics + - statistics + - galaxy-interface + - single-cell + mergedAt: '2024-12-11T11:18:54Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-11T11:18:48Z' + reactionGroups: [] + state: MERGED + title: improved by-tools page + updatedAt: '2024-12-11T14:15:23Z' + url: https://github.com/galaxyproject/training-material/pull/5515 +5630: + author: + id: MDQ6VXNlcjQ1ODY4Mw== + is_bot: false + login: hexylena + name: Helena + closedAt: '2024-12-11T11:42:30Z' + createdAt: '2024-12-10T14:28:54Z' + files: + - path: _layouts/home.html + additions: 4 + deletions: 5 + - path: _layouts/topic.html + additions: 41 + deletions: 14 + - path: assets/css/main.scss + additions: 6 + deletions: 1 + - path: bin/schema-topic.yaml + additions: 37 + deletions: 0 + - path: topics/single-cell/metadata.yaml + additions: 31 + deletions: 3 + headRefName: little-kardashev + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: + - template-and-tools + - single-cell + mergedAt: '2024-12-11T11:42:30Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-11T11:39:18Z' + reactionGroups: [] + state: MERGED + title: Improve single cell topic page + updatedAt: '2024-12-11T11:42:30Z' + url: https://github.com/galaxyproject/training-material/pull/5630 +5633: + author: + is_bot: true + login: app/github-actions + closedAt: '2024-12-11T07:28:12Z' + createdAt: '2024-12-11T01:10:10Z' + files: + - path: topics/variant-analysis/tutorials/dunovo/data-library.yaml + additions: 38 + deletions: 0 + headRefName: create-pull-request/patch-1733879408 + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: [] + mergedAt: '2024-12-11T07:28:12Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-11T07:28:03Z' + reactionGroups: [] + state: MERGED + title: Add missing data-library.yaml files + updatedAt: '2024-12-11T07:28:12Z' + url: https://github.com/galaxyproject/training-material/pull/5633 +5623: + author: + id: MDQ6VXNlcjQ1ODY4Mw== + is_bot: false + login: hexylena + name: Helena + closedAt: '2024-12-09T16:53:36Z' + createdAt: '2024-12-09T09:03:00Z' + files: + - path: topics/admin/tutorials/beacon/tutorial.md + additions: 76 + deletions: 61 + headRefName: anaconda-ionian + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: + - admin + mergedAt: '2024-12-09T16:53:36Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: + - content: THUMBS_UP + users: + totalCount: 1 + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T16:53:30Z' + reactionGroups: [] + state: MERGED + title: Fix beacon tutorial issues + updatedAt: '2024-12-09T16:53:37Z' + url: https://github.com/galaxyproject/training-material/pull/5623 +5624: + author: + id: MDQ6VXNlcjQ1ODY4Mw== + is_bot: false + login: hexylena + name: Helena + closedAt: '2024-12-09T16:51:35Z' + createdAt: '2024-12-09T11:35:08Z' + files: + - path: _layouts/learning-pathway.html + additions: 17 + deletions: 0 + - path: _plugins/jekyll-duration.rb + additions: 76 + deletions: 12 + - path: bin/schema-learning-pathway.yaml + additions: 17 + deletions: 0 + - path: bin/schema-tutorial.yaml + additions: 1 + deletions: 1 + headRefName: zebroid-bigeye + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: + - template-and-tools + mergedAt: '2024-12-09T16:51:35Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T16:51:28Z' + reactionGroups: [] + state: MERGED + title: Add total module duration + LOs per module in LPs + updatedAt: '2024-12-09T16:51:35Z' + url: https://github.com/galaxyproject/training-material/pull/5624 +5625: + author: + is_bot: true + login: app/github-actions + closedAt: '2024-12-09T16:49:37Z' + createdAt: '2024-12-09T13:45:49Z' + files: + - path: metadata/workflowhub.yml + additions: 1 + deletions: 0 + headRefName: create-pull-request/patch-1733751947 + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: [] + mergedAt: '2024-12-09T16:49:37Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T16:49:21Z' + reactionGroups: [] + state: MERGED + title: Update WorkflowHub IDs + updatedAt: '2024-12-09T16:49:37Z' + url: https://github.com/galaxyproject/training-material/pull/5625 +5620: + author: + is_bot: true + login: app/github-actions + closedAt: '2024-12-09T08:40:37Z' + createdAt: '2024-12-08T23:30:22Z' + files: + - path: assets/images/galaxy_climate.png + additions: 0 + deletions: 0 + - path: assets/images/galaxy_subdomain.png + additions: 0 + deletions: 0 + - path: news/images/2024-12-06-spoc-cofest_outputs.png + additions: 0 + deletions: 0 + - path: shared/images/nfdi4bioimage.jpeg + additions: 0 + deletions: 0 + - path: topics/assembly/images/image10.png + additions: 0 + deletions: 0 + - path: topics/community/images/tool_subdomain/add_interactive_tool.png + additions: 0 + deletions: 0 + - path: topics/community/images/tool_subdomain/add_section.png + additions: 0 + deletions: 0 + - path: topics/community/images/tool_subdomain/ecology_yml_tool.png + additions: 0 + deletions: 0 + - path: topics/contributing/tutorials/running-codespaces/images/codespace-publlish.png + additions: 0 + deletions: 0 + - path: topics/contributing/tutorials/running-codespaces/images/codespaces-branch-change1.png + additions: 0 + deletions: 0 + - path: topics/contributing/tutorials/running-codespaces/images/codespaces-branch-change2.png + additions: 0 + deletions: 0 + - path: topics/contributing/tutorials/running-codespaces/images/codespaces-commit-plus.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/all_tree_structure.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate1.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate10.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate11.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate13.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate2.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate4.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate5.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate6.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate7.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate8.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/rocrate9.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/workflow_marino.png + additions: 0 + deletions: 0 + - path: topics/fair/tutorials/earth_system_rocrate/images/workflow_rocrate.png + additions: 0 + deletions: 0 + - path: topics/imaging/images/omero-suite/omero_credential.png + additions: 0 + deletions: 0 + - path: topics/imaging/images/omero-suite/omero_import.png + additions: 0 + deletions: 0 + - path: topics/imaging/images/omero-suite/omero_metadata.png + additions: 0 + deletions: 0 + - path: topics/imaging/images/omero-suite/omero_rois.png + additions: 0 + deletions: 0 + - path: topics/imaging/images/omero-suite/workflow.png + additions: 0 + deletions: 0 + - path: topics/imaging/images/omero-suite/workflow_invocation.png + additions: 0 + deletions: 0 + - path: topics/introduction/images/101_11.png + additions: 0 + deletions: 0 + headRefName: create-pull-request/patch-1733700618 + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: [] + mergedAt: '2024-12-09T08:40:37Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T08:40:23Z' + reactionGroups: [] + state: MERGED + title: Auto Compress Images + updatedAt: '2024-12-09T08:40:37Z' + url: https://github.com/galaxyproject/training-material/pull/5620 +5621: + author: + is_bot: true + login: app/github-actions + closedAt: '2024-12-09T08:39:59Z' + createdAt: '2024-12-09T01:10:05Z' + files: + - path: metadata/shortlinks.yaml + additions: 17 + deletions: 0 + headRefName: create-pull-request/patch-1733706600 + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: [] + mergedAt: '2024-12-09T08:39:59Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T08:39:51Z' + reactionGroups: [] + state: MERGED + title: Update Persistent uniform resource locators + updatedAt: '2024-12-09T08:40:00Z' + url: https://github.com/galaxyproject/training-material/pull/5621 +5622: + author: + is_bot: true + login: app/github-actions + closedAt: '2024-12-09T08:39:35Z' + createdAt: '2024-12-09T01:11:52Z' + files: + - path: metadata/git-mod-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt + additions: 332 + deletions: 0 + - path: metadata/git-pub-8cb9a3a5113d0b6a5b4374ea1865adbe76b9da68.txt + additions: 131 + deletions: 0 + - path: metadata/github.yml + additions: 1754 + deletions: 270 + headRefName: create-pull-request/patch-1733706709 + headRepository: + id: MDEwOlJlcG9zaXRvcnkzODI0MTQ2MQ== + name: training-material + labels: [] + mergedAt: '2024-12-09T08:39:35Z' + mergedBy: + id: MDQ6VXNlcjI1NjM4NjU= + is_bot: false + login: shiltemann + name: Saskia Hiltemann + reactionGroups: [] + reviews: + - author: + login: shiltemann + state: APPROVED + submittedAt: '2024-12-09T08:39:22Z' + reactionGroups: [] + state: MERGED + title: Update Cached Commit Data + updatedAt: '2024-12-09T08:39:35Z' + url: https://github.com/galaxyproject/training-material/pull/5622 diff --git a/metadata/shortlinks.yaml b/metadata/shortlinks.yaml index b93043f8444c52..acbca2447f997a 100644 --- a/metadata/shortlinks.yaml +++ b/metadata/shortlinks.yaml @@ -80,7 +80,6 @@ name: data-science/data-manipulation-olympics: "/topics/data-science/tutorials/data-manipulation-olympics/tutorial.html" data-science/data-manipulation-olympics-sql: "/topics/data-science/tutorials/data-manipulation-olympics-sql/tutorial.html" data-science/git-cli: "/topics/data-science/tutorials/git-cli/tutorial.html" - data-science/how-to-google: "/topics/data-science/tutorials/how-to-google/tutorial.html" data-science/python-advanced-np-pd: "/topics/data-science/tutorials/python-advanced-np-pd/tutorial.html" data-science/python-argparse: "/topics/data-science/tutorials/python-argparse/tutorial.html" data-science/python-basics: "/topics/data-science/tutorials/python-basics/tutorial.html" @@ -606,6 +605,8 @@ name: community/tools_subdomains: "/topics/community/tutorials/tools_subdomains/tutorial.html" imaging/omero-suite: "/topics/imaging/tutorials/omero-suite/tutorial.html" fair/earth_system_rocrate/slides: "/topics/fair/tutorials/earth_system_rocrate/slides.html" + ecology/ENA_Biodiv_submission: "/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.html" + statistics/loris_model: "/topics/statistics/tutorials/loris_model/tutorial.html" id: T00000: "/topics/admin/tutorials/ansible/tutorial.html" T00001: "/topics/admin/tutorials/ansible-galaxy/tutorial.html" @@ -687,7 +688,6 @@ id: T00077: "/topics/data-science/tutorials/data-manipulation-olympics/tutorial.html" T00078: "/topics/data-science/tutorials/data-manipulation-olympics-sql/tutorial.html" T00079: "/topics/data-science/tutorials/git-cli/tutorial.html" - T00080: "/topics/data-science/tutorials/how-to-google/tutorial.html" T00081: "/topics/data-science/tutorials/python-advanced-np-pd/tutorial.html" T00082: "/topics/data-science/tutorials/python-argparse/tutorial.html" T00083: "/topics/data-science/tutorials/python-basics/tutorial.html" @@ -2077,7 +2077,7 @@ id: T00474: "/topics/contributing/tutorials/updating_tutorial/tutorial.html" T00475: "/topics/community/tutorials/community-tool-table/tutorial.html" T00476: "/topics/community/tutorials/tools_subdomains/tutorial.html" - T00477: "/topics/imaging/tutorials/omero-suite/tutorial.html" + T00477: "/topics/statistics/tutorials/loris_model/tutorial.html" S00127: "/topics/fair/tutorials/earth_system_rocrate/slides.html" F00438: "/topics/community/faqs/codex.html" F00439: "/topics/contributing/faqs/github-fork-gtn.html" @@ -2088,6 +2088,15 @@ id: N00098: "/news/2024/12/06/spoc_cofest.html" P00025: "/learning-pathways/dev_tools_training.html" W00292: "/topics/single-cell/tutorials/scrna-case_FilterPlotandExplore_SeuratTools/workflows/workflow-seurat-filter-plot-explore.html" + F00442: "/topics/contributing/faqs/gtn-adr-images.html" + F00443: "/topics/contributing/faqs/gtn-adr-jekyll.html" + F00444: "/topics/contributing/faqs/gtn-adr-template.html" + F00445: "/topics/contributing/faqs/gtn-adr.html" + F00446: "/topics/contributing/faqs/gtn-slow-incremental.html" + W00293: "/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.html" + W00294: "/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.html" + W00295: "/topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.html" + W00296: "/topics/statistics/tutorials/loris_model/workflows/main_workflow.html" misc: ansible-galaxy: "/topics/admin/tutorials/ansible-galaxy/tutorial.html" feeds: "/feeds/index.html" diff --git a/metadata/swagger.yaml b/metadata/swagger.yaml index 5ebb6f74e05487..4ba7ca5fa83ede 100644 --- a/metadata/swagger.yaml +++ b/metadata/swagger.yaml @@ -1,8 +1,17 @@ openapi: 3.0.1 info: title: Galaxy Training Network API - description: A collection of tutorials generated and maintained by Galaxy Community - Members across the world + description: | + A collection of e-learning materials produced and maintained by GTN Members + across the world. + + IMPORTANT! IF you build anything based off of this API, you must contact us + to let us know. This API is UNSTABLE and subject to CHANGE WITHOUT WARNING. + When you contact us, if we are interested in supporting the project, then + we can advise you to the best APIs that are useful for your use case, or + help build new ones that are more useful to you and do not impose + unnecessary load on the GTN. Additionally we will consider providing + additional stability guarantees for those specific routes you rely on. license: name: MIT & CC-BY 4.0 url: https://github.com/galaxyproject/training-material/blob/main/LICENSE.md @@ -18,6 +27,10 @@ tags: description: GTN Contributor Information - name: internal description: Internal APIs that aren't much use outside. +- name: galaxy + description: APIs being used inside Galaxy +- name: stable + description: APIs with higher reliability guarantees paths: /feedback.csv: @@ -76,6 +89,21 @@ paths: application/json: schema: $ref: '#/components/schemas/User' + + /top-tools.json: + get: + tags: + - galaxy + - stable + summary: List available tutorials by tool + responses: + 200: + description: successful operation + content: + application/json: + schema: + type: object + /topics.json: get: tags: diff --git a/news.md b/news.md index 1cc67a73fa4bf2..bb2c769038854c 100644 --- a/news.md +++ b/news.md @@ -7,12 +7,13 @@ Keep an eye on this page for the latest news around the GTN. New tutorials, GTN Want to add your own news here (e.g. new tutorial, event, publication, anything else training related)? Check out how to do that in [this FAQ]({% link faqs/gtn/gtn_news_create_post.md %}) -<div class="newslist"> -{% for n in site.categories['news'] %} +<div class="pathwaylist row"> +{% for n in site.categories['news'] %} {% include _includes/news-card.html news=n %} {% endfor %} + </div> diff --git a/news/_posts/2021-03-16-slides_to_videos.md b/news/_posts/2021-03-16-slides_to_videos.md index 527e183acc2433..faf478fd94c223 100644 --- a/news/_posts/2021-03-16-slides_to_videos.md +++ b/news/_posts/2021-03-16-slides_to_videos.md @@ -2,7 +2,7 @@ title: "New Feature: Automatic Slides-to-video conversion" contributions: authorship: [hexylena,delphine-l] -tags: [new feature, videos, gtn, pandemic, remote-teaching] +tags: [new feature, videos, gtn, pandemic, remote-teaching, contributing] layout: news --- diff --git a/news/_posts/2021-03-17-smorgasbord_report.md b/news/_posts/2021-03-17-smorgasbord_report.md index afc12cab3b2f9f..e3e5e40f491c5c 100644 --- a/news/_posts/2021-03-17-smorgasbord_report.md +++ b/news/_posts/2021-03-17-smorgasbord_report.md @@ -1,6 +1,6 @@ --- title: Report on the GTN SmĂśrgĂĽsbord event -tags: [event, pandemic, remote-teaching] +tags: [event, pandemic, remote-teaching, teaching] contributions: authorship: [shiltemann,hexylena] cover: "https://gallantries.github.io/assets/images/smorgasbord/emoji-cloud.jpeg" diff --git a/news/_posts/2021-03-18-gtn_cofest_may.md b/news/_posts/2021-03-18-gtn_cofest_may.md index b4b912e48a7b3a..bafa0adab62d1c 100644 --- a/news/_posts/2021-03-18-gtn_cofest_may.md +++ b/news/_posts/2021-03-18-gtn_cofest_may.md @@ -1,6 +1,6 @@ --- title: "Next GTN CoFest May 20, 2021" -tags: [cofest] +tags: [cofest, contributing] contributions: authorship: [shiltemann, hexylena, bebatut, jennaj, delphine-l, annasyme, mblue9] layout: news diff --git a/news/_posts/2021-03-24-faqs.md b/news/_posts/2021-03-24-faqs.md index 0fca47edcf2b6f..c2795b60436c9d 100644 --- a/news/_posts/2021-03-24-faqs.md +++ b/news/_posts/2021-03-24-faqs.md @@ -2,7 +2,7 @@ title: "New Feature: FAQs" contributions: authorship: [shiltemann, hexylena, bebatut] -tags: [gtn infrastructure, contributors, instructors, new feature] +tags: [gtn infrastructure, contributors, instructors, new feature, contributing] layout: news tutorial: "topics/contributing/tutorials/create-new-tutorial-content/tutorial.html#faqs-snippets" --- diff --git a/news/_posts/2021-04-06-new-video-player.md b/news/_posts/2021-04-06-new-video-player.md index a3a745af71696a..75737170427fd1 100644 --- a/news/_posts/2021-04-06-new-video-player.md +++ b/news/_posts/2021-04-06-new-video-player.md @@ -2,7 +2,7 @@ title: "New Feature: Video Player" contributions: authorship: [hexylena] -tags: [gtn infrastructure, video, new feature] +tags: [gtn infrastructure, video, new feature, contributing] cover: topics/contributing/images/slides-to-video.png coveralt: Example of video player mimicking youtube's interface with a video at the top, a transcript at the bottom left, and suggested videos at the bottom right. layout: news diff --git a/news/_posts/2021-05-20-spanish_project_begins.md b/news/_posts/2021-05-20-spanish_project_begins.md index e03aa10c92b078..a237acdcfcdc77 100644 --- a/news/_posts/2021-05-20-spanish_project_begins.md +++ b/news/_posts/2021-05-20-spanish_project_begins.md @@ -1,6 +1,6 @@ --- title: "ÂżHablas espaĂąol?: The first curated tutorial in Spanish!" -tags: [new tutorial, espaĂąol] +tags: [new tutorial, espaĂąol, introduction] contributions: authorship: [nomadscientist, beatrizserrano, pclo, ales-ibt, shiltemann, hexylena] cover: "topics/introduction/images/hello-languages.png" diff --git a/news/_posts/2021-05-25-abbreviations-tag.md b/news/_posts/2021-05-25-abbreviations-tag.md index c09ad47e6771c8..fabf706dcff63c 100644 --- a/news/_posts/2021-05-25-abbreviations-tag.md +++ b/news/_posts/2021-05-25-abbreviations-tag.md @@ -1,6 +1,6 @@ --- title: "New Feature: Easy Abbreviation" -tags: [new feature] +tags: [new feature, contributing] contributions: authorship: [hexylena, rikeshi, simonbray] tutorial: "topics/dev/tutorials/bioblend-dev/tutorial.html" diff --git a/news/_posts/2021-05-25-new-dev-tutorial.md b/news/_posts/2021-05-25-new-dev-tutorial.md index 60fd8e8a258202..1ec192530a2884 100644 --- a/news/_posts/2021-05-25-new-dev-tutorial.md +++ b/news/_posts/2021-05-25-new-dev-tutorial.md @@ -1,6 +1,6 @@ --- title: "Contributing to BioBlend as a developer" -tags: [new tutorial] +tags: [new tutorial, dev] contributions: authorship: [rikeshi, simonbray] tutorial: "topics/dev/tutorials/bioblend-dev/tutorial.html" diff --git a/news/_posts/2021-06-01-archive-menu.md b/news/_posts/2021-06-01-archive-menu.md index b8b19e708330c2..8d7aa2a8d21fa2 100644 --- a/news/_posts/2021-06-01-archive-menu.md +++ b/news/_posts/2021-06-01-archive-menu.md @@ -1,6 +1,6 @@ --- title: "Oh no, it changed! Quick, to the archive menu." -tags: [new feature] +tags: [new feature, contributing] contributions: authorship: [hexylena, shiltemann] layout: news diff --git a/news/_posts/2021-06-25-gitpod.md b/news/_posts/2021-06-25-gitpod.md index 77a14ad6bee1f2..7ad16e81660947 100644 --- a/news/_posts/2021-06-25-gitpod.md +++ b/news/_posts/2021-06-25-gitpod.md @@ -1,6 +1,6 @@ --- title: "New Tutorial: GitPod for contributing to the GTN" -tags: [new tutorial, contributors] +tags: [new tutorial, contributing] contributions: authorship: [shiltemann, hexylena] tutorial: "topics/contributing/tutorials/gitpod/tutorial.html" diff --git a/news/_posts/2021-06-26-tutorial-volcanoplot-r.md b/news/_posts/2021-06-26-tutorial-volcanoplot-r.md index cc57359e594e6c..5567427d59fd57 100644 --- a/news/_posts/2021-06-26-tutorial-volcanoplot-r.md +++ b/news/_posts/2021-06-26-tutorial-volcanoplot-r.md @@ -1,6 +1,6 @@ --- title: "New Tutorial: Visualization of RNA-Seq results with Volcano Plot in R" -tags: [new tutorial, visualization] +tags: [new tutorial, visualisation, transcriptomics ] contributions: authorship: [mblue9] tutorial: "topics/transcriptomics/tutorials/rna-seq-viz-with-volcanoplot-r/tutorial.html" @@ -9,7 +9,7 @@ coveralt: "Screenshot of Volcano Plot customised in R. Significant genes had the layout: news --- -The [Volcano plot]({% link topics/transcriptomics/tutorials/rna-seq-viz-with-volcanoplot/tutorial.md %}) tutorial introduced volcano plots and showed how they can be easily generated with the Galaxy Volcano plot tool. This new tutorial shows how you can customise a plot using the R script output from the tool and RStudio in Galaxy. A [short video](https://www.youtube.com/embed/4dspgiwkuxk) for the tutorial is also available on YouTube, created for the [GCC2021 Training week](https://galaxyproject.org/events/gcc2021/training/). +The [Volcano plot]({% link topics/transcriptomics/tutorials/rna-seq-viz-with-volcanoplot/tutorial.md %}) tutorial introduced volcano plots and showed how they can be easily generated with the Galaxy Volcano plot tool. This new tutorial shows how you can customise a plot using the R script output from the tool and RStudio in Galaxy. A [short video](https://www.youtube.com/embed/4dspgiwkuxk) for the tutorial is also available on YouTube, created for the [GCC2021 Training week](https://galaxyproject.org/events/gcc2021/training/). Happy plotting! {% include _includes/youtube.html id="4dspgiwkuxk" title="GTN Tutorial: Volcano Plots in R" %} diff --git a/news/_posts/2021-06-30-tutorial-sars-cov-2-variant-discovery.md b/news/_posts/2021-06-30-tutorial-sars-cov-2-variant-discovery.md index e2151d0e6c5bd9..93c116b91fa951 100644 --- a/news/_posts/2021-06-30-tutorial-sars-cov-2-variant-discovery.md +++ b/news/_posts/2021-06-30-tutorial-sars-cov-2-variant-discovery.md @@ -1,6 +1,6 @@ --- title: "New Tutorial: Mutation calling, viral genome reconstruction and lineage/clade assignment from SARS-CoV-2 sequencing data" -tags: [new tutorial, variant-analysis, covid-19] +tags: [new tutorial, variant-analysis, covid-19, one-health] contributions: authorship: [bebatut, wm75] tutorial: "topics/variant-analysis/tutorials/sars-cov-2-variant-discovery/tutorial.html" diff --git a/news/_posts/2021-07-27-a11y.md b/news/_posts/2021-07-27-a11y.md index c4c9770d3402c4..33775858501b80 100644 --- a/news/_posts/2021-07-27-a11y.md +++ b/news/_posts/2021-07-27-a11y.md @@ -2,7 +2,7 @@ title: "Accessibility Improvements" contributions: authorship: [hexylena] -tags: [new feature] +tags: [new feature, contributing] layout: news --- diff --git a/news/_posts/2021-07-28-feedback.md b/news/_posts/2021-07-28-feedback.md index 90e3ca6f439e00..a5e752566d2062 100644 --- a/news/_posts/2021-07-28-feedback.md +++ b/news/_posts/2021-07-28-feedback.md @@ -2,7 +2,7 @@ title: "New Feature: a feedback page to aggregate and display feedback answers" contributions: authorship: [bebatut] -tags: [new feature, contributors] +tags: [new feature, contributing] cover: "news/images/2021-07-28-feedback.png" coveralt: "Screenshot of the top of the feedback page with the titles and 2 graphs: the cumulative number of feedback answers over month for all topics, and a barplot with number of answers for different scores" layout: news diff --git a/news/_posts/2021-09-24-jupyter.md b/news/_posts/2021-09-24-jupyter.md index 85dc0eb3f172e5..9dce74253a6b1d 100644 --- a/news/_posts/2021-09-24-jupyter.md +++ b/news/_posts/2021-09-24-jupyter.md @@ -2,7 +2,7 @@ title: "New Feature: Automatic Jupyter Notebooks" contributions: authorship: [hexylena] -tags: [gtn infrastructure, new feature] +tags: [gtn infrastructure, new feature, contributing] cover: topics/contributing/images/jupyter-notebook.png coveralt: Image comparing Markdown, GTN materials, and new Jupyter Notebook output layout: news diff --git a/news/_posts/2021-09-29-survey.md b/news/_posts/2021-09-29-survey.md index 0d6c80341ddd31..17594bcc255f9b 100644 --- a/news/_posts/2021-09-29-survey.md +++ b/news/_posts/2021-09-29-survey.md @@ -2,7 +2,7 @@ title: "Got a minute? Take our survey about Galaxy for training and have your say!" contributions: authorship: [bebatut] -tags: [feedback] +tags: [feedback, contributing] cover: "news/images/2021-09-29-survey.png" coveralt: "Adversitement for the survey. Written: Got a minute? Did you use Galaxy for training / teaching? Have your say... Take our survey. We appreciate your feedback. Logos: Galaxy Project and Galaxy Training Network" layout: news diff --git a/news/_posts/2021-10-12-data-science.md b/news/_posts/2021-10-12-data-science.md index 06ef58db9e6bb6..85417c75a1724d 100644 --- a/news/_posts/2021-10-12-data-science.md +++ b/news/_posts/2021-10-12-data-science.md @@ -3,7 +3,7 @@ title: "New Topic: Data Science Survival Kit" contributions: authorship: [shiltemann, hexylena, bebatut, abretaud, yvanlebras, fpsom, carpentries] funding: [gallantries] -tags: [gtn infrastructure, new topic] +tags: [gtn infrastructure, new topic, data-science] layout: news --- diff --git a/news/_posts/2021-10-12-speed.md b/news/_posts/2021-10-12-speed.md index db7d178d6fc6c5..592a4f19c96c23 100644 --- a/news/_posts/2021-10-12-speed.md +++ b/news/_posts/2021-10-12-speed.md @@ -1,6 +1,6 @@ --- title: "Attention Contributors: GTN Performance Enhancements" -tags: [authors, developers, performance] +tags: [authors, developers, performance, contributing] contributions: authorship: [hexylena] layout: news diff --git a/news/_posts/2021-11-10-api.md b/news/_posts/2021-11-10-api.md index 21c1be34670109..d058a4452a1cab 100644 --- a/news/_posts/2021-11-10-api.md +++ b/news/_posts/2021-11-10-api.md @@ -2,7 +2,7 @@ title: "New Feature: GTN API with OpenAPI 3 specification" contributions: authorship: [hexylena] -tags: [gtn infrastructure, new feature] +tags: [gtn infrastructure, new feature, contributing] cover: assets/images/swagger.png coveralt: Image showing an OpenAPI / Swagger UI to the training material API featuring several APIs like topics, tutorials, contributors, and a couple internal APIs. At the bottom are a couple of data models. layout: news diff --git a/news/_posts/2021-11-23-video-library.md b/news/_posts/2021-11-23-video-library.md index 518e81f575fd28..20ebb10dda1a97 100644 --- a/news/_posts/2021-11-23-video-library.md +++ b/news/_posts/2021-11-23-video-library.md @@ -3,7 +3,7 @@ title: "New Feature: GTN Video Library" contributions: authorship: [shiltemann, hexylena] infrastructure: [shiltemann, hexylena] -tags: [gtn infrastructure, new feature] +tags: [gtn infrastructure, new feature, contributing] cover: news/images/gtn-videolib-stats.png coveralt: Screenshot of the GTN Video Library Home page layout: news diff --git a/news/_posts/2021-12-01-FAIR.md b/news/_posts/2021-12-01-FAIR.md index 3baa34d5db4bd8..10f81328a03119 100644 --- a/news/_posts/2021-12-01-FAIR.md +++ b/news/_posts/2021-12-01-FAIR.md @@ -2,7 +2,7 @@ title: "New FAQs: How does the GTN stay FAIR and Collaborative" contributions: authorship: [hexylena] -tags: [faq] +tags: [faq, fair, contributing] layout: news --- diff --git a/news/_posts/2021-12-14-funding.md b/news/_posts/2021-12-14-funding.md index ebb0ab6e3d7048..f6210f0b449686 100644 --- a/news/_posts/2021-12-14-funding.md +++ b/news/_posts/2021-12-14-funding.md @@ -1,6 +1,6 @@ --- title: Support for annotating Funding Agencies -tags: [new-feature] +tags: [new-feature, contributing] contributions: authorship: - hexylena diff --git a/news/_posts/2021-12-14-smorgasbord.md b/news/_posts/2021-12-14-smorgasbord.md index 32605622ad33ff..07a11b6ec9d351 100644 --- a/news/_posts/2021-12-14-smorgasbord.md +++ b/news/_posts/2021-12-14-smorgasbord.md @@ -1,7 +1,7 @@ --- layout: news title: "GTN SmĂśrgĂĽsbord 2: Tapas Edition" -tags: [event] +tags: [event, contributing, teaching] link: "https://gallantries.github.io/posts/2021/12/14/smorgasbord2-tapas/?utm_source=gtn&utm_medium=news&utm_campaign=smorgasbord2" external: true cover: "https://gallantries.github.io/assets/images/smorgasbord2/banner-500.png" diff --git a/news/_posts/2022-01-28-rmarkdown.md b/news/_posts/2022-01-28-rmarkdown.md index c3d582ae3965f8..6d8ee9187f80b2 100644 --- a/news/_posts/2022-01-28-rmarkdown.md +++ b/news/_posts/2022-01-28-rmarkdown.md @@ -3,7 +3,7 @@ title: "New Feature: Automatic RMarkdown" contributions: authorship: [hexylena] funding: [gallantries, avans-atgm] -tags: [gtn infrastructure, new feature] +tags: [gtn infrastructure, new feature, contributing] cover: topics/data-science/images/rstudio/r-preview-output.png coveralt: Image showing content from a tutorial, rendered as an rmarkdown html via knitting. A table of contents appears on the left, and code and outputs on the right. layout: news diff --git a/news/_posts/2022-04-12-cyot.md b/news/_posts/2022-04-12-cyot.md index bd1ed6a5d708c5..ad0d823bad8529 100644 --- a/news/_posts/2022-04-12-cyot.md +++ b/news/_posts/2022-04-12-cyot.md @@ -3,7 +3,7 @@ title: "New Tutorial Feature: Choose Your Own Tutorial" contributions: authorship: [hexylena] funding: [gallantries] -tags: [gtn infrastructure, new feature, tutorial authors] +tags: [gtn infrastructure, new feature, tutorial authors, contributing] cover: assets/images/cyot.gif coveralt: Gif showing a user switching between two branches of a tutorial layout: news diff --git a/news/_posts/2022-05-20-schema.md b/news/_posts/2022-05-20-schema.md index baa2d6605a38cc..69a8740877c731 100644 --- a/news/_posts/2022-05-20-schema.md +++ b/news/_posts/2022-05-20-schema.md @@ -1,6 +1,6 @@ --- title: GTN Metadata Schemas -tags: [new feature] +tags: [new feature, contributing] contributions: authorship: [hexylena] layout: news diff --git a/news/_posts/2022-06-02-workflow-reports-tutorial.md b/news/_posts/2022-06-02-workflow-reports-tutorial.md index ea8d084b868d8b..05545febb2b26e 100644 --- a/news/_posts/2022-06-02-workflow-reports-tutorial.md +++ b/news/_posts/2022-06-02-workflow-reports-tutorial.md @@ -1,6 +1,6 @@ --- title: "New Tutorial: Workflow Reports" -tags: [new tutorial, Galaxy Tips & Tricks] +tags: [new tutorial, Galaxy Tips & Tricks, galaxy-interface] contributions: authorship: [shiltemann] cover: "topics/galaxy-interface/tutorials/workflow-reports/images/invocations-list.png" diff --git a/news/_posts/2022-09-11-data-manipulation-tutorial.md b/news/_posts/2022-09-11-data-manipulation-tutorial.md index 89494886ff6d03..963876bf8c5b5d 100644 --- a/news/_posts/2022-09-11-data-manipulation-tutorial.md +++ b/news/_posts/2022-09-11-data-manipulation-tutorial.md @@ -1,6 +1,6 @@ --- title: "New Tutorial: Data Manipulation" -tags: [new tutorial] +tags: [new tutorial, introduction] contributions: authorship: [shiltemann] cover: "topics/introduction/tutorials/data-manipulation-olympics/images/cover.jpg" diff --git a/news/_posts/2023-01-10-gtn-paper.md b/news/_posts/2023-01-10-gtn-paper.md index f06dae69098ae9..792ecf159001f3 100644 --- a/news/_posts/2023-01-10-gtn-paper.md +++ b/news/_posts/2023-01-10-gtn-paper.md @@ -1,6 +1,6 @@ --- title: 'New GTN paper: "Galaxy Training: A powerful framework for teaching!"' -tags: [paper] +tags: [paper, contributing] contributions: authorship: [bebatut,shiltemann,hexylena] cover: "news/images/2023-01-10-gtn-paper-fig2.png" diff --git a/news/_posts/2023-01-23-new-covid19-topic.md b/news/_posts/2023-01-23-new-covid19-topic.md index 023e96cad6f5da..5efe816dec31a9 100644 --- a/news/_posts/2023-01-23-new-covid19-topic.md +++ b/news/_posts/2023-01-23-new-covid19-topic.md @@ -1,6 +1,6 @@ --- title: "New GTN Feature Tag-based Topics enables new SARS-CoV-2 topic" -tags: [new topic,new feature] +tags: [new topic,new feature, one-health] contributions: authorship: [hexylena,wm75] layout: news diff --git a/news/_posts/2023-01-24-prometheus.md b/news/_posts/2023-01-24-prometheus.md index f851e4f1968edf..d80714663c540f 100644 --- a/news/_posts/2023-01-24-prometheus.md +++ b/news/_posts/2023-01-24-prometheus.md @@ -2,7 +2,7 @@ title: "New Feature: Prometheus Metrics endpoint" contributions: authorship: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news --- diff --git a/news/_posts/2023-01-26-pathogen-detection-tutorial.md b/news/_posts/2023-01-26-pathogen-detection-tutorial.md index b2b984873dba13..49d76e95a78b81 100644 --- a/news/_posts/2023-01-26-pathogen-detection-tutorial.md +++ b/news/_posts/2023-01-26-pathogen-detection-tutorial.md @@ -1,6 +1,6 @@ --- title: "New Tutorial: Pathogen detection from (direct Nanopore) sequencing data using Galaxy - Foodborne Edition" -tags: [new tutorial] +tags: [new tutorial, microbiome] contributions: authorship: [bebatut, EngyNasr] cover: "topics/microbiome/tutorials/pathogen-detection-from-nanopore-foodborne-data/images/FoodBorne-Workflow-updated.png" diff --git a/news/_posts/2023-02-02-black-history-month.md b/news/_posts/2023-02-02-black-history-month.md index 1e83cc681d3355..87425ea5313e06 100644 --- a/news/_posts/2023-02-02-black-history-month.md +++ b/news/_posts/2023-02-02-black-history-month.md @@ -1,6 +1,6 @@ --- title: "GTN Celebrates Black History Month" -tags: [] +tags: [contributing] contributions: authorship: [hexylena] layout: news diff --git a/news/_posts/2023-02-08-top-tools.md b/news/_posts/2023-02-08-top-tools.md index 89e3c988eb7fac..75f4be4190a248 100644 --- a/news/_posts/2023-02-08-top-tools.md +++ b/news/_posts/2023-02-08-top-tools.md @@ -1,6 +1,6 @@ --- title: What are the most used tools in the GTN? -tags: [new feature, gtn] +tags: [new feature, gtn, contributing, community] contributions: authorship: - hexylena diff --git a/news/_posts/2023-03-21-galaxy-trainer-directory.md b/news/_posts/2023-03-21-galaxy-trainer-directory.md index c322f1f82225e0..8072fb3bf6c419 100644 --- a/news/_posts/2023-03-21-galaxy-trainer-directory.md +++ b/news/_posts/2023-03-21-galaxy-trainer-directory.md @@ -1,6 +1,6 @@ --- title: "New Feature: Trainer Directory! (Add yourself today!)" -tags: [new feature, community building, capacity building] +tags: [new feature, community building, capacity building, contributing] contributions: authorship: [hexylena, lldelisle] funding: [gallantries] diff --git a/news/_posts/2023-04-13-learning-pathways.md b/news/_posts/2023-04-13-learning-pathways.md index c2f0417e1fdb41..6973ff2c080c5f 100644 --- a/news/_posts/2023-04-13-learning-pathways.md +++ b/news/_posts/2023-04-13-learning-pathways.md @@ -1,6 +1,6 @@ --- title: "New Feature: Learning Pathways!" -tags: [new feature] +tags: [new feature, contributing] contributions: authorship: [shiltemann, hexylena] funding: [gallantries] diff --git a/news/_posts/2023-04-19-click-to-run.md b/news/_posts/2023-04-19-click-to-run.md index 80fae0cfb2f031..0220a93e650278 100644 --- a/news/_posts/2023-04-19-click-to-run.md +++ b/news/_posts/2023-04-19-click-to-run.md @@ -2,7 +2,7 @@ title: "New Feature: Click-to-run Workflows" contributions: authorship: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news tutorial: topics/microbiome/tutorials/mothur-miseq-sop-short/workflows/ --- diff --git a/news/_posts/2023-04-19-gtn-rdoc.md b/news/_posts/2023-04-19-gtn-rdoc.md index c1c70266db4e5e..c26128ad0380d8 100644 --- a/news/_posts/2023-04-19-gtn-rdoc.md +++ b/news/_posts/2023-04-19-gtn-rdoc.md @@ -2,7 +2,7 @@ title: "New Feature: GTN Rdoc" contributions: authorship: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news --- diff --git a/news/_posts/2023-04-19-shortlinks.md b/news/_posts/2023-04-19-shortlinks.md index a7ae099c7a96b5..30bd6e8a1b8ef3 100644 --- a/news/_posts/2023-04-19-shortlinks.md +++ b/news/_posts/2023-04-19-shortlinks.md @@ -2,7 +2,7 @@ title: "New Feature: Persistent URLs (PURLs) / Shortlinks" contributions: authorship: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news cover: "assets/images/purl.png" coveralt: the bottom left corner of an overview box from a training material is shown, a label short link clearly has a short link as described in the tutorial diff --git a/news/_posts/2023-04-20-my-galaxy-training.md b/news/_posts/2023-04-20-my-galaxy-training.md index 30eb49dcb66e89..35f0470dd33b08 100644 --- a/news/_posts/2023-04-20-my-galaxy-training.md +++ b/news/_posts/2023-04-20-my-galaxy-training.md @@ -2,7 +2,7 @@ title: "New Feature: my.galaxy.training" contributions: authorship: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news --- diff --git a/news/_posts/2023-05-11-ro-crate.md b/news/_posts/2023-05-11-ro-crate.md index 7e9cc98f2515ea..4a4214b8704604 100644 --- a/news/_posts/2023-05-11-ro-crate.md +++ b/news/_posts/2023-05-11-ro-crate.md @@ -1,6 +1,6 @@ --- title: "BY-COVID and RO-Crate collaboration brings new topic: FAIR Data, Workflows & More" -tags: [new topic,new feature] +tags: [new topic,new feature, fair] contributions: authorship: [simleo, pauldg, stain, ilveroluca, kikkomep] editing: [hexylena] @@ -38,7 +38,7 @@ These tutorials are meant to help you learn how to use and create RO-Crates in y ## Where can I learn more? -These tutorials will be taught at [SmĂśrgĂĽsbord 3](https://gallantries.github.io/video-library/events/smorgasbord3/) so +These tutorials will be taught at [SmĂśrgĂĽsbord 3](https://gallantries.github.io/video-library/events/smorgasbord3/) so if you want to learn more, [go sign up now!](https://gxy.io/smorgasbord3-register) ## I want more FAIR Training! diff --git a/news/_posts/2023-05-15-isoform-usage-training.md b/news/_posts/2023-05-15-isoform-usage-training.md index 1b3ab4e0f53160..9024524d10e1a8 100644 --- a/news/_posts/2023-05-15-isoform-usage-training.md +++ b/news/_posts/2023-05-15-isoform-usage-training.md @@ -6,7 +6,7 @@ contributions: cover: news/images/isoform_usage_post.jpg coveralt: "Schematic of an isoform switch and detection pipeline. Data is annotated and a prediction is made for isoform switch consequences" -tags: [new tutorial] +tags: [new tutorial, transcriptomics] tutorial: topics/transcriptomics/tutorials/differential-isoform-expression/tutorial.html --- diff --git a/news/_posts/2023-06-01-hart.md b/news/_posts/2023-06-01-hart.md index 81084d2140b7e6..9d4916fe617505 100644 --- a/news/_posts/2023-06-01-hart.md +++ b/news/_posts/2023-06-01-hart.md @@ -1,6 +1,6 @@ --- title: "GTN Celebrates Pride Month: Alan Hart & M. Tuberculosis" -tags: [] +tags: [contributing] contributions: authorship: [hexylena] layout: news diff --git a/news/_posts/2023-06-07-pan-galactic-search.md b/news/_posts/2023-06-07-pan-galactic-search.md index 117933346388a9..4d5298f02d3b84 100644 --- a/news/_posts/2023-06-07-pan-galactic-search.md +++ b/news/_posts/2023-06-07-pan-galactic-search.md @@ -2,7 +2,7 @@ title: "New Feature: Pan-Galactic Tool Search" contributions: authorship: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news abbreviations: SEO: Search Engine Optimisation diff --git a/news/_posts/2023-06-21-fair.md b/news/_posts/2023-06-21-fair.md index 865415774abcf1..97795122c0c325 100644 --- a/news/_posts/2023-06-21-fair.md +++ b/news/_posts/2023-06-21-fair.md @@ -1,6 +1,6 @@ --- title: "ELIXIR-UK Fellow Launches New FAIR Data Management Training" -tags: [new topic,new tutorial] +tags: [new topic,new tutorial, fair] contributions: authorship: [kkamieniecka, poterlowicz-lab] funding: [elixir-fair-data] diff --git a/news/_posts/2023-06-27-ai4life.md b/news/_posts/2023-06-27-ai4life.md index dd08d2f43a286a..4b8f520ea501be 100644 --- a/news/_posts/2023-06-27-ai4life.md +++ b/news/_posts/2023-06-27-ai4life.md @@ -1,7 +1,7 @@ --- layout: news title: AI4Life teams up with GTN to enhance training resources -tags: [new topic,new community] +tags: [new topic,new community, imaging] link: "https://ai4life.eurobioimaging.eu/ai4life-teams-up-with-galaxy-training-network-gtn-to-enhance-training-resources/" external: true contributions: diff --git a/news/_posts/2023-07-20-prefs.md b/news/_posts/2023-07-20-prefs.md index f9955c209c6f7d..63aab2234fc0d9 100644 --- a/news/_posts/2023-07-20-prefs.md +++ b/news/_posts/2023-07-20-prefs.md @@ -3,7 +3,7 @@ title: "New Feature: GTN User Preferences" contributions: authorship: [hexylena] infrastructure: [hexylena] -tags: [new feature, gtn] +tags: [new feature, gtn, contributing] layout: news --- diff --git a/news/_posts/2023-10-05-wendi.md b/news/_posts/2023-10-05-wendi.md index c8c7bab44cec22..14dce305b2bafa 100644 --- a/news/_posts/2023-10-05-wendi.md +++ b/news/_posts/2023-10-05-wendi.md @@ -3,7 +3,7 @@ title: "New Feature: Embeddable GTN Tutorial Lists and UseGalaxy Workflow List W contributions: authorship: [hexylena] testing: [nomadscientist] -tags: [new feature, gtn] +tags: [new feature, gtn, contrributing, community] layout: news --- diff --git a/news/_posts/2023-10-12-sc_subdomain.md b/news/_posts/2023-10-12-sc_subdomain.md index cf17e9f2aa2d58..b8485b3db7ea5d 100644 --- a/news/_posts/2023-10-12-sc_subdomain.md +++ b/news/_posts/2023-10-12-sc_subdomain.md @@ -3,7 +3,7 @@ title: "Single cell subdomain re-launch: Unified and feedback-driven" contributions: authorship: [nomadscientist, pavanvidem] infrastructure: [kysrpex] -tags: [gtn, singlecell] +tags: [gtn, single-cell] cover: "news/images/2023-10-12-singlecell_subdomain.png" coveralt: "screenshot of the Single Cell Omics subdomain page, with purple mastheader, Single Cell Tools categories along the side, and an updated logo combining the Single Cell Omics connected cells image with the Human Cell Atlas blue embyro logo" layout: news diff --git a/news/_posts/2023-11-20-workflow-search.md b/news/_posts/2023-11-20-workflow-search.md index 6236678c1cb977..436dbc0bd4a54d 100644 --- a/news/_posts/2023-11-20-workflow-search.md +++ b/news/_posts/2023-11-20-workflow-search.md @@ -3,7 +3,7 @@ title: "Update: Workflow List now searches WorkflowHub.eu, advanced query syntax contributions: authorship: [hexylena] testing: [paulzierep, wm75] -tags: [feature update, gtn] +tags: [feature update, gtn, contributing] layout: news --- diff --git a/news/_posts/2023-11-21-mastodon.md b/news/_posts/2023-11-21-mastodon.md index a3f6edf743eac0..8927834951cf42 100644 --- a/news/_posts/2023-11-21-mastodon.md +++ b/news/_posts/2023-11-21-mastodon.md @@ -2,7 +2,7 @@ title: "GTN has left Twitter/X as of October" contributions: authorship: [hexylena] -tags: [gtn, communications] +tags: [gtn, communications, contributing] layout: news --- diff --git a/news/_posts/2023-11-21-video.md b/news/_posts/2023-11-21-video.md index b33f8fde1bf61c..fdb52921e809f1 100644 --- a/news/_posts/2023-11-21-video.md +++ b/news/_posts/2023-11-21-video.md @@ -2,7 +2,7 @@ title: "Feedback: Easy slide recordings" contributions: authorship: [hexylena] -tags: [feedback, testimonial, gtn] +tags: [feedback, testimonial, gtn, contributing] layout: news --- diff --git a/news/_posts/2023-12-12-tutorial-run-wfh-ds.md b/news/_posts/2023-12-12-tutorial-run-wfh-ds.md index 7aae22ed6765b3..3849a50e2dda84 100644 --- a/news/_posts/2023-12-12-tutorial-run-wfh-ds.md +++ b/news/_posts/2023-12-12-tutorial-run-wfh-ds.md @@ -3,7 +3,7 @@ title: "Tutorial Feature: Easier launching of WorkflowHub & Dockstore Workflows" contributions: authorship: [hexylena] funding: [by-covid] -tags: [feature update, gtn, tutorials] +tags: [feature update, gtn, tutorials, contributing] layout: news --- diff --git a/news/_posts/2023-12-18-medchem-user.md b/news/_posts/2023-12-18-medchem-user.md index d1e41290764114..84a95a88fddca0 100644 --- a/news/_posts/2023-12-18-medchem-user.md +++ b/news/_posts/2023-12-18-medchem-user.md @@ -1,16 +1,16 @@ --- title: "User story: Where Galaxy meets medicinal chemistry" -tags: [computational chemistry,user story,gtn] +tags: [computational-chemistry,user story,gtn] contributions: authorship: [wee-snufkin] layout: news --- -As a medicinal chemistry student, I undertook a semester project on natural products isolation and derivatisation. One of the compounds my group was working on was piperine, extracted from black pepper. Inspired by literature findings, we found out that the derivatives of piperine can inhibit monoamine oxidase-B and thus can be possibly used in Parkinson’s disease. As a novelty element in our project, we came up with new structures of derivatives that could act as inhibitors. +As a medicinal chemistry student, I undertook a semester project on natural products isolation and derivatisation. One of the compounds my group was working on was piperine, extracted from black pepper. Inspired by literature findings, we found out that the derivatives of piperine can inhibit monoamine oxidase-B and thus can be possibly used in Parkinson’s disease. As a novelty element in our project, we came up with new structures of derivatives that could act as inhibitors. -Before synthesising any new molecules, it would make sense to check if they correctly dock into the active site. As an enthusiastic user and developer of Galaxy, the idea of using GTN came straight to my mind. Even though molecular docking was beyond the scope of our project, I knew that Galaxy and GTN give such an amazing opportunity to do it in the blink of an eye that I wouldn't be myself if I hadn't tried! Therefore, by using the [tutorial on molecular docking]({% link topics/computational-chemistry/tutorials/cheminformatics/tutorial.md %}), I identified the pocket, then I studied the binding of the approved drugs such as Safinamide and Zonisamide and finally I checked how our proposed molecule fits within the pocket. After that, we were ready to synthesise the derivative we came up with! +Before synthesising any new molecules, it would make sense to check if they correctly dock into the active site. As an enthusiastic user and developer of Galaxy, the idea of using GTN came straight to my mind. Even though molecular docking was beyond the scope of our project, I knew that Galaxy and GTN give such an amazing opportunity to do it in the blink of an eye that I wouldn't be myself if I hadn't tried! Therefore, by using the [tutorial on molecular docking]({% link topics/computational-chemistry/tutorials/cheminformatics/tutorial.md %}), I identified the pocket, then I studied the binding of the approved drugs such as Safinamide and Zonisamide and finally I checked how our proposed molecule fits within the pocket. After that, we were ready to synthesise the derivative we came up with! ![Picture of piperine derivative docking into MAO-B pocket.]({% link news/images/2023-12-18-medchem-user.jpg %} "Piperine derivative as a potential inhibitor of MAO-B") -This example shows how GTN and Galaxy can facilitate and accelerate the work of medicinal chemists before starting the synthesis. It is easy to follow the GTN tutorials, the results are reproducible, the datasets are stored in the history and can be easily shared with anyone on the team. +This example shows how GTN and Galaxy can facilitate and accelerate the work of medicinal chemists before starting the synthesis. It is easy to follow the GTN tutorials, the results are reproducible, the datasets are stored in the history and can be easily shared with anyone on the team. diff --git a/news/_posts/2023-12-20-matrix-bots.md b/news/_posts/2023-12-20-matrix-bots.md index 775f72cd3be01f..5c8227ac222b65 100644 --- a/news/_posts/2023-12-20-matrix-bots.md +++ b/news/_posts/2023-12-20-matrix-bots.md @@ -4,7 +4,7 @@ contributions: authorship: [mtekman] editing: [hexylena] infrastructure: [mtekman, hexylena] -tags: [gtn, communications] +tags: [gtn, communications, contributing] layout: news --- @@ -15,14 +15,14 @@ the non-Matrix side of the Galaxy-verse. ## Galaxy Help Posts The newest bot we've added looks through the [Galaxy Help](https://help.galaxyproject.org/) forum for topics in need of -answering, and forwards those to the humans in Matrix channels related to those topics! +answering, and forwards those to the humans in Matrix channels related to those topics! We are currently trialing this new integration to provide high quality human help with the Single Cell CoP. If it's successful, our first expansion will be to the admin channel. ## GTN Contributions Our existing GTN bot summarizes the contributions to the GTN each day and delivers the great news to the -[GTN Matrix Lobby](https://app.element.io/#/room/#Galaxy-Training-Network_Lobby:gitter.im), as well as single-cell specific updates just to the [Single Cell User Community](https://app.element.io/#/room/#!yuLoaCWKpFHkWPmVEO:gitter.im). +[GTN Matrix Lobby](https://app.element.io/#/room/#Galaxy-Training-Network_Lobby:gitter.im), as well as single-cell specific updates just to the [Single Cell User Community](https://app.element.io/#/room/#!yuLoaCWKpFHkWPmVEO:gitter.im). Would your community like to receive these daily GTN updates for your community? Reach out to us on [Matrix](https://app.element.io/#/room/#Galaxy-Training-Network_Lobby:gitter.im) or [GitHub](https://github.com/galaxyproject/training-material/issues/). Currently this integration is only available for Matrix but if it would be useful as an RSS feed please let us know. diff --git a/news/_posts/2024-01-17-sc-fair-data.md b/news/_posts/2024-01-17-sc-fair-data.md index 271ec9577d99bc..ecd617947dd020 100644 --- a/news/_posts/2024-01-17-sc-fair-data.md +++ b/news/_posts/2024-01-17-sc-fair-data.md @@ -1,6 +1,6 @@ --- title: "FAIR Data management in single cell analysis" -tags: [single cell,data management,data import,fair] +tags: [single-cell,data management,data import,fair] contributions: authorship: [wee-snufkin, hexhowells, nomadscientist] funding: [elixir-fair-data] @@ -9,7 +9,7 @@ layout: news # New single cell section: Changing data formats & preparing objects -Now a nightmare of switching between single cell datatypes or importing the data is gone! With the new section on Changing data formats & preparing objects, the users can now confidently jump into the analysis. Some of those tutorials were created in the framework of ELIXIR-UK: FAIR Data Stewardship training, which aims to improve Findable Accessible Interoperable Reusable (FAIR) data management in the life sciences. +Now a nightmare of switching between single cell datatypes or importing the data is gone! With the new section on Changing data formats & preparing objects, the users can now confidently jump into the analysis. Some of those tutorials were created in the framework of ELIXIR-UK: FAIR Data Stewardship training, which aims to improve Findable Accessible Interoperable Reusable (FAIR) data management in the life sciences. We have developed the following tutorials: - [Converting between common single cell data formats]({% link topics/single-cell/tutorials/scrna-data-ingest/tutorial.md %}) diff --git a/news/_posts/2024-01-29-simplified-gtn-news-submission-via-google-form.md b/news/_posts/2024-01-29-simplified-gtn-news-submission-via-google-form.md index 8e2165b3f028b5..4f4082dd1c797a 100644 --- a/news/_posts/2024-01-29-simplified-gtn-news-submission-via-google-form.md +++ b/news/_posts/2024-01-29-simplified-gtn-news-submission-via-google-form.md @@ -14,6 +14,8 @@ contributions: infrastructure: - hexylena link: https://forms.gle/TqGTr6y46wrJDri7A +tags: [contributing, community] + --- Based on user feedback of the difficulties of the GTN news submission process we have significantly simplified and removed the need for manually editing a YAML file by providing a quick and easy [Google Form](https://forms.gle/TqGTr6y46wrJDri7A) diff --git a/news/_posts/2024-01-30-oembed-testing.md b/news/_posts/2024-01-30-oembed-testing.md index f15c7b8b0b77e8..00143c70545398 100644 --- a/news/_posts/2024-01-30-oembed-testing.md +++ b/news/_posts/2024-01-30-oembed-testing.md @@ -15,6 +15,8 @@ contributions: - jennaj infrastructure: - hexylena + +tags: [contributing, community] --- Howdy teachers and support staff! We are exploring displaying GTN content directly in the Galaxy Help site, furthering the integration between the GTN and the Galaxy community infrastructure. diff --git a/news/_posts/2024-01-31-postmarketos.md b/news/_posts/2024-01-31-postmarketos.md index 17b47b97e50bad..04626eef318a1c 100644 --- a/news/_posts/2024-01-31-postmarketos.md +++ b/news/_posts/2024-01-31-postmarketos.md @@ -1,6 +1,6 @@ --- title: "🪐📲 Hosting Galaxy at the Edge: Directly in Your Pocket!" -tags: [fun, system administrators, developers, humour] +tags: [fun, system administrators, developers, humour, admin] contributions: authorship: [mtekman] editing: [hexylena] diff --git a/news/_posts/2024-02-29-new-gmod-topic.md b/news/_posts/2024-02-29-new-gmod-topic.md index 9bf7e3d6d3d6c6..bb3e05afe3c3da 100644 --- a/news/_posts/2024-02-29-new-gmod-topic.md +++ b/news/_posts/2024-02-29-new-gmod-topic.md @@ -1,6 +1,6 @@ --- title: "GTN ❤️ GMOD" -tags: [new topic, new feature, genome annotation] +tags: [new topic, new feature, genome-annotation, GMOD] contributions: authorship: [hexylena, abretaud] infrastructure: [hexylena] @@ -8,7 +8,7 @@ layout: news tutorial: topics/gmod/index.html --- -Building upon the work previously done for the [SARS-Cov-2 Topic](/training-material/news/2023/01/23/new-covid19-topic.html) we have further expanded the 'tag based topics' to support a new GMOD topic. +Building upon the work previously done for the [SARS-Cov-2 Topic](/training-material/news/2023/01/23/new-covid19-topic.html) we have further expanded the 'tag based topics' to support a new GMOD topic. # Generic Model Organism Database diff --git a/news/_posts/2024-03-18-url-persistence.md b/news/_posts/2024-03-18-url-persistence.md index b4546f5075104b..cc3c6657f567e0 100644 --- a/news/_posts/2024-03-18-url-persistence.md +++ b/news/_posts/2024-03-18-url-persistence.md @@ -4,6 +4,7 @@ layout: news tags: - gtn infrastructure - new feature +- contributing contributions: authorship: - hexylena diff --git a/news/_posts/2024-03-28-by-covid-pathways.md b/news/_posts/2024-03-28-by-covid-pathways.md index b9948715b921d6..5741bc09844c60 100644 --- a/news/_posts/2024-03-28-by-covid-pathways.md +++ b/news/_posts/2024-03-28-by-covid-pathways.md @@ -7,6 +7,9 @@ tags: - minerva - gtn infrastructure - new feature +- one-health +- transcriptomics + contributions: authorship: - hexylena diff --git a/news/_posts/2024-05-13-fair.md b/news/_posts/2024-05-13-fair.md index 21f2b5036e4da0..808733aa852ab5 100644 --- a/news/_posts/2024-05-13-fair.md +++ b/news/_posts/2024-05-13-fair.md @@ -3,6 +3,8 @@ title: Perfectly FAIR Training! layout: news tags: - gtn infrastructure +- fair +- contributing contributions: authorship: - hexylena diff --git a/news/_posts/2024-05-22-introducing-gtn-event-pages.md b/news/_posts/2024-05-22-introducing-gtn-event-pages.md index a86b13efdfa7c3..6f979ea9bbab76 100644 --- a/news/_posts/2024-05-22-introducing-gtn-event-pages.md +++ b/news/_posts/2024-05-22-introducing-gtn-event-pages.md @@ -6,6 +6,8 @@ tags: - gtn infrastructure - new feature - events +- contributing +- community contributions: authorship: - shiltemann diff --git a/news/_posts/2024-06-03-onedata.md b/news/_posts/2024-06-03-onedata.md index 6afca018129714..dbb31141009e99 100644 --- a/news/_posts/2024-06-03-onedata.md +++ b/news/_posts/2024-06-03-onedata.md @@ -6,7 +6,7 @@ contributions: funding: - eurosciencegateway - egi -tags: [esg-wp4, esg] +tags: [esg-wp4, esg, contributing] cover: news/images/galaxy-data-upload.png coveralt: screenshot of galaxy data upload interface showing a folder named GTN Training Data layout: news @@ -54,7 +54,7 @@ You can access all GTN training data using several methods: 4. **Onedata clients** — access the data using the public read-only access token and [Oneclient](https://www.onedata.org/#/home/documentation/21.02/user-guide/oneclient.html) (local POSIX mount) - or [OnedataFS](https://onedata.org/#/home/documentation/21.02/user-guide/onedatafs.html) + or [OnedataFS](https://onedata.org/#/home/documentation/21.02/user-guide/onedatafs.html) ([PyFilesystem](https://www.pyfilesystem.org/) interface), e.g.: ```bash diff --git a/news/_posts/2024-06-04-gtn-standards-rss.md b/news/_posts/2024-06-04-gtn-standards-rss.md index b12ce98489dc6f..d4cff6a45dd64c 100644 --- a/news/_posts/2024-06-04-gtn-standards-rss.md +++ b/news/_posts/2024-06-04-gtn-standards-rss.md @@ -4,6 +4,7 @@ layout: news tags: - gtn infrastructure +- contributing contributions: authorship: - hexylena diff --git a/news/_posts/2024-06-04-tool-form-integration.md b/news/_posts/2024-06-04-tool-form-integration.md index e7032f2956b137..6c04a3acc7d685 100644 --- a/news/_posts/2024-06-04-tool-form-integration.md +++ b/news/_posts/2024-06-04-tool-form-integration.md @@ -12,6 +12,7 @@ tags: - gtn - new feature - already-on-hub + - contributing abbreviations: GTN: Galaxy Training Network cover: news/images/2024-06-04-tool-form-integration.png diff --git a/news/_posts/2024-06-06-400-tutorials-milestone.md b/news/_posts/2024-06-06-400-tutorials-milestone.md index 1033bd7625e75a..11ffa9ee47d064 100644 --- a/news/_posts/2024-06-06-400-tutorials-milestone.md +++ b/news/_posts/2024-06-06-400-tutorials-milestone.md @@ -8,13 +8,15 @@ contributions: - shiltemann tags: - gtn + - contributing + - community abbreviations: GTN: Galaxy Training Network cover: news/images/2024-06-06-400-tutorials.png coveralt: 400 tutorials --- -The Galaxy Training Network (GTN) has reached an exciting milestone: **our 400th tutorial**! This achievement is a testament to the dedication and hard work of our community of educators, researchers, and developers over the last 9 years. +The Galaxy Training Network (GTN) has reached an exciting milestone: **our 400th tutorial**! This achievement is a testament to the dedication and hard work of our community of educators, researchers, and developers over the last 9 years. The GTN was established to provide comprehensive and accessible training materials for users of the [Galaxy](https://galaxyproject.org/), a widely-used, open-source platform that empowers researchers worldwide to conduct data analysis. Over the years, the network has massively grown, both in content and community engagement, reflecting the dynamic nature of scientific research and the continuous need for up-to-date training resources. diff --git a/news/_posts/2024-06-06-dada2-tutorial.md b/news/_posts/2024-06-06-dada2-tutorial.md index 583e44087d0d82..6b502f9ca2c02f 100644 --- a/news/_posts/2024-06-06-dada2-tutorial.md +++ b/news/_posts/2024-06-06-dada2-tutorial.md @@ -2,10 +2,11 @@ layout: news title: "New Galaxy training: Building an amplicon sequence variant (ASV) table from 16S data using DADA2" contributions: - authorship: + authorship: - bebatut -tags: +tags: - new tutorial +- microbiome tutorial: topics/microbiome/tutorials/dada-16S/tutorial.html --- diff --git a/news/_posts/2024-06-11-GalaxyTrainingAcademy-Call-Contribution.md b/news/_posts/2024-06-11-GalaxyTrainingAcademy-Call-Contribution.md index 67adb876662993..630aa58bd34359 100644 --- a/news/_posts/2024-06-11-GalaxyTrainingAcademy-Call-Contribution.md +++ b/news/_posts/2024-06-11-GalaxyTrainingAcademy-Call-Contribution.md @@ -3,6 +3,8 @@ title: "Open Call for Trainers for the Galaxy Training Academy" layout: news tags: - gtn + - teaching + - contributing contributions: authorship: - teresa-m diff --git a/news/_posts/2024-06-13-from-gtn-intern-to-tutorial-author-to-bioinformatician.md b/news/_posts/2024-06-13-from-gtn-intern-to-tutorial-author-to-bioinformatician.md index 73a6c7e752eafd..1f303fe5fc82b7 100644 --- a/news/_posts/2024-06-13-from-gtn-intern-to-tutorial-author-to-bioinformatician.md +++ b/news/_posts/2024-06-13-from-gtn-intern-to-tutorial-author-to-bioinformatician.md @@ -8,6 +8,7 @@ tags: - trajectory - user - contributor +- contributing from_google_form: true contributions: authorship: diff --git a/news/_posts/2024-06-13-phylogenetics-tutorial-takes-researchers-back-to-basics.md b/news/_posts/2024-06-13-phylogenetics-tutorial-takes-researchers-back-to-basics.md index 0b5f4e0d26286c..1b4860f80973cb 100644 --- a/news/_posts/2024-06-13-phylogenetics-tutorial-takes-researchers-back-to-basics.md +++ b/news/_posts/2024-06-13-phylogenetics-tutorial-takes-researchers-back-to-basics.md @@ -6,6 +6,7 @@ tags: - Tutorial - GTN - Australian BioCommons +- evolution from_google_form: true contributions: authorship: diff --git a/news/_posts/2024-06-14-gtn-video-library.md b/news/_posts/2024-06-14-gtn-video-library.md index 5b61af26f21c89..305b7b32ad17c0 100644 --- a/news/_posts/2024-06-14-gtn-video-library.md +++ b/news/_posts/2024-06-14-gtn-video-library.md @@ -3,6 +3,9 @@ title: "GTN Video Library 2.0: 107 hours of learning across 154 videos" layout: news tags: - gtn + - contributing + - teaching + - community cover: "assets/images/video-library.png" coveralt: Screenshot of the GTN Video Library showing a tutorial recording with a large youtube player and extensive metadata about who created the video (Natalie Kucher) and when, how long, etc. diff --git a/news/_posts/2024-07-08-4th-mycobamycobacterium-tuberculosis-complex-ngs-made-easy.md b/news/_posts/2024-07-08-4th-mycobamycobacterium-tuberculosis-complex-ngs-made-easy.md index a643895392fe4d..85f252067bbcb4 100644 --- a/news/_posts/2024-07-08-4th-mycobamycobacterium-tuberculosis-complex-ngs-made-easy.md +++ b/news/_posts/2024-07-08-4th-mycobamycobacterium-tuberculosis-complex-ngs-made-easy.md @@ -8,6 +8,8 @@ tags: - transmission - evolution - one-health +- microbiome + from_google_form: true contributions: authorship: diff --git a/news/_posts/2024-07-09-gcc-updates.md b/news/_posts/2024-07-09-gcc-updates.md index deab8fb1191f8a..25fb0e608dafa9 100644 --- a/news/_posts/2024-07-09-gcc-updates.md +++ b/news/_posts/2024-07-09-gcc-updates.md @@ -4,6 +4,7 @@ layout: news tags: - gcc - gtn infrastructure +- contributing contributions: authorship: - shiltemann diff --git a/news/_posts/2024-07-17-google-forms.md b/news/_posts/2024-07-17-google-forms.md index 80a35fb7b30214..f9073fdf0eade9 100644 --- a/news/_posts/2024-07-17-google-forms.md +++ b/news/_posts/2024-07-17-google-forms.md @@ -5,6 +5,7 @@ tags: - gtn infrastructure - new feature - automation +- contributing contributions: authorship: - hexylena diff --git a/news/_posts/2024-07-22-Galaxy-Administrator-Time-Burden-and-Technology-Usage.md b/news/_posts/2024-07-22-Galaxy-Administrator-Time-Burden-and-Technology-Usage.md index 68431340655106..5d2728b0c0c8ad 100644 --- a/news/_posts/2024-07-22-Galaxy-Administrator-Time-Burden-and-Technology-Usage.md +++ b/news/_posts/2024-07-22-Galaxy-Administrator-Time-Burden-and-Technology-Usage.md @@ -5,6 +5,7 @@ tags: - deploying - maintenance - survey +- admin contributions: authorship: - vladvisan @@ -21,5 +22,5 @@ tutorial: topics/admin/tutorials/poll-ssa/slides.html Have you wondered how difficult Galaxy is to run? How much time people must spend to run Galaxy? - In February 2024, we collected 9 responses from the [Galaxy Small Scale Admin group](https://galaxyproject.org/community/sig/small-scale-admins/). -- The questions cover various time burdens and technological choices. -- The report provides answers to prospective future admins' most common questions. \ No newline at end of file +- The questions cover various time burdens and technological choices. +- The report provides answers to prospective future admins' most common questions. diff --git a/news/_posts/2024-11-29-tracking-of-mitochondria-and-capturing-mitoflashes.md b/news/_posts/2024-11-29-tracking-of-mitochondria-and-capturing-mitoflashes.md index db83948cfc0b2e..6bdf2f45ffeeba 100644 --- a/news/_posts/2024-11-29-tracking-of-mitochondria-and-capturing-mitoflashes.md +++ b/news/_posts/2024-11-29-tracking-of-mitochondria-and-capturing-mitoflashes.md @@ -5,6 +5,7 @@ tags: - bioimaging - mitoflash - mitochondria +- imaging contributions: authorship: - dianichj diff --git a/news/_posts/2024-12-02-reviewing.md b/news/_posts/2024-12-02-reviewing.md index 2b7e67b5141a47..802a2094de10b6 100644 --- a/news/_posts/2024-12-02-reviewing.md +++ b/news/_posts/2024-12-02-reviewing.md @@ -5,6 +5,8 @@ tags: - gtn infrastructure - new feature - automation +- contributing +- community contributions: authorship: - hexylena @@ -21,4 +23,4 @@ We would like to recognise and thank all of the reviewers who have contributed t However given our extensive automation, the we took that one step further! The GTN has recently implemented a new automation that collects metadata about every pull request that is merged into the GTN. This metadata includes the reviewers of learning materials, so of course we can automatically annotate this on every single material within our codebase, leading to our updated headers including up to dozens of previously uncredited reviewers per tutorial. -Thank you all for your hard work and dedication to the GTN community! +Thank you all for your hard work and dedication to the GTN community! diff --git a/news/_posts/2024-12-17-gta-feedback.md b/news/_posts/2024-12-17-gta-feedback.md new file mode 100644 index 00000000000000..c1b0524b17fec4 --- /dev/null +++ b/news/_posts/2024-12-17-gta-feedback.md @@ -0,0 +1,17 @@ +--- +title: "Organizing GTA2025: Give us feedback and your availability!" +contributions: + authorship: [teresa-m] +tags: [gtn, event, teaching, contributing, community] +layout: news +cover: "events/images/galaxy-academy-logo.png" +coveralt: "A laptop displaying shapes resembling a statistical plot with a program from Galaxy Training Academy. Surrounding the laptop, there are DNA strands as well as a pen displayed." +--- + +# Give us feedback and your availability for the GTA 2025 + +As the end of the year is approaching rapidly, we are excited to announce: + +🎉We have decided to host the next Galaxy Training Academy in Spring 2025!🎉 + +The GTA is only possible through community effort, and we will need all of you again to help make the GTA a success. Therefore, we are asking you to fill out this [Form](https://docs.google.com/forms/d/e/1FAIpQLSf_DVEdf7n9MWJe7XLa3UTPGMNE9y-F8S-EHXRrZ-twNETYrg/viewform) by the 20th of December. Here, you can provide feedback on what to improve, your availability for potential GTA weeks, and how you would like to contribute. diff --git a/news/_posts/2024-12-18-spoc.md b/news/_posts/2024-12-18-spoc.md new file mode 100644 index 00000000000000..5166aad11bf274 --- /dev/null +++ b/news/_posts/2024-12-18-spoc.md @@ -0,0 +1,40 @@ +--- +title: "🖖🏾Galaxy SPOC Community: Year in Review" +contributions: + authorship: + - nomadscientist + - pavanvidem +tags: + - gtn + - communications + - single-cell +layout: news +cover: "news/images/spoc_images/2024_spoc.png" +coveralt: "A group of people standing along a staircase holding up the Vulcan salute for SPOC: 'Live long and prosper'. They look relatively happy. Taken at 2024 Galaxy Community Conference." +--- + +# 🚀 2024: A SPOC-tacular Year in Review 🌌 + +As we orbit the end of another stellar year, let’s engage warp drive and reflect on the out-of-this-world achievements of the [**Single-cell & sPatial Omics Community (SPOC 🖖)**](https://galaxyproject.org/community/sig/singlecell/). From launching new tools to charting new tutorials, SPOC’s journey through the Galaxy (platform) has been nothing short of cosmic! 🌠 + +## 🌟 The Mission: Empowering Single-Cell Scientists +This year, we continued to boldly go where no omics community has gone before, propelling the SPOC ecosystem beyond single-cell RNA sequencing. With new training materials, tools, and workflows, our community has bridged the gap between transcriptomics, multiomics, and even spatial data analysis, ensuring that researchers have the tools they need to explore biological frontiers. Think of it as assembling the ultimate research starship! 🚀 + +## 🌌 SPOC: A Nebula of Collaboration +Our [Write-a-thons]({% link events/2024-10-29-spoc-write-a-thon.md %}) this year were true cosmic collisions of brilliance, bringing together contributors to polish workflows and draft tutorials. We also unified SPOC’s subdomains into one hyper-efficient portal with our friends from Australia. The community’s fusion power—collaboration—continues to drive SPOC’s development at lightspeed, with the launch of our first [SPOC Collaboration Fest]({% link news/2024/12/06/spoc_cofest.md %}) that brought Galaxy and non-Galaxy single-cell scientists together to sustain our ever-expanding galaxy of omics science. The GTN mission control kept engineering stellar widgets for the SPOC galaxy, unveiling a shiny new constellation layout for our training page and advanced asteroid-sifting tools to navigate the meteoric flow of events and news from our community! + +## 🌠 Tools and Tutorials: A Star Cluster of Resources +In 2024, SPOC added **15 new tools and 6 new training resources** to the Galaxy Training Network (GTN)—a constellation that lights the way for researchers navigating the complexities of single-cell and spatial omics. The introduction of SnapATAC2 and Squidpy tools felt like finding life-supporting planets in uncharted galaxies. These additions - and our wider commitment to sustainability and updating materials - ensure our workflows are as sharp as a lightsaber. ✨🛠️ + +## 🚀 Research Data Management: Guiding Stars for FAIR Practices +Following the FAIR (Findable, Accessible, Interoperable, Reusable) data principles, SPOC emphasized **sustainability** by implementing best practices in data archiving and tool maintenance. We co-led a BioHackathon Europe project with our fellow Microbiome commanders aimed at effectively labelling our tools and building the single-cell toolshed category. From tutorials on history-sharing to archiving for training data preservation, we’re keeping our resources as eternal as starlight. 🌟 + +## 🌍 SPOC’s Global Universe +With SPOC’s unified platform, global events, rolling timezones, and sustainability prioritisation of user testing across servers, we’ve brought researchers from every corner of the planet into our orbit. Whether you’re beaming in from Europe, the Americas, or the Andromeda Galaxy (or maybe Australia), SPOC’s resources remain accessible, scalable, and universal. + +## 🌌 Looking to the Horizon +As SPOC’s starship accelerates toward 2025, we’ll continue to explore new omics territories and deepen our cosmic connections across the Galaxy. Thank you to every SPOC cadet, captain, and commander who contributed this year—you’ve made SPOC the brightest star in the bioinformatics universe! + +Keep reaching for the stars, SPOC-nauts! 🪐✨ + +![Chart showing: 7 new workflows, 2 new FAQs, 3 News items, 2 new Slide decks, 4 events, 4 new tutorials, 5 recordings, and 1 new learning pathway]({% link news/images/spoc_images/community_resources_2024.png %} "SPOC 2024: By the numbers") diff --git a/news/_posts/2024-12-19-community_page.md b/news/_posts/2024-12-19-community_page.md new file mode 100644 index 00000000000000..a3a7dc6dd8a9df --- /dev/null +++ b/news/_posts/2024-12-19-community_page.md @@ -0,0 +1,78 @@ +--- +title: "GTN's Gift for 2024" +contributions: + authorship: + - nomadscientist + - hexylena +tags: [gtn, single-cell, new feature, new tutorial, contributing, community] +layout: news +cover: "news/images/spoc_images/community_page.png" +coveralt: "four graphs showing increases over time: training materials including slides and tutorials; contributors; workflows; and other" +--- + +# Community Pages + +# 🎉 Ring in 2025 with the GTN’s Amazing New Feature! 🎉 + +We’re absolutely *thrilled* to introduce the fabulous **Community Page**—a glorious new addition that has us cheering! 🎊 + +Here’s why we’re so excited: this page **automatically** tracks and celebrates all the incredible resource contributions from our community over time. 🚀 Whether it’s new workflows, fresh training materials, or awesome contributors, everything is summed up for you, hassle-free! + +Gone are the days of frantically piecing together achievements to impress institutes or grant bodies. No more scratching your head at year’s end wondering, *“Wait... what *did* we accomplish?”* GTN gets it. 💡 We know trainers are superheroes juggling this alongside their full-time research or infrastructure roles. + +Now, with sleek live charts, we can *see* how our community shines 🌟—in real time! It’s a joyful reflection of all the hard work, dedication, and collaboration that make this community so special. + +So here’s a huge **THANK YOU** to the GTN team for this spectacular gift! 🥳 You’ve truly embodied the spirit of user-centered design—not just for the scientists using Galaxy, but for those of us building it, too. 💖 + +Here’s to an exciting 2024 full of growth, achievements, and community magic! 🎆 + +#GratefulForGTN + +You can find this and other useful features in a [new tutorial collecting community resources]( {% link topics/community/tutorials/community_content/tutorial.md %} ). + +## Find your community's home page + +By scrolling down on the GTN topic page, to the new **Community Resources** section, and clicking on the **Community Home** button. + +Or jump straight to your comunity home with one of the buttons below! + +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/introduction/community.md %}">Introduction to Galaxy Analyses</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/galaxy-interface/community.md %}">Using Galaxy & Managing your Data </a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/admin/community.md %}">Galaxy Administration</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/ai4life/community.md %}">AI4Life</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/assembly/community.md %}">Assembly</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/climate/community.md %}">Climate</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/community/community.md %}">Community</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/computational-chemistry/community.md %}">Computational Chemistry</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/contributing/community.md %}">Contributing</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/data-science/community.md %}">Data Science</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/dev/community.md %}">Development</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/ecology/community.md %}">Ecology</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/epigenetics/community.md %}">Epigenetics</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/evolution/community.md %}">Evolution</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/fair/community.md %}">FAIR</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/genome-annotation/community.md %}">Genome Annotation</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/imaging/community.md %}">Imaging</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/materials-science/community.md %}">Materials Science</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/metabolomics/community.md %}">Metabolomics</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/microbiome/community.md %}">Microbiome</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/proteomics/community.md %}">Proteomics</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/sequence-analysis/community.md %}">Sequence Analysis</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/single-cell/community.md %}">Single Cell</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/statistics/community.md %}">Statistics & Machine Learning</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/synthetic-biology/community.md %}">Synthetic Biology</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/teaching/community.md %}">Teaching</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/transcriptomics/community.md %}">Transcriptomics</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/variant-analysis/community.md %}">Variant Analysis</a> +<a style="margin:0.25em" class="btn btn-secondary" href="{% link topics/visualisation/community.md %}">Visualisation</a> + + +Finally, you can see an example from the Single-cell topic below: + +<iframe + src="/training-material/topics/single-cell/community.html" + width="100%" + height="800" + style="border: none;" + title="Single-Cell Topic Community Page"> +</iframe> diff --git a/news/images/spoc_images/2024_spoc.png b/news/images/spoc_images/2024_spoc.png new file mode 100644 index 00000000000000..116bbabf3b36f0 Binary files /dev/null and b/news/images/spoc_images/2024_spoc.png differ diff --git a/news/images/spoc_images/2024_spoc_pretty.png b/news/images/spoc_images/2024_spoc_pretty.png new file mode 100644 index 00000000000000..5523084ff75c1d Binary files /dev/null and b/news/images/spoc_images/2024_spoc_pretty.png differ diff --git a/news/images/spoc_images/community_page.png b/news/images/spoc_images/community_page.png new file mode 100644 index 00000000000000..13a6779c09aae3 Binary files /dev/null and b/news/images/spoc_images/community_page.png differ diff --git a/news/images/spoc_images/community_resources_2024.png b/news/images/spoc_images/community_resources_2024.png new file mode 100644 index 00000000000000..a06516b51f5e8d Binary files /dev/null and b/news/images/spoc_images/community_resources_2024.png differ diff --git a/shared/images/DFG_grant.png b/shared/images/DFG_grant.png new file mode 100644 index 00000000000000..6e013543e3e322 Binary files /dev/null and b/shared/images/DFG_grant.png differ diff --git a/shared/images/surf.png b/shared/images/surf.png new file mode 100644 index 00000000000000..9cd22f07f97db6 Binary files /dev/null and b/shared/images/surf.png differ diff --git a/shared/images/surf.svg b/shared/images/surf.svg new file mode 100644 index 00000000000000..0e42533fa7c5e9 --- /dev/null +++ b/shared/images/surf.svg @@ -0,0 +1 @@ +<svg version="1.1" id="Laag_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="100" height="52" viewBox="0 0 100 52" xml:space="preserve"><path id="a_1_" d="M92.4 31.4c4.2 0 7.6 3.5 7.6 7.8v5c0 4.3-3.4 7.8-7.6 7.8H80.9c-4.2 0-7.6-3.5-7.6-7.8v-3.1c0-5.4-4.3-9.7-9.5-9.7H9.5c-5.3 0-9.5-4.3-9.5-9.7v-12C0 4.3 4.3 0 9.5 0h54.3c5.3 0 9.5 4.3 9.5 9.7v12c0 5.4 4.3 9.7 9.5 9.7h9.6z"/><path d="M60.6 17.8c1 0 1.5-.5 1.5-1.5s-.5-1.6-1.5-1.6h-3.8v-3h6c1 0 1.5-.5 1.5-1.6 0-1-.5-1.5-1.5-1.5h-7.6c-1 0-1.6.5-1.6 1.6v11c0 1.1.5 1.6 1.6 1.6 1 0 1.6-.5 1.6-1.6v-3.5c0 .1 3.8.1 3.8.1zm-12.5-.3c1.4-.7 2.2-2.1 2.2-3.9 0-2.9-2.1-5-5.1-5h-4.5c-1 0-1.6.5-1.6 1.6v11c0 1.1.5 1.6 1.6 1.6 1 0 1.6-.5 1.6-1.6v-2.9H45l1.6 3.4c.3.7.7 1 1.3 1 .8 0 1.8-.6 1.8-1.5 0-.3-.1-.6-.2-.9l-1.4-2.8zm-3.3-2h-2.7v-3.8h2.7c1.2 0 2.2.6 2.2 1.9 0 1.3-1 1.9-2.2 1.9zm-12.7.8c0 2.2-1.3 3.5-3.1 3.5s-3.1-1.3-3.1-3.5v-6.2c0-1.1-.5-1.6-1.6-1.6-1 0-1.6.5-1.6 1.6v6.2c0 4.1 2.7 6.7 6.3 6.7 3.6 0 6.3-2.6 6.3-6.7v-6.2c0-1.1-.5-1.6-1.6-1.6-1 0-1.6.5-1.6 1.6v6.2zM14.2 20c-1.2 0-2.1-.3-2.7-.5-.5-.2-.9-.3-1.4-.3-.9 0-1.4.6-1.4 1.5 0 1.5 3 2.3 5.6 2.3 3.1 0 5.5-1.7 5.5-4.3 0-2.4-1.6-3.5-3.3-4.1l-2.6-.8c-1.1-.3-1.6-.6-1.6-1.3 0-.7 1.1-1.1 2-1.1 1.1 0 1.9.3 2.5.5.4.1.8.3 1.3.3.8 0 1.3-.6 1.3-1.5 0-1.5-2.7-2.3-5.1-2.3-3 0-5.2 1.7-5.2 4.2 0 2.1 1.5 3.3 3.1 3.8l2.3.7c1.2.4 2.1.7 2.1 1.4-.2 1-1.4 1.5-2.4 1.5z" fill="#fff"/></svg> \ No newline at end of file diff --git a/stats/top-tools.html b/stats/top-tools.html index 6da9f8186dd9f4..6c7e0935e99f94 100644 --- a/stats/top-tools.html +++ b/stats/top-tools.html @@ -13,6 +13,8 @@ </thead> <tbody> {% for group in groups %} + {% assign match = group[0] | matches: '{{' %} + {% unless match %} <tr> <td><a href="{{ site.baseurl }}/by-tool/{{group[0]}}.html">{{ group[0] }}</a></td> <td> @@ -28,6 +30,7 @@ </ul> </td> </tr> + {% endunless %} {% endfor %} </tbody> diff --git a/topics/admin/community.md b/topics/admin/community.md new file mode 100644 index 00000000000000..5207147d9d70ef --- /dev/null +++ b/topics/admin/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: admin +--- + diff --git a/topics/admin/maintainer.md b/topics/admin/maintainer.md new file mode 100644 index 00000000000000..5fb86125978c65 --- /dev/null +++ b/topics/admin/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: admin +--- + diff --git a/topics/admin/tutorials/beacon/tutorial.md b/topics/admin/tutorials/beacon/tutorial.md index 32819fb646d2c8..dd459ec5569b33 100644 --- a/topics/admin/tutorials/beacon/tutorial.md +++ b/topics/admin/tutorials/beacon/tutorial.md @@ -330,7 +330,7 @@ Now that our beacon is running, we need to get data from Galaxy to the Beacon > ```diff > --- a/templates/nginx/galaxy.j2 > +++ b/templates/nginx/galaxy.j2 -> @@ -115,4 +115,14 @@ server { +> @@ -117,4 +117,14 @@ server { > > {{ tiaas_nginx_routes }} > diff --git a/topics/admin/tutorials/cloudbursting/slides.html b/topics/admin/tutorials/cloudbursting/slides.html index 91fc1a718f7959..f5e06ea4a4df7d 100644 --- a/topics/admin/tutorials/cloudbursting/slides.html +++ b/topics/admin/tutorials/cloudbursting/slides.html @@ -3,9 +3,14 @@ logo: assets/images/gat.png title: "Galaxy on the Cloud" -contributors: +contributions: + authorship: - slugger70 + editing: + - hexylena subtopic: cloud + +priority: 0 --- # All these Clouds @@ -84,7 +89,7 @@ * Education grants * Academic: * OpenStack: open source community project - * NeCTAR in Australia, Jetstream in USA, CLIMB in UK, lots of others + * NeCTAR in Australia, Jetstream in USA, CLIMB in UK, SURF in Netherlands, lots of others * Some free for researchers (NeCTAR, CLIMB), some with project grants (Jetstream) ] @@ -129,7 +134,7 @@ * with Galaxy pre-installed * with different sets of tools installed * with access to reference data - * for different clouds (AWS globally, Jetstream, NeCTAR, CLIMB etc.) + * for different clouds (AWS globally, Jetstream, NeCTAR, CLIMB, SURF etc.) * You just need credentials for the cloud you want to "launch" on. * Credentials are generally strings * An access key and a secret key or username and password with project details @@ -282,6 +287,13 @@ <tool id="toolshed.g2.bx.psu.edu/repos/devteam/bwa/bwa/0.3.1" destination="slurm_4slots" /> ... ``` + +--- + +# CloudMan Galaxy + +* Configured for Slurm out of the box + ``` ini # COMPUTE NODES NodeName=master NodeAddr=45.113.232.91 CPUs=15 RealMemory=64431 Weight=10 State=UNKNOWN diff --git a/topics/admin/tutorials/connect-to-compute-cluster/tutorial.md b/topics/admin/tutorials/connect-to-compute-cluster/tutorial.md index 0113604f2c491a..97fcc0f08819d4 100644 --- a/topics/admin/tutorials/connect-to-compute-cluster/tutorial.md +++ b/topics/admin/tutorials/connect-to-compute-cluster/tutorial.md @@ -148,7 +148,7 @@ be taken into consideration when choosing where to run jobs and what parameters > @@ -194,6 +194,16 @@ nginx_ssl_role: usegalaxy_eu.certbot > nginx_conf_ssl_certificate: /etc/ssl/certs/fullchain.pem > nginx_conf_ssl_certificate_key: /etc/ssl/user/privkey-www-data.pem -> +> > +# Slurm > +slurm_roles: ['controller', 'exec'] # Which roles should the machine play? exec are execution hosts. > +slurm_nodes: diff --git a/topics/admin/tutorials/k8s-managing-galaxy/tutorial.md b/topics/admin/tutorials/k8s-managing-galaxy/tutorial.md index 600af2747835ea..8a42c3155212d9 100644 --- a/topics/admin/tutorials/k8s-managing-galaxy/tutorial.md +++ b/topics/admin/tutorials/k8s-managing-galaxy/tutorial.md @@ -32,7 +32,7 @@ requirements: topic_name: admin tutorials: - k8s-deploying-galaxy -priority: 1 +priority: 3 --- # Managing Galaxy on Kubernetes diff --git a/topics/admin/tutorials/monitoring/tutorial.md b/topics/admin/tutorials/monitoring/tutorial.md index 785260c51044bc..4aac6dd61f1fcf 100644 --- a/topics/admin/tutorials/monitoring/tutorial.md +++ b/topics/admin/tutorials/monitoring/tutorial.md @@ -535,15 +535,15 @@ There are some nice examples of dashboards available from the public Galaxies, w > ```diff > --- a/templates/nginx/galaxy.j2 > +++ b/templates/nginx/galaxy.j2 -> @@ -108,4 +108,9 @@ server { -> proxy_pass http://{{ galaxy_config.gravity.reports.bind }}:/; +> @@ -109,4 +109,9 @@ server { +> proxy_set_header X-Forwarded-Host $host; +> proxy_set_header X-Forwarded-Proto $scheme; > } -> +> + > + location /grafana/ { > + proxy_pass http://127.0.0.1:3000/; > + proxy_set_header Host $http_host; > + } -> + > } > {% endraw %} > ``` diff --git a/topics/admin/tutorials/reports/tutorial.md b/topics/admin/tutorials/reports/tutorial.md index 006cd7c4e211f4..9d100f08556199 100644 --- a/topics/admin/tutorials/reports/tutorial.md +++ b/topics/admin/tutorials/reports/tutorial.md @@ -32,17 +32,6 @@ requirements: The reports application gives some pre-configured analytics screens. These are very easy to setup and can help with debugging issues in Galaxy. -> <warning-title>Currently Broken, Requires Separate Domain</warning-title> -> Reports does not work, under a path prefix (the default setup that most -> people will use.) It is completely broken and the developers have no plans to fix it in the near term. -> See -> [galaxyproject/galaxy#15966](https://github.com/galaxyproject/galaxy/issues/15966) for more details. -> -> However, it should still function with a separate domain, if that is possible -> for your setup. Otherwise, it **will not work.** If you wish to follow this -> tutorial, please be aware of this. -{: .warning} - > <agenda-title></agenda-title> > > 1. TOC @@ -114,15 +103,16 @@ The reports application is included with the Galaxy codebase and this tutorial a > ```diff > --- a/templates/nginx/galaxy.j2 > +++ b/templates/nginx/galaxy.j2 -> @@ -103,4 +103,9 @@ server { +> @@ -103,4 +103,10 @@ server { > proxy_set_header Upgrade $http_upgrade; > proxy_set_header Connection "upgrade"; > } > + > + location /reports/ { > + proxy_pass http://{{ galaxy_config.gravity.reports.bind }}:/; +> + proxy_set_header X-Forwarded-Host $host; +> + proxy_set_header X-Forwarded-Proto $scheme; > + } -> + > } > {% endraw %} > ``` diff --git a/topics/admin/tutorials/subdomain/tutorial.md b/topics/admin/tutorials/subdomain/tutorial.md index 5f99dc42c31f2c..25d342aeafab50 100644 --- a/topics/admin/tutorials/subdomain/tutorial.md +++ b/topics/admin/tutorials/subdomain/tutorial.md @@ -88,7 +88,7 @@ This tutorial covers how to set up a subdomain on usegalaxy.eu. We will take her > ![Image of the modification to make in the global_host_filters.py.j2 file](../../images/create_subdomain/add_gxit.png) > > - Second, on the same fork go to **files/traefik/rules/** -> - Open and edit **template-subdomains.yml**, there you need to add the folowing line for your subdomain `{{template "subdomain" "my_new_and_shiny_subdomain"}}` +> - Open and edit **template-subdomains.yml**, there you need to add the folowing line for your subdomain `{% raw %}{{template "subdomain" "my_new_and_shiny_subdomain"}}{% endraw %}` > > - Finally, commit all your changes and write a nice message for the admin when you open your Pull Request. {: .hands_on} diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/1.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/1.png new file mode 100644 index 00000000000000..5b8660eb23c5ce Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/1.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/10.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/10.png new file mode 100644 index 00000000000000..9a25d25ce2db92 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/10.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/11.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/11.png new file mode 100644 index 00000000000000..c1ba50c6052a1d Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/11.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/12.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/12.png new file mode 100644 index 00000000000000..849e7aece3efc6 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/12.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/13.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/13.png new file mode 100644 index 00000000000000..b1a9f3682e6dca Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/13.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/14.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/14.png new file mode 100644 index 00000000000000..512770cad21381 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/14.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/15.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/15.png new file mode 100644 index 00000000000000..b3c14f957c5cc5 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/15.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/16.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/16.png new file mode 100644 index 00000000000000..54986d7c34e0b7 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/16.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/2.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/2.png new file mode 100644 index 00000000000000..3750074009c0ab Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/2.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/3.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/3.png new file mode 100644 index 00000000000000..81061629bd7e8a Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/3.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/4.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/4.png new file mode 100644 index 00000000000000..31eef705e20675 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/4.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/5.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/5.png new file mode 100644 index 00000000000000..ad5cf7c84d1ea0 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/5.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/6.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/6.png new file mode 100644 index 00000000000000..6f2cbc0c798d90 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/6.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/7.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/7.png new file mode 100644 index 00000000000000..ca200b5ec007c3 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/7.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/8.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/8.png new file mode 100644 index 00000000000000..6f6aa1bf94c219 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/8.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/images/9.png b/topics/admin/tutorials/surf-research-cloud-galaxy/images/9.png new file mode 100644 index 00000000000000..7da45ba9b072d4 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-galaxy/images/9.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-galaxy/tutorial.md b/topics/admin/tutorials/surf-research-cloud-galaxy/tutorial.md new file mode 100644 index 00000000000000..9ea5b1e4582a6f --- /dev/null +++ b/topics/admin/tutorials/surf-research-cloud-galaxy/tutorial.md @@ -0,0 +1,192 @@ +--- +layout: tutorial_hands_on + +title: "Galaxy usage on SURF Research Cloud" +zenodo_link: "" +questions: + - How do I start a Galaxy instance on SURF Research Cloud? + - How do I link to external storage from my Galaxy workspace? + - How do I install tools? +objectives: + - Understand how to work with a Galaxy on-demand instance in SURF Research Cloud +requirements: + - type: "none" + title: Access to the SURF Research Cloud + - type: "none" + title: SRAM authentication + - type: "none" + title: An SSH key connected to your account + - type: "none" + title: Have a basic understanding of how Galaxy works +time_estimation: "30m" +level: Introductory +key_points: + - With SRC you can start your own Galaxy on-demand instance in a secure environment + - You can log in and use Galaxy via the SRC with your university credentials +contributions: + authorship: + - mirelaminkova + - hexylena + - dometto + funding: + - surf + +priority: 10 + +edam_ontology: +- topic_0605 # Informatics +- topic_3071 # Data Management + +abbreviations: + SRC: SURF Research Cloud + GDPR: General Data Protection Regulations + SRAM: SURF Research Access Management + CO: Collaborative Organisation + +follow_up_training: + - type: "internal" + topic_name: admin + tutorials: + - surf-research-cloud-pulsar + +subtopic: cloud +tags: + - deploying + +--- + +Using Galaxy via the {SRC} allows researchers to start Galaxy instances on-demand and analyze their data in a secure environment following the {GDPR}. The instance provides secure authentication, where users must have a SURF Research account prior to this tutorial, have set the {SRAM} authentication method, and connect an SSH key to their accounts. In case you are not familiar with {SRC} and need help in setting up your accounts, please follow the instructions on the [SURF Knowledge Base](https://servicedesk.surf.nl/wiki/display/WIKI/SURF+Research+Cloud) + +In this training the aim is to focus on: + +- Understanding how to start Galaxy on {SRC} +- Attaching external volumes for storage + +Galaxy instances can be started and stopped on demand, depending on personal cases and requirements. Inside the SRC members should have access to all publicly available catalog items. If you are not able to create a catalog item, please [contact SURF servicedesk](mailto:servicedesk@surf.nl). + +> <agenda-title></agenda-title> +> +> 1. TOC +> {:toc} +> +{: .agenda} + + +# Prerequisites + +This tutorial assumes you are member of a {CO} in {SRAM} that has access to {SRC} and a wallet with budget in SRC with enough sources to create Galaxy and Pulsar catalog items. (For more information please refer to the [SURF Knowledge Base](https://servicedesk.surf.nl/wiki/display/WIKI/Budgets%2C+wallets%2C+contracts). + +You must have previous experience working with data inside Galaxy. + +# Starting a Galaxy instance inside SRC step-by-step + +> <hands-on-title>Access the SRC</hands-on-title> +> 1. The first and most important step is to have access to the SURF Research Cloud. +> 2. You will need to login to the [portal](https://portal.live.surfresearchcloud.nl). +> ![login screen for SURF research cloud](./images/1.png) +> +> 3. Assuming you are a Dutch researcher, you should expect to see your educational institution listed on the authentication provider screen. +> ![login with screen, showing an authentication provider being chosen.](./images/2.png) +> 4. Log in with your institute credentials and follow the SRAM authentication on your phone. Inside the environment, you can see if there are any active virtual machines, the options to create a workspace, new storage or request new wallet for your projects. +> ![SRC dashboard, options like create new workspace, storage, or request a new wallet are available. one workspace is listed as running: galaxy test tim.](./images/3.png) +{: .hands_on} + +With this, you're ready to get started launching resources within {SRC}. + +## Create an external storage + +The purpose of the storage is to attach it to your workspace, where you will save your data. This way, if the machine is deleted, your data will not be lost. We strongly advise anyone using the SRC to create storages and attach them to their machines! + + +> <hands-on-title>Create a Storage Volume</hands-on-title> +> 1. Locate the "Create new storage" option +> ![top half of the SRC dashboard showing options like create new workspace, storage, or request a new wallet.](./images/4.png) +> 2. Click on "Create new". +> 3. Select the collaborative organisation you want the storage to be part of. +> 4. Select the desired cloud provider and the size of the volume you want to use. +> > <tip-title>Unsure of the size required?</tip-title> +> > If you are unsure about the size of the storage you need, contact your administrator. Consider also consulting [Galaxy from an Administrator's Point of View]({% link topics/admin/tutorials/introduction/slides.html %}#10) +> {: .tip} +> +> ![create your storage dashboard showing a selection of providers like surf and aws and azure, and then a selection of storage sizes below ranging from 5GB to 1.5TB](./images/5.png) +> +> 6. Name your storage however you like and press on submit. +> +> ![form to provide name and preview what will be created.](./images/6.png) +> +> 7. You will be redirected to the main page, where you will be able to track the status of the storage creation. +> +> ![bottom half of home page showing the Storage tab, and a resource named storage-galaxy in state creating.](./images/7.png) +{: .hands_on} + +Once the storage is deployed, you can attach it to any of your machines! 🎉 + +Be aware, that in order to attach an external storage to an existing machine, you would need to pause the workspace first! (If you are not sure how to do that please refer to [External storage volumes](https://servicedesk.surf.nl/wiki/display/WIKI/External+storage+volumes)) + +# Create a new workspace + +Depending on user’s rights, you can create a new workspace for your collaboration. + +> <tip-title>A Workspace?</tip-title> +> Workspaces may also be known as *Virtual Machines/VMs* or *Instances* in other cloud providers. +{: .tip} + +> <hands-on-title>Create a Workspace</hands-on-title> +> 1. On the right side under the "Workspaces" click on the "Add" button. +> +> ![dashboard of workspaces showing a single running workspace titled galaxy test tim, and on the right a button to add a new workspace.](./images/8.png) +> +> 2. Then, you will be redirected to a new page, where you must first choose the collaborative organisation in which you want to create your workspace (in case you are a member of multiple organisations). Once chosen, you a new page will be loaded, where you will have access to all available catalog items. +> +> ![Create your workspace screen showing 9 catalog items on the first page with pages more. Visible are Galaxy Pulsar node amongst some docker and ubuntu options.](./images/9.png) +> +> 3. Use the magnifying glass on the right side of the panel and search for Galaxy. Two catalog items will appear - the Galaxy instance designed for SURF and a Galaxy Pulsar node that can be connected to the instance. Select "Galaxy at SURF". +> +> ![similar to previous image, but the items described above](./images/10.png) +> +> 4. SURF Research Cloud allows researchers to host their catalog items on different cloud providers. The Galaxy catalog item is currently supported only on the HPC Cloud and for Ubuntu 22.04. On this page, users can select the number of cores and RAM that they want on their machine. More sizes can be added in the future, and on request. Choose wisely and in case you are not certain contact your administrator! +> +> ![workspace creation screen, choose the cloud provider is locked to SURF HPC, flavour is locked to 22.04, and the size options are available from 1GB/8CPU to 60C/750GB Hi-mem](./images/11.png) +> +> 5. Select the storage you have created earlier so it is attached to the new workspace. +> +> ![create your workspace screen, storage only lists one option named storage-galaxy](./images/12.png) +> +> 6. Lastly, before the workspace is deployed, you need to choose for how long the machine will run. +> +> > <tip-title>Expiration Date</tip-title> +> > The standard life-time of the VM is 5 days. If you need it for longer, this option can be changed once the machine is running. +> > Note, that once the machine is expired and deleted it cannot be restored! Plan accordingly and migrate your data in time to prevent data loss! +> > +> > This is an incredibly useful feature as it saves you from forgetting to destroy a VM. Especially for GPU nodes it can help you ensure that they disappear after your computation is complete. +> {:.tip} +> +> 7. In this form, you can also select how to name your workspace, add a description (if you want) and specify the hostname. Then, scroll down and submit the workspace creation. +> ![almost there, some final details reads the heading. an expiration date is set and galaxy at surf is provided in the name. the hostname is set to galaxyatsurf and the description is blank](./images/13.png) +{: .hands_on} + +With that, your workspace will start being created. + +![A galaxy instance named galaxy demo is in status creating.](./images/14.png "Creating the workspace can take some time. You will see on the main page the status of your workspace.") + +> <tip-title>Also launching Pulsar?</tip-title> +> While you are waiting, you can create your Galaxy Pulsar node simultaneously to save time. +{: .tip} + +Once the workspace is running, press on the down arrow to check any information such as the ID, the owner of the workspace, the collaboration it is part of, when it is created and when it expires and the wallet it is using. Additionally, you can pause the workspace when you are not using it. Pressing on the storage option will display the storage that has been attached to the workspace. + +![the details panel of the galaxy demo instance is shown including fields like an id, owner, date created and expiration date, url., etc](./images/15.png) + +To access the Galaxy instance, press on the "Access" button. This will redirect you to a new webpage. The {SRAM} login will be displayed again, and after authenticating, you will be redirected to the Galaxy at SURF Research Cloud page! + +![A galaxy instance homepage is shown, with a brand indicating that it was deployed on surf research cloud featuring a cute surfer emoji.](./images/16.png) + +Congratulations! You have started a Galaxy instance inside the SURF Research Cloud! 🏄‍♀️🎉 + +If you have administrator rights, you can download tools that can be used by the members of the collaborative organisation. To obtain administrator rights you need to be part of the `src_co_admin` group inside the collaborative organisation. In case you are not sure how to download tools as a Galaxy administrator, please refer to [Installing Tools into Galaxy](https://galaxyproject.org/admin/tools/add-tool-from-toolshed-tutorial/). + +## Going Further with Pulsar? + +Another important step when starting a local Galaxy instance on {SRC} is connecting it to a Galaxy Pulsar Node, which you can do [in this Pulsar SRC tutorial]({% link topics/admin/tutorials/surf-research-cloud-pulsar/tutorial.md %}). The Pulsar is an useful component that is used to execute your tasks on remote servers/clusters. In this case, the Pulsar is started as a new workspace, allowing users to also run interactive tools. + +In SRC, this can also enable you to save costs. By choosing a smaller Galaxy instance, and attaching a large Pulsar node when you need to do computations, you can be sure to optimise your budget expenditures. diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/id1.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/id1.png new file mode 100644 index 00000000000000..60777182d726ad Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/id1.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/id2.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/id2.png new file mode 100644 index 00000000000000..03bd98194fdea7 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/id2.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf1.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf1.png new file mode 100644 index 00000000000000..206a794be61730 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf1.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf10.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf10.png new file mode 100644 index 00000000000000..af4f20e32d04f2 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf10.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf2.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf2.png new file mode 100644 index 00000000000000..9671cb1caa3f2f Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf2.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf3.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf3.png new file mode 100644 index 00000000000000..dbf7d35303f32f Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf3.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf4.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf4.png new file mode 100644 index 00000000000000..0e3ba135f36d11 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf4.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf5.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf5.png new file mode 100644 index 00000000000000..2f7609ba376943 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf5.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf6.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf6.png new file mode 100644 index 00000000000000..0fe5f9e0a40c6b Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf6.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf7.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf7.png new file mode 100644 index 00000000000000..517354bd5e8bd5 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf7.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf8.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf8.png new file mode 100644 index 00000000000000..f6e8d4bf169ecf Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf8.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf9.png b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf9.png new file mode 100644 index 00000000000000..2b29062f2a4f97 Binary files /dev/null and b/topics/admin/tutorials/surf-research-cloud-pulsar/images/surf9.png differ diff --git a/topics/admin/tutorials/surf-research-cloud-pulsar/tutorial.md b/topics/admin/tutorials/surf-research-cloud-pulsar/tutorial.md new file mode 100644 index 00000000000000..f276dd70a3d317 --- /dev/null +++ b/topics/admin/tutorials/surf-research-cloud-pulsar/tutorial.md @@ -0,0 +1,259 @@ +--- +layout: tutorial_hands_on + +title: "Pulsar usage on SURF Research Cloud" +zenodo_link: "" +questions: + - How do I start a Pulsar instance on SURF Research Cloud? + - How do I connect to Pulsar? +objectives: + - Be able to attach a Pulsar node to Galaxy + - Send jobs to Pulsar +key_points: + - With SRC you can start your own Pulsar on-demand instance in a secure environment. + - The Pulsar node is publicly accessible for a Galaxy with the credentials to use. + - You can send jobs to a Pulsar node from either a Galaxy instance running on SRC, or even inside your own network. +requirements: + - type: "none" + title: Access to the SURF Research Cloud + - type: "none" + title: SRAM authentication + - type: "none" + title: An SSH key connected to your account + - type: "none" + title: Have a basic understanding of how Galaxy works +time_estimation: "30m" +level: Introductory +contributions: + authorship: + - hexylena + - mirelaminkova + funding: + - surf + +priority: 11 + +edam_ontology: +- topic_0605 # Informatics +- topic_3071 # Data Management + +abbreviations: + SRC: SURF Research Cloud + GDPR: General Data Protection Regulations + SRAM: SURF Research Access Management + CO: Collaborative Organisation + +follow_up_training: + - type: "internal" + topic_name: admin + tutorials: + - surf-research-cloud-galaxy +subtopic: cloud +tags: + - deploying +--- + + +Using Pulsar via the {SRC} allows researchers to start Pulsar instances on-demand to expand their computational resources and even access GPUs to help and analyze their data in a secure environment following the {GDPR}. + +There are two main use cases we envision this role being useful for: + +Saving costs on SRC +: Maybe you're already running Galaxy in SRC, but you don't want to run a GPU node because it is very expensive. By using the SRC Pulsar Catalog Item, you can launch a node to do computations and then shut it down when you're done, saving money. Pulsar instances can be started and stopped on demand, depending on personal cases and requirements, giving you a lot of freedom! + +Accessing a GPU from a local (in UMC/hospital Galaxy) +: If you do not have a GPU easily available within your institute, it may be attractive to send jobs securely to SRC, by launching a Pulsar node in SRC and attaching it to your institute's Galaxy instance. + + +> <agenda-title></agenda-title> +> +> 1. TOC +> {:toc} +> +{: .agenda} + +# Prerequisites + +The instance provides secure authentication, where users must have a SURF Research account prior to this tutorial, have set the {SRAM} authentication method, and connect an SSH key to their accounts. In case you are not familiar with {SRC} and need help in setting up your accounts, please follow the instructions on the [SURF Knowledge Base](https://servicedesk.surf.nl/wiki/display/WIKI/SURF+Research+Cloud) + +Inside the SRC members should have access to all publicly available catalog items. If you are not able to create a catalog item, please [contact SURF servicedesk](mailto:servicedesk@surf.nl). + +This tutorial assumes you are member of a {CO} in {SRAM} that has access to {SRC} and a wallet with budget in SRC with enough sources to create Galaxy and Pulsar catalog items. (For more information please refer to the [SURF Knowledge Base](https://servicedesk.surf.nl/wiki/display/WIKI/Budgets%2C+wallets%2C+contracts). + +You should have previous experience working with data inside Galaxy. + +> <tip-title>SSH Access Required</tip-title> +> SSH access is required to reconfigure your Galaxy instance. Please make sure you set an SSH key in your SRAM account if you are planning to use Galaxy in SRC for this tutorial. +{: .tip} + +> <hands-on-title>Log In to SRC</hands-on-title> +> 1. Log in to [SURF Research Cloud](https://portal.live.surfresearchcloud.nl/) +> +> ![screenshot of SRC login portal with a bright yellow login button](images/surf1.png) +{: .hands_on} + +# Launching Pulsar in SRC + +> <hands-on-title>Launching Pulsar</hands-on-title> +> 1. In the **Workspaces Tab** on the bottom half of the screen, you'll find a **Plus Button** at right to add a new workspace +> +> ![A small plus button is hovered over which says Add. Below galaxy-test is shown running with a yellow Access button](images/surf3.png) +> +> 2. Clicking that will let you choose any of the *Catalog Items* from SRC. They've got a wide selection but we're only interested in the two Pulsar Catalog Items +> +> ![the two pulsar components in SRC: Galaxy Pulsar GPU Node (CUDA) and Galaxy Pulsar Node are shown. The second is expanded showing an author, Helena Rasche, and a description: let's you run galaxy jobs on another node](images/surf4.png) +> +> > <warning-title>GPUs are Expensive</warning-title> +> > The GPU nodes are *expensive*. In fact it +> > was the motivating reason for building this catalog item: to enable you to +> > launch a node, run some computations, and shut it down, saving you money. +> {: .warning} +> +> 3. Creating a "workspace" from a catalog item (a template) is easy, most of the options are fixed for you, you just need to choose the size of the item. Pick an appropriate size for whatever computations you need to do. +> +> ![workspace creation screen, choose the cloud provider is locked to SURF HPC, flavour is locked to 22.04, and the size options are available from 1GB/8CPU to 60C/750GB Hi-mem](images/surf5.png) +> +> 6. Pick a name, it can be anything, it does not matter. Check the expiration date to ensure it is just enough time for your computation and no more. +> +> > <tip-title>Expiration Date</tip-title> +> > The standard life-time of the VM is 5 days. If you need it for longer, this option can be changed once the machine is running. +> > Note, that once the machine is expired and deleted it cannot be restored! Plan accordingly and migrate your data in time to prevent data loss! +> > +> > This is an incredibly useful feature as it saves you from forgetting to destroy a VM. Especially for GPU nodes it can help you ensure that they disappear after your computation is complete. +> {:.tip} +> +> ![almost there! some final details reads this page, asking for name and description. both have been filled out with 'pulsar'. a yellow submit button waits at the bottom](images/surf6.png) +> +> 7. Click submit when you are happy with the configuration. +> +{: .hands_on} + +Once done, the workspace will be created for you. You'll need to wait ~5 minutes usually. Go for a beverage ☕️ + +![workspace list showing a workspace named pulsar being created.](images/surf7.png) + +## Using Pulsar on SRC + +> <hands-on-title>Access the Information Page</hands-on-title> +> +> 1. Once the workspace is up, you'll see an **Access** link: +> +> ![A small plus button is hovered over which says Add. Below galaxy-test is shown running with a yellow Access button](images/surf3.png) +> +> 2. Click that will show you a Pulsar information page. This page is running on your pulsar node itself, and is restricted to ensure only authorised members can access the contents. It includes some configuration you will need to copy to your Galaxy node in order to make use of the Pulsar node. +> +> ![pulsar configuration information page showing an about with admins and metadata like workspace fqdn. Configuration for galaxy is shown below including XML changes](images/surf8.png) +{: .hands_on} + +This information page should have more than enough information to connect this Pulsar instance to your Galaxy server. You will need to reference information from this page in the following steps: + +> <hands-on-title>Configuring Galaxy to use SRC Pulsar</hands-on-title> +> 1. Collect the requirements for accessing the Galaxy machine. You will need: +> +> - your username from the first step +> - your SSH key that is associated with your SRAM account +> +> 2. SSH into your *Galaxy* machine (not pulsar!). +> +> ``` +> ssh -i path/to/your/sram-ssh-key username@galaxy.src-winter-school.src.surf-hosted.nl +> ``` +> +> 3. You will need to `sudo su` to do anything useful. Do that now. +> 4. `cd /srv/galaxy/` to move into the directory Galaxy configuration is stored. +> 5. The configuration is discussed fully in the Pulsar information, but it will be briefly covered here as well. Generally there are a few steps that must be followed: +> +> - A runner must be registered +> - A destination/environment must be added with the pulsar details +> - Some tools should be redirected to this Pulsar +> +> Here is an example of what those changes *might* look like in your Galaxy node. In this example our pulsar node was called `p20` but that will be different for you. +> +> {% snippet topics/admin/faqs/diffs.md %} +> +> +> ```diff +> runners: +> local: +> load: galaxy.jobs.runners.local:LocalJobRunner +> workers: 4 +> condor: +> load: galaxy.jobs.runners.condor:CondorJobRunner +> + pulsar: +> + load: galaxy.jobs.runners.pulsar:PulsarRESTJobRunner +> +> execution: +> default: docker_dispatch +> environments: +> local_destination: +> runner: local +> # ... probably some more environments here. +> +> + remote_p20: +> + runner: pulsar +> + url: https://p20.src-sensitive-i.src.surf-hosted.nl +> + private_token: ySgfM1rnGIsiVN8Xlfk +> + dependency_resolution: remote +> + manager: _default_ +> + # Uncomment the following to enable interactive tools: +> + docker_enabled: true +> + docker_set_user: null +> + docker_memory: "8G" +> + singularity_enabled: false +> + tmp_dir: true +> + outputs_to_working_directory: false +> + container_resolvers: +> + - type: explicit +> + require_container: True +> + container_monitor_command: /mnt/pulsar/venv/bin/galaxy-container-monitor +> + container_monitor_result: callback +> + container_monitor_get_ip_method: command:echo p20.src-sensitive-i.src.surf-hosted.nl +> +> +> tools: +> - class: local # these special tools that aren't parameterized for remote execution - expression tools, upload, etc +> environment: local_env +> - id: Cut1 +> environment: condor_1x1 +> +- id: interactive_tool_jupyter_notebook +> + environment: remote_p20 +> +- id: interactive_tool_rstudio +> + environment: remote_p20 +> ``` +{: .hands_on} + +With the above, the minimal configuration is done, the Galaxy server will be aware of the Pulsar node, and two tools will be sent there: the Jupyter and RStudio Interactive Tools. + +While you will simply copy-paste the runner and environment, you will need to identify yourself which tools should go to this Pulsar node. +You can find the tool ID from the dropdown at the top right, just to the left of "Execute" at the top: + +![url bar and tool interface for the Cut1 tool](./images/id2.png) + +> <tip-title>An Easy Configuration Option: Send Every Job to Pulsar</tip-title> +> +> If you are running jobs for a limited period of time, you might consider making this pulsar node the default destination. Remember to use the `remote_...` name of your pulsar node, based on what you copied. Not `remote_p20`. +> +> ```diff +> execution: +> - default: docker_dispatch +> + default: remote_p20 +> environments: +> local_destination: +> runner: local +> ``` +{: .tip} + +With that, you're done, and for the length of time your node is running, your chosen tools (or everything) will be executed on that Pulsar node with more memory and CPU than the Galaxy host, and maybe a GPU as well! + +> <hands-on-title>Launch a Job on Pulsar</hands-on-title> +> 1. Login to your Galaxy +> 2. Run one of the tools you have decided to send to Pulsar +> 3. On the pulsar machine, you can check that it runs by following the logs: +> +> ```bash +> sudo journalctl -fu pulsar +> ``` +> +{: .hands_on} + +Congratulations on launching Pulsar in SRC! 🌌 diff --git a/topics/admin/tutorials/terraform/slides.html b/topics/admin/tutorials/terraform/slides.html index 31238267ad5cce..387062a5b41328 100644 --- a/topics/admin/tutorials/terraform/slides.html +++ b/topics/admin/tutorials/terraform/slides.html @@ -17,6 +17,8 @@ subtopic: cloud contributors: - hexylena + +priority: 30 --- ### Why Terraform diff --git a/topics/admin/tutorials/terraform/tutorial.md b/topics/admin/tutorials/terraform/tutorial.md index 5e0340fce1e5ba..2403016c61a798 100644 --- a/topics/admin/tutorials/terraform/tutorial.md +++ b/topics/admin/tutorials/terraform/tutorial.md @@ -22,7 +22,8 @@ tags: - terraform - deploying - cloud -priority: 3 +priority: 30 + --- # Overview diff --git a/topics/admin/tutorials/tiaas/tutorial.md b/topics/admin/tutorials/tiaas/tutorial.md index 22cbc7b7cd5d9e..c80a3747f5b30d 100644 --- a/topics/admin/tutorials/tiaas/tutorial.md +++ b/topics/admin/tutorials/tiaas/tutorial.md @@ -200,10 +200,11 @@ This tutorial will go cover how to set up such a service on your own Galaxy serv > ```diff > --- a/templates/nginx/galaxy.j2 > +++ b/templates/nginx/galaxy.j2 -> @@ -113,4 +113,6 @@ server { +> @@ -114,4 +114,7 @@ server { +> proxy_pass http://127.0.0.1:3000/; > proxy_set_header Host $http_host; > } -> +> + > + {{ tiaas_nginx_routes }} > + > } diff --git a/topics/admin/tutorials/wireguard-headscale/tutorial.md b/topics/admin/tutorials/wireguard-headscale/tutorial.md index c94c99bab6a688..0400a169419e0b 100644 --- a/topics/admin/tutorials/wireguard-headscale/tutorial.md +++ b/topics/admin/tutorials/wireguard-headscale/tutorial.md @@ -31,6 +31,10 @@ subtopic: cloud tags: - wireguard - networking +edam_ontology: +- topic_3263 # data security +- topic_3372 # software engineering +priority: 20 --- [Tailscale](https://tailscale.com/) makes secure networking easy, it really is like magic. If you've used wireguard before, you know it takes a bit to setup and some configuration if you need to do anything fancy. diff --git a/topics/admin/tutorials/wireguard/tutorial.md b/topics/admin/tutorials/wireguard/tutorial.md index 1d78e2b492e771..6d8ef996855035 100644 --- a/topics/admin/tutorials/wireguard/tutorial.md +++ b/topics/admin/tutorials/wireguard/tutorial.md @@ -26,6 +26,7 @@ subtopic: cloud tags: - wireguard - networking +priority: 20 --- In this tutorial we will briefly cover what [Wireguard](https://www.wireguard.com/) is and how you can leverage it for your needs. This will not make you an expert on Wireguard but will give you the tools you need in order to setup a local Wireguard network. diff --git a/topics/ai4life/community.md b/topics/ai4life/community.md new file mode 100644 index 00000000000000..bf7530f4d410ce --- /dev/null +++ b/topics/ai4life/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: ai4life +--- + diff --git a/topics/ai4life/maintainer.md b/topics/ai4life/maintainer.md new file mode 100644 index 00000000000000..42f8446ff927ff --- /dev/null +++ b/topics/ai4life/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: ai4life +--- + diff --git a/topics/assembly/community.md b/topics/assembly/community.md new file mode 100644 index 00000000000000..670e760819fb51 --- /dev/null +++ b/topics/assembly/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: assembly +--- + diff --git a/topics/assembly/tutorials/vgp_genome_assembly/tutorial.md b/topics/assembly/tutorials/vgp_genome_assembly/tutorial.md index ef50d16210f0c6..dbb43f84aa124f 100644 --- a/topics/assembly/tutorials/vgp_genome_assembly/tutorial.md +++ b/topics/assembly/tutorials/vgp_genome_assembly/tutorial.md @@ -1403,7 +1403,7 @@ Despite Hi-C generating paired-end reads, we need to map each read separately. T > <hands-on-title>Mapping Hi-C reads</hands-on-title> > -> 1. Run {% tool [BWA-MEM2](ttoolshed.g2.bx.psu.edu/repos/iuc/bwa_mem2/bwa_mem2/2.2.1+galaxy1) %} with the following parameters: +> 1. Run {% tool [BWA-MEM2](toolshed.g2.bx.psu.edu/repos/iuc/bwa_mem2/bwa_mem2/2.2.1+galaxy1) %} with the following parameters: > - *"Will you select a reference genome from your history or use a built-in index?"*: `Use a genome from history and build index` > - {% icon param-file %} *"Use the following dataset as the reference sequence"*: `Hap1 assembly bionano` > - *"Single or Paired-end reads"*: `Single` diff --git a/topics/climate/community.md b/topics/climate/community.md new file mode 100644 index 00000000000000..da17a1331a6e48 --- /dev/null +++ b/topics/climate/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: climate +--- + diff --git a/topics/community/community.md b/topics/community/community.md new file mode 100644 index 00000000000000..83f390ebb1402b --- /dev/null +++ b/topics/community/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: community +--- + diff --git a/topics/community/faqs/community_activites_calendar.md b/topics/community/faqs/community_activites_calendar.md new file mode 100644 index 00000000000000..fbe3d036e2fc61 --- /dev/null +++ b/topics/community/faqs/community_activites_calendar.md @@ -0,0 +1,12 @@ +--- +title: How can I add my SIG meetings to the Galaxy Community Activities calendar? +box_type: tip +layout: faq +contributors: [nomadscientist] +--- + +Add the following guest to all of your Google Calendar meeting events: 8a762890fbe724e9d29b67915aa0197a352642f94b22ec64a85430daaf1abb5e@group.calendar.google.com + +Then it will show up in the **Galaxy Community Activities** calendar! + +<iframe src="https://calendar.google.com/calendar/embed?height=600&wkst=1&ctz=Europe%2FLondon&showPrint=0&showCalendars=0&mode=AGENDA&src=OGE3NjI4OTBmYmU3MjRlOWQyOWI2NzkxNWFhMDE5N2EzNTI2NDJmOTRiMjJlYzY0YTg1NDMwZGFhZjFhYmI1ZUBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&color=%234285F4" style="border-width:0" width="800" height="600" frameborder="0" scrolling="no"></iframe> diff --git a/topics/community/faqs/community_home.md b/topics/community/faqs/community_home.md new file mode 100644 index 00000000000000..38358322865d63 --- /dev/null +++ b/topics/community/faqs/community_home.md @@ -0,0 +1,21 @@ +--- +title: How do I find the Community Home pages? +box_type: tip +layout: faq +contributors: [nomadscientist, hexylena, shiltemann] +--- + +The **Community Home** shows statistics for the topic (e.g. number of tutorials, slides, events, contributors, etc), +as well as annual "Year in review" sections listing all new additions to the topic/community for each year. + +You can find your **Community Home** by +1. Opening the GTN **Topic** page of your choice +2. Scrolling down to the **Community Resources** section (below the list of tutorials) +3. Clicking the **Community Home** button + + ![screenshot of the community and maintainer homepage buttons]({% link topics/community/images/community_maintainer_home_buttons.png %}) + +For example, have a look at the [Single Cell Community Home]({% link topics/single-cell/community.md %}) + +![screenshot of the single cell community home page]({% link topics/community/images/community_home.png %}) + diff --git a/topics/community/faqs/governance_gcb_join.md b/topics/community/faqs/governance_gcb_join.md index 68bfb929207ba7..a2524bea1bbf5c 100644 --- a/topics/community/faqs/governance_gcb_join.md +++ b/topics/community/faqs/governance_gcb_join.md @@ -9,6 +9,7 @@ contributors: [nomadscientist] - 📪 [Mailing list](https://lists.galaxyproject.org/lists/sig-chairs.lists.galaxyproject.org/) - 📝 [Rolling meeting notes](https://docs.google.com/document/d/19zv4rata-uVhFW43S8v_Fw83RVYq6jDGXy-1T8Aju4s/edit?usp=sharing) + - 🕶️[Add yourself to our members list](https://docs.google.com/document/d/19zv4rata-uVhFW43S8v_Fw83RVYq6jDGXy-1T8Aju4s/edit?tab=t.5cgfntg608gj) - ☕ [Chatroom](https://matrix.to/usegalaxy_commreps:matrix.org#/!JHcKrPhyfJoqHESrdD:matrix.org) - 🗓️[Community Board Google-Calendar](https://calendar.google.com/calendar/embed?height=600&wkst=1&ctz=Europe%2FLondon&bgcolor=%23ffffff&mode=AGENDA&showPrint=0&title=Galaxy%20Community%20Board&src=MDQwNDY2MDRhNGYxODE2NDk0MjBkYTQzMzUzMTBkN2E1MmQxMGJmNDkxNDgwMGEyZjNhYjEzZWE0ZWY3MzEyY0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t&color=%23F6BF26%22%20style=%22border:solid%201px%20#777%22%20width=%22800%22%20height=%22600%22%20frameborder=%220%22%20scrolling=%22no%22) - 🗓️[Community Board iCalendar](https://calendar.google.com/calendar/ical/04046604a4f181649420da4335310d7a52d10bf4914800a2f3ab13ea4ef7312c%40group.calendar.google.com/public/basic.ics) diff --git a/topics/community/faqs/maintainer_home.md b/topics/community/faqs/maintainer_home.md new file mode 100644 index 00000000000000..2c33bb30f24fb3 --- /dev/null +++ b/topics/community/faqs/maintainer_home.md @@ -0,0 +1,20 @@ +--- +title: How do I find the Maintainer Home pages? +box_type: tip +layout: faq +contributors: [nomadscientist, hexylena, shiltemann] +--- + +The **Maintainer Home** pages shows the state of the topic and its materials in terms of which available GTN features are being used, adherence to best practices, and when tutorials have last been updated, and which tutorials are the most used, etc. This can help inform where to focus your efforts. + +You can find your **Maintainer Home** by +1. Opening the GTN **Topic** page of your choice +2. Scrolling down to the **Community Resources** section (below the list of tutorials) +3. Clicking the **Maintainer Home** button + + ![screenshot of the community and maintainer homepage buttons]({% link topics/community/images/community_maintainer_home_buttons.png %}) + +For example, have a look at the [Single Cell Maintainer Home]({% link topics/single-cell/maintainer.md %}) + +![screenshot of the single cell maintainer home page]({% link topics/community/images/maintainer_home.png %}) + diff --git a/topics/community/faqs/matrix_news_bot.md b/topics/community/faqs/matrix_news_bot.md new file mode 100644 index 00000000000000..6f8c26cc6e7321 --- /dev/null +++ b/topics/community/faqs/matrix_news_bot.md @@ -0,0 +1,18 @@ +--- +title: How do I add a news feed to a Matrix channel? +box_type: tip +layout: faq +contributors: [nomadscientist, hexylena] +--- + +1. You must be an *Admin* in the channel. Find this out by going to the channel and selecting **Room info** --> **People**, or clicking on the little circle images of people in a channel. Admins can make other admins. + +2. Go to **Room info** --> **Extensions** --> **Add extension** --> **Feeds** + +3. Under *Subscribe to a feed*, add a URL from this [GTN feeds listing]({% link feeds/index.md %}). Make sure that it ends in `.xml`. For example, `https://training.galaxyproject.org/training-material/topics/community/feed.xml` would provide updates on any community-tagged GTN materials into the Matrix channel. + +4. Under *Template*, change the existing text to the following: `$LINK: $SUMMARY` + +5. Provide a reasonable name, and then hit **Subscribe**! + +Details from Matrix are here: https://ems-docs.element.io/books/element-cloud-documentation/page/migrate-to-the-new-github-and-feeds-bots diff --git a/topics/community/images/community_home.png b/topics/community/images/community_home.png new file mode 100644 index 00000000000000..f78f0a0d24c50f Binary files /dev/null and b/topics/community/images/community_home.png differ diff --git a/topics/community/images/community_maintainer_home_buttons.png b/topics/community/images/community_maintainer_home_buttons.png new file mode 100644 index 00000000000000..23d3f1076d2851 Binary files /dev/null and b/topics/community/images/community_maintainer_home_buttons.png differ diff --git a/topics/community/images/maintainer_home.png b/topics/community/images/maintainer_home.png new file mode 100644 index 00000000000000..2aa21e2908920e Binary files /dev/null and b/topics/community/images/maintainer_home.png differ diff --git a/topics/community/maintainer.md b/topics/community/maintainer.md index 4677c062c73f0f..bd1db466dab9ac 100644 --- a/topics/community/maintainer.md +++ b/topics/community/maintainer.md @@ -2,3 +2,4 @@ layout: topic-maintainer topic_name: community --- + diff --git a/topics/community/metadata.yaml b/topics/community/metadata.yaml index 1a7fc9a65718c9..fd69b4280547b9 100644 --- a/topics/community/metadata.yaml +++ b/topics/community/metadata.yaml @@ -9,5 +9,13 @@ summary: | #docker_image: "quay.io/galaxy/community" learning_path_cta: dev_tools_training +subtopics: + - id: sig + title: "Special Interest Groups & Communities" + description: "These tutorials will get you started with the basics of what a special interest group is and how to build one." + - id: subdomain + title: "Subdomains & Galaxy Labs" + description: "This tutorial will guide you on how to build subdomains / Galaxy Labs" + editorial_board: - nomadscientist diff --git a/topics/community/tutorials/community-tool-table/tutorial.md b/topics/community/tutorials/community-tool-table/tutorial.md index d54adbed6c5c94..e81efeb7e55f6a 100644 --- a/topics/community/tutorials/community-tool-table/tutorial.md +++ b/topics/community/tutorials/community-tool-table/tutorial.md @@ -4,6 +4,7 @@ title: Creation of an interactive Galaxy tools table for your community level: Introductory redirect_from: - /topics/dev/tutorials/community-tool-table/tutorial +subtopic: sig questions: - Is it possible to have an overview of all Galaxy tools for a specific scientific domain? diff --git a/topics/community/tutorials/community_content/faqs/index.md b/topics/community/tutorials/community_content/faqs/index.md new file mode 100644 index 00000000000000..9ce3fe4fce824b --- /dev/null +++ b/topics/community/tutorials/community_content/faqs/index.md @@ -0,0 +1,3 @@ +--- +layout: faq-page +--- diff --git a/topics/community/tutorials/community_content/tutorial.md b/topics/community/tutorials/community_content/tutorial.md new file mode 100644 index 00000000000000..03aab662f99681 --- /dev/null +++ b/topics/community/tutorials/community_content/tutorial.md @@ -0,0 +1,167 @@ +--- +layout: tutorial_hands_on +priority: 1 +subtopic: sig +title: "Creating community content" +questions: + - "What content does my Topic automatically get from the GTN?" + - "How can I make sure this data is collected well?" +objectives: + - "Signpost community leads and users to useful resources" + - "Explain why metadata is key for such community resources" + - "Provide a reference, rather than a tutorial" +time_estimation: "30M" +key_points: + - "The GTN has worked hard to provide automated metrics and resources to highlight and acknowledge the efforts of communities and community leads" + - "This only works if we contribute with effective tagging." +contributions: + authorship: + - nomadscientist + - shiltemann +requirements: + - + type: "internal" + topic_name: community + tutorials: + - sig_define +--- + +Galaxy *[Special Interest Group](https://galaxyproject.org/community/sig)* (**SIG**)s work hard to build and maintain training resources. The GTN has worked hard to acknowledge this and offer nice impact pages to communities. + +Here is a list of resources that you can use! + +> <comment-title></comment-title> +> - We want this material to grow (similar to the 'Creating content in markdown' tutorial) so please do add further resources that can help communities! +{: .comment} + +> <agenda-title></agenda-title> +> +> In this tutorial, we will cover: +> +> 1. TOC +> {:toc} +> +{: .agenda} + +# Maintainer Home + +Maintaining resources is **fundamental** to the quality and usefulness of any software resource. We do *not* throw code over the wall! + +To help topic maintainers to quickly recognise what materials need updating and fixing, topic **Maintainer Homes** were built. + +> <hands-on-title>Go to your topic Maintainer Home</hands-on-title> +> +> 1. Go to any GTN training topic of interest. +> 2. Scroll down past the tutorials, and click on the "Maintainer Home" button +> +> {% snippet topics/community/faqs/maintainer_home.md %} +> +> 3. Explore the Maintainer Home! +> - For example, the [Single Cell Maintainer Home]({% link topics/single-cell/maintainer.md %}) +> +{: .hands_on} + +You may instantly see some key information missing from tutorials, or how long its been since someone checked it! Time to update some materials! + +You can see an example from the Single-cell topic below: + +<iframe + src="/training-material/topics/single-cell/maintainer.html" + width="100%" + height="400" + style="border: 2px solid #00008B;" + title="Single-Cell Topic Maintainer Home"> +</iframe> + +# Community Home + +Where the **Maintainer Home** helps you sustain your community, the **Community Home** helps you show off your community. An end-of-year gift in 2024, this page will sift through news, events, and GTN contributions for your community tag of interest (example: single-cell) and provide a beautiful visualization of your efforts. + +> <hands-on-title>Go to your topic Community Home</hands-on-title> +> +> 1. Go to any GTN training topic of interest. +> 2. Scroll down past the tutorials, and click on the "Community Home" button +> +> {% snippet topics/community/faqs/community_home.md %} +> +> 3. Explore the **Community Home**! +> - For example, the [Single Cell Community Home]({% link topics/single-cell/community.md %}) +> +{: .hands_on} + +You can see an example from the Single-cell topic below: + +<iframe + src="/training-material/topics/single-cell/community.html" + width="100%" + height="400" + style="border: 2px solid #00008B;" + title="Single-Cell Topic Community Home"> +</iframe> + +# Topic usage statistics + +Next up, you might want to know how many people are actually using your materials? Welcome to your **Topic usage statistics**! You may have already found this, actually, as it's (currently) at the bottom of the Maintainer Home. + +> <hands-on-title>Go to your topic usage statistics</hands-on-title> +> +> 1. Go to any GTN training topic of interest. +> 2. Scroll down past the tutorials, and click on the "Maintainer Home" button +> +> {% snippet topics/community/faqs/maintainer_home.md %} +> +> 3. Scroll down to the section "Statistics For Your Materials" +> 4. Explore the usage statistics! +> - For example, the [Single Cell Usage Statistics]({% link topics/single-cell/maintainer.md %}#statistics-for-your-materials) +> +{: .hands_on} + +You can see an example from the Single-cell topic below. + +<iframe + src="/training-material/topics/single-cell/maintainer.html#statistics-for-your-materials" + width="100%" + height="400" + style="border: 2px solid #00008B;" + title="Single-Cell Topic Usage Statistics"> +</iframe> + +# News widgets + +You can also embed news into your pages, subdomains/ Galaxy Labs, or even your Matrix channels. + +Follow this documentation to learn how: + +1. [GTN Feeds]( {% link feeds/index.md %} ) +2. Bot integration into matrix + +{% snippet topics/community/faqs/matrix_news_bot.md %} + +You can see an example from the Single-cell topic below. + +<h3 class="mb-3">News and Events</h3> +<iframe width="100%" height="300px" src="/training-material/feeds/single-cell-month.w.html"></iframe> + +# Workflow search + +Want to see all the workflows tagged with your community tag across public servers? Look no further! + +Follow this documentation to learn how: + +1. [Galaxy Pan-Galactic Workflow Search]( {% link workflows/list.md %} ) +2. [Workflow Search Querying]( {% link news/2023/11/20/workflow-search.md %} ) + +You can see an example from the Single-cell topic below. + +<h3 class="mb-3">Public workflows</h3> +<iframe src="/training-material/workflows/embed.html?query=single-cell" height="400px" width="100%" class="gtn-embed" frameborder="1"></iframe> + +# Galaxy Community Activities calendar + +We host a GoogleCalendar on the [Galaxy Special Interest Group Hub page](https://galaxyproject.org/community/sig/). + +{% snippet topics/community/faqs/community_activites_calendar.md %} + +# Conclusion + +{% icon congratulations %} Congratulations! You've made it to the end! Hopefully you think these resources are brilliant, and are making sure to tag everything (news, events, training materials, workflows, FAQs, you name it, you should tag it!) with your community tag! diff --git a/topics/community/tutorials/sig_create/tutorial.md b/topics/community/tutorials/sig_create/tutorial.md index 28a5e6401cd813..fae1a25fc62a34 100644 --- a/topics/community/tutorials/sig_create/tutorial.md +++ b/topics/community/tutorials/sig_create/tutorial.md @@ -1,6 +1,7 @@ --- layout: tutorial_hands_on priority: 2 +subtopic: sig title: "Creating a Special Interest Group" questions: diff --git a/topics/community/tutorials/sig_define/tutorial.md b/topics/community/tutorials/sig_define/tutorial.md index a1498195a27b8e..33a99ae9945466 100644 --- a/topics/community/tutorials/sig_define/tutorial.md +++ b/topics/community/tutorials/sig_define/tutorial.md @@ -2,6 +2,7 @@ layout: tutorial_hands_on priority: 1 title: "What's a Special Interest Group?" +subtopic: sig questions: - "What is a Special Interest Group?" - "What is the purpose of a Special Interest Group?" diff --git a/topics/community/tutorials/tools_subdomains/tutorial.md b/topics/community/tutorials/tools_subdomains/tutorial.md index 7a7bf5a3c1267f..4bbabac4862a1e 100644 --- a/topics/community/tutorials/tools_subdomains/tutorial.md +++ b/topics/community/tutorials/tools_subdomains/tutorial.md @@ -2,6 +2,7 @@ layout: tutorial_hands_on title: Make your tools available on your subdomain +subtopic: subdomain questions: - How can a tool be added in a section ? - How can a section be added in a subdomain ? @@ -43,15 +44,15 @@ This tutorial explains how to make your brand new tools, once they're published > - For Galaxy Europe, fork the [usegalaxy-eu-tools repo](https://github.com/usegalaxy-eu/usegalaxy-eu-tools) > - Create a branch on your fork > - Browse through the different yaml files and select the one that correspond to your subdomain for earth system everything is made under the ecology owner. The name of the files corresponds to a toolshed owner or community and a few of those communities with a review system and CI integration gets special trust and auto-updates. -> - Once in the yaml you can add the section for your tool (you want to know what are the existing tool sections ? Go check the [categories defined here](https://github.com/usegalaxy-eu/infrastructure-playbook/blob/master/templates/galaxy/config/tool_conf.xml.j2)) -> +> - Once in the yaml you can add the section for your tool (you want to know what are the existing tool sections ? Go check the [categories defined here](https://github.com/usegalaxy-eu/infrastructure-playbook/blob/master/templates/galaxy/config/tool_conf.xml.j2)) +> > An example on how to fill in the yaml file > ``` > - name: id_of_your_tool > owner: choose_the_owner_relative_to_a_github_repo > tool_panel_section_label: 'Choose the section where your tool belongs' > ``` -> +> > ![Image of the modification to make in the ecology.yaml file](../../images/tool_subdomain/ecology_yml_tool.png) > > - Then commit your changes and write a nice message for the admin when you open your Pull Request. @@ -85,7 +86,7 @@ This part is only to make batch tool visible in your subdomain. > - Then, commit your changes and write a nice message for the admin when you open your Pull Request. {: .hands_on} -If you choose to create a new section for your interactive tool don't forget add this section to your subdomain ! +If you choose to create a new section for your interactive tool don't forget add this section to your subdomain ! # Conclusion diff --git a/topics/computational-chemistry/community.md b/topics/computational-chemistry/community.md new file mode 100644 index 00000000000000..9fc252f2ba2428 --- /dev/null +++ b/topics/computational-chemistry/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: computational-chemistry +--- + diff --git a/topics/contributing/community.md b/topics/contributing/community.md new file mode 100644 index 00000000000000..2d37741b21f355 --- /dev/null +++ b/topics/contributing/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: contributing +--- + diff --git a/topics/contributing/faqs/gtn-adr-images.md b/topics/contributing/faqs/gtn-adr-images.md new file mode 100644 index 00000000000000..cf3a6dd4c3d66c --- /dev/null +++ b/topics/contributing/faqs/gtn-adr-images.md @@ -0,0 +1,47 @@ +--- +title: "GTN ADR: Image Storage" +area: adr +box_type: tip +layout: faq +contributors: [hexylena, shiltemann, bebatut] +--- + +[FAQ: What is an ADR?]({% link topics/contributing/faqs/gtn-adr.md %}) + +## Context and Problem Statement + +Contributors to the GTN have image and occasionally datasets they wish to include in the GTN. These datasets are generally quite small (kilobytes) but, are necessary for the understanding of a tutorial. + +## Decision Drivers + +* We prioritise contributor UX very highly, we cannot ask them to learn multiple systems. Git + Markdown is already enough. +* We wish to be able to sufficiently serve the website offline, with just a clone. + +## Considered Options + +* Storage in git directly +* In another system (e.g. S3) +* Allowing linked images anywhere on the internet. + +## Decision Outcome + +Chosen option: "Storage in git directly", because it is the simplest solution that meets our requirements, and doesn't require development we cannot fund, and doesn't risk dead links over time. + +### Consequences + +* Good, because it is simple and doesn't require additional development. +* Bad, because it will permanently inflate the size of the repository, and it will never decrease. (We can offset this with + +## Pros and Cons of the Options + +### Storage in S3 + +* Good, because it's cheap and well known. +* Bad, because we would need to build a way for users to upload images as part of a GTN tutorial development, and then link to them in markdown. +* Bad, because then the website would not be hostable offline. + +### Hotlinking + +* Good, because it's easy for contributors +* Bad, because unnecessary impact on someone else's bandwidth +* Bad, because the links will rot over time, images and tutorials will not be able to be followed. diff --git a/topics/contributing/faqs/gtn-adr-jekyll.md b/topics/contributing/faqs/gtn-adr-jekyll.md new file mode 100644 index 00000000000000..354fa30f4df925 --- /dev/null +++ b/topics/contributing/faqs/gtn-adr-jekyll.md @@ -0,0 +1,98 @@ +--- +title: "GTN ADR: Why Jekyll and not another Static Site Generator (SSG)" +area: adr +box_type: tip +layout: faq +contributors: [hexylena, shiltemann, bebatut] +--- + +[FAQ: What is an ADR?]({% link topics/contributing/faqs/gtn-adr.md %}) + +## Context and Problem Statement + +We needed a static site generator for the GTN, one had to be chosen. We chose Jekyll because of it's good integration with GitHub and GitHub Pages. Over time our requirements have changed but we still need one SSG. + +## Decision Drivers + +* Must be easy for contributors to setup and use +* Needs to be relatively performant (full rebuilds may not take more than 2 minutes.) +* Must allow us to develop custom plugins + +## Considered Options + +* Jekyll +* Hugo +* A javascript option +* Another SSG. + +## Decision Outcome + +Chosen option: "Jekyll", because of the amount of time and effort we have sunk into it over the years has made it a good platform for us, despite limitations. + +Over time we have invested heavily into Jekyll, any choice to switch *must* take that into consideration. Consider the following output of `scc _plugins bin/` + + +Language | Files | Lines | Blanks | Comments | Code | Complexity +-------- | ----- | ----- | ------ | -------- | ---- | ---------- +YAML | 117 | 9830 | 71 | 33 | 9726 | 0 +Ruby | 90 | 14471 | 1795 | 2617 | 10059 | 1163 +JSON | 48 | 3075 | 0 | 0 | 3075 | 0 +Python | 24 | 3693 | 284 | 272 | 3137 | 310 +Shell | 21 | 1529 | 175 | 262 | 1092 | 84 +JavaScript | 5 | 299 | 38 | 19 | 242 | 48 +Markdown | 4 | 76 | 19 | 0 | 57 | 0 +Dockerfile | 2 | 60 | 15 | 1 | 44 | 14 +Plain | Text | 2 | 18 | 0 | 0 | 18 | 0 +BASH | 1 | 51 | 8 | 4 | 39 | 1 +CSS | 1 | 3 | 0 | 0 | 3 | 0 +Docker | ignore | 1 | 1 | 0 | 0 | 1 | 0 +gitignore | 1 | 123 | 0 | 0 | 123 | 0 +Total | 317 | 33229 | 2405 | 3208 | 27616 | 1620 + +- Estimated Cost to Develop (organic) $880,671 +- Estimated Schedule Effort (organic) 13.11 months +- Estimated People Required (organic) 5.97 +- Processed 1081253 bytes, 1.081 megabytes (SI) + +This is a *lot* of code that would need to be rewritten if another language was ever chosen. + +The YAML comprises our Kwalify Schemas. There is a good argument for moving to JSON Schema instead. The Ruby however is the bulk of the code that would need to be rewritten. It does a significant number of complex things: + +- collecting and collating files off disk / in Jekyll's Page model into "Learning Materials", very large objects with hundreds of properties that are used to render each and every template. +- Generating hundreds of pages with a multitude of calculated properties. These would all need to be hand translated. + +Additionally any layouts would need to be rewritten from our existing Liquid templates. Note that this is not the full set of templates. + + +Language | Files | Lines | Blanks | Comments | Code | Complexity +-------- | ----- | ----- | ------ | -------- | ---- | ---------- +HTML | 69 | 5937 | 830 | 96 | 5011 | 0 +Markdown | 4 | 125 | 1 | 0 | 124 | 0 +Total | 73 | 6062 | 831 | 96 | 5135 | 0 + +- Estimated Cost to Develop (organic) $150,543 +- Estimated Schedule Effort (organic) 6.70 months +- Estimated People Required (organic) 2.00 + + +### Consequences + +* Good, because it works well for us and has scaled sufficiently to an incredible number of output pages (~7k html/22k files in a full GTN production deployment.) with acceptable build times (<5 minutes in prod, most of the action execution is taken up by contacting other servers, dependencies, and uploading the results.) +* Good, because it has a well supported ecosystem of plugins we can leverage for common tasks +* Good, because we can easily write our own plugins for many tasks. +* Bad, because we it remains difficult to install +* Bad, because people must know Ruby and very few people do (but it isn't that hard to learn!) + +## Pros and Cons of the Options + +### Hugo + +* Good, because it would be a single binary, easier to install +* Bad, because plugins do not exist, it does not have a way to hook the internals and work with them which we use extensively. +* Bad, because what plugins do exist, only exist as 'shortcodes' that are written in Go templates which are not as powerful as Ruby. + +### A JavaScript option + +* Good, because we could re-use code from other places +* Bad, because the average lifetime of a JavaScript SSG is maybe one year. +* Bad, because they are also quite slow on average (Hub compile times are on the order of 10 minutes.) diff --git a/topics/contributing/faqs/gtn-adr-template.md b/topics/contributing/faqs/gtn-adr-template.md new file mode 100644 index 00000000000000..8ac74bac5a1104 --- /dev/null +++ b/topics/contributing/faqs/gtn-adr-template.md @@ -0,0 +1,76 @@ +--- +title: GTN Architectural Decision Record Template +area: adr +box_type: tip +layout: faq +contributors: [hexylena, shiltemann, bebatut] +--- + +This is based on [Markdown Architectural Decision Record](https://adr.github.io/adr-templates/) and lets us record important decisions. + +# {short title, representative of solved problem and found solution} + +## Context and Problem Statement + +{Describe the context and problem statement, e.g., in free form using two to three sentences or in the form of an illustrative story. You may want to articulate the problem in form of a question and add links to collaboration boards or issue management systems.} + +<!-- This is an optional element. Feel free to remove. --> +## Decision Drivers + +* {decision driver 1, e.g., a force, facing concern, …} +* {decision driver 2, e.g., a force, facing concern, …} +* … <!-- numbers of drivers can vary --> + +## Considered Options + +* {title of option 1} +* {title of option 2} +* {title of option 3} +* … <!-- numbers of options can vary --> + +## Decision Outcome + +Chosen option: "{title of option 1}", because {justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | … | comes out best (see below)}. + +<!-- This is an optional element. Feel free to remove. --> +### Consequences + +* Good, because {positive consequence, e.g., improvement of one or more desired qualities, …} +* Bad, because {negative consequence, e.g., compromising one or more desired qualities, …} +* … <!-- numbers of consequences can vary --> + +<!-- This is an optional element. Feel free to remove. --> +### Confirmation + +{Describe how the implementation of/compliance with the ADR can/will be confirmed. Are the design that was decided for and its implementation in line with the decision made? E.g., a design/code review or a test with a library such as ArchUnit can help validate this. Not that although we classify this element as optional, it is included in many ADRs.} + +<!-- This is an optional element. Feel free to remove. --> +## Pros and Cons of the Options + +### {title of option 1} + +<!-- This is an optional element. Feel free to remove. --> +{example | description | pointer to more information | …} + +* Good, because {argument a} +* Good, because {argument b} +<!-- use "neutral" if the given argument weights neither for good nor bad --> +* Neutral, because {argument c} +* Bad, because {argument d} +* … <!-- numbers of pros and cons can vary --> + +### {title of other option} + +{example | description | pointer to more information | …} + +* Good, because {argument a} +* Good, because {argument b} +* Neutral, because {argument c} +* Bad, because {argument d} +* … + +<!-- This is an optional element. Feel free to remove. --> +## More Information + +{You might want to provide additional evidence/confidence for the decision outcome here and/or document the team agreement on the decision and/or define when/how this decision the decision should be realized and if/when it should be re-visited. Links to other decisions and resources might appear here as well.} + diff --git a/topics/contributing/faqs/gtn-adr.md b/topics/contributing/faqs/gtn-adr.md new file mode 100644 index 00000000000000..fc13783802166c --- /dev/null +++ b/topics/contributing/faqs/gtn-adr.md @@ -0,0 +1,18 @@ +--- +title: What is an Architectural Decision Record (ADR)? +area: adr +box_type: tip +layout: faq +contributors: [hexylena] +--- + +ADRs are documents that captures an important architectural decision made along with its context and consequences. + +We keep track of some of our important Architecture decisions using a template based on +[Markdown Architectural Decision Record](https://adr.github.io/adr-templates/). + +We feel that it is important to document these decisions to help future GTN maintainers understand the context and consequences of the decisions made in the past. + +A number of our decisions were made with very explicit intentions, usually to prioritise contributors and ensure they have the best possible experience, maximising this over technical complexity and engineering efforts that are required to support it. + +Most of our ADRs follow this pattern: Learners and Contributors come first, developers and deployers will be considered where possible. diff --git a/topics/contributing/faqs/gtn-slow-incremental.md b/topics/contributing/faqs/gtn-slow-incremental.md new file mode 100644 index 00000000000000..0833c916827b1d --- /dev/null +++ b/topics/contributing/faqs/gtn-slow-incremental.md @@ -0,0 +1,9 @@ +--- +title: "Slow incremental builds" +area: jekyll +box_type: tip +layout: faq +contributors: [hexylena] +--- + +Sometimes cleaning Jekyll's cache can improve slow (~60s) incremental build times. `jekyll clean` will do this. If you continue to experience `--incremental` build (`make serve-quick`) time issues, please let us know! diff --git a/topics/contributing/maintainer.md b/topics/contributing/maintainer.md new file mode 100644 index 00000000000000..80d834fbcdd3a6 --- /dev/null +++ b/topics/contributing/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: contributing +--- + diff --git a/topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.md b/topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.md index 3f43595795b8ef..b0f569500675cf 100644 --- a/topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.md +++ b/topics/contributing/tutorials/create-new-tutorial-content/faqs/icons_list.md @@ -14,6 +14,8 @@ To use icons in your tutorial, take the name of the icon, 'details' in this exam Some icons have multiple aliases, any may be used, but we'd suggest trying to choose the most semantically appropriate one in case Galaxy later decides to change the icon. +New icons can be added in `_config.yaml`, and you can search for the corresponding icons at [FontAwesome](https://fontawesome.com/v4/icons/) + The following icons are currently available: <div class="row"> @@ -25,5 +27,3 @@ The following icons are currently available: </div> {% endfor %} </div> - -New icons can be added in `_config.yaml`, and you can search for the corresponding icons at https://fontawesome.com/v4/icons/ diff --git a/topics/data-science/community.md b/topics/data-science/community.md new file mode 100644 index 00000000000000..af5100825835fb --- /dev/null +++ b/topics/data-science/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: data-science +--- + diff --git a/topics/data-science/maintainer.md b/topics/data-science/maintainer.md new file mode 100644 index 00000000000000..ca7840b555178d --- /dev/null +++ b/topics/data-science/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: data-science +--- + diff --git a/topics/data-science/tutorials/sql-advanced/tutorial.md b/topics/data-science/tutorials/sql-advanced/tutorial.md index d388a3defc22f1..52ae188f1430cb 100644 --- a/topics/data-science/tutorials/sql-advanced/tutorial.md +++ b/topics/data-science/tutorials/sql-advanced/tutorial.md @@ -89,8 +89,6 @@ tags: - SQL --- -{% include _includes/quiz.html id="recap.yml" %} - > <comment-title></comment-title> > > This tutorial is **significantly** based on [the Carpentries](https://carpentries.org) [Databases and SQL](https://github.com/swcarpentry/sql-novice-survey/) lesson, which is licensed CC-BY 4.0. @@ -1003,5 +1001,7 @@ this technique is outside the scope of this chapter. -- Try solutions here! ``` +{% include _includes/quiz.html id="recap.yml" %} + [create-table]: https://www.sqlite.org/lang_createtable.html [drop-table]: https://www.sqlite.org/lang_droptable.html diff --git a/topics/data-science/tutorials/sql-basic/tutorial.md b/topics/data-science/tutorials/sql-basic/tutorial.md index 961f32233ebfe2..4c6ce1b8a4b599 100644 --- a/topics/data-science/tutorials/sql-basic/tutorial.md +++ b/topics/data-science/tutorials/sql-basic/tutorial.md @@ -1162,3 +1162,5 @@ detail in [the next section](#). ```sql -- Try solutions here! ``` + +{% include _includes/quiz.html id="recap.yml" %} diff --git a/topics/dev/community.md b/topics/dev/community.md new file mode 100644 index 00000000000000..a49584e07f2d7f --- /dev/null +++ b/topics/dev/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: dev +--- + diff --git a/topics/dev/maintainer.md b/topics/dev/maintainer.md new file mode 100644 index 00000000000000..b02cc701c06a66 --- /dev/null +++ b/topics/dev/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: dev +--- + diff --git a/topics/ecology/community.md b/topics/ecology/community.md new file mode 100644 index 00000000000000..e54073c5871ba0 --- /dev/null +++ b/topics/ecology/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: ecology +--- + diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image.PNG b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image.PNG new file mode 100644 index 00000000000000..399442e544e5b3 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image.PNG differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image1.png b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image1.png new file mode 100644 index 00000000000000..98406456a34432 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image1.png differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image2.png b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image2.png new file mode 100644 index 00000000000000..935cf4edd8d15b Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image2.png differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image3.png b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image3.png new file mode 100644 index 00000000000000..046a02632489e0 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image3.png differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image4.png b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image4.png new file mode 100644 index 00000000000000..55637030ac6724 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image4.png differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image5.png b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image5.png new file mode 100644 index 00000000000000..5d2559354e6458 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image5.png differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/image7.PNG b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image7.PNG new file mode 100644 index 00000000000000..399442e544e5b3 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/image7.PNG differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/images/imageI.png b/topics/ecology/tutorials/ENA_Biodiv_submission/images/imageI.png new file mode 100644 index 00000000000000..e1b0ff8b0b2433 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/images/imageI.png differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx b/topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx new file mode 100644 index 00000000000000..6084e6cb9dfba1 Binary files /dev/null and b/topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx differ diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.bib b/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.bib new file mode 100644 index 00000000000000..9206b0b6e4cae4 --- /dev/null +++ b/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.bib @@ -0,0 +1,42 @@ + +# This is the bibliography file for your tutorial. +# +# To add bibliography (bibtex) entries here, follow these steps: +# 1) Find the DOI for the article you want to cite +# 2) Go to https://doi2bib.org and fill in the DOI +# 3) Copy the resulting bibtex entry into this file +# +# To cite the example below, in your tutorial.md file +# use {% cite Batut2018 %} +# +# If you want to cite an online resourse (website etc) +# you can use the 'online' format (see below) +# +# You can remove the examples below + +@article{Batut2018, + doi = {10.1016/j.cels.2018.05.012}, + url = {https://doi.org/10.1016/j.cels.2018.05.012}, + year = {2018}, + month = jun, + publisher = {Elsevier {BV}}, + volume = {6}, + number = {6}, + pages = {752--758.e1}, + author = {B{\'{e}}r{\'{e}}nice Batut and Saskia Hiltemann and Andrea Bagnacani and Dannon Baker and Vivek Bhardwaj and + Clemens Blank and Anthony Bretaudeau and Loraine Brillet-Gu{\'{e}}guen and Martin {\v{C}}ech and John Chilton + and Dave Clements and Olivia Doppelt-Azeroual and Anika Erxleben and Mallory Ann Freeberg and Simon Gladman and + Youri Hoogstrate and Hans-Rudolf Hotz and Torsten Houwaart and Pratik Jagtap and Delphine Larivi{\`{e}}re and + Gildas Le Corguill{\'{e}} and Thomas Manke and Fabien Mareuil and Fidel Ram{\'{i}}rez and Devon Ryan and + Florian Christoph Sigloch and Nicola Soranzo and Joachim Wolff and Pavankumar Videm and Markus Wolfien and + Aisanjiang Wubuli and Dilmurat Yusuf and James Taylor and Rolf Backofen and Anton Nekrutenko and Bj\"{o}rn Gr\"{u}ning}, + title = {Community-Driven Data Analysis Training for Biology}, + journal = {Cell Systems} +} + +@online{gtn-website, + author = {GTN community}, + title = {GTN Training Materials: Collection of tutorials developed and maintained by the worldwide Galaxy community}, + url = {https://training.galaxyproject.org}, + urldate = {2021-03-24} +} diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.md b/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.md new file mode 100644 index 00000000000000..baa7c2b7601030 --- /dev/null +++ b/topics/ecology/tutorials/ENA_Biodiv_submission/tutorial.md @@ -0,0 +1,393 @@ +--- +layout: tutorial_hands_on + +title: Data submission using ENA upload Tool +questions: +- How to prepare sequences for submission to ENA? +- How to upload raw sequences to ENA? +objectives: +- Manage sequencing files (ab1, FASTQ, FASTA, FASTQ.GZ) +- Clean sequences in an automated and reproducible manner +- Perform alignments for each sequence +- Have the necessary sequence format to submit to ENA +- Submit raw reads to ENA using the ENA upload Tool +time_estimation: 2h +key_points: +- Clean raw ab1 sequences and compare filtered sequences to NCBI nucleotidic database +- Submit cleaned and unique sequences to European Nucleotide Archive (ENA) resource +contributions: + authorship: + - Najatamk + - yvanlebras + testing: + - PaulineSGN + - yvanlebras + funding: + - pndb + +subtopic: ecologymetadatamgt +--- + + +This tutorial will guide you through the necessary steps to manage and prepare sequencing files (ab1, FASTQ, FASTA) for submission to the genomic database ENA. +This workflow will take you from raw sequences in AB1 format through all the necessary steps to integrate these sequences into the ENA genomic database. We will convert the files into FASTQ and FASTA formats after performing quality control. +Additionally, we will perform alignments with the NCBI database to ensure the accuracy of your sequences.You will then need to fill a metadata Excel template to use the ENA upload Tool. +The worklow is made of 17 Galaxy tools, we will present them and explain what they do. +The goal is to present an accessible and reproductible workflow for data submission. + +> <agenda-title></agenda-title> +> +> In this tutorial, we will cover: +> +> 1. TOC +> {:toc} +> +{: .agenda} + +# Prepare raw data + +> <hands-on-title> Data Upload </hands-on-title> +> +> 1. **Create a new history** for this tutorial +> +> {% snippet faqs/galaxy/histories_create_new.md %} +> +> 2. **Import** the raw sequences files. +> +> ``` +> https://data.indores.fr/api/access/datafile/3673 +> https://data.indores.fr/api/access/datafile/3609 +> ``` +> +> {% snippet faqs/galaxy/datasets_import_via_link.md %} +> +> 3. **Rename** {% icon galaxy-pencil %} your datafiles +> - `3673` becomes `A2_RC_8F2_B.pl_HCOI.ab1` +> - `3609` becomes `A12_RC_9G4_B.md_HCOI.ab1` +> +> {% snippet faqs/galaxy/datasets_rename.md %} +> +> 4. **Check the datatype** +> - Make sure it is `ab1`, and change it if not. +> +> {% snippet faqs/galaxy/datasets_change_datatype.md %} +> +> 5. **Build a Collection** containing these two files, you can ame i "ab1" for example +> +> {% snippet faqs/galaxy/collections_build_list.md %} +> +{: .hands_on} + + +## Tools used in the "Prepare Data submission" Workflow + +Following steps take as input ab1 sequences files and produce filtered FastQ and Fasta files so sequences passing the quality checks are compared to NCBI nucleotidic database using Blastn operation. + +### Converting Ab1 files to FASTQ + +> <hands-on-title> ab1 to FASTQ converter </hands-on-title> +> +> 1. {% tool [ab1 to FASTQ converter](toolshed.g2.bx.psu.edu/repos/ecology/ab1_fastq_converter/ab1_fastq_converter/1.20.0) %} with the following parameters: +> - {% icon param-collection %} *"Input ab1 file"*: `ab1` data collection created at the previous step +> +{: .hands_on} + + +### Quality Control + +We are doing a first Quality control on the raw files using FastQC and MultiQC. + +> <hands-on-title> FastQC </hands-on-title> +> 1. {% tool [FastQC](toolshed.g2.bx.psu.edu/repos/devteam/fastqc/fastqc/0.74+galaxy0) %} with the following parameters: +> - {% icon param-file %} *"Raw read data from your current history"*: `ab1.fastq` data collection created at the previous step +> +> 2. {% tool [MultiQC](toolshed.g2.bx.psu.edu/repos/iuc/multiqc/multiqc/1.11+galaxy1) %} with the following parameters: +> - In *"Results"*: +> - {% icon param-repeat %} *"Insert Results"* +> - *"Which tool was used generate logs?"*: `FastQC` +> - In *"FastQC output"*: +> - {% icon param-file %} *"RawData FastQC output"*: `FastQC on collection X:` data collection created at the previous step +> +> 3. *Check on the HTML files the general quality statistics of your sequences* +> +{: .hands_on} + + +> <question-title> Question </question-title> +> +> 1. What is the quality of your sequences? +> 2. Do you have adapters? +> +> > <solution-title></solution-title> +> > +> > 1. Quality is quite good looking at the "status checks" section of MultiQC. As expected (because here we only have one sequence by file) "Per base sequence Content" and "Overrepresented sequences" Sections are "bad" for both sequences files. "adapter content" section also show a "bad" result for A2_RC_8F2_B.pl_HCOI.ab1 file. +> > +> > 2. A2_RC_8F2_B.pl_HCOI.ab1 file seems to have adapters in it. +> > +> {: .solution} +{: .question} + +# Cleaning the Data + +## Cutadapt + +Cutadapt enables the removal of adapters, polyA tails, and other artifacts from sequences. The tool also filters reads based on quality. + +> <hands-on-title> Cutadapt </hands-on-title> +> +> 1. {% tool [Cutadapt](toolshed.g2.bx.psu.edu/repos/lparsons/cutadapt/cutadapt/4.8+galaxy0) %} with the following parameters: +> - {% icon param-collection %} *"FASTQ/A file"*: the collection with your data (output of {% icon tool %} **ab1 to FastQ converter**) +> - **"Single-end or Paired-end reads?"**: `Single-end` +> - In **"Other Read Trimming Options"**: +> - **"Quality cutoff(s) (R1)"**: `30` +> - **"Shortening reads to a fixed length"**: `Disabled` +> +> > <comment-title> Suggestions </comment-title> +> > +> > You may consider changing these parameters depending on the quality of your dataset. +> {: .comment} +> +{: .hands_on} + +> <comment-title> Quality Control </comment-title> +> +> We do a second quality control similar to the first one to check the quality of the sequences after cleaning them. +{: .comment} + + +## Quality Control with FastQC and MultiQC + +> <hands-on-title> FastQC </hands-on-title> +> +> 1. {% tool [FastQC](toolshed.g2.bx.psu.edu/repos/devteam/fastqc/fastqc/0.74+galaxy0) %} with the following parameters: +> - {% icon param-collection %} *"Raw read data from your current history"*: output from {% icon tool%} **Cutadapt** +> +> 2. {% tool [MultiQC](toolshed.g2.bx.psu.edu/repos/iuc/multiqc/multiqc/1.11+galaxy1) %} with the following parameters: +> - In *"Results"*: +> - *"Which tool was used generate logs?"*: `FastQC` +> - {% icon param-repeat %} *"Insert FastQC output"* +> - {% icon param-collection %} *"FastQC output"*: the `raw` output from {% icon tool %} **FastQC** +> +> > <comment-title> Comment </comment-title> +> > +> > You should notice an improvement on the quality of your sequences. +> {: .comment} +> +{: .hands_on} + +## Filtering the collection + + +> <hands-on-title> Filter empty datasets </hands-on-title> +> +> 1. {% tool [Filter empty datasets](__FILTER_EMPTY_DATASETS__) %} with the following parameters +> - {% icon param-collection %} *"Input Collection"*: output collection from Cutadapt step +> +> 2. {% tool [FASTQ Groomer](toolshed.g2.bx.psu.edu/repos/devteam/fastq_groomer/fastq_groomer/1.1.5+galaxy2) %} with the following parameters: +> - {% icon param-collection %} *"File to groom"* : output collection from the {% icon tool %} **Filter empty datasets** +> +> This step is notably there to produce "standardized" fastqsanger sequences files so we can then use other tools accepting only such data format. +> +> 3. {% tool [Filter FASTQ](toolshed.g2.bx.psu.edu/repos/devteam/fastq_filter/fastq_filter/1.1.5) %} with the following parameters: +> - *"FASTQ File"*: output collecton from {% icon tool %} **FastQ Groomer** +> - *"Minimum size"*: `300` +> +> > <comment-title> Comment </comment-title> +> > +> > Here we descide to only keep sequences of 300bp or above, you may change this parameter depending on your dataset +> {: .comment} +> +{: .hands_on} + + +### Changing files names + +> <hands-on-title>Extract element identifiers and remove extensions</hands-on-title> +> +> 1. {% tool [Extract element identifiers](toolshed.g2.bx.psu.edu/repos/iuc/collection_element_identifiers/collection_element_identifiers/0.0.2) %} +> - {% icon param-collection %} *"Dataset collection"*: output from the previous step +> +> 2. {% tool [Regex Find And Replace](toolshed.g2.bx.psu.edu/repos/galaxyp/regex_find_replace/regex1/1.0.3) %} with the following parameters: +> - *"Select lines from"*: output of the previous step +> - In *"Check"*: +> - {% icon param-repeat %} *"Insert Check"* +> - *"Find Regex"*: `.ab1` +> - *"Replacement"*: `` +> +> > <comment-title> Comment </comment-title> +> > +> > This is to ensure that all your files names end with .fastq.gz +> {: .comment} +> +> 3. {% tool [Paste](Paste1) %} with the following parameters: +> - {% icon param-file %} *"Paste"*: the file from {% icon tool %} **Extract element identifiers** +> - {% icon param-file %} *"and"*: the file from {% icon tool %} **Regex Find And Replace** +> - {% icon param-select %} *"Delimited by"*: Tab +> +> 4. **Check the datatype** +> - should be 'tabular'. If not, change it now. +> +> {% snippet faqs/galaxy/datasets_change_datatype.md %} +> +{: .hands_on} + + + +> <hands-on-title> Relabel identifiers </hands-on-title> +> +> 1. {% tool [Relabel identifiers](__RELABEL_FROM_FILE__) %} with the following parameters: +> - {% icon param-collection %} *"Input Collection"*: output from {% icon tool %} **Filter FastQ** +> - *"How should the new labels be specified?"*: `Map original identifiers to new ones using a two column table.` +> +{: .hands_on} + + +## Alignments on NCBI database + +> <hands-on-title> NCBI BLAST alignment </hands-on-title> +> +> 1. {% tool [FASTQ to FASTA](toolshed.g2.bx.psu.edu/repos/devteam/fastqtofasta/fastq_to_fasta_python/1.1.5) %} with the following parameters: +> - {% icon param-collection %} *"Input FASTQ File"*: output collection from {% icon tool %} **Relabel Identifiers** +> +> 2. {% tool [NCBI BLAST+ blastn](toolshed.g2.bx.psu.edu/repos/devteam/ncbi_blast_plus/ncbi_blastn_wrapper/2.14.1+galaxy2) %} with the following parameters: +> - {% icon param-collection %} Nucleotide query sequence(s): output from the previous step +> - *"Subject database/sequences"*: `Locally installed BLAST database` +> - *"Nucleotide BLAST database"*: `NCBI NT (01 Sep 2023)` +> - *"Output format"*: `Tabular (extended 25 columns)` +> - *"Advanced Options"*: `Hide Advanced Options` +{: .hands_on} + + +> <hands-on-title> Extracting best hits </hands-on-title> +> +> 1. {% tool [Unique](toolshed.g2.bx.psu.edu/repos/bgruening/unique/bg_uniq/0.3) %} with the following parameters: +> - {% icon param-collection %} *"File to scan for unique values"*: output from the previous step +> - *"Advanced Options"*: `Show Advanced Options` +> - *"Column start"*: `c1` +> - *"Column end"*: `c1` +> +{: .hands_on} + +## Workflow Outputs + +1. **Collection of raw FASTQ files:** Input AB1 files converted into FASTQ files. + +2. **Collection of FASTQ files (after quality control)**: Renamed Fastq files ready for submission after quality control and filtering. + +3. **Collection of FASTA files**: FASTQ files converted into FASTA format. Used for conducting BLAST alignments. + +4. **FastQC Quality Control Results** before and after cleaning: Both raw FastQC results and HTML reports are created + +5. **MultiQC Quality Control Results** before and after cleaning: Both raw MultiQC statistics and HTML report are created + +6. **Raw Blast Results**: Results of BLAST alignments conducted on our sequences. Columns names are: + + | Column | NCBI name | Description | + |--------|--------------|------------------------------------------------| + | 1 | qaccver | Query accession dot version | + | 2 | saccver | Subject accession dot version (database hit) | + | 3 | pident | Percentage of identical matches | + | 4 | length | Alignment length | + | 5 | mismatch | Number of mismatches | + | 6 | gapopen | Number of gap openings | + | 7 | qstart | Start of alignment in query | + | 8 | qend | End of alignment in query | + | 9 | sstart | Start of alignment in subject (database hit) | + | 10 | send | End of alignment in subject (database hit) | + | 11 | evalue | Expectation value (E-value) | + | 12 | bitscore | Bit score | + | 13 | sallseqid | All subject Seq-id(s), separated by a ';' | + | 14 | score | Raw score | + | 15 | nident | Number of identical matches | + | 16 | positive | Number of positive-scoring matches | + | 17 | gaps | Total number of gaps | + | 18 | ppos | Percentage of positive-scoring matches | + | 19 | qframe | Query frame | + | 20 | sframe | Subject frame | + | 21 | qseq | Aligned part of query sequence | + | 22 | sseq | Aligned part of subject sequence | + | 23 | qlen | Query sequence length | + | 24 | slen | Subject sequence length | + | 25 | salltitles | All subject title(s), separated by a '<>' | + +7. **Filtered Blast Results** +Files containing the closest homologous sequences. + +8. **Collection of Fastq files** +Contains filtered sequences. + +# How to use ENA upload Tool + +## Adding ENA "Webin" credentials to your Galaxy user information + +> <comment-title> Having an ENA Submission Account </comment-title> +> +> Make sure you have a submission account with the European Nucleotide Archive (ENA). You will need the identifier and the password, available through https://www.ebi.ac.uk/ena/submit/webin/login. +> +{: .comment} + + +> <hands-on-title> Add your "WEBIN" credentials to your Galaxy account </hands-on-title> +> **Instructions:** +> - From the Menu, click on "User" > "Preferences". Click on "Manage Information". Scroll down to "Your ENA Webin account details" and enter your ENA "Webin" identifier and password. +> ![Adding ENA Webin credentials](./images/imageI.png) +{: .hands_on} + +## Submitting using a metadata template file + +For this tutorial we will use the ENA default sample checklist. + +![Excel Metadata template](./images/image2.png) + +**Note:** It is crucial to fill in all the fields marked "Mandatory" and ensure that the sequence names match exactly those indicated in the Excel file. + +> <comment-title> ENA Metadata Templates </comment-title> +> +> You can find metadata templates for each checklist in the [ELIXIR-Belgium GitHub repository](https://github.com/ELIXIR-Belgium/ENA-metadata-templates) +> +> 1. Direct download link of the [ENA default sample checklist]( https://github.com/ELIXIR-Belgium/ENA-metadata-templates/raw/main/templates/ERC000011/metadata_template_ERC000011.xlsx) +> +> 2. Direct download link of the [ENA default sample checklist filled with elements for the training](https://github.com/galaxyproject/training-material/raw/24776cf161e38ac0449755749d23e851400020aa/topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx) +> +> You will need to import this file into your Galaxy history. Then, use the ENA Upload Tool to proceed with the submission. +> +{: .comment} + + + +> <hands-on-title> Excel Metadata Template </hands-on-title> +> +> 1. Import the ENA default sample checklist file. +> +> ``` +> https://github.com/galaxyproject/training-material/raw/24776cf161e38ac0449755749d23e851400020aa/topics/ecology/tutorials/ENA_Biodiv_submission/metadata_GdBqCOI_ERC000011_Test.xlsx +> ``` +> +> {% snippet faqs/galaxy/datasets_import_via_link.md %} +> +> 2. {% tool [ENA Upload tool](toolshed.g2.bx.psu.edu/repos/iuc/multiqc/multiqc/1.11+galaxy1) %} with the following parameters: +> - *"Action to execute"*: `Add new (meta)data` +> - *"Select the metadata input method"*: `Excel file` +> - *"Select the ENA sample checklist"*: `ENA default sample checklist (ERC000011)` +> - *"Select Excel file based on template"*: `metadata_GdBqCOI_ERC000011_Test.xlsx` +> - *"Select input data"*: `Dataset or dataset collection` +> - *"Add .fastq (.gz, .bz2) extension to the Galaxy dataset names to match the ones described in the input tables?"*: `Yes` +> +> > <comment-title> Datatype </comment-title> +> > +> > The ENA upload tool will then automatically compress fastq sequences files into .fastq.gz format before submission +> {: .comment} +> +> > <warning-title> Danger: Submit to ENA test server! </warning-title> +> > We suggest you first submit to the [ENA test server](https://wwwdev.ebi.ac.uk/ena/submit/webin/) before making a public submission! Submission can be seen in `Dashboard/Study Report` +> {: .warning} +> +> ![ENA Upload tool](./images/image3.png) +> +{: .hands_on} + + + +# Conclusion + +This tutorial guides you through quality check and preparing raw data files for ENA submission. You can then verify that your sequences have been successfully sent by logging into the Test ENA portal (https://wwwdev.ebi.ac.uk/ena/submit/webin/login) and navigating to the Study Report section. diff --git a/topics/ecology/tutorials/ENA_Biodiv_submission/workflows/index.md b/topics/ecology/tutorials/ENA_Biodiv_submission/workflows/index.md new file mode 100644 index 00000000000000..e092e0ae66ddd4 --- /dev/null +++ b/topics/ecology/tutorials/ENA_Biodiv_submission/workflows/index.md @@ -0,0 +1,3 @@ +--- +layout: workflow-list +--- diff --git a/topics/epigenetics/community.md b/topics/epigenetics/community.md new file mode 100644 index 00000000000000..7099b392eb62a4 --- /dev/null +++ b/topics/epigenetics/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: epigenetics +--- + diff --git a/topics/epigenetics/tutorials/cut_and_run/tutorial.md b/topics/epigenetics/tutorials/cut_and_run/tutorial.md index 02fe29f9bc160b..5af4f043af34f5 100644 --- a/topics/epigenetics/tutorials/cut_and_run/tutorial.md +++ b/topics/epigenetics/tutorials/cut_and_run/tutorial.md @@ -32,7 +32,6 @@ abbreviations: --- -# Introduction In many organism, {TF} play an important tole in the regulation of the gene expression. In human, we have up to 2,800 proteins and more than 1,600 are TF ([list of transcription factors](https://en.wikipedia.org/wiki/List_of_human_transcription_factors)), although the number might change over timer. Investigating the role of TFs, such as [GATA1](https://en.wikipedia.org/wiki/GATA1), is a very important task to understand the regulatory mechanisms in the cell and thus ascertain @@ -179,7 +178,7 @@ The FastQC report pointed out that we have in our data some standard Illumina ad > <hands-on-title>Task description</hands-on-title> > -> 1. {% tool [Trim Galore!]( https://toolshed.g2.bx.psu.edu/view/bgruening/trim_galore/cd7e644cae1d) %} with the following parameters: +> 1. {% tool Trim Galore! %} with the following parameters: > - *"Is this library paired- or single-end?"*: `Paired Collection` > - *"Select a paired collection"*: select `2 PE fastqs` > - In *"Adapter sequence to be trimmed"*: `Illumina universal` diff --git a/topics/evolution/community.md b/topics/evolution/community.md new file mode 100644 index 00000000000000..05b816279e487c --- /dev/null +++ b/topics/evolution/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: evolution +--- + diff --git a/topics/fair/community.md b/topics/fair/community.md new file mode 100644 index 00000000000000..eb773b706a357f --- /dev/null +++ b/topics/fair/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: fair +--- + diff --git a/topics/fair/maintainer.md b/topics/fair/maintainer.md new file mode 100644 index 00000000000000..9c5e2419c71765 --- /dev/null +++ b/topics/fair/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: fair +--- + diff --git a/topics/fair/tutorials/dataplant-arcs/tutorial.md b/topics/fair/tutorials/dataplant-arcs/tutorial.md index afffab5f6372dd..1167028b36f394 100644 --- a/topics/fair/tutorials/dataplant-arcs/tutorial.md +++ b/topics/fair/tutorials/dataplant-arcs/tutorial.md @@ -35,6 +35,7 @@ contributions: #editing: funding: - nfdi4plants + - dfg subtopic: dataplant diff --git a/topics/galaxy-interface/community.md b/topics/galaxy-interface/community.md new file mode 100644 index 00000000000000..e727024606770d --- /dev/null +++ b/topics/galaxy-interface/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: galaxy-interface +--- + diff --git a/topics/galaxy-interface/tutorials/upload-data-to-ena/tutorial.md b/topics/galaxy-interface/tutorials/upload-data-to-ena/tutorial.md index a5a838619bf7da..ce7ef601fff54d 100644 --- a/topics/galaxy-interface/tutorials/upload-data-to-ena/tutorial.md +++ b/topics/galaxy-interface/tutorials/upload-data-to-ena/tutorial.md @@ -255,7 +255,7 @@ We will link it to the reads submitted in the first step using the accession num > https://zenodo.org/record/6912963/files/SRR10903401.fasta > ``` > 2. {% icon galaxy-eye %} Open the 'ENA submsission receipt' and find the Study and Sample accession numbers from the raw data submission. -> 3. {% tool [Submit consensus sequence to ENA](toolshed.g2.bx.psu.edu/repos/ieguinoa/ena_webin_cli/ena_webin_cli/7d751b5943b0) %}: +> 3. {% tool [Submit consensus sequence to ENA](toolshed.g2.bx.psu.edu/repos/ieguinoa/ena_webin_cli/ena_consensus_submit/4.3.0) %}: > - *"Submit to test ENA server?"*: `yes` > - *"Validate files and metadata but do not submit"*: `no` > - Fill the assembly metadata. For our assembly: diff --git a/topics/genome-annotation/community.md b/topics/genome-annotation/community.md new file mode 100644 index 00000000000000..661e13731f1823 --- /dev/null +++ b/topics/genome-annotation/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: genome-annotation +--- + diff --git a/topics/imaging/community.md b/topics/imaging/community.md new file mode 100644 index 00000000000000..11f520302f2b93 --- /dev/null +++ b/topics/imaging/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: imaging +--- + diff --git a/topics/imaging/tutorials/omero-suite/tutorial.md b/topics/imaging/tutorials/omero-suite/tutorial.md index a99793075468e1..3957836fe5512c 100644 --- a/topics/imaging/tutorials/omero-suite/tutorial.md +++ b/topics/imaging/tutorials/omero-suite/tutorial.md @@ -21,6 +21,7 @@ contributions: - bgruening funding: - nfdi4bioimage + - dfg --- The efficient and accurate treatment of microscopy metadata is of great importance, as it provides insights that are essential for effective image management, search, organisation, diff --git a/topics/introduction/community.md b/topics/introduction/community.md new file mode 100644 index 00000000000000..e66e3fe8aa27a3 --- /dev/null +++ b/topics/introduction/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: introduction +--- + diff --git a/topics/introduction/tutorials/galaxy-intro-101/tutorial.md b/topics/introduction/tutorials/galaxy-intro-101/tutorial.md index 3304938a00c0c5..7c0c8838b57242 100644 --- a/topics/introduction/tutorials/galaxy-intro-101/tutorial.md +++ b/topics/introduction/tutorials/galaxy-intro-101/tutorial.md @@ -367,7 +367,7 @@ Now that we have a list of all exons, and the number of SNPs they contain, we wo > <hands-on-title>Sorting</hands-on-title> > -> 1. {% tool [Sort](toolshed.g2.bx.psu.edu/repos/bgruening/text_processing/tp_sort_header_tool/1.1.1sort1) %} data in ascending or descending order: +> 1. {% tool [Sort](toolshed.g2.bx.psu.edu/repos/bgruening/text_processing/tp_sort_header_tool/1.1.1) %} data in ascending or descending order: > > - *"Sort Query"*: Output from **Datamash** {% icon tool %} > - In *"Column selections"* set the following: diff --git a/topics/materials-science/community.md b/topics/materials-science/community.md new file mode 100644 index 00000000000000..5657f43aa62078 --- /dev/null +++ b/topics/materials-science/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: materials-science +--- + diff --git a/topics/metabolomics/community.md b/topics/metabolomics/community.md new file mode 100644 index 00000000000000..18428fde1d3a57 --- /dev/null +++ b/topics/metabolomics/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: metabolomics +--- + diff --git a/topics/microbiome/community.md b/topics/microbiome/community.md new file mode 100644 index 00000000000000..7c4769123f1ffd --- /dev/null +++ b/topics/microbiome/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: microbiome +--- + diff --git a/topics/proteomics/community.md b/topics/proteomics/community.md new file mode 100644 index 00000000000000..bcb0ff72e28650 --- /dev/null +++ b/topics/proteomics/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: proteomics +--- + diff --git a/topics/sequence-analysis/community.md b/topics/sequence-analysis/community.md new file mode 100644 index 00000000000000..0d4a2c80ea7d9f --- /dev/null +++ b/topics/sequence-analysis/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: sequence-analysis +--- + diff --git a/topics/single-cell/community.md b/topics/single-cell/community.md new file mode 100644 index 00000000000000..703a3dc48657ef --- /dev/null +++ b/topics/single-cell/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: single-cell +--- + diff --git a/topics/single-cell/faqs/amplification.md b/topics/single-cell/faqs/amplification.md index c10b6dd11a31d3..e9fd8d4095e7ef 100644 --- a/topics/single-cell/faqs/amplification.md +++ b/topics/single-cell/faqs/amplification.md @@ -3,7 +3,7 @@ redirect_from: - /topics/transcriptomics/faqs/amplification title: Why is amplification more of an issue in scRNA-seq than RNA-seq? -area: Single-Cell RNA +area: Analysis box_type: tip layout: faq contributors: [nomadscientist,mtekman,rahmot] diff --git a/topics/single-cell/metadata.yaml b/topics/single-cell/metadata.yaml index 7e510aea57ab7b..91af47df863541 100644 --- a/topics/single-cell/metadata.yaml +++ b/topics/single-cell/metadata.yaml @@ -4,14 +4,42 @@ type: use topic_type: technology title: Single Cell -learning_path_cta: intro_single_cell +learning_path_ctas: + For Beginners: intro_single_cell + For Intermediate Users: beyond_single_cell + For Coding Enthusiasts: reloaded_single_cell + + +# Using the community CTAs feature opts you into managing your own requirements list here a bit manually +community_ctas: + - description: | + **Pre-requisites:** If you've never used Galaxy before, first try the: + link: https://training.galaxyproject.org/training-material/topics/introduction/ + link_text: Introduction to Galaxy + icon: tutorial + - description: | + The Single Cell & Galaxy Communities are also available via Slack + link: https://gxy.io/gtn-slack + link_text: GTN Slack Workspace + icon: comment + - description: | + Come chat Single Cell with us on Matrix + link: https://matrix.to/#/#spoc3:matrix.org + link_text: Community Matrix Chat + icon: comment + - description: | + What else do you want to see? Let us know! + link: https://docs.google.com/spreadsheets/d/15hqgqA-RMDhXR-ylKhRF-Dab9Ij2arYSKiEVoPl2df4/edit?usp=sharing + link_text: Tool/Tutorial Requests + icon: tool + gitter: Galaxy-Training-Network_galaxy-single-cell:gitter.im summary: | Training material and practicals for all kinds of single cell analysis (particularly scRNA-seq!). - What else do you want to see? You can submit tool or tutorial requests, or upvote other requests, on our <i class="fa fa-wrench" aria-hidden="true"></i> [Single-Cell & Spatial Omics Tool Request Spreadsheet](https://docs.google.com/spreadsheets/d/15hqgqA-RMDhXR-ylKhRF-Dab9Ij2arYSKiEVoPl2df4/edit?usp=sharing). - {: .alert.alert-success} +# What else do you want to see? You can submit tool or tutorial requests, or upvote other requests, on our <i class="fa fa-wrench" aria-hidden="true"></i> [Single-Cell & Spatial Omics Tool Request Spreadsheet](). +# {: .alert.alert-success} #docker_image: "quay.io/galaxy/transcriptomics-training" diff --git a/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data-tests.yml b/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data-tests.yml new file mode 100644 index 00000000000000..8a66ef2870e10e --- /dev/null +++ b/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data-tests.yml @@ -0,0 +1,27 @@ +- doc: Test outline for CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data + job: + Mito-counted AnnData: + class: File + path: https://zenodo.org/record/7053673/files/Mito-counted_AnnData + filetype: h5ad + outputs: + Final cell annotated object: + asserts: + has_h5_keys: + keys: "obs/cell_type" + keys: "obs/genotype" + keys: "var/dispersions" + keys: "uns/louvain" + keys: "uns/umap" + Markers - genotype - named: + asserts: + has_n_lines: + value: 101 + has_n_columns: + value: 9 + Markers - cluster - named: + asserts: + has_n_lines: + value: 401 + has_n_columns: + value: 9 \ No newline at end of file diff --git a/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,_Plot_and_Explore_Single-cell_RNA-seq_Data.ga b/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.ga similarity index 82% rename from topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,_Plot_and_Explore_Single-cell_RNA-seq_Data.ga rename to topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.ga index b806556141af9f..a39aefc2670678 100644 --- a/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,_Plot_and_Explore_Single-cell_RNA-seq_Data.ga +++ b/topics/single-cell/tutorials/scrna-case_basic-pipeline/workflows/CS3_Filter,-Plot-and-Explore-Single-cell-RNA-seq-Data.ga @@ -1,8 +1,20 @@ { "a_galaxy_workflow": "true", "annotation": "Updated tool versions Aug 24 2022", + "comments": [], + "creator": [ + { + "class": "Person", + "identifier": "000-0002-8170-8806", + "name": "Wendi Bacon" + } + ], "format-version": "0.1", + "license": "CC-BY-4.0", "name": "CS3_Filter, Plot and Explore Single-cell RNA-seq Data", + "report": { + "markdown": "\n# Workflow Execution Report\n\n## Workflow Inputs\n```galaxy\ninvocation_inputs()\n```\n\n## Workflow Outputs\n```galaxy\ninvocation_outputs()\n```\n\n## Workflow\n```galaxy\nworkflow_display()\n```\n" + }, "steps": { "0": { "annotation": "", @@ -28,6 +40,7 @@ "tool_version": null, "type": "data_input", "uuid": "60316d4a-07b0-4eea-a889-eef54798c5e0", + "when": null, "workflow_outputs": [] }, "1": { @@ -66,6 +79,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "99ffbf79-3be8-4905-b978-e56b02327314", + "when": null, "workflow_outputs": [ { "label": null, @@ -114,10 +128,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"categories\": [], \"export_mtx\": \"false\", \"force_recalc\": \"false\", \"gene_name\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"log1p_n_genes_by_counts\", \"min\": \"5.7\", \"max\": \"20.0\"}], \"save_layer\": null, \"save_raw\": \"false\", \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"categories\": [], \"export_mtx\": false, \"force_recalc\": false, \"gene_name\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"log1p_n_genes_by_counts\", \"min\": \"5.7\", \"max\": \"20.0\"}], \"save_layer\": null, \"save_raw\": false, \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "ab02dab9-1c72-4987-929c-6b67baae1e6d", + "when": null, "workflow_outputs": [ { "label": "Genes-filtered Object", @@ -166,10 +181,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.scatter\", \"__current_case__\": 0, \"type\": {\"type\": \"xy\", \"__current_case__\": 0, \"x\": \"log1p_total_counts\", \"y\": \"log1p_n_genes_by_counts\", \"color\": \"pct_counts_mito\", \"layers\": {\"use_layers\": \"false\", \"__current_case__\": 1}}, \"use_raw\": \"false\", \"sort_order\": \"true\", \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"color_map\": \"None\", \"palette\": \"default\", \"frameon\": \"true\", \"size\": null, \"title\": \"\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.scatter\", \"__current_case__\": 0, \"type\": {\"type\": \"xy\", \"__current_case__\": 0, \"x\": \"log1p_total_counts\", \"y\": \"log1p_n_genes_by_counts\", \"color\": \"pct_counts_mito\", \"layers\": {\"use_layers\": \"false\", \"__current_case__\": 1}}, \"use_raw\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"color_map\": \"None\", \"palette\": \"default\", \"frameon\": true, \"size\": null, \"title\": \"\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "767da31e-dc80-4d11-9f8e-8b1589029dcc", + "when": null, "workflow_outputs": [ { "label": "Scatter - genes x UMIs", @@ -218,10 +234,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.scatter\", \"__current_case__\": 0, \"type\": {\"type\": \"xy\", \"__current_case__\": 0, \"x\": \"log1p_n_genes_by_counts\", \"y\": \"pct_counts_mito\", \"color\": \"\", \"layers\": {\"use_layers\": \"false\", \"__current_case__\": 1}}, \"use_raw\": \"false\", \"sort_order\": \"true\", \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"color_map\": \"None\", \"palette\": \"default\", \"frameon\": \"true\", \"size\": null, \"title\": \"\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.scatter\", \"__current_case__\": 0, \"type\": {\"type\": \"xy\", \"__current_case__\": 0, \"x\": \"log1p_n_genes_by_counts\", \"y\": \"pct_counts_mito\", \"color\": \"\", \"layers\": {\"use_layers\": \"false\", \"__current_case__\": 1}}, \"use_raw\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"color_map\": \"None\", \"palette\": \"default\", \"frameon\": true, \"size\": null, \"title\": \"\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "ad0b1195-4bb2-46be-bb8a-8fd5de416372", + "when": null, "workflow_outputs": [ { "label": "Scatter - mito x genes", @@ -270,10 +287,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": \"false\", \"use_raw\": \"false\", \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "a6a5b2f9-9765-4f5d-b4cf-7c513b8eed3c", + "when": null, "workflow_outputs": [ { "label": "Violin - genotype - log", @@ -322,10 +340,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"batch\", \"log\": \"false\", \"use_raw\": \"false\", \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"batch\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "a6d8ecf9-1510-4c1a-8665-ddf25b0ca395", + "when": null, "workflow_outputs": [ { "label": "Violin - batch - log", @@ -370,6 +389,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "9445c5ea-8c75-48f2-b7a1-fd0e43c625c7", + "when": null, "workflow_outputs": [ { "label": null, @@ -418,10 +438,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.scatter\", \"__current_case__\": 0, \"type\": {\"type\": \"xy\", \"__current_case__\": 0, \"x\": \"log1p_total_counts\", \"y\": \"pct_counts_mito\", \"color\": \"\", \"layers\": {\"use_layers\": \"false\", \"__current_case__\": 1}}, \"use_raw\": \"false\", \"sort_order\": \"true\", \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"color_map\": \"None\", \"palette\": \"default\", \"frameon\": \"true\", \"size\": null, \"title\": \"\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.scatter\", \"__current_case__\": 0, \"type\": {\"type\": \"xy\", \"__current_case__\": 0, \"x\": \"log1p_total_counts\", \"y\": \"pct_counts_mito\", \"color\": \"\", \"layers\": {\"use_layers\": \"false\", \"__current_case__\": 1}}, \"use_raw\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"color_map\": \"None\", \"palette\": \"default\", \"frameon\": true, \"size\": null, \"title\": \"\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "7225da9d-e700-49f7-bb78-20f2954872bd", + "when": null, "workflow_outputs": [ { "label": "Scatter - mito x UMIs", @@ -466,6 +487,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "549b599e-1b3d-4be4-bfe7-91c50cef7ef7", + "when": null, "workflow_outputs": [ { "label": null, @@ -514,10 +536,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"sex\", \"log\": \"false\", \"use_raw\": \"false\", \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"sex\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "d21d8d66-0488-4382-bed4-b8534c08169f", + "when": null, "workflow_outputs": [ { "label": "Violin - sex - log", @@ -566,10 +589,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": \"false\", \"use_raw\": \"false\", \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "0ceca44a-91f3-4c85-8dfe-7c958c373fed", + "when": null, "workflow_outputs": [ { "label": "Violin - Filterbygenes", @@ -618,10 +642,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"categories\": [], \"export_mtx\": \"false\", \"force_recalc\": \"false\", \"gene_name\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"log1p_total_counts\", \"min\": \"6.3\", \"max\": \"20.0\"}], \"save_layer\": null, \"save_raw\": \"false\", \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"categories\": [], \"export_mtx\": false, \"force_recalc\": false, \"gene_name\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"log1p_total_counts\", \"min\": \"6.3\", \"max\": \"20.0\"}], \"save_layer\": null, \"save_raw\": false, \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "2b612ba2-7539-43a7-963c-da6172101f1f", + "when": null, "workflow_outputs": [ { "label": "Counts-filtered Object", @@ -674,6 +699,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "47b02be1-43e4-4bd3-a20f-ebfeddd29ad0", + "when": null, "workflow_outputs": [ { "label": "General - Filterbygenes", @@ -726,6 +752,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "85d4b86e-cff4-41bf-92d5-a4d53c72f950", + "when": null, "workflow_outputs": [ { "label": "General - Filterbycounts", @@ -774,10 +801,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"categories\": [], \"export_mtx\": \"false\", \"force_recalc\": \"false\", \"gene_name\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"pct_counts_mito\", \"min\": \"0.0\", \"max\": \"4.5\"}], \"save_layer\": null, \"save_raw\": \"false\", \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"categories\": [], \"export_mtx\": false, \"force_recalc\": false, \"gene_name\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"pct_counts_mito\", \"min\": \"0.0\", \"max\": \"4.5\"}], \"save_layer\": null, \"save_raw\": false, \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "47ab6b8d-2130-4885-b972-84f3a444b707", + "when": null, "workflow_outputs": [ { "label": "Mito-filtered Object", @@ -826,10 +854,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": \"false\", \"use_raw\": \"false\", \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "dbf56597-b286-43b9-bc04-f1e3995f7bc0", + "when": null, "workflow_outputs": [ { "label": "Violin - Filterbycounts", @@ -882,6 +911,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "d221b80d-63e9-42a8-8d4b-25f8f22b9696", + "when": null, "workflow_outputs": [ { "label": "General - Filterbymito", @@ -930,10 +960,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"categories\": [], \"export_mtx\": \"false\", \"force_recalc\": \"false\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"n_cells\", \"min\": \"3.0\", \"max\": \"1000000000.0\"}], \"save_layer\": null, \"save_raw\": \"false\", \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"categories\": [], \"export_mtx\": false, \"force_recalc\": false, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"parameters\": [{\"__index__\": 0, \"name\": \"n_cells\", \"min\": \"3.0\", \"max\": \"1000000000.0\"}], \"save_layer\": null, \"save_raw\": false, \"subsets\": [], \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "84dc3ac3-d79f-4dcd-b81b-f876de010f5e", + "when": null, "workflow_outputs": [ { "label": "Filtered Object", @@ -982,10 +1013,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": \"false\", \"use_raw\": \"false\", \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"log1p_total_counts,log1p_n_genes_by_counts,pct_counts_mito\"}, \"groupby\": \"genotype\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"1.0\"}}, \"multi_panel\": {\"multi_panel\": \"False\", \"__current_case__\": 1}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "972b30ab-cf6c-43a7-a41d-9679cbae7be6", + "when": null, "workflow_outputs": [ { "label": "Violin - Filterbymito", @@ -1038,6 +1070,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "78767f76-7f3c-416d-ac79-28f6104bb3b9", + "when": null, "workflow_outputs": [ { "label": "General - Filtered object", @@ -1078,10 +1111,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"export_mtx\": \"false\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"save_layer\": null, \"save_raw\": \"false\", \"settings\": {\"default\": \"true\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"export_mtx\": false, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"save_layer\": null, \"save_raw\": false, \"settings\": {\"default\": true, \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "405a3674-8eca-4444-abc8-9f79112efafe", + "when": null, "workflow_outputs": [ { "label": null, @@ -1122,10 +1156,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"batch_key\": \"\", \"filter\": \"false\", \"flavor\": \"seurat\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"max_disp\": \"50.0\", \"max_mean\": \"3.0\", \"min_disp\": \"0.5\", \"min_mean\": \"0.0125\", \"n_bin\": \"20\", \"n_top_gene\": null, \"output_format\": \"anndata_h5ad\", \"span\": \"0.3\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"batch_key\": \"\", \"filter\": false, \"flavor\": \"seurat\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"max_disp\": \"50.0\", \"max_mean\": \"3.0\", \"min_disp\": \"0.5\", \"min_mean\": \"0.0125\", \"n_bin\": \"20\", \"n_top_gene\": null, \"output_format\": \"anndata_h5ad\", \"span\": \"0.3\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "53fa19a2-55bd-43af-b0c9-a41d42554a6b", + "when": null, "workflow_outputs": [ { "label": null, @@ -1166,10 +1201,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"save_layer\": null, \"save_raw\": \"false\", \"scale_max\": \"10.0\", \"zero_center\": \"true\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"save_layer\": null, \"save_raw\": false, \"scale_max\": \"10.0\", \"zero_center\": true, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "a4106855-a661-4d6d-9208-99afcfc0eb55", + "when": null, "workflow_outputs": [ { "label": null, @@ -1210,10 +1246,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"extra_outputs\": null, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"n_pcs\": \"50\", \"output_format\": \"anndata_h5ad\", \"run_mode\": {\"chunked\": \"false\", \"__current_case__\": 1, \"zero_center\": \"true\", \"svd_solver\": null, \"random_seed\": \"0\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"extra_outputs\": null, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"n_pcs\": \"50\", \"output_format\": \"anndata_h5ad\", \"run_mode\": {\"chunked\": false, \"__current_case__\": 1, \"zero_center\": true, \"svd_solver\": null, \"random_seed\": \"0\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "5a1ce297-23e9-46da-be5c-9c01a3a5a9dd", + "when": null, "workflow_outputs": [ { "label": null, @@ -1262,10 +1299,11 @@ "owner": "iuc", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": \"false\"}, \"format\": \"png\", \"method\": {\"method\": \"pl.pca_variance_ratio\", \"__current_case__\": 11, \"n_pcs\": \"50\", \"log\": \"false\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.pca_variance_ratio\", \"__current_case__\": 11, \"n_pcs\": \"50\", \"log\": false}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.7.1+galaxy1", "type": "tool", "uuid": "04b47edc-87e5-4913-abe1-e525f62b9aa4", + "when": null, "workflow_outputs": [ { "label": "PCA Variance", @@ -1306,10 +1344,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": \"false\", \"__current_case__\": 1, \"key_added\": \"\", \"n_neighbors\": \"15\", \"n_neighbors_file\": null, \"use_rep\": \"X_pca\", \"n_pcs\": \"20\", \"knn\": \"true\", \"method\": \"umap\", \"metric\": \"euclidean\", \"random_seed\": \"0\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": false, \"__current_case__\": 1, \"key_added\": \"\", \"n_neighbors\": \"15\", \"n_neighbors_file\": null, \"use_rep\": \"X_pca\", \"n_pcs\": \"20\", \"knn\": true, \"method\": \"umap\", \"metric\": \"euclidean\", \"random_seed\": \"0\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy1", "type": "tool", "uuid": "f0da837f-8547-4368-9160-f4416f91768f", + "when": null, "workflow_outputs": [ { "label": null, @@ -1359,10 +1398,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"embeddings\": \"true\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": \"false\", \"__current_case__\": 1, \"key_added\": \"\", \"perplexity\": \"30.0\", \"perplexity_file\": {\"__class__\": \"RuntimeValue\"}, \"early_exaggeration\": \"12.0\", \"learning_rate\": \"1000.0\", \"fast_tsne\": \"false\", \"n_job\": null, \"n_pc\": null, \"random_seed\": \"0\"}, \"use_rep\": \"X_pca\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"embeddings\": true, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": false, \"__current_case__\": 1, \"key_added\": \"\", \"perplexity\": \"30.0\", \"perplexity_file\": {\"__class__\": \"RuntimeValue\"}, \"early_exaggeration\": \"12.0\", \"learning_rate\": \"1000.0\", \"fast_tsne\": false, \"n_job\": null, \"n_pc\": null, \"random_seed\": \"0\"}, \"use_rep\": \"X_pca\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy1", "type": "tool", "uuid": "1638ac03-a75c-4081-a33d-da080f0de1c7", + "when": null, "workflow_outputs": [ { "label": null, @@ -1412,10 +1452,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"embeddings\": \"true\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"key_added\": \"\", \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": \"true\", \"__current_case__\": 0}, \"use_graph\": \"neighbors\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"__input_ext\": \"input\", \"__workflow_invocation_uuid__\": \"190c9dc088ac11eb8307001e67d2ec02\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"embeddings\": true, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"key_added\": \"\", \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": true, \"__current_case__\": 0}, \"use_graph\": \"neighbors\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "f0847b39-f0b3-4422-a013-b1e0fc5d10af", + "when": null, "workflow_outputs": [ { "label": null, @@ -1470,10 +1511,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"method\": \"louvain\", \"output_cluster\": \"true\", \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": \"false\", \"__current_case__\": 1, \"neighbors_key\": \"neighbors\", \"layer\": \"\", \"key_added\": \"\", \"resolution\": \"0.6\", \"resolution_file\": {\"__class__\": \"RuntimeValue\"}, \"restrict_to\": \"\", \"use_weights\": \"false\", \"random_seed\": \"0\", \"directed\": \"true\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"method\": \"louvain\", \"output_cluster\": true, \"output_format\": \"anndata_h5ad\", \"settings\": {\"default\": false, \"__current_case__\": 1, \"neighbors_key\": \"neighbors\", \"layer\": \"\", \"key_added\": \"\", \"resolution\": \"0.6\", \"resolution_file\": {\"__class__\": \"RuntimeValue\"}, \"restrict_to\": \"\", \"use_weights\": false, \"random_seed\": \"0\", \"directed\": true}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "684c6061-1703-4ed3-837a-f08aa49f496d", + "when": null, "workflow_outputs": [ { "label": null, @@ -1543,10 +1585,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"groupby\": \"louvain\", \"groupby_file\": {\"__class__\": \"RuntimeValue\"}, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"n_genes\": \"50\", \"output_format\": \"anndata_h5ad\", \"output_markers\": \"true\", \"settings\": {\"default\": \"true\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"groupby\": \"louvain\", \"groupby_file\": {\"__class__\": \"RuntimeValue\"}, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"n_genes\": \"50\", \"output_format\": \"anndata_h5ad\", \"output_markers\": true, \"settings\": {\"default\": true, \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "b5fcde30-03d3-44b0-9d6a-094512e76172", + "when": null, "workflow_outputs": [ { "label": "Markers - cluster", @@ -1614,10 +1657,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"groupby\": \"genotype\", \"groupby_file\": {\"__class__\": \"RuntimeValue\"}, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"n_genes\": \"50\", \"output_format\": \"anndata_h5ad\", \"output_markers\": \"true\", \"settings\": {\"default\": \"true\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"groupby\": \"genotype\", \"groupby_file\": {\"__class__\": \"RuntimeValue\"}, \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"n_genes\": \"50\", \"output_format\": \"anndata_h5ad\", \"output_markers\": true, \"settings\": {\"default\": true, \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "cf2af5fd-4210-4a5a-b514-9786eebeb6f8", + "when": null, "workflow_outputs": [ { "label": "Markers - genotype", @@ -1658,10 +1702,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"basis\": \"tsne\", \"color_by\": \"louvain,sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts\", \"components\": \"\", \"edges\": \"false\", \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": \"false\", \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": \"false\", \"use_raw\": \"false\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"basis\": \"tsne\", \"color_by\": \"louvain,sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts\", \"components\": \"\", \"edges\": false, \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": false, \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": false, \"use_raw\": false, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "a41e2d67-8264-477d-9a61-9a9f86681a91", + "when": null, "workflow_outputs": [ { "label": null, @@ -1702,10 +1747,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"basis\": \"umap\", \"color_by\": \"louvain,sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts\", \"components\": \"\", \"edges\": \"false\", \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": \"false\", \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": \"false\", \"use_raw\": \"false\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"basis\": \"umap\", \"color_by\": \"louvain,sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts\", \"components\": \"\", \"edges\": false, \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": false, \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": false, \"use_raw\": false, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "4402764b-f095-494c-abfe-97f8854c6c25", + "when": null, "workflow_outputs": [ { "label": null, @@ -1750,6 +1796,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "91b502ec-5b8b-40a2-b70b-1d30581434ef", + "when": null, "workflow_outputs": [ { "label": null, @@ -1790,10 +1837,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"basis\": \"pca\", \"color_by\": \"louvain,sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts\", \"components\": \"\", \"edges\": \"false\", \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": \"false\", \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": \"false\", \"use_raw\": \"false\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"basis\": \"pca\", \"color_by\": \"louvain,sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts\", \"components\": \"\", \"edges\": false, \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": false, \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": false, \"use_raw\": false, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "2a77ae44-dc08-4b53-ae43-215ba91d6260", + "when": null, "workflow_outputs": [ { "label": null, @@ -1838,6 +1886,7 @@ "tool_version": "0.7.5+galaxy1", "type": "tool", "uuid": "c9832ad2-4cff-4316-a43a-11ba2691601d", + "when": null, "workflow_outputs": [ { "label": null, @@ -1861,7 +1910,12 @@ "output_name": "output_h5ad" } }, - "inputs": [], + "inputs": [ + { + "description": "runtime parameter for tool AnnData Operations", + "name": "copy_o" + } + ], "label": null, "name": "AnnData Operations", "outputs": [ @@ -1882,10 +1936,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"copy_adata_to_raw\": \"false\", \"copy_e\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_l\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_o\": {\"default\": \"true\", \"__current_case__\": 0, \"obs_keys\": [{\"__index__\": 0, \"contains\": \"louvain\"}], \"obs_sources\": {\"__class__\": \"ConnectedValue\"}}, \"copy_r\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_u\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_x\": {\"default\": \"false\", \"__current_case__\": 1}, \"gene_flags\": [], \"gene_symbols_field\": \"index\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"modifications\": [], \"output_format\": \"anndata_h5ad\", \"sanitize_varm\": \"false\", \"top_genes\": \"50\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"copy_adata_to_raw\": false, \"copy_e\": {\"default\": false, \"__current_case__\": 1}, \"copy_l\": {\"default\": false, \"__current_case__\": 1}, \"copy_o\": {\"default\": true, \"__current_case__\": 0, \"obs_keys\": [{\"__index__\": 0, \"contains\": \"louvain\"}], \"obs_sources\": {\"__class__\": \"ConnectedValue\"}}, \"copy_r\": {\"default\": false, \"__current_case__\": 1}, \"copy_u\": {\"default\": false, \"__current_case__\": 1}, \"copy_x\": {\"default\": false, \"__current_case__\": 1}, \"gene_flags\": [], \"gene_symbols_field\": \"index\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"modifications\": [], \"output_format\": \"anndata_h5ad\", \"sanitize_varm\": false, \"top_genes\": \"50\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "573bf376-2011-4572-ad91-84df828f1242", + "when": null, "workflow_outputs": [ { "label": null, @@ -1928,6 +1983,7 @@ "tool_version": "2.1.3", "type": "tool", "uuid": "db6d5777-a3dd-467d-8541-91bb998e0e72", + "when": null, "workflow_outputs": [ { "label": null, @@ -1970,6 +2026,7 @@ "tool_version": "2.1.3", "type": "tool", "uuid": "f4db0e7d-dfc8-4f77-bca0-997a0347304b", + "when": null, "workflow_outputs": [ { "label": null, @@ -2018,10 +2075,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"copy_adata_to_raw\": \"false\", \"copy_e\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_l\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_o\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_r\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_u\": {\"default\": \"false\", \"__current_case__\": 1}, \"copy_x\": {\"default\": \"false\", \"__current_case__\": 1}, \"gene_flags\": [], \"gene_symbols_field\": \"index\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"modifications\": [{\"__index__\": 0, \"from_obs\": \"louvain_0\", \"to_obs\": \"cell_type\", \"keep_original\": \"false\"}], \"output_format\": \"anndata_h5ad\", \"sanitize_varm\": \"false\", \"top_genes\": \"50\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"copy_adata_to_raw\": false, \"copy_e\": {\"default\": false, \"__current_case__\": 1}, \"copy_l\": {\"default\": false, \"__current_case__\": 1}, \"copy_o\": {\"default\": false, \"__current_case__\": 1}, \"copy_r\": {\"default\": false, \"__current_case__\": 1}, \"copy_u\": {\"default\": false, \"__current_case__\": 1}, \"copy_x\": {\"default\": false, \"__current_case__\": 1}, \"gene_flags\": [], \"gene_symbols_field\": \"index\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"modifications\": [{\"__index__\": 0, \"from_obs\": \"louvain_0\", \"to_obs\": \"cell_type\", \"keep_original\": false}], \"output_format\": \"anndata_h5ad\", \"sanitize_varm\": false, \"top_genes\": \"50\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "584bf7e3-05d7-4010-ba67-98375379c621", + "when": null, "workflow_outputs": [ { "label": "Final cell annotated object", @@ -2068,6 +2126,7 @@ "tool_version": "1.0.2", "type": "tool", "uuid": "45fddc4f-1b9d-4464-b08b-c502d4d05a30", + "when": null, "workflow_outputs": [ { "label": "Markers - cluster - named", @@ -2114,6 +2173,7 @@ "tool_version": "1.0.2", "type": "tool", "uuid": "995cfd57-925a-470e-879b-8d7eb619c67b", + "when": null, "workflow_outputs": [ { "label": "Markers - genotype - named", @@ -2133,12 +2193,7 @@ "output_name": "output_h5ad" } }, - "inputs": [ - { - "description": "runtime parameter for tool Scanpy PlotEmbed", - "name": "input_obj_file" - } - ], + "inputs": [], "label": null, "name": "Scanpy PlotEmbed", "outputs": [ @@ -2159,10 +2214,11 @@ "owner": "ebi-gxa", "tool_shed": "toolshed.g2.bx.psu.edu" }, - "tool_state": "{\"basis\": \"umap\", \"color_by\": \"sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts,Hba-a1,cell_type\", \"components\": \"\", \"edges\": \"false\", \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": \"false\", \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"RuntimeValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": \"false\", \"use_raw\": \"false\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_state": "{\"basis\": \"umap\", \"color_by\": \"sex,batch,genotype,Il2ra,Cd8b1,Cd8a,Cd4,Itm2a,Aif1,log1p_total_counts,Hba-a1,cell_type\", \"components\": \"\", \"edges\": false, \"edges_color\": \"\", \"edges_width\": \"0.1\", \"fig_dpi\": \"80\", \"fig_fontsize\": \"10\", \"fig_frame\": false, \"fig_size\": \"4,4\", \"fig_title\": \"\", \"gene_symbols_field\": \"Symbol\", \"groups\": \"\", \"input_format\": \"anndata\", \"input_obj_file\": {\"__class__\": \"ConnectedValue\"}, \"layer\": \"\", \"legend_fontsize\": \"15\", \"legend_loc\": \"right margin\", \"neighbors_key\": \"\", \"point_size\": null, \"projection\": \"2d\", \"sort_order\": false, \"use_raw\": false, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.8.1+galaxy0", "type": "tool", "uuid": "ff379bdf-e875-4803-baa0-b11bd71666db", + "when": null, "workflow_outputs": [ { "label": null, @@ -2177,6 +2233,6 @@ "name:training", "name:singlecell" ], - "uuid": "3d42c837-d379-4f2f-bc4d-08d7b502afe6", - "version": 7 + "uuid": "7777e890-b95a-4912-a208-8ac0044a0e62", + "version": 1 } \ No newline at end of file diff --git a/topics/single-cell/tutorials/scrna-case_trajectories/tutorial.md b/topics/single-cell/tutorials/scrna-case_trajectories/tutorial.md index c6fa258c0ed55d..b3cd975c62d75a 100644 --- a/topics/single-cell/tutorials/scrna-case_trajectories/tutorial.md +++ b/topics/single-cell/tutorials/scrna-case_trajectories/tutorial.md @@ -50,8 +50,6 @@ contributions: --- -# Introduction - You've done all the hard work of preparing a single-cell matrix, processing it, plotting it, interpreting it, and finding lots of lovely genes. Now you want to infer trajectories, or relationships between cells... you can do that here, using the Galaxy interface, or head over to the [Jupyter notebook version of this tutorial]({% link topics/single-cell/tutorials/scrna-case_JUPYTER-trajectories/tutorial.md %}) to learn how to perform the same analysis using Python. Traditionally, we thought that differentiating or changing cells jumped between discrete states, so 'Cell A' became 'Cell B' as part of its maturation. However, most data shows otherwise. Generally, there is a spectrum (a 'trajectory', if you will...) of small, subtle changes along a pathway of that differentiation. Trying to analyse cells every 10 seconds can be pretty tricky, so 'pseudotime' analysis takes a single sample and assumes that those cells are all on slightly different points along a path of differentiation. Some cells might be slightly more mature and others slightly less, all captured at the same 'time'. These cells are sorted accordingly along these pseudotime paths of differentiation to build a continuum of cells from one state to the next. We therefore 'assume' or 'infer' relationships from this continuum of cells. @@ -227,7 +225,7 @@ Now that we have our diffusion map, we need to re-calculate neighbors using the {: .hands_on} > <comment-title></comment-title> -> If you're using the latest versions of these tools (e.g. {% tool [Scanpy ComputeGraph](https://usegalaxy.eu/root?tool_id=toolshed.g2.bx.psu.edu/repos/ebi-gxa/scanpy_compute_graph/scanpy_compute_graph/1.9.3+galaxy0) %}, rather than the ones suggested in the tutorial (e.g. {% tool [Scanpy ComputeGraph](toolshed.g2.bx.psu.edu/repos/ebi-gxa/scanpy_compute_graph/scanpy_compute_graph/1.8.1+galaxy9) %} then you may need to change one more parameter here to set the `Number of PCs to use` to 15. These are the 15 diffusion components we just calculated, rather than actual PCs. +> If you're using the latest versions of these tools (e.g. {% tool [Scanpy ComputeGraph](toolshed.g2.bx.psu.edu/repos/ebi-gxa/scanpy_compute_graph/scanpy_compute_graph/1.9.3+galaxy0) %}, rather than the ones suggested in the tutorial (e.g. {% tool [Scanpy ComputeGraph](toolshed.g2.bx.psu.edu/repos/ebi-gxa/scanpy_compute_graph/scanpy_compute_graph/1.8.1+galaxy9) %} then you may need to change one more parameter here to set the `Number of PCs to use` to 15. These are the 15 diffusion components we just calculated, rather than actual PCs. {: .comment} ## Re-draw the FDG diff --git a/topics/single-cell/tutorials/scrna-plant/tutorial.md b/topics/single-cell/tutorials/scrna-plant/tutorial.md index b5c67cbbc74a2a..b06d55b1e399d1 100644 --- a/topics/single-cell/tutorials/scrna-plant/tutorial.md +++ b/topics/single-cell/tutorials/scrna-plant/tutorial.md @@ -6,6 +6,10 @@ priority: 3 redirect_from: - /topics/transcriptomics/tutorials/scrna-plant/tutorial zenodo_link: 'https://zenodo.org/record/4597857' +answer_histories: + - label: "UseGalaxy.eu" + history: https://singlecell.usegalaxy.eu/u/videmp/h/analysis-of-plant-scrna-seq-data-with-scanpy + date: 2024-12-13 tags: - plants - paper-replication @@ -33,11 +37,8 @@ contributors: - gallardoalba - pavanvidem - gitter: Galaxy-Training-Network/galaxy-single-cell - - recordings: - captioners: - mtekman diff --git a/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis-tests.yml b/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis-tests.yml new file mode 100644 index 00000000000000..da3eeee5e09ab1 --- /dev/null +++ b/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis-tests.yml @@ -0,0 +1,29 @@ +- doc: Test outline for scRNA-Plant-Analysis + job: + SHR (CSV.gz): + class: File + location: https://zenodo.org/record/4597857/files/GSE123818_Root_single_cell_shr_datamatrix.fixednames.transposed.csv.gz + filetype: csv + WT (CSV.gz): + class: File + location: https://zenodo.org/record/4597857/files/GSE123818_Root_single_cell_wt_datamatrix.fixednames.transposed.csv.gz + filetype: csv + Leiden Resolution: '0.35' + Min Genes: '100' + Min Cells: '2' + Max Features: '12000.0' + Max Lib Size: '120000.0' + outputs: + scRNA with clusters Dataset: + asserts: + has_h5_keys: + keys: "obs/leiden" + keys: "obs/batch" + keys: "var/n_cells_by_counts" + keys: "uns/leiden" + keys: "uns/umap" + out_png (Step 33): + asserts: + has_size: + value: 210272 + delta: 4500 diff --git a/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.ga b/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.ga new file mode 100644 index 00000000000000..2dbe017688151b --- /dev/null +++ b/topics/single-cell/tutorials/scrna-plant/workflows/scRNA-Plant-Analysis.ga @@ -0,0 +1,1448 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "Downstream Single-cell RNA Plant analysis with ScanPy", + "comments": [], + "creator": [ + { + "class": "Person", + "identifier": "0000-0002-4181-2676", + "name": "Mehmet Tekman " + }, + { + "class": "Person", + "identifier": "0000-0002-5862-6132", + "name": "Beatriz Serrano-Solano" + }, + { + "class": "Person", + "identifier": "0000-0002-5752-2155", + "name": "Crist\u00f3bal Gallardo" + }, + { + "class": "Person", + "identifier": "0000-0002-5192-126X", + "name": "Pavankumar Videm" + } + ], + "format-version": "0.1", + "license": "CC-BY-4.0", + "name": "scRNA Plant Analysis", + "report": { + "markdown": "\n# Workflow Execution Report\n\n## Workflow Inputs\n```galaxy\ninvocation_inputs()\n```\n\n## Workflow Outputs\n```galaxy\ninvocation_outputs()\n```\n\n## Workflow\n```galaxy\nworkflow_display()\n```\n" + }, + "steps": { + "0": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Leiden Resolution" + } + ], + "label": "Leiden Resolution", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 106.5, + "top": 0 + }, + "tool_id": null, + "tool_state": "{\"default\": 0.35, \"parameter_type\": \"float\", \"optional\": true}", + "tool_version": null, + "type": "parameter_input", + "uuid": "eddf8108-6913-40d2-819d-a84677dee3f4", + "when": null, + "workflow_outputs": [] + }, + "1": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 1, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "SHR (CSV.gz)" + } + ], + "label": "SHR (CSV.gz)", + "name": "Input dataset", + "outputs": [], + "position": { + "left": 101, + "top": 106.5 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": null}", + "tool_version": null, + "type": "data_input", + "uuid": "4cf4f111-d63b-47ad-bcbf-8ebb290b3431", + "when": null, + "workflow_outputs": [] + }, + "2": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 2, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "WT (CSV.gz)" + } + ], + "label": "WT (CSV.gz)", + "name": "Input dataset", + "outputs": [], + "position": { + "left": 99, + "top": 207.5 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": null}", + "tool_version": null, + "type": "data_input", + "uuid": "94fba59a-ba9d-452d-b325-cb558201f674", + "when": null, + "workflow_outputs": [] + }, + "3": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 3, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Min Genes" + } + ], + "label": "Min Genes", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 1, + "top": 328 + }, + "tool_id": null, + "tool_state": "{\"default\": 100, \"parameter_type\": \"integer\", \"optional\": true}", + "tool_version": null, + "type": "parameter_input", + "uuid": "c9a62328-f049-43b6-92fb-df5c42982573", + "when": null, + "workflow_outputs": [] + }, + "4": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 4, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Min Cells" + } + ], + "label": "Min Cells", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 2, + "top": 403 + }, + "tool_id": null, + "tool_state": "{\"default\": 2, \"parameter_type\": \"integer\", \"optional\": true}", + "tool_version": null, + "type": "parameter_input", + "uuid": "7bf5f0e1-b482-45c3-9bcb-10ca93e6b7dc", + "when": null, + "workflow_outputs": [] + }, + "5": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 5, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Max Features" + } + ], + "label": "Max Features", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 8, + "top": 476 + }, + "tool_id": null, + "tool_state": "{\"default\": 12000.0, \"parameter_type\": \"float\", \"optional\": true}", + "tool_version": null, + "type": "parameter_input", + "uuid": "8902a917-75fb-49c6-9e5d-15f87bd91d46", + "when": null, + "workflow_outputs": [] + }, + "6": { + "annotation": "", + "content_id": null, + "errors": null, + "id": 6, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Max Lib Size" + } + ], + "label": "Max Lib Size", + "name": "Input parameter", + "outputs": [], + "position": { + "left": 0, + "top": 569 + }, + "tool_id": null, + "tool_state": "{\"default\": 120000.0, \"parameter_type\": \"float\", \"optional\": true}", + "tool_version": null, + "type": "parameter_input", + "uuid": "db068cdb-53c4-48a3-95c2-141fa83c271b", + "when": null, + "workflow_outputs": [] + }, + "7": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_import/anndata_import/0.7.5+galaxy0", + "errors": null, + "id": 7, + "input_connections": { + "hd5_format|in|input": { + "id": 1, + "output_name": "output" + } + }, + "inputs": [], + "label": null, + "name": "Import Anndata and loom", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 440, + "top": 113.5 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + }, + "TagDatasetActionanndata": { + "action_arguments": { + "tags": "#shr" + }, + "action_type": "TagDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_import/anndata_import/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "55e35e9203cf", + "name": "anndata_import", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"hd5_format\": {\"filetype\": \"anndata\", \"__current_case__\": 0, \"in\": {\"adata_format\": \"tabular\", \"__current_case__\": 1, \"input\": {\"__class__\": \"ConnectedValue\"}, \"first_column_names\": true}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "75665e49-c804-4f0c-a6fd-5616ed0c40bb", + "when": null, + "workflow_outputs": [] + }, + "8": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_import/anndata_import/0.7.5+galaxy0", + "errors": null, + "id": 8, + "input_connections": { + "hd5_format|in|input": { + "id": 2, + "output_name": "output" + } + }, + "inputs": [], + "label": null, + "name": "Import Anndata and loom", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 444, + "top": 289.5 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + }, + "TagDatasetActionanndata": { + "action_arguments": { + "tags": "#wt" + }, + "action_type": "TagDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_import/anndata_import/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "55e35e9203cf", + "name": "anndata_import", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"hd5_format\": {\"filetype\": \"anndata\", \"__current_case__\": 0, \"in\": {\"adata_format\": \"tabular\", \"__current_case__\": 1, \"input\": {\"__class__\": \"ConnectedValue\"}, \"first_column_names\": true}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "c21605ec-1746-4b8d-bc4b-45344898ed7b", + "when": null, + "workflow_outputs": [] + }, + "9": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "errors": null, + "id": 9, + "input_connections": { + "input": { + "id": 7, + "output_name": "anndata" + }, + "manipulate|other_adatas": { + "id": 8, + "output_name": "anndata" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool Manipulate AnnData", + "name": "manipulate" + } + ], + "label": null, + "name": "Manipulate AnnData", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 703, + "top": 26.5 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "9bd945a03d7b", + "name": "anndata_manipulate", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"h5ad\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"input\": {\"__class__\": \"ConnectedValue\"}, \"manipulate\": {\"function\": \"concatenate\", \"__current_case__\": 0, \"other_adatas\": {\"__class__\": \"ConnectedValue\"}, \"join\": \"inner\", \"batch_key\": \"batch\", \"index_unique\": \"-\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "9f989a68-2d09-41bc-9cb4-b40afaf88bbf", + "when": null, + "workflow_outputs": [] + }, + "10": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "errors": null, + "id": 10, + "input_connections": { + "input": { + "id": 9, + "output_name": "anndata" + } + }, + "inputs": [], + "label": "Rename Batches", + "name": "Manipulate AnnData", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 940, + "top": 106.5 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "9bd945a03d7b", + "name": "anndata_manipulate", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"input\": {\"__class__\": \"ConnectedValue\"}, \"manipulate\": {\"function\": \"rename_categories\", \"__current_case__\": 3, \"key\": \"batch\", \"categories\": \"shr, wt\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "0a25ffc3-ac58-4c17-9f22-b4127952027a", + "when": null, + "workflow_outputs": [ + { + "label": "scRNA Concatenated Datasets", + "output_name": "anndata", + "uuid": "680180f6-edfc-4f66-b265-7a63aaa22b39" + } + ] + }, + "11": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "errors": null, + "id": 11, + "input_connections": { + "adata": { + "id": 10, + "output_name": "anndata" + } + }, + "inputs": [], + "label": "QC metrics", + "name": "Inspect and manipulate", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 693, + "top": 256.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "c5d3684f7c4c", + "name": "scanpy_inspect", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"pp.calculate_qc_metrics\", \"__current_case__\": 0, \"expr_type\": \"counts\", \"var_type\": \"genes\", \"qc_vars\": \"\", \"percent_top\": \"\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "26789fb1-bc53-40f8-bf3f-b0c02b50217e", + "when": null, + "workflow_outputs": [] + }, + "12": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_filter/scanpy_filter/1.7.1+galaxy0", + "errors": null, + "id": 12, + "input_connections": { + "adata": { + "id": 11, + "output_name": "anndata_out" + }, + "method|filter|min_genes": { + "id": 3, + "output_name": "output" + } + }, + "inputs": [], + "label": "Filter genes", + "name": "Filter", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 454, + "top": 496.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_filter/scanpy_filter/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "b409c2486353", + "name": "scanpy_filter", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"method\": {\"method\": \"pp.filter_cells\", \"__current_case__\": 0, \"filter\": {\"filter\": \"min_genes\", \"__current_case__\": 2, \"min_genes\": {\"__class__\": \"ConnectedValue\"}}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "53b2ac3e-031f-4d60-9769-a807949501cc", + "when": null, + "workflow_outputs": [] + }, + "13": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 13, + "input_connections": { + "adata": { + "id": 11, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Plot Violin", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1852.2833251953125, + "top": 31.649993896484375 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.violin\", \"__current_case__\": 3, \"key_variables\": {\"type\": \"custom\", \"__current_case__\": 2, \"keys\": \"n_genes_by_counts, total_counts\"}, \"groupby\": \"batch\", \"log\": false, \"use_raw\": false, \"violin_plot\": {\"stripplot\": {\"stripplot\": \"True\", \"__current_case__\": 0, \"jitter\": {\"jitter\": \"True\", \"__current_case__\": 0, \"size\": \"0.4\"}}, \"multi_panel\": {\"multi_panel\": \"True\", \"__current_case__\": 0, \"width\": null, \"height\": null}, \"scale\": \"width\"}, \"xlabel\": \"\", \"rotation\": null, \"seaborn_violinplot\": {\"bw\": \"scott\", \"orient\": null, \"linewidth\": \"0.0\", \"color\": \"AliceBlue\", \"saturation\": \"0.75\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "9dfc1e59-71aa-40b8-b504-2498a8dc5409", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "2fb1d919-0e60-4c81-92d2-83c28b1ef654" + } + ] + }, + "14": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_filter/scanpy_filter/1.7.1+galaxy0", + "errors": null, + "id": 14, + "input_connections": { + "adata": { + "id": 12, + "output_name": "anndata_out" + }, + "method|filter|min_cells": { + "id": 4, + "output_name": "output" + } + }, + "inputs": [], + "label": "Filter Cells", + "name": "Filter", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 686, + "top": 500.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_filter/scanpy_filter/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "b409c2486353", + "name": "scanpy_filter", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"method\": {\"method\": \"pp.filter_genes\", \"__current_case__\": 1, \"filter\": {\"filter\": \"min_cells\", \"__current_case__\": 2, \"min_cells\": {\"__class__\": \"ConnectedValue\"}}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "87cf962d-fb15-42a2-afbc-315092546d0a", + "when": null, + "workflow_outputs": [] + }, + "15": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "errors": null, + "id": 15, + "input_connections": { + "input": { + "id": 14, + "output_name": "anndata_out" + }, + "manipulate|filter|filter_key|value": { + "id": 5, + "output_name": "output" + } + }, + "inputs": [], + "label": "Filter Max Features", + "name": "Manipulate AnnData", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 919.5, + "top": 493 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "9bd945a03d7b", + "name": "anndata_manipulate", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"input\": {\"__class__\": \"ConnectedValue\"}, \"manipulate\": {\"function\": \"filter\", \"__current_case__\": 7, \"var_obs\": \"obs\", \"filter\": {\"filter\": \"key\", \"__current_case__\": 0, \"key\": \"n_genes_by_counts\", \"filter_key\": {\"type\": \"number\", \"__current_case__\": 0, \"filter\": \"less\", \"value\": {\"__class__\": \"ConnectedValue\"}}}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "a087d4ce-b8dc-428a-b443-985de6284202", + "when": null, + "workflow_outputs": [] + }, + "16": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "errors": null, + "id": 16, + "input_connections": { + "input": { + "id": 15, + "output_name": "anndata" + }, + "manipulate|filter|filter_key|value": { + "id": 6, + "output_name": "output" + } + }, + "inputs": [], + "label": "Filter Max Library Size", + "name": "Manipulate AnnData", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 1142, + "top": 497 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "9bd945a03d7b", + "name": "anndata_manipulate", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"input\": {\"__class__\": \"ConnectedValue\"}, \"manipulate\": {\"function\": \"filter\", \"__current_case__\": 7, \"var_obs\": \"obs\", \"filter\": {\"filter\": \"key\", \"__current_case__\": 0, \"key\": \"total_counts\", \"filter_key\": {\"type\": \"number\", \"__current_case__\": 0, \"filter\": \"less\", \"value\": {\"__class__\": \"ConnectedValue\"}}}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "69a2fa58-1db0-43e5-8b0c-95001760c45c", + "when": null, + "workflow_outputs": [] + }, + "17": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "errors": null, + "id": 17, + "input_connections": { + "input": { + "id": 16, + "output_name": "anndata" + } + }, + "inputs": [], + "label": "Save RAW", + "name": "Manipulate AnnData", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 455, + "top": 724.5 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "9bd945a03d7b", + "name": "anndata_manipulate", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"h5ad\", \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"input\": {\"__class__\": \"ConnectedValue\"}, \"manipulate\": {\"function\": \"save_raw\", \"__current_case__\": 8}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "10a8b24c-5c1d-499c-b2ed-02fd5dd60ccc", + "when": null, + "workflow_outputs": [] + }, + "18": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_normalize/scanpy_normalize/1.7.1+galaxy0", + "errors": null, + "id": 18, + "input_connections": { + "adata": { + "id": 17, + "output_name": "anndata" + } + }, + "inputs": [], + "label": "Norm to 1e4", + "name": "Normalize", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 684, + "top": 729.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_normalize/scanpy_normalize/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "ab55fe8030b6", + "name": "scanpy_normalize", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"pp.normalize_total\", \"__current_case__\": 0, \"target_sum\": \"10000.0\", \"exclude_highly_expressed\": {\"exclude_highly_expressed\": \"False\", \"__current_case__\": 1}, \"key_added\": \"\", \"layers\": \"\", \"layer_norm\": \"None\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "775ce79b-1344-492d-901b-5d7f31b592ce", + "when": null, + "workflow_outputs": [] + }, + "19": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "errors": null, + "id": 19, + "input_connections": { + "adata": { + "id": 18, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Log1p", + "name": "Inspect and manipulate", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 918, + "top": 722.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "c5d3684f7c4c", + "name": "scanpy_inspect", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"pp.log1p\", \"__current_case__\": 5}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "990bb318-b671-4682-b38e-ef8f228550df", + "when": null, + "workflow_outputs": [] + }, + "20": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_remove_confounders/scanpy_remove_confounders/1.7.1+galaxy0", + "errors": null, + "id": 20, + "input_connections": { + "adata": { + "id": 19, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Regress total counts", + "name": "Remove confounders", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 1150, + "top": 706.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_remove_confounders/scanpy_remove_confounders/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "bf2017df9837", + "name": "scanpy_remove_confounders", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"pp.regress_out\", \"__current_case__\": 0, \"keys\": \"total_counts\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "7655170c-3e6d-454b-a58e-10b38224e055", + "when": null, + "workflow_outputs": [] + }, + "21": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "errors": null, + "id": 21, + "input_connections": { + "adata": { + "id": 20, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Scale", + "name": "Inspect and manipulate", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 1388, + "top": 693.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "c5d3684f7c4c", + "name": "scanpy_inspect", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"pp.scale\", \"__current_case__\": 6, \"zero_center\": true, \"max_value\": \"10.0\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "4decb683-0d00-41af-9fe2-a0f6503829a4", + "when": null, + "workflow_outputs": [] + }, + "22": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_cluster_reduce_dimension/scanpy_cluster_reduce_dimension/1.7.1+galaxy0", + "errors": null, + "id": 22, + "input_connections": { + "adata": { + "id": 21, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "PCA", + "name": "Cluster, infer trajectories and embed", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 1366, + "top": 514.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_cluster_reduce_dimension/scanpy_cluster_reduce_dimension/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "aaa5da8e73a9", + "name": "scanpy_cluster_reduce_dimension", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"tl.pca\", \"__current_case__\": 3, \"n_comps\": \"50\", \"dtype\": \"float32\", \"pca\": {\"chunked\": \"False\", \"__current_case__\": 1, \"zero_center\": true, \"svd_solver\": \"arpack\", \"random_state\": \"0\"}, \"use_highly_variable\": false}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "f8401788-4571-44ef-9524-487d319eccf5", + "when": null, + "workflow_outputs": [] + }, + "23": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "errors": null, + "id": 23, + "input_connections": { + "adata": { + "id": 22, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Neighborhod", + "name": "Inspect and manipulate", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 943, + "top": 262.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_inspect/scanpy_inspect/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "c5d3684f7c4c", + "name": "scanpy_inspect", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"method\": {\"method\": \"pp.neighbors\", \"__current_case__\": 1, \"n_neighbors\": \"10\", \"n_pcs\": \"40\", \"use_rep\": \"\", \"knn\": true, \"random_state\": \"0\", \"pp_neighbors_method\": \"umap\", \"metric\": \"euclidean\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "180e249b-621f-4681-82d4-16037eaca3dd", + "when": null, + "workflow_outputs": [] + }, + "24": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_cluster_reduce_dimension/scanpy_cluster_reduce_dimension/1.7.1+galaxy0", + "errors": null, + "id": 24, + "input_connections": { + "adata": { + "id": 23, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "UMAP", + "name": "Cluster, infer trajectories and embed", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 1308, + "top": 72.5 + }, + "post_job_actions": { + "HideDatasetActionanndata_out": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata_out" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_cluster_reduce_dimension/scanpy_cluster_reduce_dimension/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "aaa5da8e73a9", + "name": "scanpy_cluster_reduce_dimension", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"method\": {\"method\": \"tl.umap\", \"__current_case__\": 6, \"min_dist\": \"0.5\", \"spread\": \"1.0\", \"n_components\": \"2\", \"maxiter\": null, \"alpha\": \"1.0\", \"gamma\": \"1.0\", \"negative_sample_rate\": \"5\", \"init_pos\": \"spectral\", \"random_state\": \"0\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "5aadb211-bc84-4dc5-bae0-730073b5ab93", + "when": null, + "workflow_outputs": [] + }, + "25": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_cluster_reduce_dimension/scanpy_cluster_reduce_dimension/1.7.1+galaxy0", + "errors": null, + "id": 25, + "input_connections": { + "adata": { + "id": 24, + "output_name": "anndata_out" + }, + "method|resolution": { + "id": 0, + "output_name": "output" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool Cluster, infer trajectories and embed", + "name": "method" + } + ], + "label": "Leiden", + "name": "Cluster, infer trajectories and embed", + "outputs": [ + { + "name": "anndata_out", + "type": "h5ad" + } + ], + "position": { + "left": 1374, + "top": 303.5 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_cluster_reduce_dimension/scanpy_cluster_reduce_dimension/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "aaa5da8e73a9", + "name": "scanpy_cluster_reduce_dimension", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"method\": {\"method\": \"tl.leiden\", \"__current_case__\": 1, \"resolution\": {\"__class__\": \"ConnectedValue\"}, \"random_state\": \"0\", \"key_added\": \"leiden\", \"use_weights\": false, \"n_iterations\": \"-1\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "99494ea7-377c-4f3d-a45a-18bfd81b7888", + "when": null, + "workflow_outputs": [ + { + "label": "scRNA with clusters Dataset", + "output_name": "anndata_out", + "uuid": "48cf0465-84ba-461a-bdb9-61625626de42" + } + ] + }, + "26": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 26, + "input_connections": { + "adata": { + "id": 24, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Plot PCA (batch)", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1614.433349609375, + "top": 29.649993896484375 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.pca\", \"__current_case__\": 9, \"color\": \"batch\", \"use_raw\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": null, \"legend_fontweight\": \"normal\", \"size\": null, \"color_map\": \"None\", \"palette\": \"rainbow\", \"frameon\": true, \"ncols\": \"4\", \"wspace\": \"0.1\", \"hspace\": \"0.25\", \"title\": \"\"}, \"matplotlib_pyplot_scatter\": {\"alpha\": null, \"vmin\": null, \"vmax\": null, \"linewidths\": \"0.0\", \"edgecolors\": \"face\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "e2daa4c6-9a39-4c42-852e-4f61b65202c2", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "72d1a264-3ace-4d2e-910a-5e2a0fa6721f" + } + ] + }, + "27": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 27, + "input_connections": { + "adata": { + "id": 24, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Plot UMAP (batch)", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1847, + "top": 158.5 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.umap\", \"__current_case__\": 14, \"color\": \"batch\", \"use_raw\": false, \"edges\": {\"edges\": \"False\", \"__current_case__\": 1}, \"arrows\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": \"14\", \"legend_fontweight\": \"normal\", \"size\": null, \"color_map\": \"None\", \"palette\": \"rainbow\", \"frameon\": true, \"ncols\": \"4\", \"wspace\": \"0.1\", \"hspace\": \"0.25\", \"title\": \"\"}, \"matplotlib_pyplot_scatter\": {\"alpha\": null, \"vmin\": null, \"vmax\": null, \"linewidths\": \"0.0\", \"edgecolors\": \"face\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "e70d8e14-1983-4381-8feb-bba7d5dbb4d6", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "c223e762-7e90-41e1-b354-bbe6e278b5c0" + } + ] + }, + "28": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 28, + "input_connections": { + "adata": { + "id": 25, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Plot UMAP (leiden, batch)", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1615.433349609375, + "top": 194.64999389648438 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"format\": \"png\", \"method\": {\"method\": \"pl.umap\", \"__current_case__\": 14, \"color\": \"leiden, batch\", \"use_raw\": false, \"edges\": {\"edges\": \"False\", \"__current_case__\": 1}, \"arrows\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"right margin\", \"legend_fontsize\": \"14\", \"legend_fontweight\": \"normal\", \"size\": null, \"color_map\": \"None\", \"palette\": \"rainbow\", \"frameon\": true, \"ncols\": \"4\", \"wspace\": \"0.1\", \"hspace\": \"0.25\", \"title\": \"\"}, \"matplotlib_pyplot_scatter\": {\"alpha\": null, \"vmin\": null, \"vmax\": null, \"linewidths\": \"0.0\", \"edgecolors\": \"face\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "b88358ff-0dda-4c2e-bce6-124a0ab8fa28", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "26c65cd9-74aa-4574-a5f3-ac10caced4da" + } + ] + }, + "29": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "errors": null, + "id": 29, + "input_connections": { + "input": { + "id": 25, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Rename Clusters", + "name": "Manipulate AnnData", + "outputs": [ + { + "name": "anndata", + "type": "h5ad" + } + ], + "position": { + "left": 1585, + "top": 525.5 + }, + "post_job_actions": { + "HideDatasetActionanndata": { + "action_arguments": {}, + "action_type": "HideDatasetAction", + "output_name": "anndata" + } + }, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/anndata_manipulate/anndata_manipulate/0.7.5+galaxy0", + "tool_shed_repository": { + "changeset_revision": "9bd945a03d7b", + "name": "anndata_manipulate", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"input\": {\"__class__\": \"ConnectedValue\"}, \"manipulate\": {\"function\": \"rename_categories\", \"__current_case__\": 3, \"key\": \"leiden\", \"categories\": \"0, 1, 2, trichoblasts, VC-1, 5, VC-2, atrichoblasts, endodermis, cortex, 10, columella+QC+NC, xylem\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "0.7.5+galaxy0", + "type": "tool", + "uuid": "80de3738-3923-42f3-a051-e101ff7bd86a", + "when": null, + "workflow_outputs": [] + }, + "30": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 30, + "input_connections": { + "adata": { + "id": 25, + "output_name": "anndata_out" + } + }, + "inputs": [], + "label": "Complex DotPlot", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1846, + "top": 309.5 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"format\": \"png\", \"method\": {\"method\": \"pl.dotplot\", \"__current_case__\": 2, \"var_names\": {\"type\": \"custom\", \"__current_case__\": 1, \"var_names\": \"AT1G26680,COBL2,AT3G55180,NCED2,AT5G22550,ATL63,RGF8,TEL1,DUF9,AGL42,PLT1,PLT2,PER3,PER9,PBL15,TBL30,AT2G48130,AT3G22620,AED3,T3P18-7,MBK21-8,NPF6-4,CYP71B34,IAMT1,AT1G31950,F14G6-22,GL2,ANL2,PMEI4,MUD21-4,EXPA7,MES15,ADF8,COBL9,TPR14,BHLH144,MYB83,SCPL35,MYB46,COBL4,PIN7,PIN3 \"}, \"groupby\": \"leiden\", \"num_categories\": \"9\", \"log\": false, \"use_raw\": true, \"dendrogram\": false, \"var_group_positions\": [{\"__index__\": 0, \"start\": \"0\", \"end\": \"5\", \"label\": \"Columella\"}, {\"__index__\": 1, \"start\": \"6\", \"end\": \"9\", \"label\": \"QC\"}, {\"__index__\": 2, \"start\": \"10\", \"end\": \"11\", \"label\": \"NC\"}, {\"__index__\": 3, \"start\": \"12\", \"end\": \"17\", \"label\": \"Endodermis\"}, {\"__index__\": 4, \"start\": \"18\", \"end\": \"23\", \"label\": \"Cortex\"}, {\"__index__\": 5, \"start\": \"24\", \"end\": \"29\", \"label\": \"Atrichoblast\"}, {\"__index__\": 6, \"start\": \"30\", \"end\": \"34\", \"label\": \"Trichoblast\"}, {\"__index__\": 7, \"start\": \"35\", \"end\": \"39\", \"label\": \"Xylem\"}, {\"__index__\": 8, \"start\": \"40\", \"end\": \"41\", \"label\": \"VC\"}], \"var_group_rotation\": null, \"figsize\": {\"test\": \"no\", \"__current_case__\": 1}, \"layer\": \"\", \"color_map\": \"viridis\", \"dot_max\": null, \"dot_min\": null, \"matplotlib_pyplot_scatter\": {\"alpha\": null, \"vmin\": null, \"vmax\": null, \"linewidths\": \"0.0\", \"edgecolors\": \"face\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "625a157f-f08f-4137-a27b-721bee0feb59", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "b35ffb5a-5f29-4105-a016-c02ceee0d9da" + } + ] + }, + "31": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 31, + "input_connections": { + "adata": { + "id": 29, + "output_name": "anndata" + } + }, + "inputs": [], + "label": "Complex DotPlot with new labels", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1845.449951171875, + "top": 465.16668701171875 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"format\": \"png\", \"method\": {\"method\": \"pl.dotplot\", \"__current_case__\": 2, \"var_names\": {\"type\": \"custom\", \"__current_case__\": 1, \"var_names\": \"AT1G26680,COBL2,AT3G55180,NCED2,AT5G22550,ATL63,RGF8,TEL1,DUF9,AGL42,PLT1,PLT2,PER3,PER9,PBL15,TBL30,AT2G48130,AT3G22620,AED3,T3P18-7,MBK21-8,NPF6-4,CYP71B34,IAMT1,AT1G31950,F14G6-22,GL2,ANL2,PMEI4,MUD21-4,EXPA7,MES15,ADF8,COBL9,TPR14,BHLH144,MYB83,SCPL35,MYB46,COBL4,PIN7,PIN3 \"}, \"groupby\": \"leiden\", \"num_categories\": \"9\", \"log\": false, \"use_raw\": true, \"dendrogram\": false, \"var_group_positions\": [{\"__index__\": 0, \"start\": \"0\", \"end\": \"5\", \"label\": \"Columella\"}, {\"__index__\": 1, \"start\": \"6\", \"end\": \"9\", \"label\": \"QC\"}, {\"__index__\": 2, \"start\": \"10\", \"end\": \"11\", \"label\": \"NC\"}, {\"__index__\": 3, \"start\": \"12\", \"end\": \"17\", \"label\": \"Endodermis\"}, {\"__index__\": 4, \"start\": \"18\", \"end\": \"23\", \"label\": \"Cortex\"}, {\"__index__\": 5, \"start\": \"24\", \"end\": \"29\", \"label\": \"Atrichoblast\"}, {\"__index__\": 6, \"start\": \"30\", \"end\": \"34\", \"label\": \"Trichoblast\"}, {\"__index__\": 7, \"start\": \"35\", \"end\": \"39\", \"label\": \"Xylem\"}, {\"__index__\": 8, \"start\": \"40\", \"end\": \"41\", \"label\": \"VC\"}], \"var_group_rotation\": null, \"figsize\": {\"test\": \"no\", \"__current_case__\": 1}, \"layer\": \"\", \"color_map\": \"viridis\", \"dot_max\": null, \"dot_min\": null, \"matplotlib_pyplot_scatter\": {\"alpha\": null, \"vmin\": null, \"vmax\": null, \"linewidths\": \"0.0\", \"edgecolors\": \"face\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "a2f04375-1e67-4764-ae78-2678dcf2c03a", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "329e95de-cf53-4c0b-9a9b-ad30632ceeb8" + } + ] + }, + "32": { + "annotation": "", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "errors": null, + "id": 32, + "input_connections": { + "adata": { + "id": 29, + "output_name": "anndata" + } + }, + "inputs": [], + "label": "Plot UMAP with new labels", + "name": "Plot", + "outputs": [ + { + "name": "out_png", + "type": "png" + } + ], + "position": { + "left": 1848, + "top": 637.5 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/scanpy_plot/scanpy_plot/1.7.1+galaxy0", + "tool_shed_repository": { + "changeset_revision": "6adf98e782f3", + "name": "scanpy_plot", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"input\", \"adata\": {\"__class__\": \"ConnectedValue\"}, \"advanced_common\": {\"show_log\": false}, \"chromInfo\": \"/opt/galaxy/tool-data/shared/ucsc/chrom/?.len\", \"format\": \"png\", \"method\": {\"method\": \"pl.umap\", \"__current_case__\": 14, \"color\": \"leiden, batch\", \"use_raw\": false, \"edges\": {\"edges\": \"False\", \"__current_case__\": 1}, \"arrows\": false, \"sort_order\": true, \"groups\": \"\", \"plot\": {\"components\": [], \"projection\": \"2d\", \"legend_loc\": \"on data\", \"legend_fontsize\": \"14\", \"legend_fontweight\": \"normal\", \"size\": null, \"color_map\": \"None\", \"palette\": \"rainbow\", \"frameon\": true, \"ncols\": \"4\", \"wspace\": \"0.1\", \"hspace\": \"0.25\", \"title\": \"\"}, \"matplotlib_pyplot_scatter\": {\"alpha\": null, \"vmin\": null, \"vmax\": null, \"linewidths\": \"0.0\", \"edgecolors\": \"face\"}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.7.1+galaxy0", + "type": "tool", + "uuid": "c83c81b0-cd9e-41c5-8b72-4b024b7260dc", + "when": null, + "workflow_outputs": [ + { + "label": null, + "output_name": "out_png", + "uuid": "579d19d3-a98c-4e3a-92aa-0b65e2ffcefd" + } + ] + } + }, + "tags": [ + "transcriptomics", + "single-cell" + ], + "uuid": "748a7ce0-4759-4409-8c6f-21bab42ce3ed", + "version": 1 +} \ No newline at end of file diff --git a/topics/single-cell/tutorials/scrna-raceid/data-library.yaml b/topics/single-cell/tutorials/scrna-raceid/data-library.yaml index f1b52116f76d59..53a58d61ce8fcc 100644 --- a/topics/single-cell/tutorials/scrna-raceid/data-library.yaml +++ b/topics/single-cell/tutorials/scrna-raceid/data-library.yaml @@ -15,7 +15,7 @@ items: - name: 'DOI: 10.5281/zenodo.1511582' description: latest items: - - url: https://zenodo.org/api/files/9d07494b-b15c-4a69-baf5-1e77087586b7/intestinalData.tsv + - url: https://zenodo.org/record/1511582/files/intestinalData.tsv src: url ext: tabular info: https://doi.org/10.5281/zenodo.1511582 diff --git a/topics/single-cell/tutorials/scrna-raceid/tutorial.md b/topics/single-cell/tutorials/scrna-raceid/tutorial.md index a4d7aa455614d5..254f2a0423cca3 100644 --- a/topics/single-cell/tutorials/scrna-raceid/tutorial.md +++ b/topics/single-cell/tutorials/scrna-raceid/tutorial.md @@ -6,6 +6,10 @@ priority: 2 redirect_from: - /topics/transcriptomics/tutorials/scrna-raceid/tutorial zenodo_link: 'https://zenodo.org/record/1511582' +answer_histories: + - label: "UseGalaxy.eu" + history: https://singlecell.usegalaxy.eu/u/videmp/h/downstream-single-cell-rna-analysis-with-raceid + date: 2024-12-13 tags: questions: - What is normalisation and why is it necessary? @@ -41,12 +45,15 @@ key_points: - RaceID can be used to cluster cells based on the their gene expression profiles - StemID describes a hierarchical relationship between clusters to find multipotent progenitor stem cells to provide an understanding of cell development - FateID predicts the potential lineages that cells within specific clusters are inclined towards -contributors: - - mtekman - - astrovsky01 -gitter: Galaxy-Training-Network/galaxy-single-cell +contributions: + authorship: + - mtekman + - astrovsky01 + testing: + - pavanvidem +gitter: Galaxy-Training-Network/galaxy-single-cell --- diff --git a/topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow-tests.yml b/topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow-tests.yml new file mode 100644 index 00000000000000..56526d85cfedf0 --- /dev/null +++ b/topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow-tests.yml @@ -0,0 +1,19 @@ +- doc: Test outline for RaceID-Workflow + job: + Tabular Matrix: + class: File + location: https://zenodo.org/record/1511582/files/intestinalData.tsv + filetype: tabular + outputs: + outdiffgenes (Step 8): + asserts: + has_n_lines: + value: 11 + has_text: + text: "Chmp1a" + text: "Dgat1" + outpdf (Step 9): + asserts: + has_size: + value: 1573003 + delta: 31460 \ No newline at end of file diff --git a/topics/single-cell/tutorials/scrna-raceid/workflows/main_workflow.ga b/topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.ga similarity index 72% rename from topics/single-cell/tutorials/scrna-raceid/workflows/main_workflow.ga rename to topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.ga index 9201ad7abe845c..97c19e05b4245a 100644 --- a/topics/single-cell/tutorials/scrna-raceid/workflows/main_workflow.ga +++ b/topics/single-cell/tutorials/scrna-raceid/workflows/RaceID-Workflow.ga @@ -1,8 +1,25 @@ { "a_galaxy_workflow": "true", "annotation": "Downstream Single-cell RNA analysis with RaceID", + "comments": [], + "creator": [ + { + "class": "Person", + "identifier": "0000-0002-4181-2676", + "name": "Mehmet Tekman" + }, + { + "class": "Person", + "identifier": "0000-0002-7901-7109", + "name": "Alex Ostrovsky" + } + ], "format-version": "0.1", + "license": "CC-BY-4.0", "name": "RaceID Workflow", + "report": { + "markdown": "\n# Workflow Execution Report\n\n## Workflow Inputs\n```galaxy\ninvocation_inputs()\n```\n\n## Workflow Outputs\n```galaxy\ninvocation_outputs()\n```\n\n## Workflow\n```galaxy\nworkflow_display()\n```\n" + }, "steps": { "0": { "annotation": "", @@ -20,31 +37,20 @@ "name": "Input dataset", "outputs": [], "position": { - "bottom": 167.21666717529297, - "height": 48.96665954589844, - "left": 38.75, - "right": 198.75, - "top": 118.25000762939453, - "width": 160, - "x": 38.75, - "y": 118.25000762939453 + "left": 0, + "top": 2.5 }, "tool_id": null, - "tool_state": "{\"optional\": false}", + "tool_state": "{\"optional\": false, \"tag\": null}", "tool_version": null, "type": "data_input", "uuid": "ba3fe014-c728-486e-8ce4-55be24df0682", - "workflow_outputs": [ - { - "label": null, - "output_name": "output", - "uuid": "0c858705-5e28-44e0-8023-46aec4a01725" - } - ] + "when": null, + "workflow_outputs": [] }, "1": { "annotation": "", - "content_id": "raceid_filtnormconf", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_filtnormconf/raceid_filtnormconf/0.2.3+galaxy0", "errors": null, "id": 1, "input_connections": { @@ -75,27 +81,23 @@ } ], "position": { - "bottom": 410.7666702270508, - "height": 293.76666259765625, - "left": 272.5, - "right": 432.5, - "top": 117.00000762939453, - "width": 160, - "x": 272.5, - "y": 117.00000762939453 + "left": 233.75, + "top": 1.25 }, "post_job_actions": {}, - "tool_id": "raceid_filtnormconf", - "tool_state": "{\"__input_ext\": \"tabular\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"filt\": {\"mintotal\": \"3000\", \"minexpr\": \"5\", \"minnumber\": \"5\", \"hist_geq_one\": \"false\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"intable\": {\"__class__\": \"ConnectedValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_filtnormconf/raceid_filtnormconf/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "43c146e25a43", + "name": "raceid_filtnormconf", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"tabular\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"filt\": {\"mintotal\": \"3000\", \"minexpr\": \"5\", \"minnumber\": \"5\", \"hist_geq_one\": false, \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"intable\": {\"__class__\": \"ConnectedValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "6fd8dbde-93d2-4903-865f-78cc3c2fa299", + "when": null, "workflow_outputs": [ - { - "label": null, - "output_name": "outtable", - "uuid": "d9293468-efef-4a10-89a6-0064a1740afd" - }, { "label": null, "output_name": "outpdf", @@ -110,12 +112,17 @@ "label": null, "output_name": "outlog", "uuid": "35505252-c65a-4572-9d66-b0e69893c01b" + }, + { + "label": null, + "output_name": "outtable", + "uuid": "d9293468-efef-4a10-89a6-0064a1740afd" } ] }, "2": { "annotation": "", - "content_id": "raceid_clustering", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_clustering/raceid_clustering/0.2.3+galaxy0", "errors": null, "id": 2, "input_connections": { @@ -150,21 +157,22 @@ } ], "position": { - "bottom": 466.46668243408203, - "height": 350.7166748046875, - "left": 520, - "right": 680, - "top": 115.75000762939453, - "width": 160, - "x": 520, - "y": 115.75000762939453 + "left": 481.25, + "top": 0 }, "post_job_actions": {}, - "tool_id": "raceid_clustering", - "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"clust\": {\"metric\": \"pearson\", \"funcluster\": \"kmedoids\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"extra\": {\"tablelim\": \"25\", \"plotlim\": \"10\", \"foldchange\": \"1.0\", \"pvalue\": \"0.01\"}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"outlier\": {\"outminc\": \"5\", \"outlg\": \"2\", \"final\": \"false\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"tsne\": {\"perplexity\": \"30\", \"knn\": \"10\", \"umap_nn\": \"15\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_clustering/raceid_clustering/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "a4b734cd253b", + "name": "raceid_clustering", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"clust\": {\"metric\": \"pearson\", \"funcluster\": \"kmedoids\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"extra\": {\"tablelim\": \"25\", \"plotlim\": \"10\", \"foldchange\": \"1.0\", \"pvalue\": \"0.01\"}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"outlier\": {\"outminc\": \"5\", \"outlg\": \"2\", \"final\": false, \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"tsne\": {\"perplexity\": \"30\", \"knn\": \"10\", \"umap_nn\": \"15\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "ca88c6cb-03e0-400c-8942-4c7ac0545a67", + "when": null, "workflow_outputs": [ { "label": null, @@ -195,7 +203,7 @@ }, "3": { "annotation": "", - "content_id": "raceid_trajectory", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_trajectory/raceid_trajectory/0.2.3+galaxy0", "errors": null, "id": 3, "input_connections": { @@ -218,21 +226,22 @@ } ], "position": { - "bottom": 298.08333587646484, - "height": 179.8333282470703, - "left": 778.75, - "right": 938.75, - "top": 118.25000762939453, - "width": 160, - "x": 778.75, - "y": 118.25000762939453 + "left": 740, + "top": 2.5 }, "post_job_actions": {}, - "tool_id": "raceid_trajectory", - "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"comppval\": {\"pthr\": \"0.01\", \"sensitive\": \"false\"}, \"compscore\": {\"nn\": \"1\", \"scthr\": \"0.0\"}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"plotgraph\": {\"showcells\": \"false\", \"scthr\": \"0.0\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"projback\": {\"pdishuf\": \"2000\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"projcell\": {\"knn\": \"3\", \"cthr\": \"5\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"use_log\": \"false\", \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_trajectory/raceid_trajectory/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "72979cac22b2", + "name": "raceid_trajectory", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, + "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"comppval\": {\"pthr\": \"0.01\", \"sensitive\": false}, \"compscore\": {\"nn\": \"1\", \"scthr\": \"0.0\"}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"plotgraph\": {\"showcells\": false, \"scthr\": \"0.0\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"projback\": {\"pdishuf\": \"2000\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"projcell\": {\"knn\": \"3\", \"cthr\": \"5\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"use_log\": false, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "515732a4-cba5-402a-9cc8-11980c4815c6", + "when": null, "workflow_outputs": [ { "label": null, @@ -248,7 +257,7 @@ }, "4": { "annotation": "", - "content_id": "raceid_inspectclusters", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspectclusters/raceid_inspectclusters/0.2.3+galaxy0", "errors": null, "id": 4, "input_connections": { @@ -271,37 +280,38 @@ } ], "position": { - "bottom": 512.9500045776367, - "height": 147.1999969482422, - "left": 773.75, - "right": 933.75, - "top": 365.75000762939453, - "width": 160, - "x": 773.75, - "y": 365.75000762939453 + "left": 735, + "top": 250 }, "post_job_actions": {}, - "tool_id": "raceid_inspectclusters", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspectclusters/raceid_inspectclusters/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "41f34e925bd5", + "name": "raceid_inspectclusters", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"diffgtest\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"gois\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"plotgen\": {\"do_opt\": \"yes\", \"__current_case__\": 1, \"clusts_plot\": \"\"}, \"plotsym\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "727ee241-4de7-408f-867a-fc1ed21c23cb", + "when": null, "workflow_outputs": [ { "label": null, - "output_name": "outpdf", - "uuid": "051cbdaa-6513-4239-91b6-e22004f6469f" + "output_name": "outlog", + "uuid": "bf324238-9739-4960-90c0-2935d225bd42" }, { "label": null, - "output_name": "outlog", - "uuid": "bf324238-9739-4960-90c0-2935d225bd42" + "output_name": "outpdf", + "uuid": "051cbdaa-6513-4239-91b6-e22004f6469f" } ] }, "5": { "annotation": "", - "content_id": "raceid_inspectclusters", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspectclusters/raceid_inspectclusters/0.2.3+galaxy0", "errors": null, "id": 5, "input_connections": { @@ -324,37 +334,38 @@ } ], "position": { - "bottom": 712.9500045776367, - "height": 147.1999969482422, - "left": 773.75, - "right": 933.75, - "top": 565.7500076293945, - "width": 160, - "x": 773.75, - "y": 565.7500076293945 + "left": 735, + "top": 450 }, "post_job_actions": {}, - "tool_id": "raceid_inspectclusters", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspectclusters/raceid_inspectclusters/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "41f34e925bd5", + "name": "raceid_inspectclusters", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"diffgtest\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"gois\": {\"do_opt\": \"yes\", \"__current_case__\": 1, \"inspect_goi_genes\": \"Gstm3, St3gal4, Gna11\", \"inspect_goi_cells\": \"\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"plotgen\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"plotsym\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "6db63f68-65bc-4bb5-96a2-9d3e75712287", + "when": null, "workflow_outputs": [ { "label": null, - "output_name": "outpdf", - "uuid": "344cd8bc-01a1-4614-8def-806815a615af" + "output_name": "outlog", + "uuid": "3268d277-37a0-4081-ae95-8c7a83efb903" }, { "label": null, - "output_name": "outlog", - "uuid": "3268d277-37a0-4081-ae95-8c7a83efb903" + "output_name": "outpdf", + "uuid": "344cd8bc-01a1-4614-8def-806815a615af" } ] }, "6": { "annotation": "", - "content_id": "raceid_inspectclusters", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspectclusters/raceid_inspectclusters/0.2.3+galaxy0", "errors": null, "id": 6, "input_connections": { @@ -377,37 +388,38 @@ } ], "position": { - "bottom": 827.9499664306641, - "height": 147.1999969482422, - "left": 1035, - "right": 1195, - "top": 680.7499694824219, - "width": 160, - "x": 1035, - "y": 680.7499694824219 + "left": 996.25, + "top": 564.9999618530273 }, "post_job_actions": {}, - "tool_id": "raceid_inspectclusters", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspectclusters/raceid_inspectclusters/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "41f34e925bd5", + "name": "raceid_inspectclusters", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"diffgtest\": {\"do_opt\": \"yes\", \"__current_case__\": 1, \"set_a\": {\"name_set\": \"Cells in 1\", \"meth\": {\"type\": \"cln\", \"__current_case__\": 0, \"selector\": \"1\"}}, \"set_b\": {\"name_set\": \"Cells in 3\", \"meth\": {\"type\": \"cln\", \"__current_case__\": 0, \"selector\": \"3\"}}, \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}, \"gois\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"plotgen\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"plotsym\": {\"do_opt\": \"no\", \"__current_case__\": 0}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "57c7332a-c6f5-484f-8a5c-d5467d320e72", + "when": null, "workflow_outputs": [ { "label": null, - "output_name": "outpdf", - "uuid": "d028c3e8-3acb-44ba-968a-74b1cdfb4bb1" + "output_name": "outlog", + "uuid": "8f9f3e29-3c56-4fcf-af0a-f2bea2c10234" }, { "label": null, - "output_name": "outlog", - "uuid": "8f9f3e29-3c56-4fcf-af0a-f2bea2c10234" + "output_name": "outpdf", + "uuid": "d028c3e8-3acb-44ba-968a-74b1cdfb4bb1" } ] }, "7": { "annotation": "", - "content_id": "raceid_inspecttrajectory", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspecttrajectory/raceid_inspecttrajectory/0.2.3+galaxy0", "errors": null, "id": 7, "input_connections": { @@ -434,27 +446,23 @@ } ], "position": { - "bottom": 372.61666107177734, - "height": 253.1166534423828, - "left": 1041.25, - "right": 1201.25, - "top": 119.50000762939453, - "width": 160, - "x": 1041.25, - "y": 119.50000762939453 + "left": 1002.5, + "top": 3.75 }, "post_job_actions": {}, - "tool_id": "raceid_inspecttrajectory", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspecttrajectory/raceid_inspecttrajectory/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "c8434a623268", + "name": "raceid_inspecttrajectory", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"trjfid\": {\"basic\": {\"doit\": \"no\", \"__current_case__\": 0}}, \"trjsid\": {\"basic\": {\"doit\": \"yes\", \"__current_case__\": 1, \"i\": \"3\", \"br\": \"1,3,5\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "4a519b59-2319-4811-969e-3b872f95c377", + "when": null, "workflow_outputs": [ - { - "label": null, - "output_name": "outpdf", - "uuid": "1e19e4d3-802d-4713-b920-ba8264bce2d0" - }, { "label": null, "output_name": "outdiffgenes", @@ -464,12 +472,17 @@ "label": null, "output_name": "outlog", "uuid": "4404cdf2-ee33-4e08-8fed-83b52bbfad88" + }, + { + "label": null, + "output_name": "outpdf", + "uuid": "1e19e4d3-802d-4713-b920-ba8264bce2d0" } ] }, "8": { "annotation": "", - "content_id": "raceid_inspecttrajectory", + "content_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspecttrajectory/raceid_inspecttrajectory/0.2.3+galaxy0", "errors": null, "id": 8, "input_connections": { @@ -492,31 +505,32 @@ } ], "position": { - "bottom": 625.5833358764648, - "height": 179.8333282470703, - "left": 1040, - "right": 1200, - "top": 445.75000762939453, - "width": 160, - "x": 1040, - "y": 445.75000762939453 + "left": 1001.25, + "top": 330 }, "post_job_actions": {}, - "tool_id": "raceid_inspecttrajectory", + "tool_id": "toolshed.g2.bx.psu.edu/repos/iuc/raceid_inspecttrajectory/raceid_inspecttrajectory/0.2.3+galaxy0", + "tool_shed_repository": { + "changeset_revision": "c8434a623268", + "name": "raceid_inspecttrajectory", + "owner": "iuc", + "tool_shed": "toolshed.g2.bx.psu.edu" + }, "tool_state": "{\"__input_ext\": \"rdata\", \"chromInfo\": \"/tmp/tmp8dp_jicw/galaxy-dev/tool-data/shared/ucsc/chrom/?.len\", \"inputrds\": {\"__class__\": \"ConnectedValue\"}, \"trjfid\": {\"basic\": {\"doit\": \"yes\", \"__current_case__\": 1, \"cellsfromz\": \"1,3,5\", \"use\": {\"def\": \"yes\", \"__current_case__\": 0}, \"som\": {\"doit\": \"no\", \"__current_case__\": 0}}}, \"trjsid\": {\"basic\": {\"doit\": \"no\", \"__current_case__\": 0}}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "0.2.3+galaxy0", "type": "tool", "uuid": "883ffd05-1545-449f-8028-c45c0f9edfd5", + "when": null, "workflow_outputs": [ { "label": null, - "output_name": "outpdf", - "uuid": "8ef7bad4-ff15-42d2-acc4-6c5bc5de89e4" + "output_name": "outlog", + "uuid": "23f0fab3-0128-443a-a26a-a3e40cee4578" }, { "label": null, - "output_name": "outlog", - "uuid": "23f0fab3-0128-443a-a26a-a3e40cee4578" + "output_name": "outpdf", + "uuid": "8ef7bad4-ff15-42d2-acc4-6c5bc5de89e4" } ] } @@ -525,6 +539,6 @@ "transcriptomics", "single-cell" ], - "uuid": "48285e72-0fb6-4cd6-a329-73d0e28effea", + "uuid": "f969d2cd-a16a-4a8e-b553-5d697b3ea961", "version": 1 } \ No newline at end of file diff --git a/topics/statistics/community.md b/topics/statistics/community.md new file mode 100644 index 00000000000000..41f24c6fa2788e --- /dev/null +++ b/topics/statistics/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: statistics +--- + diff --git a/topics/statistics/images/loris_tutorial/hyperparameters.png b/topics/statistics/images/loris_tutorial/hyperparameters.png new file mode 100644 index 00000000000000..ca95b8cab89d7e Binary files /dev/null and b/topics/statistics/images/loris_tutorial/hyperparameters.png differ diff --git a/topics/statistics/images/loris_tutorial/model_comparison.png b/topics/statistics/images/loris_tutorial/model_comparison.png new file mode 100644 index 00000000000000..0e2ab545a44b71 Binary files /dev/null and b/topics/statistics/images/loris_tutorial/model_comparison.png differ diff --git a/topics/statistics/images/loris_tutorial/report_tabs.png b/topics/statistics/images/loris_tutorial/report_tabs.png new file mode 100644 index 00000000000000..ce5505eedf4f1b Binary files /dev/null and b/topics/statistics/images/loris_tutorial/report_tabs.png differ diff --git a/topics/statistics/images/loris_tutorial/robustness.png b/topics/statistics/images/loris_tutorial/robustness.png new file mode 100644 index 00000000000000..e95f682080b981 Binary files /dev/null and b/topics/statistics/images/loris_tutorial/robustness.png differ diff --git a/topics/statistics/images/loris_tutorial/test_metrics_results.png b/topics/statistics/images/loris_tutorial/test_metrics_results.png new file mode 100644 index 00000000000000..79ca4120c8b7c7 Binary files /dev/null and b/topics/statistics/images/loris_tutorial/test_metrics_results.png differ diff --git a/topics/statistics/images/loris_tutorial/tutorial_schema.png b/topics/statistics/images/loris_tutorial/tutorial_schema.png new file mode 100644 index 00000000000000..969e504bc202f6 Binary files /dev/null and b/topics/statistics/images/loris_tutorial/tutorial_schema.png differ diff --git a/topics/statistics/tutorials/galaxy-ludwig/tutorial.md b/topics/statistics/tutorials/galaxy-ludwig/tutorial.md index 11f261f556040c..a49e4665f8a8fa 100644 --- a/topics/statistics/tutorials/galaxy-ludwig/tutorial.md +++ b/topics/statistics/tutorials/galaxy-ludwig/tutorial.md @@ -137,7 +137,7 @@ These are just a few of the many options that Ludwig provides out-of-the-box, al > <hands-on-title> Task description </hands-on-title> > -> 1. {% tool [Ludwig Experiment](.bx.psu.edu/repos/paulo_lyra_jr/ludwig_applications/ludwig_experiment/2024.0.10.3) %} with the following parameters: +> 1. {% tool [Ludwig Experiment](toolshed.g2.bx.psu.edu/repos/paulo_lyra_jr/ludwig_applications/ludwig_experiment/2024.0.10.3) %} with the following parameters: > - {% icon param-file %} *"Select the dataset containing model configuration"*: `config.yaml` > - {% icon param-file %} *"Input dataset"*: `mnist_dataset.csv` > - {% icon param-file %} *"Raw data"*: `mnist_images.zip` @@ -320,7 +320,7 @@ Run the experiment once again. If you need a refresher on how to set up the Ludw > >><hands-on-title> Task description </hands-on-title> >> ->> 1. {% tool [Ludwig Experiment](.bx.psu.edu/repos/paulo_lyra_jr/ludwig_applications/ludwig_experiment/2024.0.10.3) %} with the following parameters: +>> 1. {% tool [Ludwig Experiment](toolshed.g2.bx.psu.edu/repos/paulo_lyra_jr/ludwig_applications/ludwig_experiment/2024.0.10.3) %} with the following parameters: >> - {% icon param-file %} *"Select the dataset containing model configuration"*: `config.yaml` >> - {% icon param-file %} *"Input dataset"*: `mnist_dataset.csv` >> - {% icon param-file %} *"Raw data"*: `mnist_images.zip` diff --git a/topics/statistics/tutorials/galaxy-ludwig/workflows/main_workflow.ga b/topics/statistics/tutorials/galaxy-ludwig/workflows/main_workflow.ga index cbc86f450c94fd..e3365a8b123fb8 100644 --- a/topics/statistics/tutorials/galaxy-ludwig/workflows/main_workflow.ga +++ b/topics/statistics/tutorials/galaxy-ludwig/workflows/main_workflow.ga @@ -210,7 +210,7 @@ "post_job_actions":{ }, - "tool_id":"https://toolshed.g2.bx.psu.edu/view/paulo_lyra_jr/ludwig_applications/3e565bbe8b71", + "tool_id":"toolshed.g2.bx.psu.edu/repos/paulo_lyra_jr/ludwig_applications/ludwig_experiment/2024.0.10.3", "tool_state":"{\"config\": {\"__class__\": \"ConnectedValue\"}, \"data_format\": \"auto\", \"dataset\": {\"__class__\": \"ConnectedValue\"}, \"disable_parallel_threads\": \"false\", \"eval_split\": \"test\", \"k_fold\": null, \"model_load_path\": {\"__class__\": \"RuntimeValue\"}, \"model_resume_path\": {\"__class__\": \"RuntimeValue\"}, \"random_seed\": \"42\", \"raw_data\": {\"__class__\": \"ConnectedValue\"}, \"skip_save_predictions\": \"false\", \"test_set\": {\"__class__\": \"RuntimeValue\"}, \"training_set\": {\"__class__\": \"RuntimeValue\"}, \"training_set_metadata\": {\"__class__\": \"RuntimeValue\"}, \"validation_set\": {\"__class__\": \"RuntimeValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version":"0.10.3+0", "type":"tool", diff --git a/topics/statistics/tutorials/loris_model/README.md b/topics/statistics/tutorials/loris_model/README.md new file mode 100644 index 00000000000000..dc9081aeb26fc3 --- /dev/null +++ b/topics/statistics/tutorials/loris_model/README.md @@ -0,0 +1,2 @@ +# Tutorial_Galaxy-Pycaret +A Galaxy tutorial to create and run a logistic regression classifier with LORIS dataset (Chang et al., 2024) diff --git a/topics/statistics/tutorials/loris_model/data-library.yaml b/topics/statistics/tutorials/loris_model/data-library.yaml new file mode 100644 index 00000000000000..659ce885d69116 --- /dev/null +++ b/topics/statistics/tutorials/loris_model/data-library.yaml @@ -0,0 +1,23 @@ +--- +destination: + type: library + name: GTN - Material + description: Galaxy Training Network Material + synopsis: Galaxy Training Network Material. See https://training.galaxyproject.org +items: +- name: New topic + description: Topic summary + items: + - name: LORIS LLR6 model build + items: + - name: 'DOI: 10.5281/zenodo.13885908' + description: latest + items: + - url: https://zenodo.org/api/records/13885908/files/Chowell_train_Response.tsv/content + src: url + ext: '' + info: https://zenodo.org/records/13885908 + - url: https://zenodo.org/api/records/13885908/files/Chowell_test_Response.tsv/content + src: url + ext: '' + info: https://zenodo.org/records/13885908 diff --git a/topics/statistics/tutorials/loris_model/faqs/index.md b/topics/statistics/tutorials/loris_model/faqs/index.md new file mode 100644 index 00000000000000..9ce3fe4fce824b --- /dev/null +++ b/topics/statistics/tutorials/loris_model/faqs/index.md @@ -0,0 +1,3 @@ +--- +layout: faq-page +--- diff --git a/topics/statistics/tutorials/loris_model/tutorial.bib b/topics/statistics/tutorials/loris_model/tutorial.bib new file mode 100644 index 00000000000000..eeaba01b208faa --- /dev/null +++ b/topics/statistics/tutorials/loris_model/tutorial.bib @@ -0,0 +1,36 @@ + +# This is the bibliography file for your tutorial. +# +# To add bibliography (bibtex) entries here, follow these steps: +# 1) Find the DOI for the article you want to cite +# 2) Go to https://doi2bib.org and fill in the DOI +# 3) Copy the resulting bibtex entry into this file +# +# To cite the example below, in your tutorial.md file +# use {% cite Batut2018 %} +# +# If you want to cite an online resourse (website etc) +# you can use the 'online' format (see below) +# +# You can remove the examples below + +@article{Chang2024, + title = {LORIS robustly predicts patient outcomes with immune checkpoint blockade therapy using common clinical, pathologic and genomic features}, + volume = {5}, + ISSN = {2662-1347}, + url = {http://dx.doi.org/10.1038/s43018-024-00772-7}, + DOI = {10.1038/s43018-024-00772-7}, + number = {8}, + journal = {Nature Cancer}, + publisher = {Springer Science and Business Media LLC}, + author = {Chang, Tian-Gen and Cao, Yingying and Sfreddo, Hannah J. and Dhruba, Saugato Rahman and Lee, Se-Hoon and Valero, Cristina and Yoo, Seong-Keun and Chowell, Diego and Morris, Luc G. T. and Ruppin, Eytan}, + year = {2024}, + month = jun, + pages = {1158–1175} +} +@online{gtn-website, + author = {GTN community}, + title = {GTN Training Materials: Collection of tutorials developed and maintained by the worldwide Galaxy community}, + url = {https://training.galaxyproject.org}, + urldate = {2021-03-24} +} diff --git a/topics/statistics/tutorials/loris_model/tutorial.md b/topics/statistics/tutorials/loris_model/tutorial.md new file mode 100644 index 00000000000000..d4a6cef30bdac1 --- /dev/null +++ b/topics/statistics/tutorials/loris_model/tutorial.md @@ -0,0 +1,287 @@ +--- +layout: tutorial_hands_on +level: Intermediate +title: Building the LORIS LLR6 PanCancer Model Using PyCaret +zenodo_link: https://zenodo.org/records/13885908 +questions: +- How can the LORIS LLR6 model published by Chang et al., 2024 be reproduced? +- Which tools can be used in Galaxy to obtain a logistic regression model? +- How can the model be evaluated to confirm its performance? +objectives: +- Use a large dataset of immune checkpoint blockade (ICB)-treated and non-ICB-treated patients across 18 solid tumor types, encompassing a wide range of clinical, pathologic and genomic features to build a Machine Learning Model. +- Build a Machine Learning model using PyCaret in Galaxy. +- Evaluate the model for robustness by comparing it with the original LORIS LLR6 model published by Chang et al., 2024. + +time_estimation: 1H +key_points: +- Use Galaxy tools to build a model based on LORIS Score model published (Chang et al., 2024). +- Confirm the robustness of the model by comparing its metrics with the model published by Chang et al., 2024. +contributors: +- paulocilasjr +- qchiujunhao +- jgoecks +tags: +- LORIS Score Model +- Machine Learning +- PyCaret +--- + +> <comment-title>PyCaret Model Comparison Tool</comment-title> +> +> The PyCaret Model Comparison tool described in this tutorial is currently available only on: +> [Cancer-Galaxy](https://cancer.usegalaxy.org) +> +> Galaxy-ML tools > PyCaret Model Comparison +> +> This tutorial will be updated as soon as the tool is incorporated into the main Galaxy project. +{: .comment} + +In this tutorial, we will use a comprehensive dataset of patients treated with immune checkpoint blockade (ICB) and non-ICB-treated patients across 18 solid tumor types to develop LORIS (Logistic Regression-based Immunotherapy-Response Score). The goal is to accurately predict patient responses to the treatment. + +To achieve this, we will follow three essential steps: (i) upload the patient data training file to Galaxy, (ii) set up and run the PyCaret Model Comparison Tool to train the best model, and (iii) evaluate the model’s predictive performance by comparing it to the published LORIS model ({% cite Chang2024 %}). + +![schema of the whole process of training model and test.](../../images/loris_tutorial/tutorial_schema.png "Overview of the process steps to obtain the model from the LORIS dataset.") + +> <agenda-title></agenda-title> +> +> In this tutorial, we will cover: +> +> 1. TOC +> {:toc} +> +{: .agenda} + +> <comment-title>Background</comment-title> +> +> The [LORIS dataset](https://github.com/rootchang/LORIS/blob/main/02.Input/AllData.xlsx) contains clinical, pathologic, and genomic features from a diverse cohort of patients, providing a foundation for robust analysis and model development. The raw dataset (xlsx) includes 10 distinct cohorts: +> +> Chowell_train, 2) Chowell_test, 3) MSK1, 4) MSK2, 5) Kato_panCancer, 6) Shim_NSCLC, 7) Vanguri_NSCLC, 8) Ravi_NSCLC, 9) Pradat_panCancer, 10) MSK_nonICB. +> +> To keep this tutorial concise, we will focus on the Chowell_train cohort for model training and the Chowell_test cohort for testing the PanCancer model. +> +{: .comment} + +# Dataset composition (features) to train the model + +Before we begin the hands-on session, here’s a brief explanation of the features we’ll be using. These features were selected based on the findings of Chang et al. (2024), who identified them as the most important for training the model. + +It's important to note that other features were tested in different combinations and model architectures. However, the authors concluded that the six features selected are the most valuable for achieving the best model performance. + +## TMB (Tumor mutation burden) +Tumor mutation burden (TMB) is defined as the total number of somatic mutations within a specified region of the tumor genome. It has been recognized as a biomarker for predicting the efficacy of immune checkpoint blockade (ICB) in solid tumors. The [U.S. Food and Drug Administration](https://www.fda.gov/) (FDA) has approved a threshold of 10 mutations per megabase (mut/Mb) as a biomarker for response to ICB treatment. + +In this dataset, TMB values range from 0 to over 368 mutations per megabase, with some extreme values, such as 368.6 and 93.5. To mitigate the influence of these outliers, TMB values will be truncated at 50 mut/Mb, meaning any value exceeding 50 will be capped at 50. This is crucial because extreme TMB values can disproportionately skew the model’s learning process, leading to unreliable predictions. + +## Systemic Therapy History +This feature is a binary variable that indicates whether a patient received chemotherapy or targeted therapy prior to immunotherapy. It is coded as 1 if the patient had undergone such treatments before starting immunotherapy, and 0 if they had not. + +## Albumin +The albumin feature represents the albumin levels measured in patients, which is an important biomarker often associated with nutritional status and liver function. The values are measured in grams per deciliter (g/dL) and typically range between 2.1 and 4.9 g/dL in the dataset. Higher levels of albumin are generally associated with better overall health and can serve as an indicator of a patient's ability to recover or respond to treatments like immunotherapy. + +## Cancer Type +The CancerType feature represents the type of cancer diagnosed in each patient, which can vary significantly across the dataset. Common types include Non-Small Cell Lung Cancer (NSCLC), Small Cell Lung Cancer (SCLC), Melanoma, Endometrial cancer, and other cancer types such as Gastric, Colorectal, Renal, and Breast cancer. This feature is critical for understanding the heterogeneity of the patient cohort and may influence treatment decisions, response rates, and outcomes. + +Incorporating this feature into a machine learning model requires translating the categorical CancerType into one-hot encoded variables. Each cancer type will be represented as a binary feature (0 or 1), with each type becoming a separate column in the dataset. This enables the model to interpret the presence or absence of a specific cancer type for each patient. + +## NLR (blood neutrophil–lymphocyte ratio) +The neutrophil–lymphocyte ratio (NLR), a biomarker derived from the ratio of neutrophils to lymphocytes in the blood, is increasingly used in cancer research due to its association with inflammation and immune response. It can serve as a prognostic factor in various cancer types. Higher NLR values often indicate a poorer prognosis, potentially reflecting a more aggressive disease or impaired immune response. + +In this dataset, NLR values range, for example, from 0.8 to 88, with several extreme outliers. To address this, NLR values will be truncated at 25, meaning any value above 25 will be capped at 25. This truncation is important for preventing extreme outliers from disproportionately influencing the machine learning model. + +## Age +In predictive models for patient outcomes, age is a crucial feature because it is often correlated with various health factors and disease risks. As people age, their immune systems, metabolism, and ability to recover from illnesses may change, influencing how they respond to treatments, medications, or disease progression. Including age as a feature helps models account for the biological changes that occur over time and can improve the accuracy of predictions across different age groups. + +However, there are limits to how predictive age might be, particularly for extreme values. For example, patients over a certain age may share similar health characteristics, and further increases in age may not significantly add predictive value. Truncating age to a maximum value (like 85) helps avoid overemphasizing small differences between very old patients, where the added predictive power might be negligible. + +## Response +The Response feature is a categorical target variable indicating whether patients benefited from immune checkpoint blockade (ICB) therapy, classified as 0 (no benefit) or 1 (benefit). The model is trained using all previous features to predict patient outcomes when ICB is chosen as the treatment. + +# Model Architecture Selection Process + +In the article ({% cite Chang2024 %}), the model was trained using different architectures, including Decision Tree, Random Forest, Logistic Regression, and XGBoost, using the scikit-learn framework (full code is available here: [Chang et al., github repo](https://github.com/rootchang/LORIS/tree/main/code). In the end, the best model architecture was a Logistic LASSO (Least Absolute Shrinkage and Selection Operator) Regression model, fitting the six features discussed above. This model was named LLR6. + +It is important to note that PyCaret has the capability of training different model architectures, just as was done by Chang et al., 2024, but automatically. At the end of the run, a tabular list ranked by decreasing model performance is output. + +When setting up the PyCaret Model Comparison tool, one option, `Only Select Classification Models if you don't want to compare all models,` can be used to select specific architectures to be tested during training. Since the purpose of this tutorial is to replicate what was done, this option won't be used, and therefore, all possible models will be trained. Thus, we expect to obtain the same results as Chang et al., where the Logistic Regression Model has the best performance among all other models. + +# Prepare environment and get the data +> <comment-title>Preprocessing the raw data</comment-title> +> +> The raw data published by ({% cite Chang2024 %}) can be found here: +> [LORIS raw dataset](https://github.com/rootchang/LORIS/blob/main/02.Input/AllData.xlsx) +> +> We preprocessed the raw data using a Python script to: +> 1) Extract the `Chowell_train` and `Chowell_test` tab from the excel file. +> 2) Select the 7 features (`TMB`, `Systemic Therapy History`, `Albumin`, `Cancer Type`, `NLR`, `Age`, and `Response`) important for building the model. +> 3) Truncate the values for `Age`, `NLR`, and `TMB`. +> 4) Encode `Cancer Type` using one-hot encoding. +> 5) Save the dataset as a .tsv file. +> +> A Jupyter notebook for preprocessing can be found at Dockstore: [LORIS_preprocessing](https://dockstore.org/notebooks/github.com/paulocilasjr/pycaret-use-case/preprocessing:main?tab=info) +> +{: .comment} + +> <hands-on-title> Environment and Data Upload </hands-on-title> +> +> 1. Create a new history for this tutorial. If you are not inspired, you can name it *LORIS model classifier*. +> +> {% snippet faqs/galaxy/histories_create_new.md %} +> +> 2. Import the files from Zenodo or from the shared data library +> +> ``` +> https://zenodo.org/records/13885908/files/Chowell_train_Response.tsv +> https://zenodo.org/records/13885908/files/Chowell_test_Response.tsv +> ``` +>> <tip-title>Data Type</tip-title> +>> Leave the `Type` field as `Auto-Detect` when uploading (it's the default). +>> +> {: .tip} +> {% snippet faqs/galaxy/datasets_import_via_link.md %} +> +> 3. Check that the data format assigned for the file is **tabular**. +> If it is not, follow the Changing the datatype tip. +> +> {% snippet faqs/galaxy/datasets_change_datatype.md datatype="datatypes" %} +> +> 5. Add a tag (`LORIS model dataset`) to the dataset corresponding to `Chowell_train_Response.tsv` and `Chowell_test_Response.tsv` +> This is important to trace back on what dataset the model was built on. +> +> {% snippet faqs/galaxy/datasets_add_tag.md %} +> +{: .hands_on} + +# Using PyCaret Model Comparison Tool +> <hands-on-title> Task description </hands-on-title> +> +> 1. {% tool [PyCaret Model Comparison](toolshed.g2.bx.psu.edu/repos/paulo_lyra_jr/pycaret_model_comparison/PyCaret_Model_Comparison/2024.3.3.2+0) %} with the following parameters: +> - {% icon param-file %} *"Input Dataset (CSV or TSV)"*: `Chowell_train_Response.tsv` +> - {% icon param-file %} *"Test Dataset (CSV or TSV)"*: `Chowell_test_Response.tsv` +> - {% icon param-file %} *"Select the target column"*: `C22: Response` +> - {% icon param-file %} *"Task"*: `Classification` +> Run the tool +{: .hands_on} + +# Tool output files +After training and testing your model, you should see two new files in your history list: + +- PyCaret Model Comparison Best Model: The PyCaret model pickle file. This file allows the model to be reused without requiring retraining, ensuring consistent predictions. + +- PyCaret Model Report: This file contains all the plots for the models trained, along with the best model selected. + +For this tutorial, we will focus on the PyCaret Model Report. + +# PyCaret Model Report +The PyCaret HTML report provides a comprehensive and interactive overview of the trained model’s performance in an accessible, browser-ready format. This report documents key aspects of the model’s training and evaluation process, offering insights into how well the model performed on both the training and test datasets. The report consists of four tabs: Setup & Best Model, Best Model Plots, Feature Importance, and Explainer. +![report tabs](../../images/loris_tutorial/report_tabs.png "Tabs in the PyCaret Model Comparison Report") + +Below is a brief explanation of the content in each tab of the report. + +> <tip-title>Setup & Best Model Tab</tip-title> +>- Setup Parameters: Documents the initial configurations used in the PyCaret Model Comparison Tool. +>- Best Model Class and Hyperparameters: Specifies the model selected as the best performer from the comparison, along with the model’s hyperparameters as determined through tuning. +>- Performance Metrics: Summarizes key evaluation metrics, including Accuracy, ROC-AUC, Recall, Precision, F1-Score, Cohen’s Kappa, Matthews Correlation Coefficient (MCC), and Training Time (in seconds). +> +{: .tip} + +> <tip-title>Best Model Plots Tab</tip-title> +>- Training and Test Evaluation Plots: Displays visualizations of the model’s performance, including an ROC-AUC curve for binary classification, Confusion Matrix, Precision-Recall (PR) curve, Error Analysis, and a Classification Report for detailed class-level performance. +>- Additional Model Insights: Includes diagnostic plots like the Learning Curve, Calibration Plot, Validation Curve (VC), and various dimensionality reduction plots (Manifold, RFE). +>- Feature Importance: Shows the contribution of each feature to the model, both individually and collectively (All Features), providing insight into the factors influencing model decisions. +> +{: .tip} + +> <tip-title>Feature Importance Tab</tip-title> +>- Feature Importance Analysis from a Trained Random Forest: This analysis, while not directly linked to the LASSO logistic regression model, provides additional insights into feature significance within alternative models. These insights can help validate or contrast with results from the primary model, offering a broader perspective on feature influence. +>- SHAP Summary from a Trained LightGBM Model: SHAP (SHapley Additive exPlanations) values offer a unified measure of feature contributions, showing how each feature impacts predictions. This approach is particularly valuable for interpreting model decisions and understanding the feature contributions across predictions. +> +{: .tip} + +> <tip-title>Explainer Tab</tip-title> +>- Mean Absolute SHAP Value (Average Impact on Predicted Response): Displays each feature's overall contribution to predictions. Higher SHAP values indicate features with a more significant influence on the model's outcomes, helping identify the most impactful inputs. +>- Permutation Importance (Decrease in ROC AUC with Randomized Feature): Shows how essential each feature is for model performance by randomly shuffling each feature and observing the resulting decrease in ROC AUC. A significant drop suggests high feature importance. +>- Partial Dependence Plot (PDP): Displays the average effect of a feature on predictions, indicating whether it has a linear, nonlinear, or interactive effect on outcomes. +>- Percentage 1 vs. Predicted Probability: Compares the true proportion of positive cases (label=1) with predicted probabilities, helping evaluate the model’s calibration by showing how closely predictions align with observed outcomes. +>- Cumulative Percentage per Category (Top X%): Measures the cumulative impact of each category in the top percentages, useful for understanding feature value concentration and distribution. +>- Percentage Above and Below Cutoff: Analyzes model performance at a specified threshold, offering insight into sensitivity and specificity when predictions exceed or fall below the cutoff. +>- Confusion Matrix: A detailed matrix showing True Positives, False Positives, True Negatives, and False Negatives, allowing assessment of prediction accuracy, error distribution, and balance. +>- Lift Curve: Visualizes the model's improvement over random chance across prediction deciles, with lift values indicating the model's effectiveness in identifying positive instances. +>- ROC AUC Curve: Plots True Positive Rate vs. False Positive Rate across thresholds, with AUC as a measure of class distinction. A higher AUC reflects better performance in differentiating classes. +>- PR AUC Curve: Displays Precision vs. Recall, focusing on performance for the positive class. Especially useful in imbalanced datasets, where a higher AUC indicates effective identification of positive instances while maintaining precision. +> +{: .tip} + +# LORIS PanCancer LLR6 Model Robustness: + +Understanding the objective of this analysis is essential. Since we aim to build a model comparable to the one published by {% cite Chang2024 %}, and we indeed obtained the same Logistic Regression model architecture, this allows us to use the metrics from that paper as a benchmark to assess the performance of the model we develop through Galaxy-PyCaret. + +> <tip-title>Robustness definition </tip-title> +> +> Some evidence is considered robust if it holds up across reasonable variations in analysis, while fragile evidence depends heavily on specific analytical choices, such as the inclusion or exclusion of certain observations or covariates. +> +> Robustness, therefore, refers to assessing the reliability of previous findings by applying different analytical approaches to the same dataset.> +>![table showing the difference of robustness to the others analysis](../../images/loris_tutorial/robustness.png "Comparison of Robustness, Reproducibility, Generalizability, and Replicability Based on Data and Method Variations") +> +{: .tip} + +## Classification Algorithms +A key feature of PyCaret is its capability to train and compare multiple models with minimal code. By default, PyCaret evaluates a diverse range of algorithms, including linear models, tree-based models, and ensemble methods, ranking these models based on their performance. The primary metric used to determine the best-performing model is the accuracy. +![Comparison results on the cross-validation set](../../images/loris_tutorial/model_comparison.png "Comparison of Model Performance Metrics Across Algorithms") + +In this case, the best-performing algorithm for this dataset matches the findings reported in the article: the Logistic Regression model. + +## Hyperparameters +The model from {% cite Chang2024 %} for the Pan-Cancer LLR6 model has the following hyperparameters set: C = 0.1, Class Weight = Balanced, L1 ratio = 1, max iter = 100, Penalty = Elasticnet, Solver = Saga. + +The hyperparameter search performed by Galaxy-PyCaret resulted in slightly different settings for the model: C = 1.0, Class Weight = None, L1 ratio = None, max iter = 1000, Penalty = L2, Solver = LBFGS. + +![Model hyperparameters table](../../images/loris_tutorial/hyperparameters.png "Table showing the hyperparameters set for the selected model") + +The differences found in the hyperparameters guide the analysis towards testing the robustness of the LORIS Score proposed by {% cite Chang2024 %}. + +> <comment-title>Hyperparameters meaning</comment-title> +> +> *Penalty* - Defines the type of regularization applied to the model to prevent overfitting. Options for linear and logistic regression are: `L1`, `L2`, and `Elasticnet`. Briefly, L1 (Lasso Regression) removes less important features, helping to overcome overfitting as well as performing dimension reduction. However, when most features (variables) in the model are useful, L2 (Ridge Regression) is used. Elasticnet regularization combines both L1 and L2, addressing multicollinearity while also enabling feature selection. When Elasticnet is selected, it opens the L1 Ratio parameter. +> +> *L1-Ratio* - Controls the balance between L1 and L2 penalties. A value of `1` uses purely L1 regularization, which encourages sparsity. A value of `0` uses purely L2. +> +> *C* - Is the inverse of the regularization strength. A smaller C (like `0.1`) implies stronger regularization, which prevents overfitting by penalizing large coefficients and also makes the model simpler (smaller coefficients). In contrast, higher values make the model more complex. +> +> *Solver* - Specifies the optimization algorithm used for fitting the model. `SAGA` is well-suited for large datasets and supports the Elasticnet penalty. It is also effective for sparse data and is fast for L1. `LBFGS` is a quasi-Newton optimization algorithm, efficient for smaller datasets, supports L2 regularization, but does not support Elasticnet. +> +> *Class Weight* - Is used to handle imbalanced classes by adjusting the weight associated with each class. Balanced adjusts these weights inversely proportional to class frequencies in the data, giving more weight to the minority class. +> +> *Max Iter* - Specifies the `maximum number` of iterations the solver will run before stopping. If convergence is achieved earlier, it will stop; if not, you may need to increase the value. +> +{: .comment} + +## Model Evaluation Metrics +In this study, we compare the performance of machine learning models built using two different frameworks: PyCaret and Scikit-learn ({% cite Chang2024 %}). The comparison focuses on four model evaluation metrics—Accuracy, Area Under the Curve (AUC), F1 Score, and Matthews Correlation Coefficient (MCC)—to assess the relative strengths and performance of each model and draw conclusions about the robustness of the LORIS Score. + +![Model metrics table](../../images/loris_tutorial/test_metrics_results.png "Performance Metrics of the Model After Testing") + +- Accuracy +Accuracy measures the proportion of correct predictions among all predictions. The PyCaret model outperformed the Scikit-learn model in terms of accuracy, achieving 0.80 compared to 0.6803 for the Scikit-learn model. This suggests that the PyCaret model was able to correctly classify a higher percentage of samples, indicating better overall predictive performance on the given dataset. Furthermore, the higher accuracy across multiple runs demonstrates a level of consistency in the performance of the LORIS Score, regardless of the specific model-building tool used. + +- Area Under the Curve (AUC) +AUC provides an aggregate measure of performance across all classification thresholds, with higher values indicating better model performance. The PyCaret model exhibited a moderate AUC of 0.7599, slightly outperforming the Scikit-learn model’s AUC of 0.7155. This suggests that the PyCaret model had better discrimination between positive and negative classes, demonstrating a more robust ability to distinguish between the two across various thresholds. Despite using different frameworks, both models achieved relatively high AUC values, which reinforces the LORIS Score's consistent discriminatory power and overall robustness. + +- F1 Score +The F1 Score is the harmonic mean of precision and recall, offering a balanced measure of a model’s ability to correctly classify both positive and negative classes, particularly in the context of imbalanced datasets. The Scikit-learn model achieved a higher F1 score of 0.5289, compared to the PyCaret model’s F1 score of 0.4246. This suggests that, while the PyCaret model outperformed in terms of accuracy and AUC, the Scikit-learn model might have demonstrated a better balance between precision and recall, making it more effective at identifying positive instances. + +- Matthews Correlation Coefficient (MCC) +The MCC is a more balanced metric for binary classification, as it considers all four categories from the confusion matrix (True Positive, True Negative, False Positive, False Negative). The Scikit-learn model achieved a higher MCC of 0.4652, compared to the PyCaret model’s MCC of 0.3764. This suggests that, while both models showed moderate performance, the Scikit-learn model demonstrated a better balance between false positives and false negatives, making it more reliable in terms of overall classification accuracy, especially in the case of imbalanced datasets. + +The comparison between the PyCaret and Scikit-learn models demonstrates consistent performance across multiple evaluation metrics, such as accuracy, AUC, F1 score, and MCC. The slight differences in the results reflect the inherent flexibility of the LORIS Score, showing that a model built using two different frameworks—PyCaret and Scikit-learn—can still yield comparable performance. + +The results suggest that small variations in the analytical approach do not significantly impact the model's ability to distinguish between classes, thereby providing confidence in the robustness of the LORIS Score for future applications. + +# Conclusion +In this tutorial, we demonstrated how to use the Galaxy-PyCaret Comparison Tool to build a machine learning model for ICB treatment patient selection using clinical data. We followed a structured approach consisting of: +- Uploading datasets (.tsv) +- Running the PyCaret Comparison +- Evaluating the model's performance using the metrics published by {% cite Chang2024 %} as the gold standard. + +Throughout the process, we showcased how PyCaret simplifies the complexities of machine learning workflows, making them more accessible and efficient for users. Additionally, we explored the robustness of the LORIS Score by building a Logistic Regression model with different hyperparameters. + +By the end of this tutorial, you should have a solid understanding of how to deploy a traditional machine learning model using tabular data and effectively interpret its results. diff --git a/topics/statistics/tutorials/loris_model/workflows/index.md b/topics/statistics/tutorials/loris_model/workflows/index.md new file mode 100644 index 00000000000000..e092e0ae66ddd4 --- /dev/null +++ b/topics/statistics/tutorials/loris_model/workflows/index.md @@ -0,0 +1,3 @@ +--- +layout: workflow-list +--- diff --git a/topics/statistics/tutorials/loris_model/workflows/main_workflow-tests.yml b/topics/statistics/tutorials/loris_model/workflows/main_workflow-tests.yml new file mode 100644 index 00000000000000..7a3e26b546e5bf --- /dev/null +++ b/topics/statistics/tutorials/loris_model/workflows/main_workflow-tests.yml @@ -0,0 +1,19 @@ +- doc: Test the PyCaret comparison workflow + job: + 'chowell_train': + location: https://zenodo.org/records/13885908/files/Chowell_train_Response.tsv + class: File + filetype: .tsv + 'chowell_test': + location: https://zenodo.org/records/13885908/files/Chowell_test_Response.tsv + class: File + filetype: .tsv + outputs: + comparison_result: + asserts: + has_text: + text: "Model Training Report" + model: + asserts: + has_text: + text: "pycaret.internal.pipeline" diff --git a/topics/statistics/tutorials/loris_model/workflows/main_workflow.ga b/topics/statistics/tutorials/loris_model/workflows/main_workflow.ga new file mode 100644 index 00000000000000..93793a295b264c --- /dev/null +++ b/topics/statistics/tutorials/loris_model/workflows/main_workflow.ga @@ -0,0 +1,176 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "Generates LORIS LLR6 model ", + "creator":[ + { + "class":"Person", + "identifier":"https://orcid.org/0000-0002-4403-6684", + "name":"Paulo Cilas Morais Lyra Junior" + }, + { + "class":"Person", + "identifier":"https://orcid.org/0009-0005-4322-3401", + "name":"Junhao Qiu" + }, + { + "class":"Person", + "identifier":"https://orcid.org/0000-0002-4583-5226", + "name":"Jeremy Goecks" + } + ], + "format-version":"0.1", + "license": "AGPL-3.0-or-later", + "name":"Ludwig - Image recognition model - MNIST", + "steps": { + "0": { + "annotation": "Chowell train cohort - 964 samples", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Chowell_Train_Response.tsv" + } + ], + "label": "Chowell_Train_Response.tsv", + "name": "Input dataset", + "outputs": [], + "position": { + "bottom": 292.9513931274414, + "height": 82.46527862548828, + "left": 356.9618225097656, + "right": 556.9618377685547, + "top": 210.48611450195312, + "width": 200.00001525878906, + "x": 356.9618225097656, + "y": 210.48611450195312 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": \"\"}", + "tool_version": null, + "type": "data_input", + "uuid": "53bffd14-f503-4c81-8eb2-e70c0cec0559", + "workflow_outputs": [ + { + "label": null, + "output_name": "output", + "uuid": "66a19810-a401-47b6-8983-98e3f5e8757e" + } + ] + }, + "1": { + "annotation": "Chowell test cohort - 515 samples", + "content_id": null, + "errors": null, + "id": 1, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "Chowell_Test_Response.tsv" + } + ], + "label": "Chowell_Test_Response.tsv", + "name": "Input dataset", + "outputs": [], + "position": { + "bottom": 402.9513931274414, + "height": 82.46527862548828, + "left": 355.97222900390625, + "right": 555.9722442626953, + "top": 320.4861145019531, + "width": 200.00001525878906, + "x": 355.97222900390625, + "y": 320.4861145019531 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": \"\"}", + "tool_version": null, + "type": "data_input", + "uuid": "176d36e8-a459-4f51-9cff-932952f07f2f", + "workflow_outputs": [ + { + "label": null, + "output_name": "output", + "uuid": "2b5382f7-f9b6-4715-aaf7-87e27fbdf8c6" + } + ] + }, + "2": { + "annotation": "", + "content_id": "pycaret_tool", + "errors": null, + "id": 2, + "input_connections": { + "input_file": { + "id": 0, + "output_name": "output" + }, + "test_file": { + "id": 1, + "output_name": "output" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool PyCaret Model Comparison", + "name": "input_file" + }, + { + "description": "runtime parameter for tool PyCaret Model Comparison", + "name": "test_file" + } + ], + "label": null, + "name": "PyCaret Model Comparison", + "outputs": [ + { + "name": "model", + "type": "data" + }, + { + "name": "comparison_result", + "type": "html" + } + ], + "position": { + "bottom": 507.48268127441406, + "height": 297.013916015625, + "left": 684.96533203125, + "right": 884.9653472900391, + "top": 210.46876525878906, + "width": 200.00001525878906, + "x": 684.96533203125, + "y": 210.46876525878906 + }, + "post_job_actions": {}, + "tool_id": "toolshed.g2.bx.psu.edu/repos/paulo_lyra_jr/pycaret_model_comparison/PyCaret_Model_Comparison/2024.3.3.2+0", + "tool_state": "{\"advanced_settings\": {\"customize_defaults\": \"false\", \"__current_case__\": 1}, \"input_file\": {\"__class__\": \"RuntimeValue\"}, \"model_selection\": {\"model_type\": \"regression\", \"__current_case__\": 1, \"regression_models\": null}, \"random_seed\": \"42\", \"target_feature\": \"\", \"test_file\": {\"__class__\": \"RuntimeValue\"}, \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "3.3.2+0", + "type": "tool", + "uuid": "2a5e0015-457e-475f-97aa-72183a56af18", + "workflow_outputs": [ + { + "label": "PyCaret Model Comparison best model on input dataset(s)", + "output_name": "model", + "uuid": "5925cd98-b856-4dc2-87ae-0b07f2844386" + }, + { + "label": "PyCaret Model Comparison Comparison result on input dataset(s)", + "output_name": "comparison_result", + "uuid": "a292d802-b28f-4707-862c-5454e4ebb886" + } + ] + } + }, + "tags":[ + "LORIS", + "LLR6_model", + "Chowell_train_test", + "Logistic_Regression_Model" + ], + "uuid":"a59d72a3-a167-4f94-87ea-0e3e18df1d4c", + "version": 1 +} diff --git a/topics/synthetic-biology/community.md b/topics/synthetic-biology/community.md new file mode 100644 index 00000000000000..80a2a43217b351 --- /dev/null +++ b/topics/synthetic-biology/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: synthetic-biology +--- + diff --git a/topics/teaching/community.md b/topics/teaching/community.md new file mode 100644 index 00000000000000..986d4393e0325f --- /dev/null +++ b/topics/teaching/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: teaching +--- + diff --git a/topics/teaching/maintainer.md b/topics/teaching/maintainer.md new file mode 100644 index 00000000000000..09bcf3c04af112 --- /dev/null +++ b/topics/teaching/maintainer.md @@ -0,0 +1,5 @@ +--- +layout: topic-maintainer +topic_name: teaching +--- + diff --git a/topics/transcriptomics/community.md b/topics/transcriptomics/community.md new file mode 100644 index 00000000000000..a39ff33b94c99d --- /dev/null +++ b/topics/transcriptomics/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: transcriptomics +--- + diff --git a/topics/variant-analysis/community.md b/topics/variant-analysis/community.md new file mode 100644 index 00000000000000..987a88c819ae87 --- /dev/null +++ b/topics/variant-analysis/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: variant-analysis +--- + diff --git a/topics/variant-analysis/tutorials/dunovo/data-library.yaml b/topics/variant-analysis/tutorials/dunovo/data-library.yaml new file mode 100644 index 00000000000000..494d8f6da67872 --- /dev/null +++ b/topics/variant-analysis/tutorials/dunovo/data-library.yaml @@ -0,0 +1,38 @@ +--- +destination: + type: library + name: GTN - Material + description: Galaxy Training Network Material + synopsis: Galaxy Training Network Material. See https://training.galaxyproject.org +items: +- name: Variant Analysis + description: | + Genetic differences (variants) between healthy and diseased tissue, + between individuals of a population, or between strains of an organism + can provide mechanistic insight into disease processes and the natural + function of affected genes. + + The tutorials in this section show how to detect evidence for + genetic variants in next-generation sequencing data, a process termed + variant calling. + + Of equal importance, they also demonstrate how you can interpret, for + a range of different organisms, the resulting sets of variants by + predicting their molecular effects on genes and proteins, by + annotating previously observed variants with published knowledge, and + by trying to link phenotypes of the sequenced samples to their variant + genotypes. + items: + - name: Calling very rare variants + items: + - name: 'DOI: 10.5281/zenodo.3554549' + description: latest + items: + - url: https://zenodo.org/record/3554549/files/SRR1799908_forward.fastq + src: url + ext: + info: https://doi.org/10.5281/zenodo.3554549 + - url: https://zenodo.org/record/3554549/files/SRR1799908_reverse.fastq + src: url + ext: + info: https://doi.org/10.5281/zenodo.3554549 diff --git a/topics/visualisation/community.md b/topics/visualisation/community.md new file mode 100644 index 00000000000000..3793720e320799 --- /dev/null +++ b/topics/visualisation/community.md @@ -0,0 +1,5 @@ +--- +layout: community +topic_name: visualisation +--- + diff --git a/videos/index.html b/videos/index.html new file mode 100644 index 00000000000000..63781826f34e71 --- /dev/null +++ b/videos/index.html @@ -0,0 +1,84 @@ +--- +title: GTN Video Library +layout: page +redirect_from: + - /topics/admin/videos/index + - /topics/assembly/videos/index + - /topics/climate/videos/index + - /topics/computational-chemistry/videos/index + - /topics/contributing/videos/index + - /topics/dev/videos/index + - /topics/ecology/videos/index + - /topics/epigenetics/videos/index + - /topics/galaxy-interface/videos/index + - /topics/genome-annotation/videos/index + - /topics/imaging/videos/index + - /topics/instructors/videos/index + - /topics/introduction/videos/index + - /topics/metabolomics/videos/index + - /topics/metagenomics/videos/index + - /topics/microbiome/videos/index + - /topics/proteomics/videos/index + - /topics/sequence-analysis/videos/index + - /topics/statistics/videos/index + - /topics/transcriptomics/videos/index + - /topics/variant-analysis/videos/index + - /topics/visualisation/videos/index +--- + +The GTN now automatically generates videos for selected slide decks. Click on a topic below to jump to the video page for that topic! +Additionally there is a much larger library of human mande recordings also available, as seen below! + +{% assign vids = site | get_videos_for_videos_page %} + +{% for k in vids %} + + {% assign topic = k[1] %} + <h2 id="{{ topic.topic_id }}">{{ topic.topic_name }}</h2> + + {% if topic.automated_videos %} + <h3>Automated Videos from Slides</h3> + <div class="playlist"> + {% for video in topic.automated_videos %} + <div class="pl-item"> + <a href="watch.html?v={{ video.vid }}&utm_source=videos_page&utm_medium=automated_video&utm_campaign={{ vid }}"> + <div class="cover"> + <img src="{{ video.cover }}" width="200px" alt="cover"/> + </div> + <div> + <div class="title">{{ video.title }}</div> + <div class="description"> + {% for captioner in video.captioners %} + {% include _includes/contributor-badge-inline.html id=captioner %} + {% endfor %} + </div> + </div> + </a> + </div> + {% endfor %} + </div> + {% endif %} + + {% if topic.manual_videos %} + <h3>Human Recordings </h3> + <div class="playlist"> + {% for video in topic.manual_videos %} + <div class="pl-item"> + <a href="watch.html?v={{ video.vid }}&utm_source=videos_page&utm_medium=manual_video&utm_campaign={{ vid }}&player=youtube"> + <div class="cover"> + <img src="{{ video.cover }}" width="200px" alt="cover"/> + </div> + </a> + <div> + <div class="title">{{ video.title }}</div> + <div class="description"> + {% for speaker in video.speakers %} + {% include _includes/contributor-badge-inline.html id=speaker %} + {% endfor %} + </div> + </div> + </div> + {% endfor %} + </div> + {% endif %} +{% endfor %} diff --git a/videos/index.md b/videos/index.md deleted file mode 100644 index bf3465ded5075d..00000000000000 --- a/videos/index.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: GTN Automated Videos -layout: page -redirect_from: - - /topics/admin/videos/index - - /topics/assembly/videos/index - - /topics/climate/videos/index - - /topics/computational-chemistry/videos/index - - /topics/contributing/videos/index - - /topics/dev/videos/index - - /topics/ecology/videos/index - - /topics/epigenetics/videos/index - - /topics/galaxy-interface/videos/index - - /topics/genome-annotation/videos/index - - /topics/imaging/videos/index - - /topics/instructors/videos/index - - /topics/introduction/videos/index - - /topics/metabolomics/videos/index - - /topics/metagenomics/videos/index - - /topics/microbiome/videos/index - - /topics/proteomics/videos/index - - /topics/sequence-analysis/videos/index - - /topics/statistics/videos/index - - /topics/transcriptomics/videos/index - - /topics/variant-analysis/videos/index - - /topics/visualisation/videos/index ---- - -{% assign sorted_topics = "" | split: "," %} -{% assign sorted_topics_pre = site.data | sort | order: "title" %} - -{% for topic in sorted_topics_pre %} - {% if topic[0] == "introduction" %} - {% assign sorted_topics = sorted_topics | unshift: topic %} - {% else %} - {% assign sorted_topics = sorted_topics | push: topic %} - {% endif %} -{% endfor %} - -The GTN now automatically generates videos for selected slide decks. Click on a topic below to jump to the video page for that topic! -Additionally there is a much larger [Video Library](https://gallantries.github.io/video-library/) available with recordings from human instructors teaching each tutorial. - -{% for topic in sorted_topics %} -{% assign topic_id = topic[0] %} -{% assign t = site.data[topic_id] %} - - {% assign has_video = false %} - {% assign topic_material = site | topic_filter:topic[0] %} - {% for material in topic_material %} - {% if material.video %} - {% assign has_video = true %} - {% endif %} - {% endfor %} - -{% if has_video == true %} -<h2>{{ t.title }}</h2> -<div id="playlist"> - {% for material in topic_material %} - {% if material.video %} - {% capture vid %}{{ topic_id }}/tutorials/{{ material.tutorial_name }}/slides{% endcapture %} - <div class="pl-item"> - <a href="watch.html?v={{ vid }}"> - <div class="cover"> - <img src="https://training.galaxyproject.org/videos/topics/{{ vid }}.mp4.png" width="200px"/> - </div> - <div> - <div class="title">{{ material.title }}</div> - </div> - </a> - </div> - {% for lang in material.translations.slides %} - {% capture vid %}{{ topic_id }}/tutorials/{{ material.tutorial_name }}/slides_{{ lang | upcase }}{% endcapture %} - <div class="pl-item"> - <a href="watch.html?v={{ vid }}"> - <div class="cover"> - <img src="https://training.galaxyproject.org/videos/topics/{{ vid }}.mp4.png" width="200px"/> - </div> - <div> - <div class="title">[{{ lang | upcase }}] {{ material.title }}</div> - </div> - </a> - </div> - {% endfor %} - {% endif %} - {% endfor %} -</div> -{% endif %} -{% endfor %} diff --git a/videos/watch.html b/videos/watch.html new file mode 100644 index 00000000000000..0d91267d86f7b7 --- /dev/null +++ b/videos/watch.html @@ -0,0 +1,194 @@ +--- +layout: base +title: GTN Videos +--- + +<div class="row"> + <div class="col-sm-12 recording-video"> + <video id="player" width="100%" controls preload="metadata" style="background: black"> + </video> + <div id="youtube"> + </div> + </div> +</div> +<div class="row"> + <div class="col-sm-8"> + <div class="row"> + <div class="col-sm-12"> + <h2 id="title"></h2> + <p id="description"></p> + </div> + </div> + <div class="row" id="internal-only"> + <div class="col-sm-6"> + <b>Transcript</b> + </div> + <div class="col-sm-2" id="transcript-edit"> + Edit Source Slide + </div> + <div class="col-sm-2" id="source-slides"> + View Slides + </div> + <div class="col-sm-2" id="transcript-plain"> + View Plain Text + </div> + </div> + <div class="row" id="transcript"> + </div> + </div> + <div class="col-sm-4"> + <div class="row"> + <div class="col-sm-12"> + <h3>Other Videos</h3> + <div><a href="{% link videos/index.md %}">See all GTN Videos</a></div> + <div id="playlist" class="vertical"> + </div> + </div> + </div> + </div> +</div> + + +<script type="text/javascript"> +var params = (new URL(document.location)).searchParams, + videoid = params.get('v').startsWith('/') ? params.get('v').substring(1) : params.get('v'), + seekTo = params.get('t'), + videoPlayer = params.get('player'), + videohost = 'https://training.galaxyproject.org', + vtt = `${videohost}/videos/topics/${videoid}.en.vtt`, + mp4 = `${videohost}/videos/topics/${videoid}.mp4`, + png = `${videohost}/videos/topics/${videoid}.mp4.png`, + player = document.getElementById("player"); + +const library_data = {{ site | get_videos_for_videos_page | jsonify }}; +const contributors = {{ site.data.contributors | jsonify }}; + +let video_data = {}; +Object.keys(library_data).map(k => { + if(library_data[k].automated_videos){ + library_data[k].automated_videos.map(v => { + video_data[v.vid] = v; + }); + } + if(library_data[k].manual_videos){ + library_data[k].manual_videos.map(v => { + video_data[v.vid] = v; + }); + } +}); + +let this_videos_meta = video_data[videoid]; +document.getElementById("title").innerHTML = this_videos_meta.title; + +if (videoPlayer === 'youtube'){ + document.getElementById("player").style.display = "none"; + document.getElementById("internal-only").style.display = "none"; + document.getElementById("youtube").innerHTML = ` +<iframe + credentialless + allowfullscreen + referrerpolicy="no-referrer" + sandbox="allow-scripts allow-same-origin allow-popups" + allow="accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; bluetooth 'none'; browsing-topics 'none'; camera 'none'; ch-ua 'none'; display-capture 'none'; domain-agent 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered 'none'; execution-while-out-of-viewport 'none'; gamepad 'none'; geolocation 'none'; gyroscope 'none'; hid 'none'; identity-credentials-get 'none'; idle-detection 'none'; keyboard-map 'none'; local-fonts 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; navigation-override 'none'; otp-credentials 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-create 'none'; publickey-credentials-get 'none'; screen-wake-lock 'none'; serial 'none'; speaker-selection 'none'; sync-xhr 'none'; usb 'none'; web-share 'none'; window-management 'none'; xr-spatial-tracking 'none'" + csp="sandbox allow-scripts allow-same-origin;" + width="100%" + src="https://www.youtube.com/embed/${videoid}" + frameborder="0" + loading="lazy" +></iframe> + `; + + let speakers = this_videos_meta.speakers.map(x => { + return `<a href="/training-material/hall-of-fame/${x}/">${contributors[x].name}</a>` + }).join(', '); + let captioners = this_videos_meta.captioners.map(x => { + return `<a href="/training-material/hall-of-fame/${x}/">${contributors[x].name}</a>` + }).join(', '); + + document.getElementById("description").innerHTML = ` + <b>Speaker(s)</b> ${speakers}<br/> + <b>Captioner(s)</b> ${captioners} + `; +} else { + + let authors = this_videos_meta.captioners.map(x => { + return `<a href="/training-material/hall-of-fame/${x}/">${contributors[x].name}</a>` + }).join(', '); + +document.getElementById("description").innerHTML = ` + <b>Author(s)</b> ${authors} +`; + player.setAttribute('poster', png); + player.innerHTML = ` + <source src="${mp4}" type="video/mp4"> + <track label="English" kind="captions" srclang="en" src="${vtt}" default> + `; + + document.getElementById("transcript-edit").innerHTML = `<a href="https://github.com/galaxyproject/training-material/edit/main/topics/${videoid}.html">Edit Source Slide</a>` + document.getElementById("source-slides").innerHTML = `<a href="https://training.galaxyproject.org/training-material/topics/${videoid}.html">View Slides</a>` + document.getElementById("transcript-plain").innerHTML = `<a href="https://training.galaxyproject.org/training-material/topics/${videoid}-plain.html">View Plain Text</a>` + + if(seekTo !== null){ + if(seekTo.indexOf(":") > -1){ + var seekToparts = seekTo.split(":"); + if(seekToparts.length == 2) { + player.currentTime = (parseInt(seekToparts[0]) * 60) + parseInt(seekToparts[1]); + } else if (seekToparts.length == 3){ + player.currentTime = (parseInt(seekToparts[0]) * 3600) + (parseInt(seekToparts[1]) * 60) + parseInt(seekToparts[2]); + } else { + console.error("Could not parse time") + } + } else { + player.currentTime = parseInt(seekTo); + } + } + + + fetch(vtt) + .then(response => response.text()) + .then(data => { + lines = data.split("\n").slice(4).filter((x, i) => { return i % 4 == 0 || i % 4 == 1}); + + timestamps = lines.filter((x, i) => i % 2 == 0).map(x => x.split(' ')[0]); + words = lines.filter((x, i) => i % 2 == 1); + + var zipped = timestamps.map(function(e, i) { + return [e, words[i]]; + }); + lines = zipped.map(x => { return `<tr><td>${x[0]}</td><td>${x[1]}</td></tr>` }).join(''); + document.getElementById("transcript").innerHTML = '<table>' + lines + '</table>'; + }); + + fetch('{{ site.baseurl }}/api/videos.json') + .then(response => response.json()) + .then(data => { + // Remove empty + data = data.filter(x => x !== null); + // We've got a 'list' of video, we'll pretend this is a 'ring' buffer. + + var idx = data.findIndex(x => x.id == videoid); + var videoSelf = data[idx]; + document.getElementById("title").innerHTML = videoSelf.title; + + + var ring = [...data.slice(idx + 1), ...data.slice(0, idx)].slice(0, 8); + var fmt = ring.map(x => { + return ` + <div class="pl-item"> + <a href="?v=${x.id}"> + <div class="cover"> + <img src="https://training.galaxyproject.org/videos/topics/${x.id}.mp4.png" width="200px"/> + </div> + <div> + <div class="title">${x.title}</div> + <div class="topic">${x.topic}</div> + </div> + </a> + </div> + `; + }) + document.getElementById("playlist").innerHTML = fmt; + }); + +} +</script> diff --git a/videos/watch.md b/videos/watch.md deleted file mode 100644 index e24820e95c6ebe..00000000000000 --- a/videos/watch.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -layout: base -title: GTN Videos ---- - -<div class="row"> - <div class="col-md-12"> - <video id="player" width="100%" height="610" controls preload="metadata" style="background: black"> - </video> - </div> -</div> -<div class="row"> - <div class="col-sm-8"> - <div class="row"> - <h2 id="title"></h2> - </div> - <div class="row"> - <div class="col-sm-6"> - <b>Transcript</b> - </div> - <div class="col-sm-2" id="transcript-edit"> - Edit Source Slide - </div> - <div class="col-sm-2" id="source-slides"> - View Slides - </div> - <div class="col-sm-2" id="transcript-plain"> - View Plain Text - </div> - </div> - <div class="row" id="transcript"> - </div> - </div> - <div class="col-sm-4"> - <div class="row"> - <div class="col-sm-12"> - <h3>Other Videos</h3> - <div><a href="{% link videos/index.md %}">See all GTN Videos</a></div> - <div id="playlist" class="vertical"> - </div> - </div> - </div> - </div> -</div> - - -<script type="text/javascript"> -var params = (new URL(document.location)).searchParams, - videoid = params.get('v').startsWith('/') ? params.get('v').substring(1) : params.get('v'), - seekTo = params.get('t'), - videohost = 'https://training.galaxyproject.org', - vtt = `${videohost}/videos/topics/${videoid}.en.vtt`, - mp4 = `${videohost}/videos/topics/${videoid}.mp4`, - png = `${videohost}/videos/topics/${videoid}.mp4.png`, - player = document.getElementById("player"); - - -player.setAttribute('poster', png); -player.innerHTML = ` - <source src="${mp4}" type="video/mp4"> - <track label="English" kind="captions" srclang="en" src="${vtt}" default> -`; - -document.getElementById("transcript-edit").innerHTML = `<a href="https://github.com/galaxyproject/training-material/edit/main/topics/${videoid}.html">Edit Source Slide</a>` -document.getElementById("source-slides").innerHTML = `<a href="https://training.galaxyproject.org/training-material/topics/${videoid}.html">View Slides</a>` -document.getElementById("transcript-plain").innerHTML = `<a href="https://training.galaxyproject.org/training-material/topics/${videoid}-plain.html">View Plain Text</a>` - -if(seekTo !== null){ - if(seekTo.indexOf(":") > -1){ - var seekToparts = seekTo.split(":"); - if(seekToparts.length == 2) { - player.currentTime = (parseInt(seekToparts[0]) * 60) + parseInt(seekToparts[1]); - } else if (seekToparts.length == 3){ - player.currentTime = (parseInt(seekToparts[0]) * 3600) + (parseInt(seekToparts[1]) * 60) + parseInt(seekToparts[2]); - } else { - console.error("Could not parse time") - } - } else { - player.currentTime = parseInt(seekTo); - } -} - - -fetch(vtt) - .then(response => response.text()) - .then(data => { - lines = data.split("\n").slice(4).filter((x, i) => { return i % 4 == 0 || i % 4 == 1}); - - timestamps = lines.filter((x, i) => i % 2 == 0).map(x => x.split(' ')[0]); - words = lines.filter((x, i) => i % 2 == 1); - - var zipped = timestamps.map(function(e, i) { - return [e, words[i]]; - }); - lines = zipped.map(x => { return `<tr><td>${x[0]}</td><td>${x[1]}</td></tr>` }).join(''); - document.getElementById("transcript").innerHTML = '<table>' + lines + '</table>'; - }); - -fetch('{{ site.baseurl }}/api/videos.json') - .then(response => response.json()) - .then(data => { - // Remove empty - data = data.filter(x => x !== null); - // We've got a 'list' of video, we'll pretend this is a 'ring' buffer. - - var idx = data.findIndex(x => x.id == videoid); - var videoSelf = data[idx]; - document.getElementById("title").innerHTML = videoSelf.title; - - - var ring = [...data.slice(idx + 1), ...data.slice(0, idx)].slice(0, 8); - var fmt = ring.map(x => { - return ` - <div class="pl-item"> - <a href="?v=${x.id}"> - <div class="cover"> - <img src="https://training.galaxyproject.org/videos/topics/${x.id}.mp4.png" width="200px"/> - </div> - <div> - <div class="title">${x.title}</div> - <div class="topic">${x.topic}</div> - </div> - </a> - </div> - `; - }) - document.getElementById("playlist").innerHTML = fmt; - }); - -</script>