diff --git a/Gemfile b/Gemfile index 8309654a1b..8cc430e0b7 100644 --- a/Gemfile +++ b/Gemfile @@ -88,6 +88,9 @@ gem 'view_component', '~> 2.72' # Pagination library for Rails gem 'will_paginate', '~> 3.0' +# String similarity, used by federated search to rank results +gem 'string-similarity' + # Render SVG files in Rails views gem 'inline_svg' @@ -97,9 +100,9 @@ gem 'iso-639', '~> 0.3.6' gem 'countries', '~> 5.7' # Custom API client -gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'master' - +gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' # Ruby 2.7.8 pinned gems (to remove when migrating to Ruby >= 3.0) + gem 'ffi', '~> 1.16.3' gem 'net-ftp', '~> 0.2.0', require: false gem 'net-http', '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 97facd1e1b..73328d8412 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,10 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 24fb2549f7b69841e052491439bc8375ed5acfd9 - branch: master + revision: 7423b46ff6fa7e5ef0f1d36548f7c04466939f71 + branch: development specs: ontologies_api_client (2.2.0) - activesupport + activesupport (~> 7.0.4) excon faraday faraday-excon (~> 2.0.0) @@ -12,6 +12,8 @@ GIT lz4-ruby multi_json oj + parallel + request_store spawnling (= 2.1.5) GEM @@ -145,20 +147,21 @@ GEM crass (1.0.6) css_parser (1.17.1) addressable + csv (3.3.0) dalli (3.2.8) date (3.3.4) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) - deepl-rb (2.5.3) + deepl-rb (3.0.2) diff-lcs (1.5.1) docile (1.4.1) domain_name (0.6.20240107) ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (0.111.0) - execjs (2.9.1) + excon (0.112.0) + execjs (2.10.0) faraday (2.0.1) faraday-net_http (~> 2.0) ruby2_keywords (>= 0.0.4) @@ -176,7 +179,7 @@ GEM sass-rails globalid (1.2.1) activesupport (>= 6.1) - graphql (2.3.14) + graphql (2.3.19) base64 fiber-storage graphql-client (0.23.0) @@ -203,7 +206,7 @@ GEM http-accept (1.7.0) http-cookie (1.0.7) domain_name (~> 0.5) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (0.9.37) activesupport (>= 4.0.2) @@ -217,7 +220,7 @@ GEM terminal-table (>= 1.5.1) i18n-tasks-csv (1.1) i18n-tasks (~> 0.9) - importmap-rails (2.0.1) + importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -225,10 +228,11 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) - irb (1.14.0) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) - iso-639 (0.3.6) + iso-639 (0.3.8) + csv jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -237,15 +241,15 @@ GEM railties (>= 3.2.16) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.7.2) - json-jwt (1.16.6) + json (2.7.4) + json-jwt (1.16.7) activesupport (>= 4.2) aes_key_wrap base64 bindata faraday (~> 2.0) faraday-follow_redirects - jwt (2.8.2) + jwt (2.9.3) base64 language_server-protocol (3.17.0.3) launchy (3.0.1) @@ -262,7 +266,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.6.1) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) lookbook (1.5.5) @@ -287,12 +291,13 @@ GEM marcel (1.0.4) matrix (0.4.2) method_source (1.1.0) - mime-types (3.5.2) + mime-types (3.6.0) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.0903) + mime-types-data (3.2024.1001) mini_mime (1.1.5) minitest (5.25.1) - msgpack (1.7.2) + msgpack (1.7.3) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.4.1) @@ -302,7 +307,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.16) + net-imap (0.4.17) date net-protocol net-pop (0.1.2) @@ -315,9 +320,9 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.0) net-protocol - net-ssh (7.2.3) + net-ssh (7.3.0) netrc (0.11.0) - newrelic_rpm (9.13.0) + newrelic_rpm (9.14.0) nio4r (2.7.3) nokogiri (1.15.6-x86_64-linux) racc (~> 1.4) @@ -328,7 +333,7 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - oj (3.16.5) + oj (3.16.6) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.2) @@ -338,8 +343,8 @@ GEM omniauth-github (2.0.1) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) - omniauth-google-oauth2 (1.1.3) - jwt (>= 2.0) + omniauth-google-oauth2 (1.2.0) + jwt (>= 2.9) oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) @@ -369,10 +374,10 @@ GEM psych (5.1.2) stringio public_suffix (5.1.1) - puma (5.6.8) + puma (5.6.9) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) @@ -422,19 +427,21 @@ GEM regexp_parser (2.9.2) reline (0.5.10) io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.7) - rouge (4.3.0) - rspec-core (3.13.1) + rexml (3.3.9) + rouge (4.4.0) + rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (7.0.1) @@ -446,7 +453,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.66.1) + rubocop (1.67.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -487,7 +494,7 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.13.0) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) snaky_hash (2.0.1) hashie @@ -500,7 +507,7 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.23.1) + sshkit (1.23.2) base64 net-scp (>= 1.1.2) net-sftp (>= 2.1.2) @@ -508,25 +515,25 @@ GEM ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) + string-similarity (2.1.0) stringio (3.1.1) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terser (1.2.3) + terser (1.2.4) execjs (>= 0.3.0, < 3) thor (1.3.2) tilt (2.4.0) time (0.4.0) date timeout (0.4.1) - turbo-rails (2.0.6) + turbo-rails (2.0.11) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uri (0.13.1) version_gem (1.1.4) view_component (2.83.0) @@ -538,7 +545,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.23.1) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -620,6 +627,7 @@ DEPENDENCIES simplecov-cobertura sprockets-rails stimulus-rails + string-similarity terser turbo-rails tzinfo-data diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index ac5160afc2..52b88a0a6d 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -58,7 +58,9 @@ @import "agent_tooltip"; @import "content_finder"; @import "tools"; +@import "portal_configuration"; @import "taxonomy"; +@import "federation"; /* Bootstrap and Font Awesome */ @import "bootstrap"; diff --git a/app/assets/stylesheets/browse.scss b/app/assets/stylesheets/browse.scss index 2dc96db265..a55292bde0 100644 --- a/app/assets/stylesheets/browse.scss +++ b/app/assets/stylesheets/browse.scss @@ -451,6 +451,25 @@ margin-top: 10px; } +.browse-federation-input-chips{ + display: flex; + flex-wrap: wrap; + margin: 0 15px 15px 15px; + div { + flex-grow: 1; + } + + label{ + display: grid; + } +} +.browse-federation-input-chip-container{ + p{ + font-size: 14px; + font-weight: 400; + color: #666666; + } +} @media only screen and (max-width: 1250px) { .browse-first-row { diff --git a/app/assets/stylesheets/components/chip_button.scss b/app/assets/stylesheets/components/chip_button.scss index 1b19a29815..18916b7fec 100644 --- a/app/assets/stylesheets/components/chip_button.scss +++ b/app/assets/stylesheets/components/chip_button.scss @@ -10,6 +10,7 @@ } .chip-button-component-container { display: inline-block; + text-wrap: nowrap !important; } .chip-button-component-container svg path{ fill: var(--primary-color); diff --git a/app/assets/stylesheets/components/chips.scss b/app/assets/stylesheets/components/chips.scss index ac1a2a939f..13c15630d2 100644 --- a/app/assets/stylesheets/components/chips.scss +++ b/app/assets/stylesheets/components/chips.scss @@ -32,7 +32,8 @@ } .chips-container.disabled div label > span, .chips-container div label span:has(span.disabled){ - background-color: #f8f9fa !important; + opacity: 60%; + background-color: #f8f9fa !important; } .chips-container div label input[type="checkbox"]:checked ~ span{ @@ -42,4 +43,40 @@ .chips-container div label input[type="checkbox"]:checked ~ span .chips-check-icon{ display:unset; -} \ No newline at end of file +} + +.chips-container.loading div { + cursor: default; + opacity: 0.6; +} + +.chips-container .skeleton { + width: 80px; + height: 36px; + background-color: #e0e0e0; + border-radius: 5px; + animation: shimmer 4s infinite; + position: relative; + overflow: hidden; +} + +@keyframes shimmer { + 0% { + background-position: -200px 0; + } + 100% { + background-position: 200px 0; + } +} + +.chips-container .skeleton::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.6) 50%, rgba(255, 255, 255, 0.2) 100%); + animation: shimmer 4s infinite; + border-radius: 5px; +} diff --git a/app/assets/stylesheets/components/search_result.scss b/app/assets/stylesheets/components/search_result.scss index fc99288618..f5d8510458 100644 --- a/app/assets/stylesheets/components/search_result.scss +++ b/app/assets/stylesheets/components/search_result.scss @@ -41,7 +41,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: 4px; + border-radius: 5px; background-color: var(--light-color); padding: 5px 13px; margin-right: 10px; @@ -52,6 +52,7 @@ } .search-result-component .actions .button svg{ width: 16px; + height: 16px; } .search-result-component .actions .button svg path{ diff --git a/app/assets/stylesheets/federation.scss b/app/assets/stylesheets/federation.scss new file mode 100644 index 0000000000..6bb893e251 --- /dev/null +++ b/app/assets/stylesheets/federation.scss @@ -0,0 +1,31 @@ +.federation-portal-button{ + display: flex; + justify-content: center; + align-items: center; + border-radius: 5px; + background-color: var(--light-color); + padding: 5px 13px; + margin-right: 0px; + height: 100%; +} + + +.federation-portal-button:hover{ + cursor: pointer; +} + +.federation-portal-button svg{ + width: 16px; + height: 16px; +} +.federation-portal-button svg path{ + fill: var(--primary-color); +} + +.federation-portal-button .text{ + color: var(--primary-color); + margin-left: 8px; +} +.federation-portal-button.icon-right .text{ + margin:0 8px; +} \ No newline at end of file diff --git a/app/assets/stylesheets/mappings.scss b/app/assets/stylesheets/mappings.scss index 89a7ebffbe..20ddbb6940 100644 --- a/app/assets/stylesheets/mappings.scss +++ b/app/assets/stylesheets/mappings.scss @@ -320,6 +320,28 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { #concept_mappings_table { width: 100%; + word-break: break-word; + font-size: 13px; +} +.mappings-table-mapping-to{ + width: 45%; +} +.mappings-table-mapping-to .chip-button-component-container{ + text-wrap: wrap !important; +} +.mappings-table-mapping-to a{ + background-color: unset; + line-height: unset; + padding: unset; + font-weight: unset; + font-size: unset; +} +.mappings-table-icon{ + width: 18px; + height: 18px; +} +.mappings-table-icon path{ + fill: var(--gray-color); } .summary-mappings-tab-table { @@ -341,4 +363,7 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { padding: 10px; outline: none; margin-left: 0 !important; +} +.mappings-table-actions svg path{ + fill: var(--primary-color); } \ No newline at end of file diff --git a/app/assets/stylesheets/portal_configuration.scss b/app/assets/stylesheets/portal_configuration.scss new file mode 100644 index 0000000000..0f72b14730 --- /dev/null +++ b/app/assets/stylesheets/portal_configuration.scss @@ -0,0 +1,68 @@ + +.portal-configuration{ + padding: 10px; + .portal-configuration-title{ + h3{ + font-size: 20px; + font-weight: 600; + margin-bottom: 0; + } + span{ + font-size: 16px; + } + } + + h4, .home-section-title .text{ + font-size: 15px !important; + } + + p { + font-size: 14px; + } + .home-support-items div { + font-size: 12px; + } + .home-support-items a img { + height: 48px; + width: 48px; + } + .home-logo-instances-small{ + padding: 7px; + margin: 2px 5px !important; + border-radius: 50%; + display: inline-block; + width: 38px; + height: 38px; + display: flex; + justify-content: center; + align-items: center; + } + .portal-config-ontologies{ + display: flex; + align-items: center; + margin-top: 2px; + } + .portal-config-ontologies svg{ + width: 14px; + height: 14px; + margin-right: 4px; + } + .portal-config-ontologies svg path{ + fill: var(--primary-color); + } + .portal-description{ + font-size: 15px; + padding: 11px 0; + } + .portal-config-federated-with{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-right: 15px; + } + .portal-config-title-text{ + font-weight: 500; + font-size: 15px !important; + } +} diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index e66312f7e2..5dbea30de7 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -69,6 +69,7 @@ .search-page-advanced-button .text{ margin-left: 10px; color: var(--primary-color); + margin-top: 2px; } .search-page-advanced-button .icon svg path{ @@ -76,6 +77,7 @@ } .search-page-number-of-results{ color: #888888; + max-width: 800px; } .search-page-result-element{ diff --git a/app/components/chips_component.rb b/app/components/chips_component.rb index 624681f812..101643a865 100644 --- a/app/components/chips_component.rb +++ b/app/components/chips_component.rb @@ -1,16 +1,26 @@ class ChipsComponent < ViewComponent::Base renders_one :count - def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: nil) + def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: nil, disabled: false, loading: false) @id = id || name @name = name @value = value || 'true' @checked = checked @label = label || @value @tooltip = tooltip + @disabled = disabled + @loading = loading end def checked? @checked end -end \ No newline at end of file + + def disabled_class_name + @disabled ? 'disabled' : '' + end + + def loading_class_name + @loading ? 'loading' : '' + end +end diff --git a/app/components/chips_component/chips_component.html.haml b/app/components/chips_component/chips_component.html.haml index f08d5a33dd..74acecd425 100644 --- a/app/components/chips_component/chips_component.html.haml +++ b/app/components/chips_component/chips_component.html.haml @@ -1,9 +1,13 @@ -.chips-container{class: @disabled ? 'disabled' : '', 'data-controller': 'tooltip', title: @tooltip} +.chips-container{class: "#{disabled_class_name} #{loading_class_name}", 'data-controller': 'tooltip', title: @tooltip} %div - %label{:for => "chips-#{@id}-check"} - %input{:id => "chips-#{@id}-check", :name => @name, :type => "checkbox", :value => @value, checked: checked?, disabled: @disabled} - %span - = inline_svg_tag 'check.svg', class: 'chips-check-icon' - %div - = @label - = count + - if @loading + %label + %span.skeleton + - else + %label{:for => "chips-#{@id}-check"} + %input{:id => "chips-#{@id}-check", :name => @name, :type => "checkbox", :value => @value, checked: checked?, disabled: @disabled} + %span + = inline_svg_tag 'check.svg', class: 'chips-check-icon' + %div + = @label + = count diff --git a/app/components/display/header_component.rb b/app/components/display/header_component.rb index 76784068e1..48ae752754 100644 --- a/app/components/display/header_component.rb +++ b/app/components/display/header_component.rb @@ -4,7 +4,7 @@ class Display::HeaderComponent < ViewComponent::Base include ComponentsHelper - renders_one :text + def initialize(text: nil, tooltip: nil) super @@ -14,7 +14,7 @@ def initialize(text: nil, tooltip: nil) def call content_tag(:div, class: 'header-component') do - out = content_tag(:p, text || @text) + out = content_tag(:p, content&.html_safe || @text) if @info && !@info.empty? out = out + info_tooltip(content_tag(:div, @info, style: 'max-width: 300px')) end diff --git a/app/components/display/search_result_component.rb b/app/components/display/search_result_component.rb index 18d56a288e..4b112b60f8 100644 --- a/app/components/display/search_result_component.rb +++ b/app/components/display/search_result_component.rb @@ -2,17 +2,24 @@ class Display::SearchResultComponent < ViewComponent::Base include UrlsHelper include ModalHelper include MultiLanguagesHelper + include FederationHelper + include ComponentsHelper renders_many :subresults, Display::SearchResultComponent renders_many :reuses, Display::SearchResultComponent - def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false) + + def initialize(number: 0,title: nil, ontology_id: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) @title = title @uri = uri @definition = definition @link = link @is_sub_component = is_sub_component - @ontology_acronym = ontology_acronym + @ontology_acronym = ontology_id&.split('/')&.last @number = number.to_s + @portal_name = portal_name + @portal_color = portal_color + @portal_light_color = portal_light_color + @other_portals = other_portals end def sub_component_class @@ -61,12 +68,22 @@ def visualize_button end def reveal_ontologies_button(text,id,icon) - content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id) do - inline_svg_tag(icon) + - content_tag(:div, class: 'text') do + content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id, style: @portal_color ? "background-color: #{@portal_light_color} !important" : '') do + inline_svg_tag(icon, class: "federated-icon-#{@portal_name}") + + content_tag(:div, class: 'text', style: @portal_color ? "color: #{@portal_color} !important" : '') do text end + - inline_svg_tag("icons/arrow-down.svg") + inline_svg_tag("icons/arrow-down.svg", class: "federated-icon-#{@portal_name}") end end + + def external_class? + !@portal_name.nil? + end + + def all_federated_portals + out = Array(@other_portals) + out.prepend({name: @portal_name, color: @portal_color, light_color: @portal_light_color, link: @link}) if external_class? + out + end end diff --git a/app/components/display/search_result_component/search_result_component.html.haml b/app/components/display/search_result_component/search_result_component.html.haml index 9d4d48bc08..4771756fd2 100644 --- a/app/components/display/search_result_component/search_result_component.html.haml +++ b/app/components/display/search_result_component/search_result_component.html.haml @@ -1,20 +1,25 @@ .search-result-component{class: sub_component_class, 'data-controller': 'reveal-component'} - %a.title{href: @link} - = @title + %a.title{href: @link, style: @portal_color ? "color: #{@portal_color} !important" : '', target: @portal_color ? "_blank" : ''} + .d-flex.align-items-center + = @title + = inline_svg_tag 'icons/external-link.svg', class: "ml-1 federated-icon-#{@portal_name} #{@portal_color ? '' : 'd-none'}" + - if @uri .uri = @uri - if @definition - = display_in_multiple_languages(@definition) .actions - = details_button - = visualize_button - = mappings_button + - unless external_class? + = details_button + = visualize_button + = mappings_button - if subresults? = reveal_ontologies_button("#{subresults.size} #{t('search.result_component.more_from_ontology')}", sub_ontologies_id, 'icons/three-dots.svg') - if reuses? = reveal_ontologies_button("#{t('search.result_component.reuses_in')} #{reuses.size} ontologies", reuses_id, 'icons/reuses.svg') + - all_federated_portals.each do |p| + = portal_button(name: p[:name], color: p[:color], light_color: p[:light_color], link: p[:link], tooltip: "Source #{p[:name].humanize.gsub("portal", "Portal")}") - if subresults? .more-from-ontology.d-none{id: sub_ontologies_id} .vertical-line @@ -28,4 +33,4 @@ .search-result-sub-components - reuses.each do |reuse| .search-result-sub-component - = reuse \ No newline at end of file + = reuse diff --git a/app/components/dropdown_container_component.rb b/app/components/dropdown_container_component.rb index e06b3fa7ac..f9d8ce38d2 100644 --- a/app/components/dropdown_container_component.rb +++ b/app/components/dropdown_container_component.rb @@ -2,13 +2,16 @@ class DropdownContainerComponent < ViewComponent::Base renders_one :empty_state - def initialize(title:, id:, tooltip:nil, is_open: false) + renders_one :title + + def initialize(title: nil, id:, tooltip:nil, is_open: false) super @title = title @id = id @tooltip = tooltip @is_open = is_open end + def open_class @is_open ? "show" : "" end diff --git a/app/components/dropdown_container_component/dropdown_container_component.html.haml b/app/components/dropdown_container_component/dropdown_container_component.html.haml index 7b9d4965e8..398f3199a0 100644 --- a/app/components/dropdown_container_component/dropdown_container_component.html.haml +++ b/app/components/dropdown_container_component/dropdown_container_component.html.haml @@ -1,6 +1,9 @@ .dropdown-container .dropdown-title-bar{"data-toggle" => "collapse", "data-target" => "##{@id}"} - = render Display::HeaderComponent.new(text: @title, tooltip: @tooltip) + - if title? + = title + - else + = render Display::HeaderComponent.new(text: @title, tooltip: @tooltip) = image_tag("summary/arrow-down.svg", class: 'ml-2') .collapse{id: @id, class: open_class} diff --git a/app/components/federated_portal_button_component.rb b/app/components/federated_portal_button_component.rb new file mode 100644 index 0000000000..4d8a1e886c --- /dev/null +++ b/app/components/federated_portal_button_component.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class FederatedPortalButtonComponent < ViewComponent::Base + attr_reader :name, :tooltip, :link, :color, :light_color + + def initialize(name:, link:, color:, tooltip:, light_color:) + @name = name + @tooltip = tooltip + @link = link + @color = color + @light_color = light_color + end +end diff --git a/app/components/federated_portal_button_component/federated_portal_button_component.html.haml b/app/components/federated_portal_button_component/federated_portal_button_component.html.haml new file mode 100644 index 0000000000..e45a6f0520 --- /dev/null +++ b/app/components/federated_portal_button_component/federated_portal_button_component.html.haml @@ -0,0 +1,7 @@ +%span{'data-controller': 'federation-portals-colors', + 'data-federation-portals-colors-color-value': color, + 'data-federation-portals-colors-portal-name-value': name.downcase} +%a{ href: link, target: '_blank', 'data-controller' => 'tooltip', title: tooltip, class: 'federation-portal-button button icon-right', style: color ? "background-color: #{light_color} !important" : '' } + = inline_svg_tag('logos/ontoportal.svg', class: "federated-icon-#{name.downcase}") + %div{ class: 'text', style: color ? "color: #{color} !important" : '' } + = name.humanize.gsub("portal", "Portal") diff --git a/app/components/federated_portal_button_component/federated_portal_button_component_controller.js b/app/components/federated_portal_button_component/federated_portal_button_component_controller.js new file mode 100644 index 0000000000..dd6d4a708d --- /dev/null +++ b/app/components/federated_portal_button_component/federated_portal_button_component_controller.js @@ -0,0 +1,18 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + color: String, + portalName: String, + } + connect() { + this.#initIconsStyle() + } + + #initIconsStyle(){ + const style = document.createElement('style'); + style.innerHTML = `.federated-icon-${this.portalNameValue} path { fill: ${this.colorValue} !important; }\n`; + document.head.appendChild(style); + } + + } diff --git a/app/components/link_text_component.rb b/app/components/link_text_component.rb index 47ea0f6dfc..36dc79ba33 100644 --- a/app/components/link_text_component.rb +++ b/app/components/link_text_component.rb @@ -10,7 +10,7 @@ def initialize(text:, icon: nil, target: nil) end def call - svg_icon = !@icon&.empty? ? inline_svg(@icon) : '' + svg_icon = !@icon&.empty? ? inline_svg(@icon, width: '14px', height: '14px') : '' extra_span = @text == t('mappings.upload_mappings') ? '' : "#{svg_icon}" "#{@text}#{extra_span}".html_safe end diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index 84251d7f0d..cb40202109 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -1,14 +1,40 @@ # frozen_string_literal: true class OntologyBrowseCardComponent < ViewComponent::Base - include OntologiesHelper + include ApplicationHelper, OntologiesHelper, FederationHelper, ComponentsHelper - def initialize(ontology: nil) + def initialize(ontology: nil, onto_link: nil, text_color: nil, bg_light_color: nil, portal_name: nil) super @ontology = ontology + @text_color = text_color + @bg_light_color = bg_light_color + @onto_link = onto_link || "/ontologies/#{@ontology[:acronym]}" if @ontology + @portal_name = portal_name end def ontology @ontology end + + def external_ontology? + !internal_ontology?(@ontology[:id]) + end + + def onto_link + @onto_link + end + + def style_text + external_ontology? ? "color: #{@text_color} !important" : '' + end + + def portal_color + @text_color + end + alias :color :portal_color + + def style_bg + external_ontology? ? "#{style_text} ; background-color: #{@bg_light_color}" : '' + end + end diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index aaf97e85fd..4f02e889f4 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -3,9 +3,12 @@ .d-flex .browse-ontology-description .browse-ontology-title-bar - %a.browse-ontology-title{:href => "/ontologies/#{ontology[:acronym]}", data: {'turbo': 'false'}} + %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text, target: external_ontology? ? '_blank' : ''} = ontology[:name]+" ("+ontology[:acronym]+")" = private_ontology_icon(ontology[:private]) + - if external_ontology? + %div{class: "federated-icon-#{@portal_name&.downcase} d-inline"} + = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') - if session[:user]&.admin? - ontology_status = status_string(ontology) = render Display::InfoTooltipComponent.new(text: ontology_status, icon: submission_status_icons(ontology_status)) @@ -21,27 +24,32 @@ %p.browse-fair-title = t('components.fair_score') .browse-progress-bar - .browse-faire-progress{:style => "width: #{ontology[:normalizedFairScore].to_s+"%"}"} + .browse-faire-progress{:style => "width: #{ontology[:normalizedFairScore].to_s+"%"}; #{color ? 'background-color:' + color : ''}"} %p.browse-fair-score = ontology[:fairScore] - %a.browse-fair-details{:href => "/ontologies/#{ontology[:acronym]}#fair-details", 'data-turbo': 'false'}= t('components.details_details') + %a.browse-fair-details{:href => "#{onto_link}#fair-details", 'data-turbo': 'false', style: style_text}= t('components.details_details') - .browse-ontology-cards - = render SquareBadgeComponent.new(label: t('components.classes'), count: ontology[:class_count_formatted], link: "/ontologies/#{ontology[:acronym]}?p=classes" ) + .browse-ontology-cards{style: color ? "color: #{color} !important" : ''} + = render SquareBadgeComponent.new(label: t('components.classes'), + count: ontology[:class_count_formatted], + link: "#{onto_link}?p=classes", + color: color) = render SquareBadgeComponent.new(label: ontology[:format] == 'SKOS' ? t('components.concepts') : t('components.instances'), - count: ontology[:individual_count_formatted], - link: "/ontologies/#{ontology[:acronym]}?p=#{ontology[:format] == 'SKOS' ? "classes" : "instances"}") + count: ontology[:individual_count_formatted], color: color, + link: "#{onto_link}?p=#{ontology[:format] == 'SKOS' ? "classes" : "instances"}") - = render SquareBadgeComponent.new(label: t('components.projects'), count: ontology[:project_count], link: "/ontologies/#{ontology[:acronym]}#projects_section" ) + = render SquareBadgeComponent.new(label: t('components.projects'), count: ontology[:project_count], color: color, + link: "#{onto_link}#projects_section" ) - = render SquareBadgeComponent.new(label: t('components.notes'), count: ontology[:note_count], link: "/ontologies/#{ontology[:acronym]}?p=notes" ) + = render SquareBadgeComponent.new(label: t('components.notes'), count: ontology[:note_count], color: color, + link: "#{onto_link}?p=notes" ) - .d-flex.align-items-baseline.mt-1 + .d-flex.w-100.mt-1.flex-wrap - if ontology[:creationDate] %span.mr-1 - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do %span.mr-1= t('components.submitted') %span.browse-uploaded-date{data:{controller: 'timeago', 'timeago-datetime-value': ontology[:creationDate], 'timeago-add-suffix-value': 'true'}} - if ontology[:contact] @@ -52,19 +60,26 @@ - if ontology[:released] - date = render DateTimeFieldComponent.new(value: ontology[:released]) %span{data:{controller:'tooltip'}, title: t('components.creation_date', date: date)} - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do = DateTime.parse(date).year rescue date - if ontology[:format] %span.mx-1 - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do = ontology[:format] - if ontology_retired?(ontology) %span.mx-1 = ontology_retired_badge(ontology) - if ontology[:viewOfOnt] %span.mx-1{data:{controller:'tooltip'}, title: t('components.view_of_the_ontology', ontology: ontology[:viewOfOnt].split('/').last )} - = render ChipButtonComponent.new(type: "clickable", text: t('components.view')) + = render ChipButtonComponent.new(type: "clickable", text: t('components.view'), style: style_bg) + + + - ontology[:sources]&.each do |id| + - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} + - unless config.blank? + %span{style: "padding: 3px 0; margin-right: 0.25rem;"} + = portal_button(name: config[:name], color: config[:color], light_color: config[:"light-color"], link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} @@ -82,4 +97,4 @@ .two .browse-sket-column-three .one - .two \ No newline at end of file + .two diff --git a/app/components/square_badge_component.rb b/app/components/square_badge_component.rb index 9d85a8c34f..bfbc95c6db 100644 --- a/app/components/square_badge_component.rb +++ b/app/components/square_badge_component.rb @@ -2,15 +2,17 @@ class SquareBadgeComponent < ViewComponent::Base - def initialize(label: , count: ,link: nil) + def initialize(label: , count: ,link: nil, color: nil) @label = label @count = count @link = link + @color = color end + def call return if @count.to_i.zero? - link_to(@link, class: 'browse-onology-card', 'data-turbo' => 'false') do + link_to(@link, class: 'browse-onology-card', 'data-turbo' => 'false', style: @color ? "color: #{@color} !important; border-color: #{@color}" : "") do concat(content_tag(:p, @count, class: 'browse-card-number')) concat(content_tag(:p, @label, class: 'browse-card-text')) end diff --git a/app/components/tab_item_component.rb b/app/components/tab_item_component.rb index 88f11f890a..8d5f25c8e4 100644 --- a/app/components/tab_item_component.rb +++ b/app/components/tab_item_component.rb @@ -44,12 +44,8 @@ def page_name end def call - if title && !title.empty? - link_to(title, @path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) - else - link_to(@path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) do - content - end + link_to(@path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) do + (title && !title.empty?) ? title.html_safe : content end end diff --git a/app/components/tree_link_component.rb b/app/components/tree_link_component.rb index 73da902f20..fd23706d96 100644 --- a/app/components/tree_link_component.rb +++ b/app/components/tree_link_component.rb @@ -2,26 +2,19 @@ class TreeLinkComponent < ViewComponent::Base include MultiLanguagesHelper, ModalHelper, ApplicationHelper - def initialize(child:, href:, children_href: , selected: false , data: {}, muted: false, target_frame: nil, open_in_modal: false, is_reused: nil) + + def initialize(child:, href:, children_href:, selected: false, data: {}, muted: false, target_frame: nil, open_in_modal: false, is_reused: nil) + super + @child = child @active_style = selected ? 'active' : '' - #@icons = child.relation_icon(node) @muted_style = muted ? 'text-muted' : '' @href = href @children_link = children_href - label = (@child.prefLabel || @child.label) rescue @child.id - if label.nil? - @pref_label_html = link_last_part(child.id) - else - pref_label_lang, @pref_label_html = select_language_label(label) - pref_label_lang = pref_label_lang.to_s.upcase - @tooltip = pref_label_lang.eql?("@NONE") ? "" : pref_label_lang - if child.obsolete? - @pref_label_html = "#{@pref_label_html}".html_safe - end - end - @data ||= { controller: 'tooltip', 'tooltip-position-value': 'right', turbo: true, 'turbo-frame': target_frame, action: 'click->simple-tree#select'} + @pref_label_html, @tooltip = node_label(child) + + @data ||= { controller: 'tooltip', 'tooltip-position-value': 'right', turbo: true, 'turbo-frame': target_frame, action: 'click->simple-tree#select' } @data.merge!(data) do |_, old, new| "#{old} #{new}" @@ -32,7 +25,6 @@ def initialize(child:, href:, children_href: , selected: false , data: {}, muted @is_reused = is_reused end - # This gives a very hacky short code to use to uniquely represent a class # based on its parent in a tree. Used for unique ids in HTML for the tree view def short_uuid @@ -49,7 +41,7 @@ def open? end def border_left - !@child.hasChildren ? 'pl-3 tree-border-left' : '' + !@child.hasChildren ? 'pl-3 tree-border-left' : '' end def li_id @@ -75,4 +67,26 @@ def open_children_link end + private + + def node_label(child) + label = begin + child.prefLabel || child.label + rescue + child.id + end + + if label.nil? + pref_label_html = link_last_part(child.id) + else + pref_label_lang, pref_label_html = select_language_label(label) + pref_label_lang = pref_label_lang.to_s.upcase + tooltip = pref_label_lang.eql?("@NONE") ? "" : pref_label_lang + + pref_label_html = "#{pref_label_html}".html_safe if child.obsolete? + end + + [pref_label_html, tooltip] + end + end diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb index f2c0d3ea00..1e3ba2ed8b 100644 --- a/app/controllers/admin/categories_controller.rb +++ b/app/controllers/admin/categories_controller.rb @@ -122,7 +122,7 @@ def unescape_id end def category_params - params.require(:category).permit(:acronym, :name, :description, :parentCategory, {ontologies:[]}).to_h + params.require(:category).permit(:acronym, :name, :description, {parentCategory: []}, {ontologies:[]}).to_h end def _categories diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f75a395b75..7af57ee341 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -176,6 +176,10 @@ def rest_url helpers.rest_url end + def request_portals + helpers.request_portals + end + def parse_response_body(response) return nil if response.nil? @@ -383,23 +387,6 @@ def get_apikey() return apikey end - def total_mapping_count - total_count = 0 - - begin - stats = LinkedData::Client::HTTP.get("#{REST_URI}/mappings/statistics/ontologies") - unless stats.blank? - stats = stats.to_h.compact - # Some of the mapping counts are erroneously stored as strings - stats.transform_values!(&:to_i) - total_count = stats.values.sum - end - rescue - LOG.add :error, e.message - end - - return total_count - end def determine_layout if Rails.env.appliance? @@ -437,6 +424,10 @@ def json_link(url, optional_params) optional_params_str = filtered_params.map { |param, value| "#{param}=#{value}" }.join("&") return base_url + optional_params_str + "&apikey=#{$API_KEY}" end + + def set_federated_portals + RequestStore.store[:federated_portals] = params[:portals]&.split(',') + end private def not_found_record(exception) diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 31b5db7786..c01c0cf879 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -37,9 +37,12 @@ def index end def show - redirect_to(ontology_path(id: params[:ontology_id], p: 'collections', collectionid: params[:id], lang: request_lang)) and return unless turbo_frame_request? + + redirect_to(ontology_path(id: params[:ontology], p: 'collections', collectionid: params[:id], lang: request_lang)) and return unless turbo_frame_request? @collection = get_request_collection + + render partial: "collections/show" end def show_label diff --git a/app/controllers/concepts_controller.rb b/app/controllers/concepts_controller.rb index 196a61f97d..1063c953b4 100644 --- a/app/controllers/concepts_controller.rb +++ b/app/controllers/concepts_controller.rb @@ -45,14 +45,15 @@ def index @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology]).first @ob_instructions = helpers.ontolobridge_instructions_template(@ontology) - @submission = @ontology.explore.latest_submission(include: 'all') + @submission = @ontology.explore.latest_submission(include:'uriRegexPattern,preferredNamespaceUri') + + @concept = @ontology.explore.single_class({dispay: 'prefLabel'}, params[:id]) - @concept = @ontology.explore.single_class({full: true}, params[:id]) concept_not_found(params[:id]) if @concept.nil? @schemes = params[:concept_schemes].split(',') @concept.children = @concept.explore.children(pagesize: 750, concept_schemes: Array(@schemes).join(','), language: request_lang, display: 'prefLabel,obsolete,hasChildren').collection || [] - @concept.children.sort! { |x, y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase } unless @concept.children.empty? + render turbo_stream: [ replace(helpers.child_id(@concept) + '_open_link') { TreeLinkComponent.tree_close_icon }, replace(helpers.child_id(@concept) + '_childs') do diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index d50d369a8f..078c89c5c9 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -1,5 +1,6 @@ module SearchAggregator - include UrlsHelper, MultiLanguagesHelper + include UrlsHelper, MultiLanguagesHelper, FederationHelper + require 'string-similarity' extend ActiveSupport::Concern BLACKLIST_FIX_STR = [ "https://", @@ -27,29 +28,43 @@ module SearchAggregator def aggregate_results(query, results) ontologies = aggregate_by_ontology(results) grouped_results = add_subordinate_ontologies(query, ontologies) + all_ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym,name', include_views: true, display_links: false, display_context: false) - grouped_results.map do |group| + + search_results = grouped_results.map do |group| format_search_result(group, all_ontologies) end + + if federated_request? + search_results = merge_sort_federated_results(query, search_results) + search_results = swap_canonical_portal_results_first(search_results) + end + + search_results end def format_search_result(result, ontologies) same_ont = result[:same_ont] same_cls = result[:sub_ont] result = same_ont.shift - ontology = result.links['ontology'].split('/').last + ontology = result.links['ontology'] { - root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), - descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, - reuses: same_cls.map do |x| - format_search_result(x, ontologies) - end + root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), + descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, + reuses: same_cls.map do |x| + format_search_result(x, ontologies) + end } end private + def merge_sort_federated_results(query, search_results) + search_results = merge_federated_results(search_results) + sort_results_by_string_similarity(query, search_results) + end + def search_concept_label(label) label = language_hash(label) @@ -59,22 +74,26 @@ def search_concept_label(label) pref_lab.downcase.include?(@search_query.downcase) || @search_query.downcase.include?(pref_lab.downcase) end.first || label.first end - + label end - def search_result_elem(class_object, ontology_acronym, title) + def search_result_elem(class_object, ontology_id, title) label = search_concept_label(class_object.prefLabel) - - { + request_lang = helpers.request_lang&.eql?("ALL") ? '' : "&language=#{helpers.request_lang}" + ontology_acronym = link_last_part(ontology_id) + result = { uri: class_object.id.to_s, - title: title.nil? || title.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", - ontology_acronym: ontology_acronym, - link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{helpers.request_lang&.eql?("ALL") ? '' : "&language="+helpers.request_lang.to_s}", - definition: class_object.definition + title: title.to_s.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", + ontology_id: ontology_id, + link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{request_lang}", + definition: class_object.definition, } - end + result.merge!(class_federation_configuration(class_object)) if federated_request? + + result + end def ontology_name_acronym(ontologies, selected_acronym) ontology = ontologies.select { |x| x.acronym.eql?(selected_acronym.split('/').last) }.first @@ -228,5 +247,63 @@ def blacklist_cls_id_components(cls_id, blacklist_words) stripped_id end -end + def merge_federated_results(search_results) + search_results.each do |element| + element[:root][:other_portals] = [] + element[:reuses].reject! do |reuse| + if (element[:root][:ontology_acronym] == reuse[:root][:ontology_acronym]) && (element[:root][:uri] == reuse[:root][:uri]) + portal_name = reuse[:root][:portal_name] + link = reuse[:root][:link] + element[:root][:other_portals] << { + name: portal_name, + color: federated_portal_color(portal_name), + light_color: federated_portal_light_color(portal_name), + link: link, + ontology_id: reuse[:root][:ontology_id] + } + true + else + false + end + end + end + end + + def swap_canonical_portal_results_first(search_results) + all_submissions = LinkedData::Client::Models::OntologySubmission.all(include: 'pullLocation', include_views: true, display_links: false, display_context: false) + + search_results.each do |result| + next if result[:root][:portal_name].nil? || result[:root][:other_portals].blank? + + result_ontology_ids = [result[:root][:ontology_id]] + result[:root][:other_portals].map { |p| p[:ontology_id] } + + result_submissions = all_submissions.select do |submission| + result_ontology_ids.any? { |ontology_id| submission.id.include?(ontology_id) } + end + + canonical_portal = most_referred_portal(result_submissions) + is_internal_ontology = result[:root][:portal_name].eql?(canonical_portal.to_s) + + next if canonical_portal.nil? || is_internal_ontology + + canonical_portal_result = result[:root][:other_portals].find { |r| r[:name] == canonical_portal.to_s } + swap_portal_attributes(result[:root], canonical_portal_result) if canonical_portal_result + end + search_results + end + + + def swap_portal_attributes(root_portal, new_portal) + [:link, :portal_name, :portal_color, :portal_light_color].each do |attribute| + root_portal[attribute], new_portal[attribute] = new_portal[attribute], root_portal[attribute] + end + end + + def sort_results_by_string_similarity(query, search_results) + search_results = search_results.sort_by do |entry| + root_similarity = String::Similarity.cosine(query.downcase, entry[:root][:title].split('-').first.gsub(" ", "").downcase) + -root_similarity + end + end +end diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 45746e36e2..c039147280 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -1,7 +1,7 @@ module SubmissionFilter extend ActiveSupport::Concern - include SearchContent + include FederationHelper BROWSE_ATTRIBUTES = ['ontology', 'submissionStatus', 'description', 'pullLocation', 'creationDate', 'contact', 'released', 'naturalLanguage', 'hasOntologyLanguage', @@ -21,17 +21,19 @@ def submissions_paginate_filter(params) filter_params = params.permit(@filters.keys).to_h init_filters(params) - @analytics = Rails.cache.fetch("ontologies_analytics-#{Time.now.year}-#{Time.now.month}") do + @analytics = Rails.cache.fetch("ontologies_analytics-#{Time.now.year}-#{Time.now.month}-#{request_portals.join('-')}") do helpers.ontologies_analytics end @ontologies = LinkedData::Client::Models::Ontology.all(include: 'all', also_include_views: true, display_links: false, display_context: false) + @ontologies, @errors = @ontologies.partition { |x| !x.errors } + # get fair scores of all ontologies @fair_scores = fairness_service_enabled? ? get_fair_score('all') : nil @total_ontologies = @ontologies.size - search_backend = params[:search_backend] + params = { query: @search, status: request_params[:status], show_views: @show_views, @@ -43,22 +45,20 @@ def submissions_paginate_filter(params) groups: request_params[:group], categories: request_params[:hasDomain], formats: request_params[:hasOntologyLanguage] } + submissions = filter_submissions(@ontologies, **params) - if search_backend.eql?('index') - submissions = filter_using_index(**params) - submissions = @ontologies.map{ |ont| ontology_hash(ont, submissions) } - else - submissions = filter_using_data(@ontologies, **params) - end + submissions = merge_by_acronym(submissions) if federation_enabled? - submissions = sort_submission_by(submissions, @sort_by, @search) + submissions = sort_submission_by(submissions, @sort_by, @search) @page = paginate_submissions(submissions, request_params[:page].to_i, request_params[:pagesize].to_i) count = @page.page.eql?(1) ? count_objects(submissions) : {} - [@page.collection, @page.totalCount, count, filter_params] + federation_counts = federated_browse_counts(submissions) + + [@page.collection, @page.totalCount, count, filter_params, federation_counts] end def ontologies_with_filters_url(filters, page: 1, count: false) @@ -67,49 +67,47 @@ def ontologies_with_filters_url(filters, page: 1, count: false) private - def filter_using_index(query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) - search_ontologies( - query: query, - status: status, - show_views: show_views, - private_only: private_only, - languages: languages, - page_size: page_size, - formality_level: formality_level, - is_of_type: is_of_type, - groups: groups, categories: categories, - formats: formats - ) - + def merge_by_acronym(submissions) + merged_submissions = [] + submissions.group_by { |x| x[:ontology]&.acronym }.each do |acronym, ontologies| + ontology = canonical_ontology(ontologies) + ontology[:sources] = ontologies.map { |x| x[:id] } + merged_submissions << ontology + end + merged_submissions end - def filter_using_data(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) + + def filter_submissions(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) submissions = LinkedData::Client::Models::OntologySubmission.all(include: BROWSE_ATTRIBUTES.join(','), also_include_views: true, display_links: false, display_context: false) + + submissions = submissions.map { |x| x[:ontology] ? [x[:ontology][:id], x] : nil }.compact.to_h + submissions = ontologies.map { |ont| ontology_hash(ont, submissions) } submissions.map do |s| - out = ((s.ontology.viewingRestriction.eql?('public') && !private_only) || private_only && s.ontology.viewingRestriction.eql?('private')) - out = out && (groups.blank? || (s.ontology.group.map { |x| helpers.link_last_part(x) } & groups.split(',')).any?) - out = out && (categories.blank? || (s.ontology.hasDomain.map { |x| helpers.link_last_part(x) } & categories.split(',')).any?) - out = out && (status.blank? || status.eql?('alpha,beta,production,retired') || status.split(',').include?(s.status)) - out = out && (formats.blank? || formats.split(',').any? { |f| s.hasOntologyLanguage.eql?(f) }) - out = out && (is_of_type.blank? || is_of_type.split(',').any? { |f| helpers.link_last_part(s.isOfType).eql?(f) }) - out = out && (formality_level.blank? || formality_level.split(',').any? { |f| helpers.link_last_part(s.hasFormalityLevel).eql?(f) }) - out = out && (languages.blank? || languages.split(',').any? { |f| s.naturalLanguage.any? { |n| helpers.link_last_part(n).eql?(f) } }) - out = out && (s.ontology.viewOf.blank? || (show_views && !s.ontology.viewOf.blank?)) - - out = out && (query.blank? || [s.description, s.ontology.name, s.ontology.acronym].any? { |x| (x|| '').downcase.include?(query.downcase) }) + out = ((s[:ontology].viewingRestriction.eql?('public') && !private_only) || private_only && s[:ontology].viewingRestriction.eql?('private')) + out = out && (groups.blank? || (s[:ontology].group.map { |x| helpers.link_last_part(x) } & groups.split(',')).any?) + out = out && (categories.blank? || (s[:ontology].hasDomain.map { |x| helpers.link_last_part(x) } & categories.split(',')).any?) + out = out && (status.blank? || status.eql?('alpha,beta,production,retired') || status.split(',').include?(s[:status])) + out = out && (formats.blank? || formats.split(',').any? { |f| s[:hasOntologyLanguage].eql?(f) }) + out = out && (is_of_type.blank? || is_of_type.split(',').any? { |f| helpers.link_last_part(s[:isOfType]).eql?(f) }) + out = out && (formality_level.blank? || formality_level.split(',').any? { |f| helpers.link_last_part(s[:hasFormalityLevel]).eql?(f) }) + out = out && (languages.blank? || languages.split(',').any? { |f| Array(s[:naturalLanguage]).any? { |n| helpers.link_last_part(n).eql?(f) } }) + out = out && (s[:ontology].viewOf.blank? || (show_views && !s[:ontology].viewOf.blank?)) + + out = out && (query.blank? || [s[:description], s[:ontology].name, s[:ontology].acronym].any? { |x| (x || '').downcase.include?(query.downcase) }) if out s[:rank] = 0 next s if query.blank? - s[:rank] += 3 if s.ontology.acronym && s.ontology.acronym.downcase.include?(query.downcase) + s[:rank] += 3 if s[:ontology].acronym && s[:ontology].acronym.downcase.include?(query.downcase) - s[:rank] += 2 if s.ontology.name && s.ontology.name.downcase.include?(query.downcase) + s[:rank] += 2 if s[:ontology].name && s[:ontology].name.downcase.include?(query.downcase) - s[:rank] += 1 if s.description && s.description.downcase.include?(query.downcase) + s[:rank] += 1 if s[:description] && s[:description].downcase.include?(query.downcase) s else @@ -131,7 +129,7 @@ def paginate_submissions(all_submissions, page, size) end def sort_submission_by(submissions, sort_by, query = nil) - return submissions.sort_by { |x| x[:rank] ? -x[:rank] : 0} unless query.blank? + return submissions.sort_by { |x| x[:rank] ? -x[:rank] : 0 } unless query.blank? if sort_by.eql?('visits') submissions = submissions.sort_by { |x| -(x[:popularity] || 0) } @@ -209,13 +207,17 @@ def filters_params(params, includes: BROWSE_ATTRIBUTES.join(','), page: 1, pages @filters[:search] = params[:search] end + unless params[:portals].blank? + @filters[:portals] = params[:portals] + end + request_params.delete(:order_by) if %w[visits fair].include?(request_params[:sort_by].to_s) request_params end def ontology_hash(ont, submissions) o = {} - sub = submissions.select{|x| x.ontology&.id.eql?(ont.id)}.first + sub = submissions[ont.id] o[:ontology] = ont @@ -225,7 +227,7 @@ def ontology_hash(ont, submissions) o[:hasOntologyLanguage] = sub&.hasOntologyLanguage - if sub&.metrics + if sub&.metrics && !sub.metrics.is_a?(String) o[:class_count] = sub.metrics.classes o[:individual_count] = sub.metrics.individuals else @@ -237,10 +239,10 @@ def ontology_hash(ont, submissions) o[:note_count] = ont.notes&.length || 0 o[:project_count] = ont.projects&.length || 0 - o[:popularity] = @analytics[ont.acronym] || 0 - o[:rank] = sub&[:rank] || 0 + o[:popularity] = @analytics[ont.id.split('/').last.to_s] || 0 + o[:rank] = sub ? sub[:rank] : 0 - OpenStruct.new(o) + o end def add_submission_attributes(ont_hash, sub) @@ -346,10 +348,12 @@ def check_id(name_value, objects, name_key) def object_filter(objects, object_name, name_key = 'acronym') checks = params[object_name]&.split(',') || [] - checks = checks.map { |x| check_id(x, objects, name_key) }.compact + checks = checks.map { |x| helpers.link_last_part(check_id(x, objects, name_key)) }.compact - ids = objects.map { |x| x['id'] } + objects.uniq! { |x| helpers.link_last_part(x['id']) } + ids = objects.map { |x| helpers.link_last_part(x['id']) } count = ids.count { |x| checks.include?(x) } + [objects, checks, count] end @@ -370,7 +374,7 @@ def count_objects(ontologies) object_names.each do |name| values = Array(ontology[name]) values.each do |v| - v.gsub!('http://data.bioontology.org', rest_url) + v = helpers.link_last_part(v) objects_count[name] = {} unless objects_count[name] objects_count[name][v] = (objects_count[name][v] || 0) + 1 diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 3f75bdd3f6..2d20328b74 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,31 +3,15 @@ class HomeController < ApplicationController layout :determine_layout - - include FairScoreHelper + include FairScoreHelper, FederationHelper,MetricsHelper def index @analytics = helpers.ontologies_analytics - # Calculate BioPortal summary statistics - - @ont_count = if @analytics.empty? - LinkedData::Client::Models::Ontology.all.size - else - @analytics.keys.size - end - metrics = LinkedData::Client::Models::Metrics.all - metrics = metrics.each_with_object(Hash.new(0)) do |h, sum| - h.to_hash.slice(:classes, :properties, :individuals).each { |k, v| sum[k] += v } - end @slices = LinkedData::Client::Models::Slice.all - @cls_count = metrics[:classes] - @individuals_count = metrics[:individuals] - @prop_count = metrics[:properties] - @map_count = total_mapping_count - @projects_count = LinkedData::Client::Models::Project.all.length - @users_count = LinkedData::Client::Models::User.all.length + @metrics = portal_metrics(@analytics) + @upload_benefits = [ t('home.benefit1'), @@ -51,6 +35,15 @@ def set_cookies render 'cookies', layout: nil end + def portal_config + @config = $PORTALS_INSTANCES.select { |x| x[:name].downcase.eql?((params[:portal] || helpers.portal_name).downcase) }.first + if @config && @config[:api] + @portal_config = LinkedData::Client::Models::Ontology.top_level_links(@config[:api]).to_h + else + @portal_config = {} + end + end + def tools @tools = { search: { @@ -157,6 +150,16 @@ def annotator_recommender_form end end + + def federation_portals_status + @name = params[:name] + @acronym = params[:acronym] + @key = params[:portal_name] + @checked = params[:checked].eql?('true') + @portal_up = federation_portal_status(portal_name: @key.downcase.to_sym) + render inline: helpers.federation_chip_component(@key, @name, @acronym, @checked, @portal_up) + end + private # Dr. Musen wants 5 specific groups to appear first, sorted by order of importance. diff --git a/app/controllers/instances_controller.rb b/app/controllers/instances_controller.rb index 70db773446..acaa005a1f 100644 --- a/app/controllers/instances_controller.rb +++ b/app/controllers/instances_controller.rb @@ -41,7 +41,7 @@ def show redirect_to(ontology_path(id: params[:ontology], p: 'instances', instanceid: params[:id] || params[:instanceid], lang: request_lang)) and return unless turbo_frame_request? - render partial: 'instances/details', layout: nil + render partial: 'show' end private diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index b1bcfc8c32..df582acb4e 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -342,4 +342,56 @@ def valid_values?(values) end errors end -end \ No newline at end of file + + def set_mapping_target(concept_to_id:, ontology_to:, mapping_type: ) + case mapping_type + when 'interportal' + @map_to_interportal, @map_to_interportal_ontology = ontology_to.match(%r{(.*)/ontologies/(.*)}).to_a[1..] + @map_to_interportal_class = concept_to_id + when 'external' + @map_to_external_ontology = ontology_to + @map_to_external_class = concept_to_id + else + @map_to_bioportal_ontology_id = ontology_to + @map_to_bioportal_full_id = concept_to_id + end + end + + def get_mappings_target_params + mapping_type = Array(params[:mapping_type]).first + external = true + case mapping_type + when 'interportal' + ontology_to = "#{params[:map_to_interportal]}/ontologies/#{params[:map_to_interportal_ontology]}" + concept_to_id = params[:map_to_interportal_class] + when 'external' + ontology_to = params[:map_to_external_ontology] + concept_to_id = params[:map_to_external_class] + else + ontology_to = params[:map_to_bioportal_ontology_id] + concept_to_id = params[:map_to_bioportal_full_id] + external = false + end + [ontology_to, concept_to_id, external] + end + + def get_mappings_target + ontology_to, concept_to_id, external_mapping = get_mappings_target_params + target = '' + if external_mapping + target_ontology = ontology_to + target = concept_to_id + else + if helpers.link?(ontology_to) + target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) + else + target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first + end + if target_ontology + target = target_ontology.explore.single_class(concept_to_id).id + target_ontology = target_ontology.id + end + end + [target_ontology, target, external_mapping] + end +end diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index a8c744fe4a..041c1e6361 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -26,6 +26,8 @@ class OntologiesController < ApplicationController before_action :authorize_and_redirect, :only => [:edit, :update, :create, :new] before_action :submission_metadata, only: [:show] + before_action :set_federated_portals, only: [:index, :ontologies_filter] + KNOWN_PAGES = Set.new(["terms", "classes", "mappings", "notes", "widgets", "summary", "properties", "instances", "schemes", "collections", "sparql"]) EXTERNAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/ExternalMappings" INTERPORTAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/InterportalMappings" @@ -42,20 +44,33 @@ def index def ontologies_filter @time = Benchmark.realtime do - @ontologies, @count, @count_objects, @request_params = submissions_paginate_filter(params) + @ontologies, @count, @count_objects, @request_params, @federation_counts = submissions_paginate_filter(params) end if @page.page.eql?(1) streams = [prepend("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] + streams += @count_objects.map do |section, values_count| values_count.map do |value, count| - replace("count_#{section}_#{value}") do - helpers.turbo_frame_tag("count_#{section}_#{value}") do - helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") + replace("count_#{section}_#{link_last_part(value)}") do + helpers.turbo_frame_tag("count_#{section}_#{link_last_part(value)}") do + helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") end end end end.flatten + + unless request_portals.empty? + streams += [ + replace('categories_refresh_for_federation') do + key = "categories" + objects, checked_values, _ = @filters[key.to_sym] + helpers.browse_filter_section_body(checked_values: checked_values, + key: key, objects: objects, + counts: @count_objects[key.to_sym]) + end + ] + end else streams = [replace("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] end @@ -190,7 +205,8 @@ def instances def schemes @schemes = get_schemes(@ontology) scheme_id = params[:schemeid] || @submission_latest.URI || nil - @scheme = get_scheme(@ontology, scheme_id) if scheme_id + @scheme = scheme_id ? get_scheme(@ontology, scheme_id) : @schemes.first + render partial: 'ontologies/sections/schemes', layout: 'ontology_viewer' end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 490072b4bd..5816730466 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -26,13 +26,13 @@ def show redirect_to projects_path return end - + @project = projects.first @ontologies_used = [] onts_used = @project.ontologyUsed onts_used.each do |ont_used| ont = LinkedData::Client::Models::Ontology.find(ont_used) - unless ont.nil? + unless ont.nil? || ont.errors @ontologies_used << Hash["name", ont.name, "acronym", ont.acronym] end end @@ -62,7 +62,7 @@ def edit @project = projects.first @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} @user_select_list.sort! {|a,b| a[1].downcase <=> b[1].downcase} - @usedOntologies = @project.ontologyUsed || [] + @usedOntologies = @project.ontologyUsed&.map{|o| o.split('/').last} @ontologies = LinkedData::Client::Models::Ontology.all end @@ -76,7 +76,7 @@ def create @project = LinkedData::Client::Models::Project.new(values: project_params) @project_saved = @project.save - + # Project successfully created. if response_success?(@project_saved) flash[:notice] = t('projects.project_successfully_created') @@ -160,10 +160,10 @@ def destroy def project_params p = params.require(:project).permit(:name, :acronym, :institution, :contacts, { creator:[] }, :homePage, :description, { ontologyUsed:[] }) - + p[:creator]&.reject!(&:blank?) - p[:ontologyUsed]&.reject!(&:blank?) - p.to_h + p[:ontologyUsed] ||= [] + p = p.to_h end def flash_error(msg) diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 7fbbcc973e..8c5c97f1f0 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -32,9 +32,11 @@ def index end def show - redirect_to(ontology_path(id: params[:ontology_id], p: 'schemes', schemeid: params[:id],lang: request_lang)) and return unless turbo_frame_request? + redirect_to(ontology_path(id: params[:ontology], p: 'schemes', schemeid: params[:id],lang: request_lang)) and return unless turbo_frame_request? @scheme = get_request_scheme + + render partial: "schemes/show" end def show_label diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 841f0f948a..e0d15048da 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,7 +1,7 @@ require 'uri' class SearchController < ApplicationController - include SearchAggregator, SearchContent + include SearchAggregator, SearchContent, FederationHelper skip_before_action :verify_authenticity_token @@ -13,14 +13,26 @@ def index @advanced_options_open = false @search_results = [] @json_url = json_link("#{rest_url}/search", {}) + params[:portals] = params[:portals]&.join(',') return if @search_query.empty? params[:pagesize] = "150" - results = LinkedData::Client::Models::Class.search(@search_query, params).collection + set_federated_portals + + params[:ontologies] = nil if federated_request? + + @time = Benchmark.realtime do + results = LinkedData::Client::Models::Class.search(@search_query, params) + @federation_errors = federation_error(results) if federation_error?(results) + results = results.collection + + + @search_results = aggregate_results(@search_query, results) + @federation_counts = federated_search_counts(@search_results) + end @advanced_options_open = !search_params_empty? - @search_results = aggregate_results(@search_query, results) @json_url = json_link("#{rest_url}/search", params.permit!.to_h) end @@ -36,6 +48,7 @@ def json_search params.delete("ontologies") end search_page = LinkedData::Client::Models::Class.search(params[:q], params) + @results = search_page.collection response = "" @@ -141,7 +154,7 @@ def search_params [ :ontologies, :categories, :also_search_properties, :also_search_obsolete, :also_search_views, - :require_exact_match, :require_definition + :require_exact_match, :require_definition, :portals ] end diff --git a/app/controllers/taxonomy_controller.rb b/app/controllers/taxonomy_controller.rb index 06832abe8b..b3afa2542a 100644 --- a/app/controllers/taxonomy_controller.rb +++ b/app/controllers/taxonomy_controller.rb @@ -8,7 +8,6 @@ def index end private - def initialize_taxonomy @groups = LinkedData::Client::Models::Group.all slices = LinkedData::Client::Models::Slice.all @@ -33,13 +32,13 @@ def nest_categories_children(categories) category_index[category[:id]] = category end categories.each do |category| - if category.parentCategory - parent = category_index[category.parentCategory] + category[:parentCategory].each do |parent_id| + parent = category_index[parent_id] parent[:children] ||= [] parent[:children] << category end end - categories.reject! { |category| category.parentCategory } + categories.reject! { |category| category[:parentCategory]&.any? } categories end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c9fb873758..6d565bdb22 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,6 +11,7 @@ module ApplicationHelper include ModalHelper, MultiLanguagesHelper, UrlsHelper + RESOLVE_NAMESPACE = {:omv => "http://omv.ontoware.org/2005/05/ontology#", :skos => "http://www.w3.org/2004/02/skos/core#", :owl => "http://www.w3.org/2002/07/owl#", :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", :rdfs => "http://www.w3.org/2000/01/rdf-schema#", :metadata => "http://data.bioontology.org/metadata/", :metadata_def => "http://data.bioontology.org/metadata/def/", :dc => "http://purl.org/dc/elements/1.1/", :xsd => "http://www.w3.org/2001/XMLSchema#", @@ -23,17 +24,16 @@ module ApplicationHelper :oboInOwl => "http://www.geneontology.org/formats/oboInOwl#", :idot => "http://identifiers.org/idot/", :sd => "http://www.w3.org/ns/sparql-service-description#", :cclicense => "http://creativecommons.org/licenses/", 'skos-xl' => "http://www.w3.org/2008/05/skos-xl#"} - def url_to_endpoint(url) - uri = URI.parse(url) - endpoint = uri.path.sub(/^\//, '') - endpoint - end def search_json_link(link = @json_url, style: '') custom_style = "font-size: 50px; line-height: 0.5; margin-left: 6px; #{style}".strip render IconWithTooltipComponent.new(icon: "json.svg",link: link, target: '_blank', title: t('fair_score.go_to_api'), size:'small', style: custom_style) end + def portal_name_from_uri(uri) + URI.parse(uri).hostname.split('.').first + end + def resolve_namespaces RESOLVE_NAMESPACE end @@ -41,7 +41,7 @@ def resolve_namespaces def ontologies_analytics begin data = LinkedData::Client::Analytics.last_month.onts - data.map{|x| [x[:ont].to_s, x[:views]]}.to_h + data.map{|x| [x[:ont].split('/').last.to_s, x[:views]]}.to_h rescue StandardError {} end @@ -55,18 +55,6 @@ def get_apikey end end - def rest_hostname - extract_hostname(REST_URI) - end - - def extract_hostname(url) - begin - uri = URI.parse(url) - uri.hostname - rescue URI::InvalidURIError - url - end - end def omniauth_providers_info $OMNIAUTH_PROVIDERS @@ -80,11 +68,6 @@ def omniauth_token_provider(strategy) omniauth_provider_info(strategy.to_sym).keys.first end - def encode_param(string) - CGI.escape(string) - end - - def current_user session[:user] end @@ -98,9 +81,6 @@ def child_id(child) child.id.to_s.split('/').last end - - - # Create a popup button with a ? inside to display help when hovered def help_tooltip(content, html_attribs = {}, icon = 'fas fa-question-circle', css_class = nil, text = nil) html_attribs["title"] = content attribs = [] @@ -126,36 +106,29 @@ def error_message_alert end end - def onts_for_select - ontologies ||= LinkedData::Client::Models::Ontology.all(include: "acronym,name") + def onts_for_select(include_views: false) + ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name,viewOf", include_views: include_views}) onts_for_select = [['', '']] ontologies.each do |ont| next if ( ont.acronym.nil? or ont.acronym.empty? ) acronym = ont.acronym name = ont.name abbreviation = acronym.empty? ? "" : "(#{acronym})" - ont_label = "#{name.strip} #{abbreviation}" + ont_label = "#{name.strip} #{abbreviation}#{ont.viewOf ? ' [view]' : ''}" onts_for_select << [ont_label, acronym] end onts_for_select.sort! { |a,b| a[0].downcase <=> b[0].downcase } onts_for_select end - def link_last_part(url) - return "" if url.nil? - - if url.include?('#') - url.split('#').last - else - url.split('/').last - end + def slices_enabled? + $ENABLE_SLICES.eql?(true) end def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true end - def add_comment_button(parent_id, parent_type) if session[:user].nil? link_to t('application.add_comment'), login_index_path(redirect: request.url), class: "secondary-button regular-button slim" @@ -183,14 +156,6 @@ def add_proposal_button(parent_id, parent_type) end - def link?(str) - # Regular expression to match strings starting with "http://" or "https://" - link_pattern = /\Ahttps?:\/\// - str = str&.strip - # Check if the string matches the pattern - !!(str =~ link_pattern) - end - def subscribe_button(ontology_id) return if ontology_id.nil? render TurboFrameComponent.new(id: 'subscribe_button', src: ontology_subscriptions_path(ontology_id: ontology_id.split('/').last), class: 'ml-1') do |t| @@ -360,10 +325,6 @@ def help_path(anchor: nil) "#{Rails.configuration.settings.links[:help]}##{anchor}" end - def uri?(url) - url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ - end - def extract_label_from(uri) label = uri.to_s.chomp('/').chomp('#') index = label.index('#') @@ -414,6 +375,11 @@ def portal_name $SITE end + def current_slice_name + name = @subdomain_filter[:name] + name.blank? ? nil : name + end + def navitems items = [["/ontologies", t('layout.header.browse')], ["/mappings", t('layout.header.mappings')], @@ -503,7 +469,7 @@ def insert_sample_text_button(text) end end - def empty_state(text) + def empty_state(text = t('no_result_was_found')) content_tag(:div, class:'browse-empty-illustration') do inline_svg_tag('empty-box.svg') + content_tag(:p, text) @@ -546,6 +512,7 @@ def cancel_button_component(class_name: nil, id: , value:, data: nil) def categories_select(id: nil, name: nil, selected: 'None') categories_for_select = LinkedData::Client::Models::Category.all.map{|x| ["#{x.name} (#{x.acronym})", x.id]}.unshift(["None", '']) - render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected) + render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected, multiple: true) end + end diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index a577f4190d..9a80d24111 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -1,10 +1,11 @@ module CollectionsHelper + include MultiLanguagesHelper def get_collections(ontology, add_colors: false) collections = ontology.explore.collections(language: request_lang) generate_collections_colors(collections) if add_colors - collections.sort_by{ |x| helpers.main_language_label(x.prefLabel) } + collections.sort_by{ |x| main_language_label(x.prefLabel) || '' } if collections end def get_collection(ontology, collection_uri) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 210e4ab57c..a63ba8edb5 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,23 +1,41 @@ module ComponentsHelper include TermsReuses - def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, &block) + def dropdown_component(id: ,title: nil, tooltip:nil , is_open: false, &block) + render DropdownContainerComponent.new(id: id, title: title, tooltip: tooltip, is_open: is_open) do |d| + capture(d, &block) if block_given? + end + end + + def portal_button(name: nil , color: nil , light_color: nil, link: nil, tooltip: nil) + render FederatedPortalButtonComponent.new(name: name, color: color, link: link, tooltip: tooltip, light_color: light_color) + end + + def tab_item_component(container_tabs:, title:, path:, selected: false, json_link: "", &content) + container_tabs.item(title: title.html_safe, path: path, selected: selected, json_link: json_link) + container_tabs.item_content { capture(&content) } + end + + def alert_component(message, type: "info") + render Display::AlertComponent.new(type: type, message: message) + end + + def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, disabled: false, &block) content_tag(:div, data: { controller: 'tooltip' }, title: tooltip) do - check_input(id: id, name: name, value: value, label: label, checked: checked, &block) + check_input(id: id, name: name, value: value, label: label, checked: checked, disabled: disabled, &block) end end - def group_chip_component(id: nil, name: , object: , checked: , value: nil, title: nil, &block) + def group_chip_component(id: nil, name: , object: , checked: , value: nil, title: nil, disabled: false, &block) title ||= object["name"] value ||= (object["value"] || object["acronym"] || object["id"]) chips_component(id: id || value, name: name, label: object["acronym"], checked: checked, - value: value, tooltip: title, &block) + value: value, tooltip: title, disabled: disabled, &block) end alias :category_chip_component :group_chip_component - def rdf_highlighter_container(format, content) render Display::RdfHighlighterComponent.new(format: format, text: content) end @@ -29,7 +47,7 @@ def check_resolvability_container(url) end end end - + def search_page_input_component(name:, value: nil, placeholder: , button_icon: 'icons/search.svg', type: 'text', &block) content_tag :div, class: 'search-page-input-container', data: { controller: 'reveal' } do search_input = content_tag :div, class: 'search-page-input' do @@ -63,7 +81,7 @@ def paginated_list_component(id:, results:, next_page_url:, child_url:, child_tu end end) end - + concepts = c.collection if concepts && !concepts.empty? concepts.each do |concept| @@ -88,7 +106,6 @@ def paginated_list_component(id:, results:, next_page_url:, child_url:, child_tu end end - def resolvability_check_tag(url) content_tag(:span, check_resolvability_container(url), style: 'display: inline-block;', onClick: "window.open('#{check_resolvability_url(url: url)}', '_blank');") end @@ -103,8 +120,7 @@ def copy_link_to_clipboard(url, show_content: false) end end - - def generated_link_to_clipboard(url, acronym) + def generated_link_to_clipboard(url, acronym) url = "#{$UI_URL}/ontologies/#{acronym}/#{link_last_part(url)}" content_tag(:span, id: "generate_portal_link", style: 'display: inline-block;') do render ClipboardComponent.new(icon: 'icons/copy_link.svg', title: "#{t("components.copy_portal_uri", portal_name: portal_name)} #{link_to(url)}", message: url, show_content: false) @@ -119,11 +135,10 @@ def htaccess_tag(acronym) end end - def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_resolvability: true, generate_link: true, generate_htaccess: false) tag = link_to_tag url = link_to_tag if url.nil? - + tag += content_tag(:span, class: 'mx-1') do concat copy_link_to_clipboard(url) if copy concat generated_link_to_clipboard(url, acronym) if generate_link @@ -136,11 +151,11 @@ def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_ def tree_component(root, selected, target_frame:, sub_tree: false, id: nil, auto_click: false, submission: nil, &child_data_generator) root.children.sort! { |a, b| (a.prefLabel || a.id).downcase <=> (b.prefLabel || b.id).downcase } - + render TreeViewComponent.new(id: id, sub_tree: sub_tree, auto_click: auto_click) do |tree_child| root.children.each do |child| children_link, data, href = child_data_generator.call(child) - + if children_link.nil? || data.nil? || href.nil? raise ArgumentError, t('components.error_block') end @@ -170,8 +185,8 @@ def chart_component(title: '', type:, labels:, datasets:, index_axis: 'x', show_ content_tag(:canvas, nil, data: data) end - def loader_component(type = 'pulsing') - render LoaderComponent.new(type: type) + def loader_component(type:'pulsing', small: false ) + render LoaderComponent.new(type: type, small: small) end def info_tooltip(text, interactive: true) @@ -255,14 +270,12 @@ def properties_dropdown(id, title, tooltip, properties, is_open: false, &block) end end - def regular_button(id, value, variant: "secondary", state: "regular", size: "slim", &block) render Buttons::RegularButtonComponent.new(id:id, value: value, variant: variant, state: state, size: size) do |btn| capture(btn, &block) if block_given? end end - def form_save_button render Buttons::RegularButtonComponent.new(id: 'save-button', value: t('components.save_button'), variant: "primary", size: "slim", type: "submit") do |btn| btn.icon_left do @@ -279,5 +292,9 @@ def form_cancel_button end end - + def text_with_icon(text:, icon:) + content_tag(:div, class: 'd-flex align-items-center icon') do + inline_svg_tag(icon, height: '18', weight: '18') + content_tag(:div, class: 'text') {text} + end + end end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb new file mode 100644 index 0000000000..d961712bc0 --- /dev/null +++ b/app/helpers/federation_helper.rb @@ -0,0 +1,256 @@ +module FederationHelper + include ApplicationHelper + + def federated_portals + $FEDERATED_PORTALS ||= LinkedData::Client.settings.federated_portals + end + + def internal_portal_config(id) + return unless internal_ontology?(id) + + { + name: portal_name, + api: rest_url, + apikey: $API_KEY, + ui: $UI_URL, + color: "var(--primary-color)", + 'light-color': 'var(--light-color)', + } + end + + def federated_portal_config(name_key) + federated_portals[name_key.to_sym] + end + + def federated_portal_name(key) + config = federated_portal_config(key) + config ? config[:name] : key + end + + def federated_portal_color(key) + config = federated_portal_config(key) + config[:color] if config + end + + def federated_portal_light_color(key) + config = federated_portal_config(key) + config[:'light-color'] if config + end + + def ontology_portal_config(id) + rest_url = id.split('/')[0..-3].join('/') + federated_portals.select { |_, config| config[:api].start_with?(rest_url) }.first + end + + def ontology_portal_name(id) + portal_key, _ = ontology_portal_config(id) + portal_key ? federated_portal_name(portal_key) : nil + end + + def ontology_portal_color(id) + portal_key, _ = ontology_portal_config(id) + federated_portal_color(portal_key) if portal_key + end + + def ontoportal_ui_link(id) + portal_key, config = ontology_portal_config(id) + return nil unless portal_key + + ui_link = config[:ui] + api_link = config[:api] + + id.gsub(api_link, "#{ui_link}/") rescue id + end + + def internal_ontology?(id) + id.start_with?(rest_url) + end + + def federated_ontology?(id) + !internal_ontology?(id) + end + + def request_portals + portals = RequestStore.store[:federated_portals] || [] + [portal_name] + portals + end + + def request_portals_names(counts, time) + output = request_portals.map do |x| + config = federated_portal_config(x) + + if config + name = config[:name] + color = config[:color] + elsif portal_name.downcase.eql?(x.downcase) + name = portal_name + color = nil + else + next nil + end + + content_tag(:span, "#{federated_portal_name(name)} (#{counts[federated_portal_name(name).downcase]})", style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") + end.compact.join(", ") + + "#{output} in #{sprintf("%.2f", time)}s" + end + + def federated_request? + params[:portals] + end + + def federation_enabled? + !federated_portals.blank? + end + + + def federation_error?(response) + !response[:errors].blank? + end + + def federation_error(response) + federation_errors = response[:errors].map { |e| ontology_portal_name(e.split(' ').last.gsub('search', '')) } + federation_errors.map { |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + end + + def alert_message_if_federation_error(errors, &block) + return if errors.blank? + + content_tag(:div, class: 'my-1') do + render Display::AlertComponent.new(type: 'warning') do + capture(&block) + end + end + end + + def class_federation_configuration(class_object) + is_external = federation_external_class?(class_object) + portal_name = is_external ? helpers.portal_name_from_uri(class_object.links['ui']) : nil + + result = { + portal_name: portal_name, + portal_color: is_external ? federated_portal_color(portal_name) : nil, + portal_light_color: is_external ? federated_portal_light_color(portal_name) : nil + } + result[:link] = class_object.links['ui'] if is_external + result + end + + def federation_external_class?(class_object) + !class_object.links['self'].include?($REST_URL) + end + + def canonical_ontology(ontologies) + if ontologies.size.eql?(1) + ontologies.first + else + internal_ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first + if internal_ontology + internal_ontology + else + external_canonical_ontology_portal(ontologies) + end + end + end + + def federation_portal_status(portal_name: nil) + Rails.cache.fetch("federation_portal_up_#{portal_name}", expires_in: 2.hours) do + portal_api = federated_portals&.dig(portal_name,:api) + return false unless portal_api + portal_up = false + begin + response = Faraday.new(url: portal_api) do |f| + f.adapter Faraday.default_adapter + f.request :url_encoded + f.options.timeout = 20 + f.options.open_timeout = 20 + end.head + portal_up = response.success? + rescue StandardError => e + Rails.logger.error("Error checking portal status for #{portal_name}: #{e.message}") + end + portal_up + end + end + + def federation_chip_component(key, name, acronym, checked, portal_up) + render TurboFrameComponent.new(id:"federation_portals_status_#{key}") do + content_tag(:div, style: "cursor: default;") do + title = "#{!portal_up ? "#{key.humanize.gsub('portal', 'Portal')} #{t('federation.not_responding')}" : ''}" + group_chip_component(name: name, + object: { "acronym" => acronym, "value" => key }, + checked: checked, + title: title , + disabled: !portal_up) + end + end + end + + def federation_input_chips(name: nil) + federated_portals.map do |key, config| + turbo_frame_component = TurboFrameComponent.new( + id: "federation_portals_status_#{key}", + src: "/status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" + ) + + content_tag :div do + render(turbo_frame_component) do |container| + container.loader do + render ChipsComponent.new(name: '', loading: true, tooltip: t('federation.check_status', portal: key.to_s.humanize.gsub('portal', 'Portal'))) + end + end + end + end.join.html_safe + end + + def init_federation_portals_status + content_tag(:div, class: 'd-none') do + federation_input_chips + end + end + + def federated_search_counts(search_results) + ids = search_results.map do |result| + result.dig(:root, :ontology_id) || rest_url + end + counts_ontology_ids_by_portal_name(ids) + end + + def federated_browse_counts(ontologies) + ids = ontologies.map { |ontology| ontology[:id] } + counts_ontology_ids_by_portal_name(ids) + end + + private + + def counts_ontology_ids_by_portal_name(portals_ids) + counts = Hash.new(0) + current_portal, *federation_portals = request_portals + portals_ids.each do |id| + counts[current_portal.downcase] += 1 if id.include?(current_portal.to_s.downcase) + + federation_portals.each do |portal| + portal_api = federated_portals[portal.downcase.to_sym][:api] + counts[portal.downcase] += 1 if id.include?(portal_api) + end + end + + counts + end + + def external_canonical_ontology_portal(ontologies) + canonical_portal = most_referred_portal(ontologies) + ontologies.select{|o| o[:id].include?(canonical_portal.to_s)}.first + end + + def most_referred_portal(ontology_submissions) + portal_counts = Hash.new(0) + ontology_submissions.each do |submission| + federated_portals.keys.each do |portal| + portal_counts[portal] += 1 if submission[:pullLocation]&.include?(portal.to_s) + end + end + portal_counts.max_by { |_, count| count }&.first + end + +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 5df6609674..2ddcdc5275 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -22,6 +22,22 @@ def format_number_abbreviated(number) end end + def portal_config_tooltip(portal_name, &block) + portal_id = portal_name&.downcase + title = if federation_portal_status(portal_name: portal_id) + render( + TurboFrameComponent.new( + id: "portal_config_tooltip_#{portal_id}", + src: "/config?portal=#{portal_id}", + style: "width: 600px !important; max-height: 300px; overflow: scroll" + ) + ) + end + render Display::InfoTooltipComponent.new(text: title, interactive: true) do + capture(&block) + end + end + def discover_ontologies_button render Buttons::RegularButtonComponent.new(id: 'discover-ontologies-button', value: t('home.discover_ontologies_button'), variant: "secondary", state: "regular", href: "/ontologies") do |btn| btn.icon_right do @@ -36,4 +52,5 @@ def home_ontoportal_description content_tag(:div, t('home.ontoportal_description', ontoportal_link: ontoportal_link, github_link: github_link).html_safe, style: "margin-bottom: 20px") end + end diff --git a/app/helpers/inputs_helper.rb b/app/helpers/inputs_helper.rb index cd4782b6f3..242de76990 100644 --- a/app/helpers/inputs_helper.rb +++ b/app/helpers/inputs_helper.rb @@ -28,8 +28,8 @@ def number_input(name: , label: '', value: ) value: value) end - def check_input(id:, name:, value:, label: '', checked: false, &block) - render ChipsComponent.new(name: name, id: id, label: label, value: value, checked: checked) do |c| + def check_input(id:, name:, value:, label: '', checked: false, disabled: false, &block) + render ChipsComponent.new(name: name, id: id, label: label, value: value, checked: checked, disabled: disabled) do |c| if block_given? capture(c, &block) end @@ -82,4 +82,4 @@ def attribute_error(attr) def input_error_message(name) attribute_error(method_name(name)) end -end \ No newline at end of file +end diff --git a/app/helpers/instances_helper.rb b/app/helpers/instances_helper.rb index db5170b233..79e5b17586 100644 --- a/app/helpers/instances_helper.rb +++ b/app/helpers/instances_helper.rb @@ -57,7 +57,7 @@ def link_to_property(property, ontology_acronym) end def instance_property_value(property, ontology_acronym) - if uri?(property) + if link?(property) instance, types = get_instance_and_type(property, ontology_acronym) return link_to_instance(instance, ontology_acronym) unless instance.empty? end diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb index afc85907a8..c1e503b547 100644 --- a/app/helpers/mappings_helper.rb +++ b/app/helpers/mappings_helper.rb @@ -2,43 +2,85 @@ module MappingsHelper # Used to replace the full URI by the prefixed URI RELATIONSHIP_PREFIX = { - "http://www.w3.org/2004/02/skos/core#" => "skos:", - "http://www.w3.org/2000/01/rdf-schema#" => "rdfs:", - "http://www.w3.org/2002/07/owl#" => "owl:", - "http://www.w3.org/1999/02/22-rdf-syntax-ns#" => "rdf:", - "http://purl.org/linguistics/gold/" => "gold:", - "http://lemon-model.net/lemon#" => "lemon:" + 'http://www.w3.org/2004/02/skos/core#' => 'skos:', + 'http://www.w3.org/2000/01/rdf-schema#' => 'rdfs:', + 'http://www.w3.org/2002/07/owl#' => 'owl:', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' => 'rdf:', + 'http://purl.org/linguistics/gold/' => 'gold:', + 'http://lemon-model.net/lemon#' => 'lemon:' } INTERPORTAL_HASH = $INTERPORTAL_HASH - - # a little method that returns true if the URIs array contain a gold:translation or gold:freeTranslation - def translation?(relation_array) - if relation_array.kind_of?(Array) - relation_array.map!(&:downcase) - if relation_array.include? "http://purl.org/linguistics/gold/translation" - true - elsif relation_array.include? "http://purl.org/linguistics/gold/freetranslation" - true - else - false + def mapping_links(mapping, concept) + target_concept = mapping.classes.select do |c| + c.id != concept.id && c.links['ontology'] != concept.links['ontology'] + end.first + target_concept ||= mapping.classes.last + process = mapping.process || {} + + if inter_portal_mapping?(target_concept) + cls_link = ajax_to_inter_portal_cls(target_concept) + ont_name = target_concept.links['ontology'] + ont_link = link_to ont_name, get_inter_portal_ui_link(ont_name, process['name']), target: '_blank' + source_tooltip = 'Internal-portal' + elsif internal_mapping?(target_concept) + begin + ont = target_concept.explore.ontology + ont_name = ont.acronym + ont_link = link_to ont_name, ontology_path(ont_name), 'data-turbo-frame': '_top' + rescue + ont_name = target_concept.links['ontology'] || target_concept.id + ont_link = ont_name end + cls_link = raw(get_link_for_cls_ajax(target_concept.id, ont_name, '_top')) + source_tooltip = 'Internal' else - false + cls_label = ExternalLinkTextComponent.new(text: target_concept.links['self']).call + cls_link = raw("#{cls_label}") + ont_name = target_concept.links['ontology'] + ont_link = link_to ExternalLinkTextComponent.new(text: ont_name).call, target_concept.links['ontology'], + target: '_blank' + source_tooltip = 'External' end + + [cls_link, ont_link, source_tooltip] + end + + def mapping_prefixed_relations(mapping) + process = mapping.process || {} + Array(process[:relation]).each { |relation| get_prefixed_uri(relation) } + end + + def mapping_type_tooltip(map) + relations = mapping_prefixed_relations(map) + process = map.process || {} + type = if map.source.to_s.include? 'SKOS' + 'SKOS' + else + map.source + end + types_description = { + 'CUI' => t('mappings.types_description.cui'), + 'LOOM' => t('mappings.types_description.loom'), + 'REST' => t('mappings.types_description.rest'), + 'SAME_URI' => t('mappings.types_description.same_uri'), + 'SKOS' => t('mappings.types_description.skos') + } + type_tooltip = content_tag(:div, "#{map.source} #{relations.join(', ')} : #{types_description[type]} #{process[:source_name]}".strip, style: 'width: 300px') + [type, type_tooltip] end # a little method that returns the uri with a prefix : http://purl.org/linguistics/gold/translation become gold:translation def get_prefixed_uri(uri) RELATIONSHIP_PREFIX.each { |k, v| uri.sub!(k, v) } - return uri + uri end # method to get (using http) prefLabel for interportal classes # Using bp_ajax_controller.ajax_process_interportal_cls will try to resolve class labels. def ajax_to_inter_portal_cls(cls) - inter_portal_acronym = get_inter_portal_acronym(cls.links["ui"]) + inter_portal_acronym = get_inter_portal_acronym(cls.links['ui']) href_cls = " href='#{cls.links["ui"]}' " if inter_portal_acronym data_cls = " data-cls='#{cls.links["self"]}?apikey=' " @@ -52,7 +94,7 @@ def ajax_to_inter_portal_cls(cls) def ajax_to_internal_cls(cls) link_to("#{cls.id}".html_safe, - ontology_path(cls.explore.ontology.acronym, p: 'classes', conceptid: cls.id), target: "_blank") + ontology_path(cls.explore.ontology.acronym, p: 'classes', conceptid: cls.id), target: '_blank') end # to get the apikey from the interportal instance of the interportal class. @@ -60,7 +102,7 @@ def ajax_to_internal_cls(cls) def get_inter_portal_acronym(class_ui_url) if !INTERPORTAL_HASH.nil? INTERPORTAL_HASH.each do |key, value| - if class_ui_url.start_with?(value["ui"]) + if class_ui_url.start_with?(value['ui']) return key else return nil @@ -71,11 +113,11 @@ def get_inter_portal_acronym(class_ui_url) # method to extract the prefLabel from the external class URI def get_label_for_external_cls(class_uri) - if class_uri.include? "#" - prefLabel = class_uri.split("#")[-1] - else - prefLabel = class_uri.split("/")[-1] - end + prefLabel = if class_uri.include? '#' + class_uri.split('#')[-1] + else + class_uri.split('/')[-1] + end return prefLabel end @@ -86,11 +128,11 @@ def ajax_to_external_cls(cls) # Replace the inter_portal mapping ontology URI (that link to the API) by the link to the ontology in the UI def get_inter_portal_ui_link(uri, process_name) process_name = '' if process_name.nil? - interportal_acronym = process_name.split(" ")[2] - if interportal_acronym.nil? || interportal_acronym.empty? + interportal_acronym = process_name.split(' ')[2] + if interportal_acronym.nil? || interportal_acronym.empty? || INTERPORTAL_HASH[interportal_acronym].nil? uri else - uri.sub!(INTERPORTAL_HASH[interportal_acronym]["api"], INTERPORTAL_HASH[interportal_acronym]["ui"]) + uri.sub!(INTERPORTAL_HASH[interportal_acronym]['api'], INTERPORTAL_HASH[interportal_acronym]['ui']) end end @@ -99,66 +141,14 @@ def internal_mapping?(cls) end def inter_portal_mapping?(cls) - !internal_mapping?(cls) && cls.links.has_key?("ui") - end - - def get_mappings_target_params - mapping_type = Array(params[:mapping_type]).first - external = true - case mapping_type - when 'interportal' - ontology_to = "#{params[:map_to_interportal]}/ontologies/#{params[:map_to_interportal_ontology]}" - concept_to_id = params[:map_to_interportal_class] - when 'external' - ontology_to = params[:map_to_external_ontology] - concept_to_id = params[:map_to_external_class] - else - ontology_to = params[:map_to_bioportal_ontology_id] - concept_to_id = params[:map_to_bioportal_full_id] - external = false - end - [ontology_to, concept_to_id, external] - end - - def set_mapping_target(concept_to_id:, ontology_to:, mapping_type: ) - case mapping_type - when 'interportal' - @map_to_interportal, @map_to_interportal_ontology = ontology_to.match(%r{(.*)/ontologies/(.*)}).to_a[1..] - @map_to_interportal_class = concept_to_id - when 'external' - @map_to_external_ontology = ontology_to - @map_to_external_class = concept_to_id - else - @map_to_bioportal_ontology_id = ontology_to - @map_to_bioportal_full_id = concept_to_id - end - end - - def get_mappings_target - ontology_to, concept_to_id, external_mapping = get_mappings_target_params - target = '' - if external_mapping - target_ontology = ontology_to - target = concept_to_id - else - if helpers.uri?(ontology_to) - target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) - else - target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first - end - if target_ontology - target = target_ontology.explore.single_class(concept_to_id).id - target_ontology = target_ontology.id - end - end - [target_ontology, target, external_mapping] + !internal_mapping?(cls) && cls.links.has_key?('ui') end def type?(type) @mapping_type.nil? && type.eql?('internal') || @mapping_type.eql?(type) end - def concept_mappings_loader(ontology_acronym: ,concept_id: ) + def concept_mappings_loader(ontology_acronym:, concept_id:) content_tag(:span, id: 'mapping_count') do concat(content_tag(:div, class: 'concepts-mapping-count ml-1 mr-1') do render(TurboFrameComponent.new( @@ -173,21 +163,23 @@ def concept_mappings_loader(ontology_acronym: ,concept_id: ) end def client_filled_modal - link_to_modal "", "" + link_to_modal '', '' end def mappings_bubble_view_legend content_tag(:div, class: 'mappings-bubble-view-legend') do - mappings_legend_section(t('mappings.bubble_view_legend.bubble_size'), t('mappings.bubble_view_legend.bubble_size_desc'), 'mappings-bubble-size-legend') + + mappings_legend_section(t('mappings.bubble_view_legend.bubble_size'), + t('mappings.bubble_view_legend.bubble_size_desc'), 'mappings-bubble-size-legend') + mappings_legend_section( - t('mappings.bubble_view_legend.color_degree'),t('mappings.bubble_view_legend.color_degree_desc'),'mappings-bubble-color-legend') + + t('mappings.bubble_view_legend.color_degree'), t('mappings.bubble_view_legend.color_degree_desc'), 'mappings-bubble-color-legend') + content_tag(:div, class: 'content-container') do content_tag(:div, class: 'bubble-view-legend-item') do content_tag(:div, class: 'title') do - content_tag(:div, t('mappings.bubble_view_legend.yellow_bubble'), class: 'd-inline') + content_tag(:span, t('mappings.bubble_view_legend.selected_bubble')) + content_tag(:div, t('mappings.bubble_view_legend.yellow_bubble'), + class: 'd-inline') + content_tag(:span, t('mappings.bubble_view_legend.selected_bubble')) end + - content_tag(:div, class: "mappings-bubble-size-legend d-flex justify-content-center") do - content_tag(:div, '', class: "bubble yellow") + content_tag(:div, class: 'mappings-bubble-size-legend d-flex justify-content-center') do + content_tag(:div, '', class: 'bubble yellow') end end end @@ -209,7 +201,7 @@ def mappings_legend_section(title_text, description_text, css_class) def mappings_legend(css_class) content_tag(:div, class: css_class) do content_tag(:div, t('mappings.bubble_view_legend.less_mappings'), class: 'mappings-legend-text') + - (1..6).map { |i| content_tag(:div, "", class: "bubble bubble#{i}") }.join.html_safe + + (1..6).map { |i| content_tag(:div, '', class: "bubble bubble#{i}") }.join.html_safe + content_tag(:div, t('mappings.bubble_view_legend.more_mappings'), class: 'mappings-legend-text') end end diff --git a/app/helpers/metrics_helper.rb b/app/helpers/metrics_helper.rb new file mode 100644 index 0000000000..5eb39444ce --- /dev/null +++ b/app/helpers/metrics_helper.rb @@ -0,0 +1,69 @@ +module MetricsHelper + + def portal_metrics(analytics) + ontologies_acronym = if analytics.empty? + LinkedData::Client::Models::Ontology.all.map { |x| x.acronym } + else + analytics.keys + end + + metrics = ontologies_metrics(ontologies_acronym) + + ont_count = ontologies_acronym.size + cls_count = metrics[:classes] + individuals_count = metrics[:individuals] + prop_count = metrics[:properties] + map_count = total_mapping_count(ontologies_acronym) + projects_count = projects_count(ontologies_acronym) + users_count = LinkedData::Client::Models::User.all.length + + { + ontologies_count: ont_count, + class_count: cls_count, + individuals_count: individuals_count, + properties_count: prop_count, + mappings_count: map_count, + projects_count: projects_count, + users_count: users_count + } + end + + def ontologies_metrics(ontologies_acronym = []) + metrics = LinkedData::Client::Models::Metrics.all + + metrics.each_with_object(Hash.new(0)) do |h, sum| + acronym = h.submission&.first&.split('/')&.dig(-3) + next nil if acronym.nil? + next nil unless ontologies_acronym.blank? || ontologies_acronym.include?(acronym) + + h.to_hash.slice(:classes, :properties, :individuals).each { |k, v| sum[k] += v } + end + end + + private + + def projects_count(ontologies_acronym = []) + projects = LinkedData::Client::Models::Project.all + projects.select! { |p| ontologies_acronym.intersection(p.ontologyUsed.map{|x| x.split('/').last}).any? } unless ontologies_acronym.empty? + projects.size + end + + def total_mapping_count(ontologies_acronym = []) + total_count = 0 + begin + stats = LinkedData::Client::HTTP.get(MappingStatistics::MAPPING_STATISTICS_URL) + unless stats.blank? + stats = stats.to_h.compact + # Some of the mapping counts are erroneously stored as strings + stats.select!{ |acronym, count| ontologies_acronym.include?(acronym.to_s) } if helpers.at_slice? + stats.transform_values!(&:to_i) + total_count = stats.values.sum + end + rescue StandardError => e + LOG.add :error, e.message + end + + total_count + end + +end diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb index 9e21f2e0ed..6e127b94e0 100644 --- a/app/helpers/multi_languages_helper.rb +++ b/app/helpers/multi_languages_helper.rb @@ -153,7 +153,7 @@ def select_language_label(concept_label, platform_languages = %i[en fr]) end end - concept_value || concept.to_a.first + concept_value || concept.reject { |k| k.to_s.eql?('@none') }.first || concept.first end def main_language_label(label) diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 33adc9efd5..f3a7ae05f7 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -71,32 +71,10 @@ def ontology_alternative_names(submission = @submission_latest) end) end end + def private_ontology_icon(is_private) raw(content_tag(:i, '', class: 'fas fa-key', title: t('ontologies.private_ontology'))) if is_private end - def browse_filter_section_label(key) - labels = { - categories: t('ontologies.categories'), - groups: t('ontologies.groups'), - hasFormalityLevel: t('ontologies.formality_levels'), - isOfType: t('ontologies.ontology_types'), - naturalLanguage: t('ontologies.natural_languages') - } - - labels[key] || key.to_s.underscore.humanize.capitalize - end - - def browser_counter_loader - content_tag(:div, class: "browse-desc-text", style: "margin-bottom: 15px;") do - content_tag(:div, class: "d-flex align-items-center") do - str = content_tag(:span, t('ontologies.showing')) - str += content_tag(:span, "", class: "p-1 p-2", style: "color: #a7a7a7;") do - render LoaderComponent.new(small: true) - end - str - end - end - end def ontologies_browse_skeleton(pagesize = 5) pagesize.times do @@ -530,6 +508,51 @@ def language_selector_hidden_tag(section) data: { controller: "language-change", 'language-change-section-value': section, action: "change->language-change#dispatchLangChangeEvent" } end + def ontology_object_json_link(ontology_acronym, object_type, id) + "#{rest_url}/ontologies/#{ontology_acronym}/#{object_type}/#{escape(id)}?display=all&apikey=#{get_apikey}" + end + + def render_permalink_link + content_tag(:div, class: 'mx-1') do + link_to("#classPermalinkModal", class: "class-permalink nav-link", title: t('concepts.permanent_link_class'), aria: { label: t('concepts.permanent_link_class') }, data: { toggle: "modal", current_purl: @current_purl }) do + content_tag(:i, '', class: "fas fa-link", aria: { hidden: "true" }) + end + end + end + + def render_concepts_json_button(link) + content_tag(:div, class: 'concepts_json_button') do + render RoundedButtonComponent.new(link: link, target: '_blank') + end + end + + + def ontology_object_details_component(frame_id: , ontology_id:, objects_title:, object:, &block) + render TurboFrameComponent.new(id: frame_id, data: {"turbo-frame-target": "frame"}) do + return if !object.present? + return alert_component(object.errors.join) if object.errors + + ontology_object_tabs_component(ontology_id: ontology_id, objects_title: objects_title, object_id: object["@id"]) do |tabs| + tab_item_component(container_tabs: tabs, title: t('concepts.details'), path: '#details', selected: true) do + capture(&block) + end + end + end + end + + def ontology_object_tabs_component(ontology_id:, objects_title:, object_id:, &block) + resource_url = ontology_object_json_link(ontology_id, objects_title, object_id) + render TabsContainerComponent.new(type: 'outline') do |c| + concat(c.pinned_right do + content_tag(:div, '', 'data-concepts-json-target': 'button') do + concat(render_permalink_link) if $PURL_ENABLED + concat(render_concepts_json_button(resource_url)) + end + end) + + capture(c, &block) + end + end def display_complex_text(definitions) @@ -770,7 +793,6 @@ def n_triples_to_table(n_triples_string) end end - private def submission_languages(submission = @submission) Array(submission&.naturalLanguage).map { |natural_language| natural_language["iso639"] && natural_language.split('/').last }.compact @@ -780,12 +802,87 @@ def id_to_acronym(id) id.split('/').last end - def browse_taxonomy_tooltip(texonomy) - content_tag(:div, class: 'd-flex') do - content_tag(:div, "See more information about #{texonomy} in ", class: 'mr-1') + - content_tag(:a, 'here', href: "/#{texonomy}", target: '_blank') + def browse_taxonomy_tooltip(taxonomy_type) + return nil unless taxonomy_type.eql?("categories") || taxonomy_type.eql?("groups") + + content_tag(:div, class: '') do + content_tag(:span, "See more information about #{taxonomy_type} in ", class: 'mr-1') + + content_tag(:a, 'here', href: "/#{taxonomy_type}", target: '_blank') + end + end + + def browse_chip_filter(key:, object:, values:, countable: true, count: nil) + title = (key.to_s.eql?("categories") || key.to_s.eql?("groups")) ? nil : '' + checked = values.any? { |obj| [link_last_part(object["id"]), link_last_part(object["value"])].include?(obj) } + + group_chip_component(name: key, object: object, checked: checked, title: title) do |c| + c.count { browse_chip_count_badge(key: key, id: object["id"], count: count) } if countable + end + end + + def browse_chip_count_badge(id:, key:, count: nil) + content_tag :span, class: 'badge badge-light ml-1' do + turbo_frame_tag("count_#{key}_#{link_last_part(id)}", busy: true) + + if count || count == 0 + content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") + else + content_tag(:span, class: 'show-if-loading') do + loader_component(small: true, type: nil) + end + end + end + end + + def browse_filter_section_label(key) + labels = { + categories: t('ontologies.categories'), + groups: t('ontologies.groups'), + hasFormalityLevel: t('ontologies.formality_levels'), + isOfType: t('ontologies.ontology_types'), + naturalLanguage: t('ontologies.natural_languages') + } + + labels[key] || key.to_s.underscore.humanize.capitalize + end + + def browse_filter_section_header(key: nil, count: nil, title: nil) + render Display::HeaderComponent.new(tooltip: key ? browse_taxonomy_tooltip(key.to_s) : nil) do + content_tag(:span, class: "browse-filter-title-bar") do + concat title || browse_filter_section_label(key) + + concat content_tag(:span, count, class: "badge badge-primary mx-1", + "data-show-filter-count-target": "countSpan", + style: "#{count&.positive? ? '' : 'display: none;'}") + end + + end + end + + def browse_filter_section_body(checked_values: , key:, objects:, countable: true, counts: nil) + output = content_tag(:div, class: "browse-filter-checks-container px-3") do + Array(objects).map do |object| + count = counts ? counts[link_last_part(object["id"])] || 0 : nil + concat browse_chip_filter(key: key, object: object, values: checked_values, countable: countable, count: count) + end + end + + if key.to_s.include?("categories") + turbo_frame_tag('categories_refresh_for_federation') { output.html_safe } + else + output end end + def browser_counter_loader + content_tag(:div, class: "browse-desc-text", style: "margin-bottom: 15px;") do + content_tag(:div, class: "d-flex align-items-center") do + str = content_tag(:span, t('ontologies.showing')) + str += content_tag(:span, "", class: "p-1 p-2", style: "color: #a7a7a7;") do + render LoaderComponent.new(small: true) + end + str + end + end + end end diff --git a/app/helpers/urls_helper.rb b/app/helpers/urls_helper.rb index 5e26db110b..2c149e29d4 100644 --- a/app/helpers/urls_helper.rb +++ b/app/helpers/urls_helper.rb @@ -1,4 +1,40 @@ module UrlsHelper + def url_to_endpoint(url) + uri = URI.parse(url) + endpoint = uri.path.sub(/^\//, '') + endpoint + end + def rest_hostname + extract_hostname($REST_URL) + end + + def extract_hostname(url) + begin + uri = URI.parse(url) + uri.hostname + rescue URI::InvalidURIError + url + end + end + + def link?(str) + # Regular expression to match strings starting with "http://" or "https://" + link_pattern = /\Ahttps?:\/\// + str = str&.strip + # Check if the string matches the pattern + !!(str =~ link_pattern) + end + + def link_last_part(url) + return "" if url.nil? + + if url.include?('#') + url.split('#').last + else + url.split('/').last + end + end + def escape(string) CGI.escape(string) if string end @@ -6,4 +42,8 @@ def escape(string) def unescape(string) CGI.unescape(string) if string end + + def encode_param(string) + escape(string) + end end diff --git a/app/javascript/component_controllers/index.js b/app/javascript/component_controllers/index.js index c0d3bcd20f..bd6df841a2 100644 --- a/app/javascript/component_controllers/index.js +++ b/app/javascript/component_controllers/index.js @@ -25,6 +25,7 @@ import Table_component_controller from '../../components/table_component/table_c import clipboard_component_controller from '../../components/clipboard_component/clipboard_component_controller' import range_slider_component_controller from '../../components/input/range_slider_component/range_slider_component_controller' import RDFHighlighter from '../../components/display/rdf_highlighter_component/rdf_highlighter_component_controller' +import FederationController from "../../components/federated_portal_button_component/federated_portal_button_component_controller" application.register("rdf-highlighter", RDFHighlighter) application.register('turbo-modal', TurboModalController) @@ -35,10 +36,12 @@ application.register('subscribe-notes', Ontology_subscribe_button_component_cont application.register('search-input', Search_input_component_controller) application.register('tabs-container', Tabs_container_component_controller) application.register('circle-progress-bar', CircleProgressBarComponentController) -application.register('alert-component', alert_component_controller) +application.register('alert-component', alert_component_controller) application.register('progress-pages', Progress_pages_component_controller) application.register('reveal-component', Reveal_component_controller) application.register('table-component', Table_component_controller) application.register('clipboard', clipboard_component_controller) -application.register('range-slider', range_slider_component_controller) \ No newline at end of file + +application.register('range-slider', range_slider_component_controller) +application.register("federation-portals-colors", FederationController) diff --git a/app/javascript/controllers/browse_filters_controller.js b/app/javascript/controllers/browse_filters_controller.js index 74272dc26a..51eff387bb 100644 --- a/app/javascript/controllers/browse_filters_controller.js +++ b/app/javascript/controllers/browse_filters_controller.js @@ -2,6 +2,7 @@ import {Controller} from "@hotwired/stimulus" import debounce from "debounce" // Connects to data-controller="browse-filters" export default class extends Controller { + static targets = ['sort'] initialize() { this.dispatchInputEvent = debounce(this.dispatchInputEvent.bind(this), 700); @@ -46,11 +47,16 @@ export default class extends Controller { filter = "private_only" break; default: - checks = this.#getSelectedChecks().map(x => x.value) + checks = this.#getSelectedChecks(event).map(x => x.value) filter = event.target.name } - this.#dispatchEvent(filter, checks) + event.stopPropagation() + } + + federationChange(event){ + this.sortTarget.value = "ontology_name" + this.sortTarget.dispatchEvent(new Event('change', { bubbles: true })) } @@ -63,11 +69,10 @@ export default class extends Controller { data: data }, bubbles: true }); - this.element.dispatchEvent(customEvent); } - #getSelectedChecks() { - return Array.from(this.element.querySelectorAll('input:checked')) + #getSelectedChecks(event) { + return Array.from(event.currentTarget.querySelectorAll('input:checked')) } } diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 24f39bf453..fa09b85416 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -105,4 +105,4 @@ import MappingsController from "./mappings_visualization_controller" application.register('mappings', MappingsController) import ConceptsJsonButtonController from "./concepts_json_button_controller.js" -application.register('concepts-json', ConceptsJsonButtonController) \ No newline at end of file +application.register('concepts-json', ConceptsJsonButtonController) diff --git a/app/javascript/controllers/simple_tree_controller.js b/app/javascript/controllers/simple_tree_controller.js index 858bfde2c8..4cece829a7 100644 --- a/app/javascript/controllers/simple_tree_controller.js +++ b/app/javascript/controllers/simple_tree_controller.js @@ -1,4 +1,7 @@ import { Controller } from '@hotwired/stimulus' + +const TREE_VIEW_PAGES = ['classes', 'properties', 'schemes', 'collections', 'instances'] + // Connects to data-controller="simple-tree" export default class extends Controller { @@ -7,17 +10,7 @@ export default class extends Controller { } connect () { - setTimeout(() => { - let activeElem = this.element.querySelector('.tree-link.active'); - if (activeElem) { - activeElem.scrollIntoView({ block: 'center' }); - window.scrollTo({top: 0,}); - if (this.autoClickValue) { - activeElem.click(); - } - } - this.#onClickTooManyChildrenInit(); - }, 0); + this.#centerTreeView() } select (event) { @@ -33,6 +26,29 @@ export default class extends Controller { event.target.nextElementSibling.nextElementSibling.classList.toggle('hidden') } + #centerTreeView() { + setTimeout(() => { + const location = window.location.href; + + const isTreeViewPage = TREE_VIEW_PAGES.some(param => location.includes(`p=${param}`)); + + if (isTreeViewPage) { + const activeElem = this.element.querySelector('.tree-link.active'); + + if (activeElem) { + activeElem.scrollIntoView({ block: 'center' }); + window.scrollTo({ top: 0 }); + + if (this.autoClickValue) { + activeElem.click(); + } + } + + this.#onClickTooManyChildrenInit(); + } + }, 0); + } + #onClickTooManyChildrenInit () { jQuery('.too_many_children_override').live('click', (event) => { event.preventDefault() diff --git a/app/javascript/controllers/turbo_frame_controller.js b/app/javascript/controllers/turbo_frame_controller.js index 6520acb64a..aacc99eca1 100644 --- a/app/javascript/controllers/turbo_frame_controller.js +++ b/app/javascript/controllers/turbo_frame_controller.js @@ -32,7 +32,6 @@ export default class extends Controller { this.urlValue = this.#updatedPageUrl(data) this.frame.src = this.urlValue - } } @@ -44,7 +43,7 @@ export default class extends Controller { if (currentDisplayedUrl.toString().includes(this.urlValue)){ return true - } else if (currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ + } else if (currentDisplayedUrl.searchParams.has('p') && currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ // this is a custom fix for only the ontology viewer page, // that use the parameter ?p=section to tell which section is displayed return true diff --git a/app/views/admin/categories/_form.html.haml b/app/views/admin/categories/_form.html.haml index 75974b5a3a..fff9d1a6cf 100644 --- a/app/views/admin/categories/_form.html.haml +++ b/app/views/admin/categories/_form.html.haml @@ -34,7 +34,7 @@ %th = t('admin.categories.form.parent_category') %td.top - = categories_select(id: 'category_parent_select', name: 'category[parentCategory]', selected: @category&.parentCategory) + = categories_select(id: 'category_parent_select', name: 'category[parentCategory][]', selected: @category&.parentCategory) - unless new_record %tr %th diff --git a/app/views/annotator/index.html.haml b/app/views/annotator/index.html.haml index 8b83bab08c..8fcdaf4bcd 100644 --- a/app/views/annotator/index.html.haml +++ b/app/views/annotator/index.html.haml @@ -27,7 +27,7 @@ = render(ChipsComponent.new(name: 'exclude_synonyms', label: t('annotator.exclude_synonyms'), checked: params[:exclude_synonyms])) .select-ontologies - = ontologies_selector(id:'annotator_page_ontologies', label: 'Select ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) + = ontologies_selector(id:'annotator_page_ontologies', label: t('annotator.select_ontologies') ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) = show_advanced_options_button(text: t('show_advanced_options'), init: @advanced_options_open) = hide_advanced_options_button(text: t('hide_advanced_options'), init: @advanced_options_open) .more-advanced-options{'data-reveal-component-target': 'item', class: "#{@advanced_options_open ? '' : 'd-none'}"} diff --git a/app/views/collections/_collection.html.haml b/app/views/collections/_collection.html.haml deleted file mode 100644 index 16503de745..0000000000 --- a/app/views/collections/_collection.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -= turbo_frame_tag 'collection' do - = render ConceptDetailsComponent.new(id:'collection-label', acronym: @ontology.acronym, concept_id: collection.id, - properties: collection.properties, - top_keys: %w[created modified comment note], - bottom_keys: [], - exclude_keys: %w[member]) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t("collections.id")}, {td: link_to_with_actions(collection["@id"], acronym: @ontology.acronym)}) - - t.add_row({th: t("collections.preferred_name")}, {td: display_in_multiple_languages(get_collection_label(collection))}) - - t.add_row({th: t("collections.members_count")}) do |r| - - r.td do - = link_to collection["memberCount"], "/ontologies/" + @ontology.acronym + "/?p=classes&sub_menu=list&concept_collections=" + collection["@id"], 'data-turbo-frame':'_top' - - t.add_row({th: t("collections.type")}, {td: collection["@type"]}) diff --git a/app/views/collections/_show.html.haml b/app/views/collections/_show.html.haml new file mode 100644 index 0000000000..d79a3e47c8 --- /dev/null +++ b/app/views/collections/_show.html.haml @@ -0,0 +1,14 @@ += ontology_object_details_component(frame_id: "collection", ontology_id: @ontology.acronym, objects_title: "collections", object: @collection) do + = render ConceptDetailsComponent.new(id:'collection-label', acronym: @ontology.acronym, concept_id: @collection.id, + properties: @collection.properties, + top_keys: %w[created modified comment note], + bottom_keys: [], + exclude_keys: %w[member]) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t("collections.id")}, {td: link_to_with_actions(@collection["@id"], acronym: @ontology.acronym)}) + - t.add_row({th: t("collections.preferred_name")}, {td: display_in_multiple_languages(get_collection_label(@collection))}) + - t.add_row({th: t("collections.members_count")}) do |r| + - r.td do + = link_to @collection["memberCount"], "/ontologies/" + @ontology.acronym + "/?p=classes&sub_menu=list&concept_collections=" + @collection["@id"], 'data-turbo-frame':'_top' + - t.add_row({th: t("collections.type")}, {td: @collection["@type"]}) + diff --git a/app/views/collections/show.html.haml b/app/views/collections/show.html.haml deleted file mode 100644 index 4ef79429cf..0000000000 --- a/app/views/collections/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: 'collection', locals: {collection: @collection} \ No newline at end of file diff --git a/app/views/concepts/_show.html.haml b/app/views/concepts/_show.html.haml index a30f061fdf..8d690b65fd 100644 --- a/app/views/concepts/_show.html.haml +++ b/app/views/concepts/_show.html.haml @@ -4,63 +4,32 @@ = t('concepts.use_jump_to') - else %div{'data-controller': 'concepts-json', 'data-action': 'click->concepts-json#update'} - = render TabsContainerComponent.new(type:'outline') do |c| - - c.pinned_right do - - if $PURL_ENABLED - %div.mx-1 - = link_to("#classPermalinkModal", class: "class-permalink nav-link", title: t('concepts.permanent_link_class'), aria: {label: t('concepts.permanent_link_class')}, data: {toggle: "modal", current_purl: "#{@current_purl}"}) do - %i{class: "fas fa-link", aria: {hidden: "true"}} - %div{'data-concepts-json-target': 'button'} - .concepts_json_button - = render RoundedButtonComponent.new(link: "#{@ontology.id}/classes/#{escape(@concept.id)}?display=all&apikey=#{get_apikey}", target:'_blank') + = ontology_object_tabs_component(ontology_id: @ontology.acronym, objects_title: "classes", object_id: @concept.id) do |c| - apikey = "apikey=#{get_apikey}" - baseClassUrl = "#{@ontology.id}/classes/#{escape(@concept.id)}" - - c.item(title: t('concepts.details'), path: '#details', selected: true, json_link: "#{baseClassUrl}?#{apikey}&display=all") - - - unless skos? - - c.item(id: 'instances', path: '#instances', json_link: "#{baseClassUrl}/instances?#{apikey}") do - = t('concepts.instances') - ( - %span#concept_instances_sorted_list_count - ) - - - c.item(title: t('concepts.visualization'), path: '#visualization') - - - c.item(id: 'notes', path: '#notes', json_link: "#{baseClassUrl}/notes?#{apikey}") do - = t('concepts.notes') - %span#note_count_wrapper - ( - %span#note_count= @notes.length - ) - - c.item(id: 'mappings', path: '#mappings', json_link: "#{baseClassUrl}/mappings?#{apikey}") do - .d-flex - #{t('concepts.mappings')} - ( - = concept_mappings_loader(ontology_acronym: @ontology.acronym, concept_id: @concept.id) - ) - - - if @enable_ontolobridge - - c.item(title: t('concepts.new_term_requests'), path: '#request_term') - - - c.item_content do + - tab_item_component(container_tabs: c, title: t('concepts.details'), path: '#details', selected: true, json_link: "#{baseClassUrl}?#{apikey}&display=all") do = render :partial =>'/concepts/details' - unless skos? - - c.item_content do + - count_span = content_tag(:span, "#{t('concepts.instances')} (#{content_tag(:span, "", id: 'concept_instances_sorted_list_count')})".html_safe) + - tab_item_component(container_tabs: c, title: count_span, path: '#instances', json_link: "#{baseClassUrl}/instances?#{apikey}") do = render :partial =>'instances/instances' , locals: {id: "class-instances-data-table"} - - c.item_content do + + + - tab_item_component(container_tabs: c, title: t('concepts.visualization'), path: '#visualization') do = render :partial =>'/concepts/biomixer' - - c.item_content do + + - count_span = content_tag(:span, "#{t('concepts.notes')} (#{content_tag(:span, @notes.length, id: 'note_count')})".html_safe) + - tab_item_component(container_tabs: c, title: count_span, path: '#notes', json_link: "#{baseClassUrl}/notes?#{apikey}") do = render :partial =>'/notes/list' - - c.item_content do + + - count_span = content_tag(:span, "#{t('concepts.mappings')} (#{content_tag(:span, concept_mappings_loader(ontology_acronym: @ontology.acronym, concept_id: @concept.id))})".html_safe, class: "d-flex") + - tab_item_component(container_tabs: c, title: count_span, path: '#mappings', json_link: "#{baseClassUrl}/mappings?#{apikey}") do = render TurboFrameComponent.new(id:'concept_mappings', src:"/ajax/mappings/get_concept_table?ontologyid=#{@ontology.acronym}&conceptid=#{CGI.escape(@concept.id)}") - - if @enable_ontolobridge - - c.item_content do - = render :partial =>'/concepts/request_term' :javascript jQuery(document).ready(function(){ diff --git a/app/views/home/cookies.html.haml b/app/views/home/cookies.html.haml index d02f687a9b..513df903f7 100644 --- a/app/views/home/cookies.html.haml +++ b/app/views/home/cookies.html.haml @@ -10,4 +10,4 @@ %div = link_button_component(id:'accept-cookie-selector', value: t('cookies_modal.accept_button'), href: cookies_path(cookies: true), size: "slim", variant: 'primary') %div.cookie-privacy-link - = link_to t('cookies_modal.privacy_link'), $FOOTER_LINKS.dig(:sections, :agreements, :privacy_policy) + = link_to t('cookies_modal.privacy_link'), "#{$FOOTER_LINKS.dig(:sections, :agreements, :privacy_policy)}#{t('cookies_modal.privacy_policy_anchor')}" diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 83fc781581..8bc39eba97 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -22,17 +22,17 @@ .home-bubble.home-bubble-one %a.h5{href:"/ontologies/#{@anal_ont_names[0]}", style: "color: white !important"} = @anal_ont_names[0] - %p + %p = @anal_ont_numbers[0].to_s + " " + t('visits.visits') .home-bubble.home-bubble-two %a.h5{href:"/ontologies/#{@anal_ont_names[1]}", style: "color: white !important"} = @anal_ont_names[1] - %p + %p = @anal_ont_numbers[1].to_s + " " + t('visits.visits') .home-bubble.home-bubble-three %a.h5{href:"/ontologies/#{@anal_ont_names[2]}", style: "color: white !important"} = @anal_ont_names[2] - %p + %p = @anal_ont_numbers[2].to_s + " " + t('visits.visits') %a.home-bubble.home-bubble-four{:href => "/visits"} .h5 ... @@ -44,7 +44,7 @@ %p = t('home.index.tagline') = ontologies_content_autocomplete - + .home-body-container .home-section %h4= t('home.ontology_upload') @@ -115,19 +115,19 @@ %a{:href => "/landscape#fairness_assessment"} %div.home-fair-details %p= t('home.fair_details') - + .home-sub-section-right %h4= t('home.twitter_news') %hr.home-section-line .home-card.home-twitter-news %a.twitter-timeline{"data-height" => "360", :href => "https://twitter.com/lagroportal?ref_src=twsrc%5Etfw"} - .home-twitter-loader + .home-twitter-loader = render LoaderComponent.new(type: 'pulsing') %script{:async => "", :charset => "utf-8", :src => "https://platform.twitter.com/widgets.js"} .home-section %h4 - = t('home.agroportal_figures', site: portal_name) + = t('home.agroportal_figures', site: current_slice_name || portal_name) %hr.home-section-line/ .home-statistics-container .home-statistics @@ -135,54 +135,54 @@ %hr/ %div %h4 - = format_number_abbreviated(@ont_count) + = format_number_abbreviated(@metrics[:ontologies_count]) %p= t("home.ontologies") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@cls_count) + = format_number_abbreviated(@metrics[:class_count]) %p= t("home.classes") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@individuals_count) + = format_number_abbreviated(@metrics[:individuals_count]) %p= t("home.individuals") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@prop_count) + = format_number_abbreviated(@metrics[:properties_count]) %p= t("home.properties") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@projects_count) + = format_number_abbreviated(@metrics[:projects_count]) %p= t("home.projects") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@map_count) + = format_number_abbreviated(@metrics[:mappings_count]) %p= t("home.mappings") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@users_count) + = format_number_abbreviated(@metrics[:users_count]) %p= t("home.users") .home-statistics-link.justify-content-end{style: @analytics.empty? && "visibility: hidden"} = link_to t("home.see_details"),'/statistics', target: "_blank" - - if @slices + - if slices_enabled? .home-section .home-section-title .text = "#{portal_name} slices" %hr.home-section-line/ - .home-section-description + .home-section-description .div = t('home.slices_description') .home-slices-container @@ -194,8 +194,8 @@ .home-slice-ontologies = slice.ontologies.length = inline_svg 'icons/slices.svg', width: "70", height: "70" - - .home-slice-name + + .home-slice-name = "#{slice.name} (#{slice.acronym})" = render Buttons::RegularButtonComponent.new(id:'regular-button', value: t('home.suggest_slice'), variant: "secondary", state: "regular", href: '/feedback') do |btn| - btn.icon_right do @@ -208,17 +208,18 @@ = t('home.ontoportal_instances') %hr.home-section-line/ - .home-section-description + .home-section-description .div = home_ontoportal_description .home-support-items - $PORTALS_INSTANCES&.each do |portal| - %div.text-center - = link_to portal[:link], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do - = inline_svg 'logo-white.svg', width: "35", height: "26" - %p{style: "color: #{portal[:color]}"} - = portal[:portal] - + = portal_config_tooltip(portal[:name]) do + %div.text-center + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do + = inline_svg('logo-white.svg', width: "35", height: "26") + %p{style: "color: #{portal[:color]}"} + = portal[:name] + .home-section .home-section-title .text @@ -229,6 +230,8 @@ %a{href:logo[:url], target: "_blanc"} %img{src: asset_path(logo[:img_src])} += init_federation_portals_status + :javascript function submitAnnotator(){ document.getElementById("annotator_submit").click() @@ -236,4 +239,3 @@ function submitRecommender(){ document.getElementById("recommender_submit").click() } - diff --git a/app/views/home/portal_config.html.haml b/app/views/home/portal_config.html.haml new file mode 100644 index 0000000000..b484e9431c --- /dev/null +++ b/app/views/home/portal_config.html.haml @@ -0,0 +1,42 @@ += turbo_frame_tag "portal_config_tooltip_#{params[:portal]}" do + .portal-configuration + .home-section-description.mb-1 + .div.d-flex.align-items-center + %div + %div.text-center + = link_to @portal_config[:ui] || @config[:ui], target: '_blank', class: 'home-logo-instances mr-1 m-0', style: "background-color: #{@portal_config[:color] || @config[:color]}" do + = inline_svg 'logo-white.svg', width: "35", height: "26" + %div + %div.portal-configuration-title{style: "color: #{@portal_config[:color] || @config[:color]}"} + %h3 + = @portal_config[:title] || @config[:name] + - if @portal_config[:numberOfArtefacts] + .portal-config-ontologies + = inline_svg_tag 'icons/ontology.svg' + %span + = "#{@portal_config[:numberOfArtefacts]} ontologies" + - if @portal_config[:description] + .portal-description + = @portal_config[:description] + - if @portal_config[:federated_portals] + %div.mb-1 + .home-section-title + .text + Federated with + .d-flex.flex-wrap.my-1 + - @portal_config[:federated_portals].to_h.values.compact.each do |portal| + .portal-config-federated-with + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances-small', style: "background-color: #{portal[:color]};" do + = inline_svg 'logo-white.svg', width: "18", height: "13" + %p{style: "color: #{portal[:color]}"} + = portal[:name] + + - if @portal_config[:fundedBy] + %div.mb-1 + .home-section-title + .portal-config-title-text + = t('home.support_and_collaborations') + .home-support-items.d-flex.flex-wrap.my-1 + - @portal_config[:fundedBy]&.each do |logo| + %a.mx-2.my-1{href:logo[:url], target: "_blanc"} + %img{src: asset_path(logo[:img_src]), width: "18", height: "13"} diff --git a/app/views/home/tools.html.haml b/app/views/home/tools.html.haml index 941d60ec74..d4f0455b69 100644 --- a/app/views/home/tools.html.haml +++ b/app/views/home/tools.html.haml @@ -3,10 +3,9 @@ = link_to tool[:link], class: "mx-2 d-block", style: "width: 35%" do = render Layout::CardComponent.new do |c| - c.header do |h| - - h.text do - %div.d-flex.flex-column.align-items-center.text-primary.pt-4 - = inline_svg_tag(tool[:icon], height: "50px", width: "50px") - %h3.text-primary.mt-2 - = tool[:title] + %div.d-flex.flex-column.align-items-center.text-primary.pt-4 + = inline_svg_tag(tool[:icon], height: "50px", width: "50px") + %h3.text-primary.mt-2 + = tool[:title] %p.px-4.tool-description = tool[:description] diff --git a/app/views/instances/_details.html.haml b/app/views/instances/_details.html.haml deleted file mode 100644 index 146dcd9464..0000000000 --- a/app/views/instances/_details.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -= render TurboFrameComponent.new(id: params[:modal]&.to_s.eql?('true') ? modal_frame_id : 'instance_show') do - - if @instance && @instance["@id"] - %div - - ontology_acronym = params[:ontology_id] || @ontology.acronym - - filter_properties = ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "http://www.w3.org/2000/01/rdf-schema#label", "http://www.w3.org/2004/02/skos/core#prefLabel"] - - = render ConceptDetailsComponent.new(id:'instance-details', acronym: ontology_acronym, concept_id: @instance["@id"]) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t("instances.id")}, {td: link_to_with_actions(@instance["@id"], acronym: @ontology.acronym) }) - - - label = @instance['label'] || @instance['prefLabel'] - - unless label.blank? - - t.add_row({th: t('instances.label') }, {td: label.join(',').html_safe}) - - - types = @instance.types.reject{|x| x['NamedIndividual']} - - unless types.empty? - - t.add_row({th: t('instances.type') }) do |r| - - r.td do - = types.reject{|x| x['NamedIndividual']}.map {|cls| link_to_class(ontology_acronym,cls)}.join(', ').html_safe - - properties = @instance[:properties].to_h.select{|k,v| !filter_properties.include? k.to_s} - - properties.each do |prop| - - if !prop[1].nil? - - t.add_row({th: link_to_property(prop[0], ontology_acronym)}, {td: prop[1].map { |value| instance_property_value(value , ontology_acronym) }.join(', ').html_safe}) - - - - diff --git a/app/views/instances/_instances.html.haml b/app/views/instances/_instances.html.haml index 78b8ce0c29..9e5ea8eb33 100644 --- a/app/views/instances/_instances.html.haml +++ b/app/views/instances/_instances.html.haml @@ -9,7 +9,7 @@ - if params[:p].eql?('instances') %div#prop_contents{data: {'container-splitter-target': 'container'}} - = render partial: 'instances/details' + = render partial: 'instances/show' diff --git a/app/views/instances/_show.html.haml b/app/views/instances/_show.html.haml new file mode 100644 index 0000000000..71fbc857ca --- /dev/null +++ b/app/views/instances/_show.html.haml @@ -0,0 +1,22 @@ += ontology_object_details_component(frame_id: params['modal'].eql?('true') ? modal_frame_id : "instance_show", ontology_id: @ontology.acronym, objects_title: "instances", object: @instance) do + = render ConceptDetailsComponent.new(id: 'instance-details', acronym: @ontology.acronym, concept_id: @instance["@id"]) do |c| + - c.header(stripped: true) do |t| + - t.add_row({ th: t("instances.id") }, { td: link_to_with_actions(@instance["@id"], acronym: @ontology.acronym) }) + + - label = @instance['label'] || @instance['prefLabel'] + - t.add_row({ th: t('instances.label') }, { td: label.join(', ').html_safe }) unless label.blank? + + - types = @instance.types.reject { |x| x['NamedIndividual'] } + - unless types.empty? + - t.add_row({ th: t('instances.type') }) do |r| + - r.td do + = types.map { |cls| link_to_class(@ontology.acronym, cls) }.join(', ').html_safe + + - filter_properties = %w[http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://www.w3.org/2000/01/rdf-schema#label http://www.w3.org/2004/02/skos/core#prefLabel] + - properties = @instance[:properties].to_h.reject { |k, _| filter_properties.include?(k.to_s) } + - properties.each do |prop, values| + - if values.present? + - t.add_row({ th: link_to_property(prop, @ontology.acronym) }, { td: values.map { |value| instance_property_value(value, @ontology.acronym) }.join(', ').html_safe }) + + + diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 9d89089615..68575a0c2b 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -20,8 +20,10 @@ %h2 = t("layout.footer."+key.to_s) %div + - section_links.each do |section , link| - %a{:href => link, :target => "_blank"} + - anchor = I18n.t("layout.footer.#{section}_anchor", default: "") + %a{:href => "#{link}#{anchor}", :target => "_blank"} = t("layout.footer."+section.to_s) diff --git a/app/views/mappings/_concept_mappings.html.haml b/app/views/mappings/_concept_mappings.html.haml index c71fe9066b..2a6b56b29e 100644 --- a/app/views/mappings/_concept_mappings.html.haml +++ b/app/views/mappings/_concept_mappings.html.haml @@ -2,17 +2,16 @@ = "#{@mappings.size}" = turbo_frame_tag @type.eql?('modal') ? 'application_modal_content' : 'concept_mappings' do - %div{:style => "padding: 1%; width: 98%"} + %div.p-1 - if session[:user].nil? = link_to "Create New Mapping", "/login?redirect=/ontologies/#{@ontology.acronym}/?p=classes&t=mappings&conceptid=#{escape(@concept.id)}", :method => :get, :class => "btn btn-default mb-3" - else - = link_to_modal("Create New Mapping", - new_mapping_path(ontology_from: "#{@ontology.id}", conceptid_from: "#{@concept.id}"), - id: "new_mapping_btn", - role: "button", - class: "btn btn-default mb-3", - data: { show_modal_title_value: "Create a new mapping for #{@concept.prefLabel}" }, - ) + %div{style: 'width: 250px; margin: 0 0 10px 0;'} + = link_to_modal(nil, + new_mapping_path(ontology_from: "#{@ontology.id}", conceptid_from: "#{@concept.id}"), + data: { show_modal_title_value: "Create a new mapping for #{@concept.prefLabel}" }, + ) do + = render Buttons::RegularButtonComponent.new(id:'new_mapping_btn' , value:t('mappings.create_new_mapping'), variant: 'secondary', size: 'slim', state: 'no-anim') #mapping_details - = render :partial => '/mappings/mapping_table' \ No newline at end of file + = render :partial => '/mappings/mapping_table' diff --git a/app/views/mappings/_mapping_table.html.haml b/app/views/mappings/_mapping_table.html.haml index 1d91b3b5bb..20efcd92d5 100644 --- a/app/views/mappings/_mapping_table.html.haml +++ b/app/views/mappings/_mapping_table.html.haml @@ -1,16 +1,14 @@ = check_box_tag "delete_mappings_permission", @delete_mapping_permission, @delete_mapping_permission, style: "display: none;" %div#concept_mappings_tables_div = render_alerts_container(MappingsController) - %table#concept_mappings_table.table-content-stripped.table-content{width: "100%", style:'word-break: break-word'} + %table#concept_mappings_table.table-content-stripped.table-content.table-mini %thead %tr %th= t("mappings.mapping_table.mapping_to") - %th{width: "30%"}= t("mappings.count.ontology") - %th= t("mappings.mapping_table.relations") - %th= t("mappings.mapping_table.source") + %th= t("mappings.count.ontology") %th= t("mappings.mapping_table.type") - - if current_user_admin? - %th{:class => 'delete_mappings_column'}= t("mappings.mapping_table.actions") + - if current_user_admin? && !@mappings.all? { |obj| !obj.source.eql?('REST') } + %th{:class => 'delete_mappings_column'} %tbody#concept_mappings_table_content - @mappings.each do |map| = render partial: 'mappings/show_line' , locals: {map: map, concept: @concept} @@ -19,4 +17,4 @@ :javascript jQuery(document).ready(function(){ ajax_process_init(); - }) \ No newline at end of file + }) diff --git a/app/views/mappings/_show_line.html.haml b/app/views/mappings/_show_line.html.haml index bbfab2b78e..4c94555c4d 100644 --- a/app/views/mappings/_show_line.html.haml +++ b/app/views/mappings/_show_line.html.haml @@ -1,56 +1,23 @@ -- process = map.process || {} -- source = "#{map.source} #{process[:source_name]}" -- relations = process[:relation]&.each { |relation| get_prefixed_uri(relation)} +- type, type_tooltip = mapping_type_tooltip(map) +- cls_link, ont_link, source_tooltip = mapping_links(map, concept) - map_id = map.id.to_s.split("/").last -- target_concept = map.classes.select {|target_concept| target_concept.id != concept.id && target_concept.links['ontology'] != concept.links['ontology']}.first || map.classes.last -- if inter_portal_mapping?(target_concept) - - cls_link = ajax_to_inter_portal_cls(target_concept) - - ont_name = target_concept.links['ontology'] - - ont_acronym = ont_name - - ont_link = link_to ont_acronym , get_inter_portal_ui_link(target_concept.links['ontology'], process["name"]), target: '_blank' - - type = 'Inter-portal' -- elsif internal_mapping?(target_concept) - - begin - - ont = target_concept.explore.ontology - - ont_name = ont.acronym - - ont_link = link_to ont_name, ontology_path(ont_name), 'data-turbo-frame':'_top' - - rescue - - ont_name = target_concept.links['ontology'] || target_concept.id - - ont_link = ont_name - - cls_link = raw(get_link_for_cls_ajax(target_concept.id, ont_name, '_top')) - - type = 'Internal' -- else - - cls_label = get_label_for_external_cls(target_concept.links["self"]) - - cls_link = raw("#{cls_label}") - - ont_name = target_concept.links['ontology'] - - ont_link = link_to ont_name, target_concept.links['ontology'], target: "_blank" - - type = 'External' - %tr.human{:id => map_id} - %td + %td.mappings-table-mapping-to = cls_link - %td + %td.mappings-table-mapping-to = ont_link %td - - relations&.each do |r| - = r - %br/ - %td - #{source} - - if !process.nil? - - if translation?(process["relation"]) - %img{:src => asset_path('sifr/english_language_flag.png'), :style => "padding: 5px", :align => "right", :title => "Traduction"} - %td - = type + = render ChipButtonComponent.new(class: 'chip_button_small mr-1', text: type, tooltip:"#{source_tooltip} mapping of type #{type_tooltip}") + - if current_user_admin? %td{:class => 'delete_mappings_column'} - if map.id && !map.id.empty? && session[:user] && (session[:user].id.to_i == map.creator || session[:user].admin?) && map.source.eql?('REST') - %div.d-flex - = link_to_modal(t("mappings.show_line.edit_modal"), + %div.d-flex.mappings-table-actions + = link_to_modal(nil, mapping_path(map_id, {conceptid_from: @concept.id}), - role: "button", - class: "btn btn-link", + class: 'btn btn-link p-0 mr-1', data: { show_modal_title_value: t("mappings.show_line.edit_mapping", preflabel: @concept.prefLabel)}, - ) - = button_to t("mappings.show_line.delete_button"), CGI.unescape(mapping_path(map.id)), method: :delete, class:'btn btn-link', form: {data: { turbo: true, turbo_confirm: t("mappings.show_line.turbo_confirm"), turbo_frame: '_top'}} \ No newline at end of file + ) do + = inline_svg_tag "edit.svg", width: '15px', height: '15px' + = button_to inline_svg_tag('icons/delete.svg', width: '16px', heigth: '16px'), CGI.unescape(mapping_path(map.id)), class: 'btn btn-link p-0', method: :delete, form: {data: { turbo: true, turbo_confirm: t("mappings.show_line.turbo_confirm"), turbo_frame: '_top'}} diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index bd3aed0cf5..b9ba32c32e 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -4,14 +4,24 @@ current_page: @page.page, next_page: @page.nextPage) do |c| - if @page.page.eql?(1) - = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") { "#{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies)} (#{sprintf("%.2f", @time)}s)" } + = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") do + = t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names(@federation_counts, @time)).html_safe + + = alert_message_if_federation_error(@errors) do + - @errors.each do |e| + %div + = e.errors || e - ontologies = c.collection - ontologies.each do |ontology| - = render OntologyBrowseCardComponent.new(ontology: ontology) + - config = ontology_portal_config(ontology[:id])&.last || {} + + = render OntologyBrowseCardComponent.new(ontology: ontology, + portal_name: config[:name], + onto_link: ontoportal_ui_link(ontology[:id]), + text_color: config[:color], + bg_light_color: config[:'light-color']) - c.loader do - ontologies_browse_skeleton - c.error do - .browse-empty-illustration - %img{:src => "#{asset_path("empty-box.svg")}"} - %p No result was found \ No newline at end of file + = empty_state diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index d1979fd359..d6632fbc27 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -1,7 +1,7 @@ .browse-center .browse-container .container.align-alert - - if session[:user]&.admin? + - if current_user_admin? %div{style:'width: 70%;'} = render Display::AlertComponent.new(type: 'info') do %span.d-flex.align-items-center @@ -33,44 +33,33 @@ %div{data: { controller: "turbo-frame history browse-filters" , "turbo-frame-url-value": "/ontologies_filter?page=1&#{request.original_url.split('?').last}", action: "change->browse-filters#dispatchFilterEvent changed->history#updateURL changed->turbo-frame#updateFrame"}} .browse-sub-container - .browse-first-row{data:{controller: "browse-filters", action: "change->browse-filters#dispatchFilterEvent changed->history#updateURL"}} + .browse-first-row %div.pt-1 = upload_ontology_button %div{style:'margin-top: 30px'} %p.browse-filters-title= t("ontologies.filters") - - if session[:user]&.admin? + - if current_user_admin? %div.browse-filter.admin-border - = render SwitchInputComponent.new(id:'filter-private', name:'private_only', checked: @show_private_only) do - = t("ontologies.browser.show_private_ontology") + = switch_input(id:'filter-private', name:'private_only', checked: @show_private_only, label: t("ontologies.browser.show_private_ontology")) %div.browse-filter - = render SwitchInputComponent.new(id:'filter-views', name:'views', checked: @show_views) do - = t("ontologies.browser.show_ontology_views") - = render SwitchInputComponent.new(id:'filter-retired', name:'retired',checked: @show_retired) do - = t("ontologies.browser.show_retired_ontologies") + = switch_input(id:'filter-views', name:'views', checked: @show_views, label: t("ontologies.browser.show_ontology_views")) + = switch_input(id:'filter-retired', name:'retired',checked: @show_retired , label: t("ontologies.browser.show_retired_ontologies")) - @filters.each do |key, values| - - if session[:user]&.admin? || key != :missingStatus - .browse-filter{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "#{key}_filter_container", style: "#{"border-color: var(--admin-color);" if key == :missingStatus}"} - .browse-filter-title-bar{"data-target" => "#browse-#{key}-filter", "data-toggle" => "collapse"} - %p - = browse_filter_section_label(key) - %span.badge.badge-primary{"data-show-filter-count-target":"countSpan", style: "#{values[2] && values[2].positive? ? '' : 'display: none;'}"} - = values[2] - .d-flex.align-items-center - - if key.eql?(:categories) || key.eql?(:groups) - .mr-2 - = render Display::InfoTooltipComponent.new(text: browse_taxonomy_tooltip(key.to_s)) - = inline_svg_tag 'arrow-down.svg' - .collapse{id: "browse-#{key}-filter", class: "#{values[2].positive? ? 'show': ''}"} - .browse-filter-checks-container - - values.first.each do |object| - - title = (key.eql?(:categories) || key.eql?(:groups)) ? nil : '' - = group_chip_component(name: key, object: object, checked: values[1]&.include?(object["id"]) || values[1]&.include?(object["value"]) , title: title) do |c| - - c.count do - %span.badge.badge-light.ml-1 - = turbo_frame_tag "count_#{key}_#{object["id"]}", busy: true - %span.show-if-loading - = render LoaderComponent.new(small:true) + %div{ id: "#{key}_filter_container", data:{controller: "browse_filters show-filter-count", + action: "change->show-filter-count#updateCount + change->browse-filters#dispatchFilterEvent"}} + - objects, checked_values, count = values + = dropdown_component(id: "browse-#{key}-filter", is_open: count.positive?) do |d| + - d.title { browse_filter_section_header(key: key, count: count)} + = browse_filter_section_body(key: key, checked_values: checked_values, objects: objects) + + - if federation_enabled? + %div{ data:{action: "change->browse-filters#federationChange"}} + = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| + - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar @@ -79,11 +68,10 @@ .browse-search-filters %select#format.browse-format-filter{:name => "format"} = options_for_select(@formats, @selected_format) - %select#Sort_by.browse-sort-by-filter{:name => "Sort_by"} + %select#Sort_by.browse-sort-by-filter{name: "Sort_by", 'data-browse-filters-target': "sort"} = options_for_select(@sorts_options, @sort_by) .browse-ontologies = render TurboFrameComponent.new(id: "ontologies_list_view-page-1" , src: "/ontologies_filter?page=1&#{request.original_url.split('?').last}", data:{"turbo-frame-target":"frame", "turbo-frame-url-value": "/ontologies_filter"}) do |list| - list.loader do = browser_counter_loader - ontologies_browse_skeleton - diff --git a/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml b/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml index 2aeafa23df..9bd25a770a 100644 --- a/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml +++ b/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml @@ -2,7 +2,7 @@ .ontologies-selector-results .horizontal-line .results-number - = t("ontologies.showing_ontologies_size", ontologies_size: @ontologies.length, analytics_size: @total_ontologies_number) + = t("ontologies.showing_ontologies_size", ontologies_size: @ontologies.length, analytics_size: @total_ontologies_number, portals: portal_name) %span.select-all{'data-action': 'click->ontologies-selector#selectall'} = t('ontologies_selector.select_all') .ontologies diff --git a/app/views/ontologies/sections/_collections.html.haml b/app/views/ontologies/sections/_collections.html.haml index 280c647603..9463602928 100644 --- a/app/views/ontologies/sections/_collections.html.haml +++ b/app/views/ontologies/sections/_collections.html.haml @@ -1,17 +1,15 @@ = render TurboFrameComponent.new(id: "collections", data: {"turbo-frame-target": "frame"} ) do - - if no_collections? - = no_collections_alert - - else - %div.ont-collections{data:{controller: 'container-splitter'}} - %div#collectionsTree.card.sidebar{data:{'container-splitter-target': 'container'}} + %div.ont-collections{data:{controller: 'container-splitter'}} + %div#collectionsTree.card.sidebar{data:{'container-splitter-target': 'container'}} + - if no_collections? + = no_collections_alert + - else = tree_container_component(id: "collections_sorted_list_view-page-1", placeholder: t('ontologies.sections.collections_search_placeholder', acronym: @ontology.acronym), frame_url: "/ontologies/#{@ontology.acronym}/collections", tree_url: "/ontologies/#{@ontology.acronym}/collections?#{request.original_url.split('?')[1]}") - - %div#collection_contents{data:{'container-splitter-target': 'container'}} - = render TurboFrameComponent.new(id: 'collection') do - - if @collection - = render partial: 'collections/collection', locals: {collection: @collection} + %div#collection_contents{data:{'container-splitter-target': 'container'}} + = render TurboFrameComponent.new(id: 'collection') do + = render partial: 'collections/show' diff --git a/app/views/ontologies/sections/_metadata.html.haml b/app/views/ontologies/sections/_metadata.html.haml index 69aadbc570..be2083e5cb 100755 --- a/app/views/ontologies/sections/_metadata.html.haml +++ b/app/views/ontologies/sections/_metadata.html.haml @@ -22,7 +22,7 @@ = properties_dropdown('person_and_organization',t("ontologies.sections.person_and_organization"),'', @agents_properties) do |values| = horizontal_list_container(values) do |v| = agent_chip_component(v) - + = properties_dropdown('link',t("ontologies.sections.other_links"), t("ontologies.sections.info_tooltip_links") , @links_properties) do |values| = horizontal_list_container(values) do |v| = render LinkFieldComponent.new(value: v, raw: true) @@ -51,11 +51,10 @@ = Array(@content_properties['metadataVoc']).map{|x| metadata_vocabulary_display(x)}.join.html_safe = render Layout::CardComponent.new do |c| - c.header do |h| - - h.text do - = t("ontologies.sections.visits") - - if visits_data(@ontology) - = link_to(@ontology.links["analytics"] + "?apikey=#{get_apikey}&format=csv", title: t("ontologies.sections.download_as_csv")) do - = inline_svg("summary/download.svg", width: '30px', height: '20px') + = t("ontologies.sections.visits") + - if visits_data(@ontology) + = link_to(@ontology.links["analytics"] + "?apikey=#{get_apikey}&format=csv", title: t("ontologies.sections.download_as_csv")) do + = inline_svg("summary/download.svg", width: '30px', height: '20px') = render Layout::ListComponent.new do |l| - l.row do @@ -64,9 +63,8 @@ - unless @ontology.view? = render Layout::CardComponent.new do |d| - d.header do |h| - - h.text do - = t("ontologies.sections.views", acronym: @ontology.acronym) - = new_element_link(t("ontologies.sections.create_new_view"), new_view_path(@ontology.id)) + = t("ontologies.sections.views", acronym: @ontology.acronym) + = new_element_link(t("ontologies.sections.create_new_view"), new_view_path(@ontology.id)) = render Layout::ListComponent.new do |l| - l.row do = render partial: 'ontology_views' diff --git a/app/views/ontologies/sections/_schemes.html.haml b/app/views/ontologies/sections/_schemes.html.haml index aa01c54aa4..fc91ee21e7 100644 --- a/app/views/ontologies/sections/_schemes.html.haml +++ b/app/views/ontologies/sections/_schemes.html.haml @@ -11,7 +11,7 @@ %div#scheme_contents{data:{'container-splitter-target': 'container'}} = render TurboFrameComponent.new(id:'scheme') do - = render partial: 'schemes/scheme', locals: {scheme: @scheme} + = render partial: 'schemes/show' diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index cd3eea4235..55bdb61edc 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -53,5 +53,5 @@ %div#ontology_picker_project{style: "padding-top: 2em;"} - selected_ontologies = @project.ontologyUsed && @project.ontologyUsed.map {|id| id.split('/').last } || [] - locals = { sel_text: t('projects.form.select_ontologies'), selected_ontologies: selected_ontologies, form_object: :project, form_attribute: "ontologyUsed" } - = ontologies_selector(id:'projects_page_ontologies_selector' ,name: 'ontologies') + = ontologies_selector(id:'projects_page_ontologies_selector' ,name: 'project[ontologyUsed][]', selected: @usedOntologies) diff --git a/app/views/properties/_show.html.haml b/app/views/properties/_show.html.haml index f74964f85e..d6ea4ce531 100644 --- a/app/views/properties/_show.html.haml +++ b/app/views/properties/_show.html.haml @@ -1,18 +1,13 @@ -= render TurboFrameComponent.new(id: 'property_show', data: {"turbo-frame-target": "frame"}) do - - if @property - - if @property.errors - = render Display::AlertComponent.new(type:'info', message: @property.errors.join) - - else - - properties = LinkedData::Client::Models::Property.properties_to_hash(@property).first - = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, - properties: @property.properties, - top_keys: [], - bottom_keys: properties.keys.map(&:to_s), - exclude_keys: []) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) - - t.add_row({th: t('properties.type')}, {td: @property.type }) - - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? - - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? - - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? - - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? += ontology_object_details_component(frame_id: "property_show", ontology_id: @acronym, objects_title: "properties", object: @property) do + = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, + properties: @property.properties, + top_keys: [], + bottom_keys: LinkedData::Client::Models::Property.properties_to_hash(@property).first.keys.map(&:to_s), + exclude_keys: []) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) + - t.add_row({th: t('properties.type')}, {td: @property.type }) + - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? + - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? + - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? + - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? \ No newline at end of file diff --git a/app/views/recommender/index.html.haml b/app/views/recommender/index.html.haml index 5ac33ba53c..57df61d9db 100644 --- a/app/views/recommender/index.html.haml +++ b/app/views/recommender/index.html.haml @@ -78,7 +78,7 @@ - btn.icon_left do = inline_svg_tag "edit.svg" - if @results && @results.empty? - = empty_state(t('no_result_was_found')) + = empty_state - unless @results.nil? || @results.empty? .recommender-page-results .title @@ -122,5 +122,3 @@ = render Buttons::RegularButtonComponent.new(id:'recommender_go_annotator', value: t('recommender.call_annotator'), variant: "secondary", href: "/annotator?text=#{params[:input]}&ontologies=#{params[:ontologies]}", size: "slim", target: '_blank', state: "regular") do |btn| - btn.icon_right do = inline_svg_tag "arrow-right-outlined.svg" - - diff --git a/app/views/schemes/_scheme.html.haml b/app/views/schemes/_scheme.html.haml deleted file mode 100644 index 5cfac8c316..0000000000 --- a/app/views/schemes/_scheme.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -= turbo_frame_tag 'scheme' do - - if @scheme && !@scheme.empty? - = render ConceptDetailsComponent.new(id:'scheme-label', acronym: @ontology.acronym, concept_id: @scheme.id, - properties: scheme.properties, - top_keys: %w[description comment], - bottom_keys: %w[disjoint subclass is_a has_part], - exclude_keys: []) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t('schemes.id')} , {td: link_to_with_actions(scheme["@id"], acronym: @ontology.acronym)}) - - t.add_row({th: t('schemes.preferred_name')} , {td: display_in_multiple_languages(get_scheme_label(scheme))}) - - t.add_row({th: t('schemes.type')} , {td: scheme["@type"]}) - - diff --git a/app/views/schemes/_show.html.haml b/app/views/schemes/_show.html.haml new file mode 100644 index 0000000000..b0af055f4b --- /dev/null +++ b/app/views/schemes/_show.html.haml @@ -0,0 +1,11 @@ += ontology_object_details_component(frame_id: "scheme", ontology_id: @ontology.acronym, objects_title: "schemes", object: @scheme) do + = render ConceptDetailsComponent.new(id:'scheme-label', acronym: @ontology.acronym, concept_id: @scheme.id, + properties: @scheme.properties, + top_keys: %w[description comment], + bottom_keys: %w[disjoint subclass is_a has_part], + exclude_keys: []) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t('schemes.id')} , {td: link_to_with_actions(@scheme["@id"], acronym: @ontology.acronym)}) + - t.add_row({th: t('schemes.preferred_name')} , {td: display_in_multiple_languages(get_scheme_label(@scheme))}) + - t.add_row({th: t('schemes.type')} , {td: @scheme["@type"]}) + diff --git a/app/views/schemes/show.html.haml b/app/views/schemes/show.html.haml deleted file mode 100644 index 79eb63ac8f..0000000000 --- a/app/views/schemes/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: 'scheme', locals: {scheme: @scheme} \ No newline at end of file diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index 28ecbf2eed..a87850f86e 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -14,8 +14,13 @@ .title = t("search.advanced_options.ontologies") .field - = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - + = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(','), ontologies: onts_for_select(include_views: true)) + - if federation_enabled? + .filter-container + .title + = t('federation.results_from_external_portals') + .field.d-flex + = federation_input_chips(name: "portals[]") .right .filter-container .title @@ -35,38 +40,37 @@ .search-page-options{class: @search_results.empty? ? 'justify-content-end': ''} - unless @search_results.empty? .search-page-number-of-results - = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')}" + = t('search.match_in', results_size: @search_results.length, portals: request_portals_names(@federation_counts, @time)).html_safe %div.d-flex - .search-page-json.mx-4.mt-1 - = search_json_link + .search-page-advanced-button.mr-4{class: @search_results.blank? ? 'd-none' : ''} + %a.d-flex{href: @json_url, target: '_blank'} + = text_with_icon(text: "API JSON", icon: 'json.svg') + .search-page-advanced-button.show-options{class: "#{@advanced_options_open ? 'd-none' : ''}",'data': {'action': 'click->reveal-component#show', 'reveal-component-target': 'showButton'}} - .icon - =inline_svg_tag 'icons/settings.svg' - .text - = t('search.show_advanced_options') + = text_with_icon(text: t('search.show_advanced_options'), icon: 'icons/settings.svg') + .search-page-advanced-button.hide-options{class: "#{@advanced_options_open ? '' : 'd-none'}", 'data': {'action': 'click->reveal-component#hide', 'reveal-component-target': 'hideButton'}} - .icon - =inline_svg_tag 'icons/hide.svg' - .text - = t('search.hide_advanced_options') - - if @search_results - .search-page-results-container - - number = 0 - - @search_results.each do |result| - .search-page-result-element - - number = number + 1 - - descendants = result[:descendants] - - reuses = result[:reuses] - - result[:root][:number] = number - = render Display::SearchResultComponent.new(result[:root]) do |c| - - descendants.each { |d| c.subresult(d.merge(is_sub_component: true))} - - reuses.each do |r| - - number = number + 1 - - c.reuse(r[:root].merge(is_sub_component: true, number: number)) do |b| - - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} + = text_with_icon(text: t('search.hide_advanced_options'), icon: 'icons/hide.svg') + + + + + = alert_message_if_federation_error(@federation_errors) {@federation_errors} + + .search-page-results-container + - number = 0 + - @search_results.each do |result| + .search-page-result-element + - number = number + 1 + - descendants = result[:descendants] + - reuses = result[:reuses] + - result[:root][:number] = number + = render Display::SearchResultComponent.new(result[:root]) do |c| + - descendants.each { |d| c.subresult(d.merge(is_sub_component: true))} + - reuses.each do |r| + - number = number + 1 + - c.reuse(r[:root].merge(is_sub_component: true, number: number)) do |b| + - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} - if @search_results.empty? && !@search_query.empty? - .browse-empty-illustration - %img{:src => "#{asset_path("empty-box.svg")}"} - %p No result was found - \ No newline at end of file + = empty_state diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 277b972015..90dad17f3c 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -1,3 +1,6 @@ +$DEBUG_API_CLIENT = false +$API_CLIENT_INVALIDATE_CACHE = false + # Organization info $ORG = ENV['ORG'] $ORG_URL = ENV['ORG_URL'] @@ -37,9 +40,6 @@ $NCBO_API_KEY = ENV['NCBO_API_KEY'] $FAIRNESS_DISABLED = ENV['FAIRNESS_DISABLED'] $FAIRNESS_URL = ENV['FAIRNESS_URL'] - - - # Used to define other bioportal that can be mapped to # Example to map to ncbo bioportal : {"ncbo" => {"api" => "http://data.bioontology.org", "ui" => "http://bioportal.bioontology.org", "apikey" => ""} # Then create the mapping using the following class in JSON : "http://purl.bioontology.org/ontology/MESH/C585345": "ncbo:MESH" @@ -213,51 +213,81 @@ $HOME_PAGE_LOGOS = [ $PORTALS_INSTANCES = [ { - color: '#31b403', - portal: 'AgroPortal', - link: 'https://agroportal.lirmm.fr/' + name: 'AgroPortal', + api: 'https://data.agroportal.lirmm.fr', + ui: 'https://agroportal.lirmm.fr/', + color: '#3CB371', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + 'light-color': '#F1F6FA', }, { + name: 'BioPortal', + ui: 'https://bioportal.bioontology.org/', + api: 'https://data.bioontology.org/', + apikey: '8b5b7825-538d-40e0-9e9e-5ab9274a9aeb', color: '#234979', - portal: 'BioPortal', - link: 'https://bioportal.bioontology.org/' + 'light-color': '#E9F2FA', }, { + name: 'SIFR BioPortal', + ui: 'https://bioportal.lirmm.fr/', + api: 'https://data.bioportal.lirmm.fr/', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', color: '#74a9cb', - portal: 'SIFR BioPortal', - link: 'https://bioportal.lirmm.fr/' + 'light-color': '#E9F2FA', }, { - color: '#0d508a', - portal: 'EcoPortal', - link: 'https://ecoportal.lifewatch.eu/' + name: 'EcoPortal', + ui: 'https://ecoportal.lifewatch.eu/', + api: 'https://data.ecoportal.lifewatch.eu/', + apikey: "43a437ba-a437-4bf0-affd-ab520e584719", + color: '#2076C9', + 'light-color': '#E9F2FA', }, { + name: 'MedPortal', + ui: 'http://medportal.bmicc.cn/', color: '#234979', - portal: 'MedPortal', - link: 'http://medportal.bmicc.cn/' }, { + name: 'MatPortal', + ui: 'https://matportal.org/', color: '#009574', - portal: 'MatPortal', - link: 'https://matportal.org/' }, { + name: 'IndustryPortal', + ui: 'http://industryportal.enit.fr', + api: 'https://data.industryportal.enit.fr/', + apikey: '019adb70-1d64-41b7-8f6e-8f7e5eb54942', color: '#1c0f5d', - portal: 'IndustryPortal', - link: 'http://industryportal.enit.fr' + 'light-color': '#F0F5F6', }, { - color: '#1e2251', - portal: 'EarthPortal', - link: 'https://earthportal.eu/' + name: 'EarthPortal', + ui: 'https://earthportal.eu/', + api: 'https://data.earthportal.eu/', + apikey: "c9147279-954f-41bd-b068-da9b0c441288", + color: '#404696', + 'light-color': '#F0F5F6' }, { - color: '#33691B', - portal: 'BiodivPortal', - link: 'https://biodivportal.gfbio.org/' + name: 'TestPortal', + ui: 'https://testportal.lirmm.fr/', + api: 'https://data.testportal.lirmm.fr/', + color: '#74a9cb', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + }, + { + name: 'BiodivPortal', + ui: 'https://biodivportal.gfbio.org/', + api: 'https://data.biodivportal.gfbio.org/', + apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", + color: '#349696', + 'light-color': '#EBF5F5', } ] + + $ONTOPORTAL_WEBSITE_LINK = "https://ontoportal.org/" $ONTOPORTAL_GITHUB_REPO = "https://github.com/ontoportal" @@ -287,8 +317,8 @@ $FOOTER_LINKS = { }, agreements: { terms: $TERMS_AND_CONDITIONS_LINK, - privacy_policy: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq#h-privacy-policy", - legal_notices: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq#h-legal-notice" + privacy_policy: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq", + legal_notices: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq" }, about: { about_us: "https://github.com/agroportal/project-management", diff --git a/config/initializers/ontologies_api_client.rb b/config/initializers/ontologies_api_client.rb index ca8f07e9eb..068da53829 100644 --- a/config/initializers/ontologies_api_client.rb +++ b/config/initializers/ontologies_api_client.rb @@ -7,4 +7,5 @@ config.debug_client = $DEBUG_RUBY_CLIENT || false config.debug_client_keys = $DEBUG_RUBY_CLIENT_KEYS || [] config.apikey = $API_KEY + config.federated_portals = $PORTALS_INSTANCES ? $PORTALS_INSTANCES.map{|x| x[:api] && x[:apikey] ? [x[:name].downcase.to_sym, x] : nil }.compact.to_h : {} end diff --git a/config/locales/en.yml b/config/locales/en.yml index fd04ba863c..a63e3500eb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -303,6 +303,7 @@ en: and fungi, in an attempt to replace existing methods of chemical control and avoid extensive use of fungicides, which often lead to resistance in plant pathogens. In agriculture, plant growth-promoting and biocontrol microorganisms have emerged as safe alternatives to chemical pesticides. Streptomyces spp. and their metabolites may have great potential as excellent agents for controlling various fungal and bacterial phytopathogens. + select_ontologies: Select ontologies concepts: error_valid_concept: "Error: You must provide a valid concept id" missing_roots: Missing roots @@ -518,8 +519,11 @@ en: issues_and_requests: Issues and Requests agreements: Legal terms: Terms and Conditions + terms_anchor: "#h-agroportal-terms-and-conditions-english" privacy_policy: Privacy Policy + privacy_policy_anchor: "#h-privacy-policy" legal_notices: Legal Notices + legal_notices_anchor: "#h-legal-notice" cite_us: Cite Us acknowledgments: Acknowledgments about: About @@ -568,12 +572,19 @@ en: date: Date ontology_visits: Ontology visits mappings: + create_new_mapping: Create new mapping all: All description: Dive into an overview of the mappings in the bubble view, efficiently locate a specific ontology in the table view or upload your own mappings. tabs: bubble_view: Bubbles view table_view: Table view upload_mappings: Upload mappings + types_description: + cui: Created between 2 concepts that have the same CUI (Concept Unique Identifiers) + loom: Lexical mappings created between 2 concepts with very similar labels (preferred name) + rest: A mapping added by a user using the REST API (or the UI, which is calling the API to create it) + same_uri: Created between 2 concepts with the same URI. + skos: Mappings based on SKOS relationships, (e.g. skos:exactMatch or skos:closeMatch) filter_ontologies: Filter ontologies in the bubble view filter_bubbles: Filter bubbles external_mappings: "External Mappings (%{number_with_delimiter})" @@ -917,8 +928,7 @@ en: classes_with_definitions: Classes with definitions show_advanced_options: Show options hide_advanced_options: Hide options - match_in: Match in - ontologies: ontologies + match_in: Match in %{results_size} ontologies from %{portals} result_component: details: Details visualize: Vizualize @@ -1162,7 +1172,7 @@ en: update_the_current_displayed_content: "will update the current displayed content to all the following submissions:" save: Save ontologies: - showing_ontologies_size: "Showing %{ontologies_size} of %{analytics_size}" + showing_ontologies_size: "Showing %{ontologies_size} of %{analytics_size} from %{portals}" filters: Filters no_license: No license view_license: View license @@ -1493,7 +1503,13 @@ en: paragraph2: "The cookies are functional and non-optional. By staying on %{portal}, you acknowledge the information was delivered to you." accept_button: "Accept" privacy_link: "Privacy policy" + privacy_policy_anchor: "#h-privacy-policy" taxonomy: groups_and_categories: Groups and Categories description: In AgroPortal, ontologies are organized in groups and tagged with categories. Typically, groups associate ontologies from the same project or organization for better identification of the provenance. Whereas categories are about subjects/topics and enable to classify ontologies. As of 2016, AgroPortal's categories were established in cooperation with FAO AIMS. In 2024, we moved to UNESCO nomenclature for fields of science and technology. Groups and categories, along with other metadata, can be used on the “Browse” page of AgroPortal to filter out the list of ontologies. - show_sub_categories: Show sub categories \ No newline at end of file + show_sub_categories: Show sub categories + + federation: + results_from_external_portals: Results from external portals + not_responding: is not responding. + check_status: Checking %{portal} availability diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f537fc23ad..d0969e5795 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -305,7 +305,7 @@ fr: et aériennes, dans le but de remplacer les méthodes actuelles de contrôle chimique et d'éviter l'utilisation extensive de fongicides, qui conduisent souvent à une résistance chez les pathogènes des plantes. En agriculture, les microorganismes promoteurs de croissance des plantes et de biocontrôle ont émergé comme des alternatives sûres aux pesticides chimiques. Les espèces de Streptomyces et leurs métabolites peuvent avoir un grand potentiel en tant qu'agents excellents pour contrôler divers phytopathogènes fongiques et bactériens. - + select_ontologies: Sélectionner des ontologies concepts: error_valid_concept: "Erreur : Vous devez fournir un identifiant de concept valide" missing_roots: Racines manquantes @@ -524,8 +524,11 @@ fr: issues_and_requests: Problèmes et demandes agreements: Légal terms: Termes et conditions + terms_anchor: "#h-conditions-generales-dutilisation-dagroportal-francais" privacy_policy: Politique de confidentialité + privacy_policy_anchor: "#h-vie-privee" legal_notices: Mentions légales + legal_notices_anchor: "#h-mentions-legales" cite_us: Citez-nous acknowledgments: Remerciements about: À propos @@ -577,12 +580,20 @@ fr: ontology_visits: Visites d'ontologies mappings: + create_new_mapping: Créer un nouveau mapping all: Tous description: Plongez dans une vue d'ensemble des mappings dans la vue en bulles, localisez efficacement une ontologie spécifique dans la vue en tableau ou téléchargez vos propres mappings. tabs: bubble_view: Vue en bulles table_view: Vue en tableau upload_mappings: Télécharger des mappings + types_description: + cui: Créé entre 2 concepts ayant le même CUI (Identifiants Uniques de Concepts) + loom: Mappings lexicaux créés entre 2 concepts avec des libellés très similaires (nom préféré) + rest: Un mapping ajouté par un utilisateur via l'API REST (ou l'interface utilisateur, qui appelle l'API pour le créer) + same_uri: Créé entre 2 concepts ayant la même URI. + skos: Mapping basés sur des relations SKOS (par exemple, skos:exactMatch ou skos:closeMatch) + filter_ontologies: Filtrer les ontologies dans la vue en bulles filter_bubbles: Filtrer les bulles external_mappings: "Mappings externes (%{number_with_delimiter})" @@ -934,8 +945,7 @@ fr: classes_with_definitions: Classes avec définitions show_advanced_options: Afficher les options hide_advanced_options: Cacher les options - match_in: Correspondance dans - ontologies: ontologies + match_in: Correspondance dans %{results_size} ontologies de %{portals} result_component: details: Détails visualize: Visualiser @@ -1188,7 +1198,7 @@ fr: save: Sauvegarder ontologies: - showing_ontologies_size: "Affichage de %{ontologies_size} sur %{analytics_size}" + showing_ontologies_size: "Affichage de %{ontologies_size} sur %{analytics_size} de %{portals}" filters: Filtres no_license: Pas de licence view_license: Voir la licence @@ -1532,7 +1542,12 @@ fr: paragraph2: "Les cookies sont fonctionnels et non facultatifs. En restant sur %{portal}, vous reconnaissez que les informations vous ont été transmises." accept_button: "Accepter" privacy_link: "Vie privé" + privacy_policy_anchor: "#h-vie-privee" taxonomy: groups_and_categories: Groupes et Catégories description: Dans AgroPortal, les ontologies sont organisées en groupes et étiquetées avec des catégories. Typiquement, les groupes associent des ontologies provenant du même projet ou de la même organisation pour une meilleure identification de la provenance. Tandis que les catégories concernent des sujets/thématiques et permettent de classifier les ontologies. En 2016, les catégories d'AgroPortal ont été établies en coopération avec FAO AIMS. En 2024, nous sommes passés à la nomenclature de l'UNESCO pour les domaines des sciences et des technologies. Les groupes et les catégories, ainsi que d'autres métadonnées, peuvent être utilisés sur la page “Parcourir” d'AgroPortal pour filtrer la liste des ontologies. - show_sub_categories: Afficher les sous-catégories \ No newline at end of file + show_sub_categories: Afficher les sous-catégories + federation: + results_from_external_portals: Résultats provenant de portails externes + not_responding: ne répond pas. + check_status: Vérification de la disponibilité de %{portal} diff --git a/config/routes.rb b/config/routes.rb index f31620d09a..b3760280fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ get 'auth/:provider/callback', to: 'login#create_omniauth' get 'locale/:language', to: 'language#set_locale_language' get 'metadata_export/index' + get '/config', to: 'home#portal_config' get '/notes/new_comment', to: 'notes#new_comment' get '/notes/new_proposal', to: 'notes#new_proposal' @@ -130,6 +131,7 @@ end get '' => 'home#index' + get 'status/:portal_name', to: 'home#federation_portals_status' match 'sparql_proxy', to: 'admin#sparql_endpoint', via: [:get, :post] diff --git a/test/helpers/application_test_helpers.rb b/test/helpers/application_test_helpers.rb index 975c220eac..28530711f7 100644 --- a/test/helpers/application_test_helpers.rb +++ b/test/helpers/application_test_helpers.rb @@ -16,17 +16,27 @@ module Users def sign_in_as(username) user = fixtures(:users)[username] logged_in_user = LinkedData::Client::Models::User.authenticate(user.username, user.password) - if logged_in_user && !logged_in_user.errors - logged_in_user = create_user(user) - end + logged_in_user = create_user(user) if logged_in_user && !logged_in_user.errors logged_in_user end def create_user(user, admin: false) - admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') if admin + admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') existent_user = LinkedData::Client::Models::User.find_by_username(user.username).first - existent_user.delete if existent_user + conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| + faraday.request :url_encoded + faraday.response :logger + faraday.adapter Faraday.default_adapter + faraday.headers = { + "Accept" => "application/json", + "Authorization" => "apikey token=#{admin_user.apikey}", + "User-Agent" => "NCBO API Ruby Client v0.1.0" + } + + end + + conn.delete("/users/#{user.username}") if existent_user values = user.to_h values[:role] = ["ADMINISTRATOR"] if admin @@ -34,17 +44,6 @@ def create_user(user, admin: false) if admin # Overwrite the normal ".save" to accept creating admin user - conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| - faraday.request :url_encoded - faraday.response :logger - faraday.adapter Faraday.default_adapter - faraday.headers = { - "Accept" => "application/json", - "Authorization" => "apikey token=#{admin_user.apikey}", - "User-Agent" => "NCBO API Ruby Client v0.1.0" - } - - end conn.post(existent_user.class.collection_path, existent_user.to_hash.to_json, 'Content-Type' => 'application/json') else existent_user.save @@ -61,7 +60,22 @@ def delete_users(users = LinkedData::Client::Models::User.all) end def delete_user(user) - LinkedData::Client::Models::User.find_by_username(user.username).first&.delete + admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') + existent_user = LinkedData::Client::Models::User.find_by_username(user.username).first + + conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| + faraday.request :url_encoded + faraday.response :logger + faraday.adapter Faraday.default_adapter + faraday.headers = { + "Accept" => "application/json", + "Authorization" => "apikey token=#{admin_user.apikey}", + "User-Agent" => "NCBO API Ruby Client v0.1.0" + } + + end + + conn.delete("/users/#{user.username}") if existent_user end end @@ -133,4 +147,4 @@ def delete_agents(agents = LinkedData::Client::Models::Agent.all) Array(agents).each { |g| g.delete } end end -end \ No newline at end of file +end diff --git a/test/system/login_flows_test.rb b/test/system/login_flows_test.rb index d6ff0deb49..8bed283d20 100644 --- a/test/system/login_flows_test.rb +++ b/test/system/login_flows_test.rb @@ -22,8 +22,6 @@ class LoginFlowsTest < ApplicationSystemTestCase new_user = @user_john delete_user(new_user) - LinkedData::Client::Models::User.find_by_username(new_user.username).first&.delete - fill_in 'user_firstName', with: new_user.firstName fill_in 'user_lastName', with: new_user.lastName fill_in 'user_username', with: new_user.username diff --git a/test/system/submission_flows_test.rb b/test/system/submission_flows_test.rb index 6045308d04..a0c06a4f47 100644 --- a/test/system/submission_flows_test.rb +++ b/test/system/submission_flows_test.rb @@ -46,7 +46,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text cat.acronym.titleize end - assert_text @new_submission.URI assert_text @new_submission.description assert_text @new_submission.pullLocation @@ -103,7 +102,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase click_on "Licensing" submission_licensing_edit_fill(ontology_2, submission_2) - # Persons and organizations tab click_on "Persons and organizations" submission_agent_edit_fill(submission_2) @@ -148,7 +146,10 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.URI assert_text submission_2.versionIRI + + wait_for '.submission-status' assert_selector '.submission-status', text: submission_2.version + assert_selector ".flag-icon-fr" # todo fix this submission_2.identifier.each do |id| assert_text id @@ -258,7 +259,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text "rdfs" assert_text "dct" - open_dropdown "#configuration" submission_2.keyClasses.each do |key| @@ -341,10 +341,8 @@ class SubmissionFlowsTest < ApplicationSystemTestCase sleep 0.5 click_button 'Back' - fill_ontology(ontology_2, submission_2, add_submission: true) - assert_selector 'h2', text: 'Ontology submitted successfully!' click_on current_url.gsub("/ontologies/success/#{existent_ontology.acronym}", '') + ontology_path(existent_ontology.acronym) @@ -362,7 +360,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.description assert_text submission_2.pullLocation - # check assert_selector '.fas.fa-key' if submission_2.status.eql?('private') @@ -380,7 +377,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text group.name end - open_dropdown "#dates" assert_date submission_2.modificationDate assert_date existent_submission.released @@ -483,7 +479,6 @@ def submission_agent_edit_fill(submission) list_inputs "#submissioncontact_from_group_input", "submission[contact]", submission.contact - agent1 = fixtures(:agents)[:agent1] agent2 = fixtures(:agents)[:agent2] @@ -624,7 +619,6 @@ def fill_ontology(new_ontology, new_submission, add_submission: false) list_checks new_ontology.hasDomain.map(&:acronym), @categories.map(&:acronym) list_checks new_ontology.group.map(&:acronym), @groups.map(&:acronym) - click_button 'Next' # Page 2 @@ -644,9 +638,9 @@ def fill_ontology(new_ontology, new_submission, add_submission: false) # Page 3 if add_submission - date_picker_fill_in 'submission[modificationDate]', new_submission.modificationDate + date_picker_fill_in 'submission[modificationDate]', new_submission.modificationDate else - date_picker_fill_in 'submission[released]', new_submission.released + date_picker_fill_in 'submission[released]', new_submission.released end list_inputs "#submissioncontact_from_group_input", "submission[contact]", new_submission.contact