diff --git a/.gitignore b/.gitignore index e5eb8ea5bd..ab0ea98b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ node_modules /config/credentials/test.key /config/credentials/staging.key /config/credentials/production.key +/config/credentials/appliance.key /app/assets/builds/* !/app/assets/builds/.keep diff --git a/Gemfile b/Gemfile index 710616a597..c423cef31f 100644 --- a/Gemfile +++ b/Gemfile @@ -53,12 +53,11 @@ gem 'graphql', '~> 2.0.27' gem 'graphql-client' gem 'haml', '~> 5.1' gem 'i18n' -gem 'iconv' gem 'iso-639', '~> 0.3.6' gem 'multi_json' gem 'mysql2', '0.5.5' gem 'oj' -gem 'ontologies_api_client', github: 'ncbo/ontologies_api_ruby_client', tag: 'v2.2.5' +gem 'ontologies_api_client', github: 'ncbo/ontologies_api_ruby_client', tag: 'v2.4.0' gem 'open_uri_redirections' gem 'pry' gem 'psych', '< 4' @@ -69,9 +68,16 @@ gem 'recaptcha', '~> 5.9.0' gem 'rest-client' gem 'rexml', '~> 3' gem 'stackprof', require: false + +# pinning strscan to v 3.0.1 to deal with deployment issue. Remove line below when issue is fixed +gem 'strscan', '3.0.1' + gem 'terser' gem 'thin' gem 'will_paginate', '~> 3.0' +gem 'net-ftp' +gem 'flag-icons-rails', '~> 3.4' +gem 'inline_svg' group :staging, :production do # Application monitoring diff --git a/Gemfile.lock b/Gemfile.lock index e8efaa91ba..4103dea808 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/ncbo/ontologies_api_ruby_client.git - revision: 115cf36f54f73eb10c503147f54bb6f6672d3d99 - tag: v2.2.5 + revision: f589b13dfbbc133ea67cbae1a8f92b41ea85c14b + tag: v2.4.0 specs: - ontologies_api_client (2.2.5) + ontologies_api_client (2.4.0) activesupport (= 7.0.8) addressable (~> 2.8) excon @@ -13,7 +13,6 @@ GIT lz4-ruby multi_json oj - spawnling (= 2.1.5) GEM remote: https://rubygems.org/ @@ -83,29 +82,33 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - airbrussh (1.5.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + airbrussh (1.5.3) sshkit (>= 1.6.1, != 1.7.0) ast (2.4.2) - autoprefixer-rails (10.4.16.0) + autoprefixer-rails (10.4.19.0) execjs (~> 2) base64 (0.1.0) - bcrypt_pbkdf (1.1.0) - bootsnap (1.17.1) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) + bigdecimal (3.1.8) + bootsnap (1.18.4) msgpack (~> 1.2) bootstrap (5.2.3) autoprefixer-rails (>= 9.1.0) popper_js (>= 2.11.6, < 3) sassc-rails (>= 2.0.0) - brakeman (6.0.1) - builder (3.2.4) - capistrano (3.18.0) + brakeman (6.2.1) + racc + builder (3.3.0) + capistrano (3.19.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) + capistrano-bundler (2.1.1) capistrano (~> 3.1) capistrano-locally (0.3.0) capistrano (~> 3.0) @@ -119,11 +122,11 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) @@ -131,41 +134,47 @@ GEM chart-js-rails (0.1.7) railties (> 3.1) coderay (1.1.3) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.4) crass (1.0.6) cube-ruby (0.0.3) daemons (1.4.1) - dalli (3.2.6) + dalli (3.2.8) date (3.3.4) - debug (1.9.1) + debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) - diff-lcs (1.5.0) - domain_name (0.6.20231109) + diff-lcs (1.5.1) + domain_name (0.6.20240107) ed25519 (1.3.0) - erubi (1.12.0) + erubi (1.13.0) erubis (2.7.0) eventmachine (1.2.7) - excon (0.108.0) + excon (0.111.0) execjs (2.9.1) - faraday (2.7.12) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-excon (2.1.0) - excon (>= 0.27.4) - faraday (~> 2.0) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) + json + logger + faraday-excon (2.2.0) + excon (>= 0.109.0) + faraday (>= 2.11.0, < 3) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.0.2) - ffi (1.16.3) + faraday-net_http (3.3.0) + net-http + ffi (1.17.0) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-darwin) + flag-icons-rails (3.4.6.1) + sass-rails flamegraph (0.9.5) globalid (1.2.1) activesupport (>= 6.1) - graphql (2.0.27) - graphql-client (0.18.0) + graphql (2.0.31) + base64 + graphql-client (0.23.0) activesupport (>= 3.0) - graphql + graphql (>= 1.13.0) haml (5.2.2) temple (>= 0.8.0) tilt @@ -175,29 +184,32 @@ GEM nokogiri (>= 1.6.0) ruby_parser (~> 3.5) http-accept (1.7.0) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - iconv (1.0.8) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.7.2) - irb (1.12.0) - rdoc + irb (1.14.0) + rdoc (>= 4.0.0) reline (>= 0.4.2) iso-639 (0.3.6) jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) + jquery-ui-rails (7.0.0) railties (>= 3.2.16) - jsbundling-rails (1.3.0) + jsbundling-rails (1.3.1) railties (>= 6.0.0) json (2.7.2) language_server-protocol (3.17.0.3) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -207,19 +219,24 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) - method_source (1.0.0) - mime-types (3.5.1) + method_source (1.1.0) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2023.1003) + mime-types-data (3.2024.0903) mini_mime (1.1.5) - minitest (5.21.2) + minitest (5.25.1) msgpack (1.7.2) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.1) mysql2 (0.5.5) - net-imap (0.4.7) + net-ftp (0.3.7) + net-protocol + time + net-http (0.4.1) + uri + net-imap (0.4.16) date net-protocol net-pop (0.1.2) @@ -228,23 +245,27 @@ GEM timeout net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) - net-smtp (0.4.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.0) net-protocol - net-ssh (7.2.0) + net-ssh (7.2.3) netrc (0.11.0) - newrelic_rpm (9.6.0) - base64 - nio4r (2.6.1) - nokogiri (1.16.0-arm64-darwin) + newrelic_rpm (9.13.0) + nio4r (2.7.3) + nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.0-x86_64-darwin) + nokogiri (1.16.7-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.0-x86_64-linux) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) - oj (3.16.1) + oj (3.16.6) + bigdecimal (>= 3.0) + ostruct (>= 0.2) open_uri_redirections (0.2.1) - parallel (1.24.0) - parser (3.3.0.5) + ostruct (0.6.0) + parallel (1.26.3) + parser (3.3.5.0) ast (~> 2.4.1) racc popper_js (2.11.8) @@ -252,10 +273,10 @@ GEM coderay (~> 1.1) method_source (~> 1.0) psych (3.3.4) - public_suffix (5.0.4) - racc (1.7.3) - rack (2.2.8) - rack-mini-profiler (3.1.1) + public_suffix (6.0.1) + racc (1.8.1) + rack (2.2.9) + rack-mini-profiler (3.3.1) rack (>= 1.2.0) rack-test (2.1.0) rack (>= 1.3) @@ -294,55 +315,56 @@ GEM rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) rdoc (6.3.4.1) recaptcha (5.9.0) json redis (4.8.1) - regexp_parser (2.9.0) - reline (0.5.0) + regexp_parser (2.9.2) + reline (0.5.10) io-console (~> 0.5) 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.2.6) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rexml (3.3.7) + rspec-core (3.13.1) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.1.0) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - rspec-support (3.12.1) - rubocop (1.63.3) + rspec-support (~> 3.13.0) + rspec-rails (7.0.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + rubocop (1.66.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.2) - parser (>= 3.3.0.4) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - ruby_parser (3.20.3) + ruby_parser (3.21.1) + racc (~> 1.5) sexp_processor (~> 4.16) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) ffi (~> 1.9) sassc-rails (2.1.2) @@ -352,48 +374,54 @@ GEM sprockets-rails tilt select2-rails (4.0.13) - sexp_processor (4.17.0) - spawnling (2.1.5) + sexp_processor (4.17.2) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.21.6) + sshkit (1.23.1) + base64 net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.25) - stimulus-rails (1.3.3) + ostruct + stackprof (0.2.26) + stimulus-rails (1.3.4) railties (>= 6.0.0) + strscan (3.0.1) temple (0.10.3) - terser (1.1.20) + terser (1.2.3) execjs (>= 0.3.0, < 3) thin (1.8.2) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (1.3.0) - tilt (2.3.0) + thor (1.3.2) + tilt (2.4.0) + time (0.4.0) + date timeout (0.4.1) - turbo-rails (1.5.0) + turbo-rails (2.0.9) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) + uri (0.13.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) will_paginate (3.3.1) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.12) + zeitwerk (2.6.18) PLATFORMS arm64-darwin-22 + arm64-darwin-23 x86_64-darwin-21 x86_64-linux @@ -416,13 +444,14 @@ DEPENDENCIES dalli debug ed25519 (>= 1.2, < 2.0) + flag-icons-rails (~> 3.4) flamegraph graphql (~> 2.0.27) graphql-client haml (~> 5.1) html2haml i18n - iconv + inline_svg iso-639 (~> 0.3.6) jquery-rails jquery-ui-rails @@ -430,6 +459,7 @@ DEPENDENCIES listen multi_json mysql2 (= 0.5.5) + net-ftp newrelic_rpm oj ontologies_api_client! @@ -451,6 +481,7 @@ DEPENDENCIES sprockets-rails stackprof stimulus-rails + strscan (= 3.0.1) terser thin turbo-rails @@ -458,4 +489,4 @@ DEPENDENCIES will_paginate (~> 3.0) BUNDLED WITH - 2.4.19 + 2.5.11 diff --git a/app/assets/javascripts/bp_class_tree.js.erb b/app/assets/javascripts/bp_class_tree.js.erb index e3cbbc8c95..74c7e9f622 100644 --- a/app/assets/javascripts/bp_class_tree.js.erb +++ b/app/assets/javascripts/bp_class_tree.js.erb @@ -86,7 +86,7 @@ function initClassTree() { }); }; -function nodeClicked(node_id) { +function nodeClicked(node_id, lang = "en") { // Get current html and store data in cache (to account for changes since the cache was retrieved) setCacheCurrent(); @@ -128,12 +128,12 @@ function nodeClicked(node_id) { // Insert notes table insertNotesTable(tabData["notes_table_data"]); - wrapupTabChange(selectedTab); } else { jQuery.blockUI({ message: '

' + '<%= image_tag src="jquery.simple.tree/spinner.gif" %>' + ' Loading Class...

', showOverlay: false }); - jQuery.get('/ajax_concepts/'+jQuery(document).data().bp.ont_viewer.ontology_id+'/?conceptid='+node_id+'&callback=load', - function(data){ + var ajaxUrl = '/ajax_concepts/'+jQuery(document).data().bp.ont_viewer.ontology_id+'/?conceptid='+node_id+'&lang=' + lang + '&callback=load' + jQuery.get(ajaxUrl, + function(data) { var tabData = data.split("|||"); // the tabs @@ -181,14 +181,16 @@ function placeTreeView(treeHTML) { } // Retrieve the tree view using ajax -function getTreeView() { +function getTreeView(lang = "en") { + var url = "/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&lang=" + lang + "&conceptid="+encodeURIComponent(jQuery(document).data().bp.ont_viewer.concept_id); jQuery.ajax({ - url: "/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&conceptid="+encodeURIComponent(jQuery(document).data().bp.ont_viewer.concept_id), + url: url, success: function(data) { placeTreeView(data); }, error: function(data) { - jQuery.get("/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&conceptid=root", function(data){ + var url = "/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&lang=" + lang + "&conceptid=root"; + jQuery.get(url, function(data) { var rootTree = "
Displaying the path to this class has taken too long. You can browse classes below.
" + data; placeTreeView(rootTree); }); @@ -201,6 +203,7 @@ function getTreeView() { // We do this right after writing #sd_content to the dom to make sure it loads before other async portions of the page jQuery(document).ready(function(){ if (pageUsesTreeView()) { - getTreeView(); + var lang = jQuery(document).data().bp.ont_viewer.lang; + getTreeView(lang); } }); diff --git a/app/assets/javascripts/bp_ontology_viewer.js.erb b/app/assets/javascripts/bp_ontology_viewer.js.erb index e595cf3c23..276cfa27d5 100644 --- a/app/assets/javascripts/bp_ontology_viewer.js.erb +++ b/app/assets/javascripts/bp_ontology_viewer.js.erb @@ -82,18 +82,16 @@ function displayTree(data) { var metadata_only = jQuery(document).data().bp.ont_viewer.metadata_only; var purl_prefix = jQuery(document).data().bp.ont_viewer.purl_prefix; var concept_name_title = jQuery(document).data().bp.ont_viewer.concept_name_title; + var lang = jQuery(document).data().bp.ont_viewer.lang; // Check to see if we're actually loading a new concept or just displaying the one we already loaded previously if (typeof new_concept_id === 'undefined' || new_concept_id == concept_id) { - if (concept_id !== "") { History.replaceState({p:"classes", conceptid:concept_id}, jQuery.bioportal.ont_pages["classes"].page_name + " | " + org_site, "?p=classes" + "&conceptid=" + concept_id); } jQuery.unblockUI(); return; - } else { - var new_concept_param = (typeof new_concept_id === 'undefined') ? "" : "&conceptid=" + new_concept_id; if (typeof new_concept_id !== 'undefined') { @@ -137,7 +135,7 @@ function displayTree(data) { simpleTreeCollection.get(0).setTreeNodes(list); // Simulate node click - nodeClicked(data.conceptid); + nodeClicked(data.conceptid, lang); // Make "clicked" node active jQuery("a.active").removeClass("active"); @@ -146,7 +144,7 @@ function displayTree(data) { // Clear the search box jQuery("#search_box").val(""); } else { - nodeClicked(data.conceptid); + nodeClicked(data.conceptid, lang); // Clear the search box jQuery("#search_box").val(""); @@ -161,7 +159,7 @@ function displayTree(data) { if (document.getElementById(new_concept_id) !== null) { // We have a visible node that's been clicked, get the details for that node jQuery.bioportal.ont_pages["classes"].published = true; - nodeClicked(new_concept_id); + nodeClicked(new_concept_id, lang); } else { // Get a new copy of the tree because our concept isn't visible // This could be due to using the forward/back button @@ -201,6 +199,12 @@ function showOntologyContent(content_section) { // Instead, fire some history events var nav_ont = function(link) { var page = jQuery(link).attr("data-bp-ont-page"); + + if (jQuery(document).data().bp.ont_viewer.lang_sections.includes(page)) { + jQuery("#navbar-ontology li.lang-dropdown").show(); + } else { + jQuery("#navbar-ontology li.lang-dropdown").hide(); + } History.pushState({p:page}, jQuery.bioportal.ont_pages[page].page_name + " | " + jQuery(document).data().bp.ont_viewer.org_site, "?p=" + page); } diff --git a/app/assets/javascripts/bp_visualize.js.erb b/app/assets/javascripts/bp_visualize.js.erb index 0e4b255426..9fdcebe5b7 100644 --- a/app/assets/javascripts/bp_visualize.js.erb +++ b/app/assets/javascripts/bp_visualize.js.erb @@ -88,6 +88,10 @@ jQuery(document).trigger("visualize_tab_change", [{tabType: tabId}]); } + function isNotEmpty(arr) { + return Array.isArray(arr) && arr.length > 0; + } + // Only show BioMixer when tab is clicked jQuery(document).live("visualize_tab_change", function(event, data){ if (data.tabType == "visualization") { @@ -97,8 +101,16 @@ jQuery(document).data().bp.classesTab.search_box_init = function(){ if (jQuery("#search_box").bioportal_autocomplete) { + let extraParams = { + objecttypes: 'class' + }; + + if (isNotEmpty(jQuery(document).data().bp.ont_viewer.submission_lang)) { + extraParams["lang"] = jQuery(document).data().bp.ont_viewer.lang; + } + jQuery("#search_box").bioportal_autocomplete("/search/json_search/"+jQuery(document).data().bp.ontology.acronym, { - extraParams: { objecttypes: "class" }, + extraParams: extraParams, selectFirst: true, lineSeparator: "~!~", matchSubset: 0, diff --git a/app/assets/stylesheets/account.scss b/app/assets/stylesheets/account.scss new file mode 100644 index 0000000000..f371532d9f --- /dev/null +++ b/app/assets/stylesheets/account.scss @@ -0,0 +1,53 @@ +.signup { + display: flex; + align-items: center; + justify-content: center; + padding-top: 40px; + padding-bottom: 40px; +} + +.form-signup { + width: 100%; + max-width: 400px; + padding: 15px; + margin: 0 auto; + + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + h4, p { + text-align: center; + } +} + +.form-signup .enable-lists { + color: red; +} + +.edit-user-info { + display: flex; + align-items: center; + justify-content: center; + + form { + width: 100%; + max-width: 640px; + padding: 15px; + margin: 0 auto; + } +} + +.account-info { + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 99eabc57e8..3647b07fa4 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -19,16 +19,19 @@ *= require thickbox *= require select2 *= require trumbowyg + *= require flag-icon * */ /* BioPortal */ @import "admin"; +@import "account"; @import "annotator"; @import "bioportal"; @import "concepts"; @import "footer"; @import "home"; +@import "login"; @import "mappings"; @import "notes"; @import "notice"; diff --git a/app/assets/stylesheets/login.scss b/app/assets/stylesheets/login.scss new file mode 100644 index 0000000000..8a8db9b9c5 --- /dev/null +++ b/app/assets/stylesheets/login.scss @@ -0,0 +1,41 @@ +.signin { + display: flex; + align-items: center; + justify-content: center; + padding-top: 40px; + padding-bottom: 40px; +} + +.form-signin { + width: 100%; + max-width: 400px; + padding: 15px; + margin: 0 auto; + + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + h4, p { + text-align: center; + } +} + +.form-signin .enable-lists { + color: red; +} + +.password-reset { + display: flex; + justify-content: center; + padding-top: 40px; + padding-bottom: 40px; + + form { + max-width: 640px; + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 488cbda5a1..06cd2e3024 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -243,7 +243,8 @@ def redirect_new_api(class_view = false) not_found unless acronym if class_view @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(acronym).first - concept = get_class(params).first.to_s + @submission = get_ontology_submission_ready(@ontology) + concept = get_class(params, @submission).first.to_s redirect_to "/ontologies/#{acronym}?p=classes#{params_string_for_redirect(params, prefix: "&")}", :status => :moved_permanently else redirect_to "/ontologies/#{acronym}#{params_string_for_redirect(params)}", :status => :moved_permanently @@ -349,10 +350,10 @@ def using_captcha? ENV['USE_RECAPTCHA'].present? && ENV['USE_RECAPTCHA'] == 'true' end - def get_class(params) + def get_class(params, submission) + lang = helpers.request_lang(submission) if @ontology.flat? - ignore_concept_param = params[:conceptid].nil? || params[:conceptid].empty? || params[:conceptid].eql?("root") || @@ -367,34 +368,37 @@ def get_class(params) @concept.children = [] else # Display only the requested class in the tree - @concept = @ontology.explore.single_class({full: true}, params[:conceptid]) + @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid]) @concept.children = [] end @root = LinkedData::Client::Models::Class.new @root.children = [@concept] - else - # not ignoring 'bp_fake_root' here ignore_concept_param = params[:conceptid].nil? || params[:conceptid].empty? || params[:conceptid].eql?("root") + if ignore_concept_param # get the top level nodes for the root # TODO_REV: Support views? Replace old view call: @ontology.top_level_classes(view) - roots = @ontology.explore.roots + roots = @ontology.explore.roots(lang: lang) + if roots.nil? || roots.empty? LOG.add :debug, "Missing roots for #{@ontology.acronym}" not_found end @root = LinkedData::Client::Models::Class.new(read_only: true) - @root.children = roots.sort{|x,y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} + @root.children = roots.sort{|x,y| + x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty? + y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty? + (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} # get the initial concept to display root_child = @root.children.first + @concept = root_child.explore.self(full: true, lang: lang) - @concept = root_child.explore.self(full: true) # Some ontologies have "too many children" at their root. These will not process and are handled here. if @concept.nil? LOG.add :debug, "Missing class #{root_child.links.self}" @@ -402,20 +406,23 @@ def get_class(params) end else # if the id is coming from a param, use that to get concept - @concept = @ontology.explore.single_class({full: true}, params[:conceptid]) + @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid]) if @concept.nil? || @concept.errors LOG.add :debug, "Missing class #{@ontology.acronym} / #{params[:conceptid]}" not_found end # Create the tree - rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete") + rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete", lang: lang) + if rootNode.nil? || rootNode.empty? - roots = @ontology.explore.roots + roots = @ontology.explore.roots(lang: lang) + if roots.nil? || roots.empty? LOG.add :debug, "Missing roots for #{@ontology.acronym}" not_found end + if roots.any? {|c| c.id == @concept.id} rootNode = roots else @@ -423,10 +430,12 @@ def get_class(params) end end @root = LinkedData::Client::Models::Class.new(read_only: true) - @root.children = rootNode.sort{|x,y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} + @root.children = rootNode.sort{|x,y| + x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty? + y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty? + (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} end end - @concept end diff --git a/app/controllers/concepts_controller.rb b/app/controllers/concepts_controller.rb index f518eed670..b945f04e60 100644 --- a/app/controllers/concepts_controller.rb +++ b/app/controllers/concepts_controller.rb @@ -16,13 +16,15 @@ def show # find_by_acronym includes views by default @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology]).first + @submission = get_ontology_submission_ready(@ontology) @ob_instructions = helpers.ontolobridge_instructions_template(@ontology) if request.xhr? display = params[:callback].eql?('load') ? { full: true } : { display: 'prefLabel' } + display[:language] = helpers.request_lang(@submission) @concept = @ontology.explore.single_class(display, params[:id]) not_found if @concept.nil? - show_ajax_request + show_ajax_request(@submission) else render plain: 'Non-AJAX requests are not accepted at this URL', status: :forbidden end @@ -53,7 +55,8 @@ def show_tree if @ontology.nil? not_found else - get_class(params) + @submission = get_ontology_submission_ready(@ontology) + get_class(params, @submission) render partial: 'ontologies/treeview' end end @@ -105,14 +108,18 @@ def redirect_new_api # Load data for a concept or retrieve a concept's children, depending on the value of the :callback parameter. # Children are retrieved for drawing ontology class trees. - def show_ajax_request + def show_ajax_request(submission) case params[:callback] when 'load' gather_details render partial: 'load' when 'children' - @children = @concept.explore.children(pagesize: 750).collection || [] - @children.sort! { |x, y| (x.prefLabel || '').downcase <=> (y.prefLabel || '').downcase } + @children = @concept.explore.children(pagesize: 750, language: helpers.request_lang(submission)).collection || [] + @children.sort! do |x, y| + x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty? + y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty? + (x.prefLabel || '').downcase <=> (y.prefLabel || '').downcase + end render partial: 'child_nodes' end end @@ -123,4 +130,5 @@ def gather_details @delete_mapping_permission = check_delete_mapping_permission(@mappings) update_tab(@ontology, @concept.id) end + end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index eca05fcba4..c0d98a519a 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -107,11 +107,15 @@ def account @user_ontologies = @user.customOntology @user_ontologies ||= [] - onts = LinkedData::Client::Models::Ontology.all - @admin_ontologies = onts.select { |o| o.administeredBy.include? @user.id } + @admin_ontologies = LinkedData::Client::Models::Ontology.where do |o| + o.administeredBy.include? @user.id + end + @admin_ontologies.sort! { |a, b| a.name.downcase <=> b.name.downcase } - projects = LinkedData::Client::Models::Project.all - @user_projects = projects.select { |p| p.creator.include? @user.id } + @user_projects = LinkedData::Client::Models::Project.where do |p| + p.creator.include? @user.id + end + @user_projects.sort! { |a, b| a.name.downcase <=> b.name.downcase } render 'users/show' end diff --git a/app/controllers/language_controller.rb b/app/controllers/language_controller.rb new file mode 100644 index 0000000000..0ccf6b59c5 --- /dev/null +++ b/app/controllers/language_controller.rb @@ -0,0 +1,21 @@ +class LanguageController < ApplicationController + + # set locale to the language selected by the user + def set_locale_language + language = params[:language].strip.downcase.to_sym + supported_languages = I18n.available_locales + + if language + if supported_languages.include?(language) + cookies.permanent[:locale] = language + else + # in case we want to show a message if the language is not available + flash.now[:notice] = t('language.translation_not_available', language: language) + logger.error flash.now[:notice] + end + end + + redirect_to request.referer || root_path + end + +end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 2a9587268a..6c331fb199 100755 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,5 +1,6 @@ -class LoginController < ApplicationController +# frozen_string_literal: true +class LoginController < ApplicationController layout :determine_layout def index @@ -7,7 +8,7 @@ def index if params[:redirect] # Get the original, encoded redirect uri = URI.parse(request.url) - orig_params = Hash[uri.query.split("&").map {|e| e.split("=",2)}].symbolize_keys + orig_params = Hash[uri.query.split("&").map { |e| e.split("=", 2) }].symbolize_keys session[:redirect] = orig_params[:redirect] else session[:redirect] = request.referer @@ -17,31 +18,30 @@ def index # logs in a user def create @errors = validate(params[:user]) - if @errors.size < 1 + if @errors.empty? logged_in_user = LinkedData::Client::Models::User.authenticate(params[:user][:username], params[:user][:password]) if logged_in_user && !logged_in_user.errors login(logged_in_user) - redirect = "/" + redirect = '/' if session[:redirect] redirect = CGI.unescape(session[:redirect]) end - redirect_to redirect else - @errors << "Invalid account name/password combination" - render :action => 'index' + @errors << 'Invalid account name/password combination' + render 'index' end else - render :action => 'index' + render 'index' end end # Login as the provided username (only for admin users) def login_as unless session[:user] && session[:user].admin? - redirect_to "/" + redirect_to '/' return end @@ -54,8 +54,8 @@ def login_as session[:user].apikey = session[:admin_user].apikey end - #redirect_to request.referer rescue redirect_to "/" - redirect_to "/" + # redirect_to request.referer rescue redirect_to "/" + redirect_to '/' end # logs out a user @@ -67,25 +67,24 @@ def destroy flash[:success] = "Logged out #{old_user.username}, returned to #{session[:user].username}".html_safe else session[:user] = nil - flash[:success] = "You have successfully logged out" + flash[:success] = 'You have successfully logged out' end - redirect_to request.referer || "/" + redirect_to request.referer || '/' end - def lost_password - end + def lost_password; end # Sends a new password to the user def send_pass username = params[:user][:account_name] email = params[:user][:email] - resp = LinkedData::Client::HTTP.post("/users/create_reset_password_token", {username: username, email: email}) + resp = LinkedData::Client::HTTP.post('/users/create_reset_password_token', { username: username, email: email }) if resp.nil? - redirect_to login_index_path, notice: "Please check your email for a message with reset instructions" + redirect_to login_index_path, notice: 'Please check your email for a message with reset instructions' else - flash[:notice] = resp.errors.first + ". Please try again." - redirect_to "/lost_pass" + flash[:notice] = "#{resp.errors.first}. Please try again." + redirect_to '/lost_pass' end end @@ -93,14 +92,13 @@ def reset_password username = params[:un] email = params[:em] token = params[:tk] - @user = LinkedData::Client::HTTP.post("/users/reset_password", {username: username, email: email, token: token}) + @user = LinkedData::Client::HTTP.post('/users/reset_password', { username: username, email: email, token: token }) if @user.is_a?(LinkedData::Client::Models::User) - @user.validate_password = true login(@user) - render "users/edit" + render 'passwords/edit' else - flash[:notice] = @user.errors.first + ". Please reset your password again." - redirect_to "/lost_pass" + flash[:notice] = "#{@user.errors.first}. Please reset your password again." + redirect_to '/lost_pass' end end @@ -108,6 +106,7 @@ def reset_password def login(user) return unless user + session[:user] = user custom_ontologies_text = session[:user].customOntology && !session[:user].customOntology.empty? ? "The display is now based on your Custom Ontology Set." : "" notice = "Welcome " + user.username.to_s + "! " + custom_ontologies_text @@ -115,17 +114,15 @@ def login(user) end def validate(params) - errors=[] + errors = [] - if params[:username].nil? || params[:username].length <1 - errors << "Please enter an account name" + if params[:username].empty? + errors << 'Please enter an account name' end - if params[:password].nil? || params[:password].length <1 - errors << "Please enter a password" + if params[:password].empty? + errors << 'Please enter a password' end - return errors + errors end - - end diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index d61b1bfa59..aa8fadeed4 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -132,14 +132,14 @@ def index end def classes - get_class(params) + @submission = get_ontology_submission_ready(@ontology) + get_class(params, @submission) if ["application/ld+json", "application/json"].include?(request.accept) render plain: @concept.to_jsonld, content_type: request.accept and return end @current_purl = @concept.purl if Rails.configuration.settings.purl[:enabled] - @submission = get_ontology_submission_ready(@ontology) unless @concept.id == "bp_fake_root" @notes = @concept.explore.notes @@ -188,7 +188,7 @@ def create def edit # Note: find_by_acronym includes ontology views - @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first + @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id], include: 'all').first redirect_to_home unless session[:user] && @ontology.administeredBy.include?(session[:user].id) || session[:user].admin? @categories = LinkedData::Client::Models::Category.all @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} @@ -254,9 +254,9 @@ def show end # Note: find_by_acronym includes ontology views - @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology]).first + @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology], include: 'all').first not_found if @ontology.nil? - + # Handle the case where an ontology is converted to summary only. # See: https://github.com/ncbo/bioportal_web_ui/issues/133. if @ontology.summaryOnly && params[:p].present? @@ -313,7 +313,7 @@ def show end def submit_success - @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first + @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id], include: 'all').first render 'submit_success' end @@ -345,9 +345,9 @@ def update return end # Note: find_by_acronym includes ontology views - @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology][:acronym] || params[:id]).first + @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first @ontology.update_from_params(ontology_params) - error_response = @ontology.update + error_response = @ontology.update(cache_refresh_all: false) if response_error?(error_response) @categories = LinkedData::Client::Models::Category.all @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000000..d6127809da --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class PasswordsController < ApplicationController + before_action :require_logged_in_user + before_action :set_user + + layout :determine_layout + + def edit; end + + def update + if params[:password] != params[:password_confirmation] + flash.now[:warning] = 'New password and password confirmation do not match. Please try again.' + render 'edit' + return + end + + response = @user.update(values: { password: params[:password] }) + if response_error?(response) + @errors = response_errors(response) + render 'edit' + else + flash[:success] = 'Password successfully updated!' + redirect_to user_path(@user.username) + end + end + + private + + def password_params + p = params.permit(:password, :password_confirmation) + p.to_h + end + + def require_logged_in_user + if session[:user].blank? + flash[:warning] = 'You must be logged in to access that page' + redirect_to login_index_path + end + end + + def set_user + @user = session[:user] + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7de8f345f7..973fa9f641 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -17,18 +17,11 @@ def index end def show - projects = LinkedData::Client::Models::Project.find_by_acronym(params[:id]) - if projects.blank? - flash.alert = "Project not found: #{params[:id]}" - redirect_to projects_path - return - end - - @project = projects.first + @project = LinkedData::Client::Models::Project.get(params[:id]) @ontologies_used = [] onts_used = @project.ontologyUsed onts_used.each do |ont_used| - ont = LinkedData::Client::Models::Ontology.find(ont_used) + ont = LinkedData::Client::Models::Ontology.get(ont_used, include: 'name,acronym') @ontologies_used << Hash['name', ont.name, 'acronym', ont.acronym] unless ont.nil? end @ontologies_used.sort_by! { |o| o['name'].downcase } @@ -45,13 +38,7 @@ def new end def edit - projects = LinkedData::Client::Models::Project.find_by_acronym(params[:id]) - if projects.blank? - flash.alert = "Project not found: #{params[:id]}" - redirect_to projects_path - return - end - @project = projects.first + @project = LinkedData::Client::Models::Project.get(params[:id]) @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 || [] @@ -84,21 +71,15 @@ def create end def update - projects = LinkedData::Client::Models::Project.find_by_acronym(params[:id]) - if projects.blank? - flash.alert = "Project not found: #{params[:id]}" - redirect_to projects_path - return - end - @project = projects.first + @project = LinkedData::Client::Models::Project.get(params[:id]) @project.update_from_params(project_params) - @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 || [] - @ontologies = LinkedData::Client::Models::Ontology.all - error_response = @project.update + error_response = @project.update(cache_refresh_all: false) if response_error?(error_response) @errors = response_errors(error_response) + @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 || [] + @ontologies = LinkedData::Client::Models::Ontology.all render :edit else flash[:notice] = 'Project successfully updated' @@ -107,13 +88,7 @@ def update end def destroy - projects = LinkedData::Client::Models::Project.find_by_acronym(params[:id]) - if projects.blank? - flash.alert = "Project not found: #{params[:id]}" - redirect_to projects_path - return - end - @project = projects.first + @project = LinkedData::Client::Models::Project.get(params[:id]) error_response = @project.delete if response_error?(error_response) @errors = response_errors(error_response) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 042c3e4365..9280fdfb6f 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -5,15 +5,8 @@ class SubmissionsController < ApplicationController before_action :authorize_and_redirect, only: [:edit, :update, :create, :new] def new - begin - # REVIEW: do we really need this double attempt to locate an ontology? I think find_by_acronym (below) should - # be sufficient. It's not evident that we call the new method with a full URI anymore. - @ontology = LinkedData::Client::Models::Ontology.get(CGI.unescape(params[:ontology_id])) - rescue MultiJson::ParseError - nil - end - - @ontology ||= LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_id]).first + # NOTE: find_by_acronym includes ontology views + @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_id]).first @submission = @ontology.explore.latest_submission @submission ||= LinkedData::Client::Models::OntologySubmission.new end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4d769260db..cc65fc2408 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UsersController < ApplicationController before_action :unescape_id, only: [:edit, :show, :update] before_action :verify_owner, only: [:edit, :show] @@ -5,8 +7,6 @@ class UsersController < ApplicationController layout :determine_layout - # GET /users - # GET /users.xml def index @users = LinkedData::Client::Models::User.all respond_to do |format| @@ -15,94 +15,71 @@ def index end end - # GET /users/1 - # GET /users/1.xml def show - @user = if session[:user].admin? && params.has_key?(:id) - LinkedData::Client::Models::User.find_by_username(params[:id]).first - else - LinkedData::Client::Models::User.find(session[:user].id) - end + @user = LinkedData::Client::Models::User.get(params[:id], include: 'all') @all_ontologies = LinkedData::Client::Models::Ontology.all(ignore_custom_ontologies: true) @user_ontologies = @user.customOntology - ## Copied from home controller , account action - onts = LinkedData::Client::Models::Ontology.all; - @admin_ontologies = onts.select {|o| o.administeredBy.include? @user.id } + # Copied from home controller, account action + @admin_ontologies = LinkedData::Client::Models::Ontology.where do |o| + o.administeredBy.include? @user.id + end + @admin_ontologies.sort! { |a, b| a.name.downcase <=> b.name.downcase } - projects = LinkedData::Client::Models::Project.all; - @user_projects = projects.select {|p| p.creator.include? @user.id } + @user_projects = LinkedData::Client::Models::Project.where do |p| + p.creator.include? @user.id + end + @user_projects.sort! { |a, b| a.name.downcase <=> b.name.downcase } end - # GET /users/new def new @user = LinkedData::Client::Models::User.new end - # GET /users/1;edit def edit - @user = LinkedData::Client::Models::User.find(params[:id]) - @user ||= LinkedData::Client::Models::User.find_by_username(params[:id]).first - - if (params[:password].eql?("true")) - @user.validate_password = true - end + @user = LinkedData::Client::Models::User.get(params[:id], include: 'all') end - # POST /users - # POST /users.xml def create @errors = validate(user_params) @user = LinkedData::Client::Models::User.new(values: user_params) - if @errors.size < 1 + if @errors.empty? @user_saved = @user.save if response_error?(@user_saved) @errors = response_errors(@user_saved) # @errors = {acronym: "Username already exists, please use another"} if @user_saved.status == 409 - render action: "new" + render 'new' else - # Attempt to register user to list - if params[:user][:register_mail_list] - Notifier.register_for_announce_list(@user.email).deliver rescue nil - end - flash[:notice] = 'Account was successfully created' session[:user] = LinkedData::Client::Models::User.authenticate(@user.username, @user.password) - redirect_to_browse + redirect_to user_path(@user.username) end else - render action: "new" + render 'new' end end - # PUT /users/1 - # PUT /users/1.xml def update - @user = LinkedData::Client::Models::User.find(params[:id]) - @user = LinkedData::Client::Models::User.find_by_username(params[:id]).first if @user.nil? - @errors = validate_update(user_params) - if @errors.size < 1 - - if params[:user][:password] - error_response = @user.update(values: { password: params[:user][:password] }) - else - user_roles = @user.role + @user = LinkedData::Client::Models::User.get(params[:id], include: 'all') - if @user.admin? != (params[:user][:admin].to_i == 1) - user_roles = update_role(@user) - end + @errors = validate_update(user_params) + if @errors.empty? + user_roles = @user.role - @user.update_from_params(user_params.merge!(role: user_roles)) - error_response = @user.update + if @user.admin? != (params[:user][:admin].to_i == 1) + user_roles = update_role(@user) end + @user.update_from_params(user_params.merge!(role: user_roles)) + error_response = @user.update(cache_refresh_all: false) + if response_error?(error_response) @errors = response_errors(error_response) # @errors = {acronym: "Username already exists, please use another"} if error_response.status == 409 - render action: "edit" + render 'edit' else - flash[:notice] = 'Account was successfully updated' + flash[:notice] = 'Account successfully updated!' if session[:user].username == @user.username session[:user].update_from_params(user_params) @@ -110,19 +87,16 @@ def update redirect_to user_path(@user.username) end else - render action: "edit" + render 'edit' end end - # DELETE /users/1 def destroy - response = {errors: '', success: ''} - @user = LinkedData::Client::Models::User.find(params[:id]) - @user = LinkedData::Client::Models::User.find_by_username(params[:id]).first if @user.nil? - if(session[:user].admin?) + response = { errors: String.new(''), success: String.new('') } + @user = LinkedData::Client::Models::User.get(params[:id]) + if session[:user].admin? @user.delete response[:success] << 'User deleted successfully ' - else response[:errors] << 'Not permitted ' end @@ -131,24 +105,23 @@ def destroy end def custom_ontologies - @user = LinkedData::Client::Models::User.find(params[:id]) - @user = LinkedData::Client::Models::User.find_by_username(params[:id]).first if @user.nil? + @user = LinkedData::Client::Models::User.get(params[:id]) custom_ontologies = params[:ontology] ? params[:ontology][:ontologyId] : [] custom_ontologies.reject!(&:blank?) @user.update_from_params(customOntology: custom_ontologies) - error_response = @user.update + response = @user.update - if error_response - flash[:notice] = 'Error saving Custom Ontologies, please try again' - else - updated_user = LinkedData::Client::Models::User.find(@user.id) + if response.success? + updated_user = LinkedData::Client::Models::User.get(@user.id, include: 'customOntology') session[:user].update_from_params(customOntology: updated_user.customOntology) flash[:notice] = if updated_user.customOntology.empty? - 'Custom Ontologies were cleared' + 'Custom ontology set successfully cleared' else - 'Custom Ontologies were saved' + 'Custom ontology set successfully saved' end + else + flash[:error] = 'Error saving custom ontology set. Please try again.' end redirect_to user_path(@user.username) end @@ -156,8 +129,8 @@ def custom_ontologies private def user_params - p = params.require(:user).permit(:firstName, :lastName, :username, :email, :email_confirmation, :password, - :password_confirmation, :register_mail_list, :admin) + p = params.require(:user).permit(:firstName, :lastName, :username, :email, :password, :password_confirmation, + :admin, :githubId, :orcidId) p.to_h end @@ -167,53 +140,42 @@ def unescape_id def verify_owner return if current_user_admin? + if session[:user].nil? || (!session[:user].id.eql?(params[:id]) && !session[:user].username.eql?(params[:id])) redirect_to controller: 'login', action: 'index', redirect: "/accounts/#{params[:id]}" end end def get_ontology_list(ont_hash) - return "" if ont_hash.nil? + return '' if ont_hash.nil? + ontologies = [] ont_hash.each do |ont, checked| ontologies << ont if checked.to_i == 1 end - ontologies.join(";") + ontologies.join(';') end def validate(params) errors = [] - if params[:email].nil? || params[:email].length < 1 || !params[:email].match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i) - errors << "Please enter an email address" - end - if !params[:email].eql?(params[:email_confirmation]) - errors << "Your Email and Email Confirmation do not match" - end - if params[:password].nil? || params[:password].length < 1 - errors << "Please enter a password" - end - if !params[:password].eql?(params[:password_confirmation]) - errors << "Your Password and Password Confirmation do not match" + if params[:email].length < 1 || !params[:email].match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i) + errors << 'invalid email address' end if using_captcha? if !verify_recaptcha - errors << "Please fill in the proper text from the supplied image" + errors << 'reCAPTCHA verification failed, please try again' end end - return errors + errors end def validate_update(params) errors = [] - if params[:email].nil? || params[:email].length < 1 || !params[:email].match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i) - errors << "Please enter an email address" - end - if !params[:password].eql?(params[:password_confirmation]) - errors << "Your Password and Password Confirmation do not match" + if params[:email].length < 1 || !params[:email].match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i) + errors << 'invalid email address' end - - return errors + errors end def update_role(user) @@ -222,9 +184,9 @@ def update_role(user) if session[:user].admin? user_roles = user_roles.dup if user.admin? - user_roles.map!{ |role| role == "ADMINISTRATOR" ? "LIBRARIAN" : role} + user_roles.map! { |role| role == 'ADMINISTRATOR' ? 'LIBRARIAN' : role } else - user_roles.map!{ |role| role == "LIBRARIAN" ? "ADMINISTRATOR" : role} + user_roles.map! { |role| role == 'LIBRARIAN' ? 'ADMINISTRATOR' : role } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3948fe3a2b..12ec47cfe5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,46 +1,27 @@ -# Methods added to this helper will be available to all templates in the application. - require 'uri' require 'cgi' require 'digest/sha1' -require 'pry' # used in a rescue module ApplicationHelper - def get_apikey unless session[:user].nil? - return session[:user].apikey + session[:user].apikey else - return LinkedData::Client.settings.apikey - end - end - - def isOwner?(id) - unless session[:user].nil? - if session[:user].admin? - return true - elsif session[:user].id.eql?(id) - return true - else - return false - end + LinkedData::Client.settings.apikey end end def clean(string) - string = string.gsub("\"",'\'') - return string.gsub("\n",'') + string = string.gsub("\"", '\'') + string.gsub("\n", '') end def clean_id(string) - new_string = string.gsub(":","").gsub("-","_").gsub(".","_") - return new_string + string.gsub(':', '').gsub('-', '_').gsub('.', '_') end def get_username(user_id) - user = LinkedData::Client::Models::User.find(user_id) - username = user.nil? ? user_id : user.username - username + user_id.split('/').last end def current_user_admin? @@ -49,61 +30,44 @@ def current_user_admin? def remove_owl_notation(string) # TODO_REV: No OWL notation, but should we modify the IRI? - return string - - unless string.nil? - strings = string.split(":") - if strings.size<2 - #return string.titleize - return string - else - #return strings[1].titleize - return strings[1] - end - end + string end - def draw_tree(root, id = nil, type = "Menu") + def draw_tree(root, id = nil, type = 'Menu', submission) if id.nil? id = root.children.first.id end # TODO: handle tree view for obsolete classes, e.g. 'http://purl.obolibrary.org/obo/GO_0030400' - raw build_tree(root, "", id) # returns a string, representing nested list items + raw build_tree(root, '', id, submission) # returns a string, representing nested list items end - def build_tree(node, string, id) - if node.children.nil? || node.children.length < 1 - return string # unchanged + def build_tree(node, string, id, submission) + if node.children.nil? || node.children.empty? + return string end - node.children.sort! {|a,b| (a.prefLabel || a.id).downcase <=> (b.prefLabel || b.id).downcase} - for child in node.children - if child.id.eql?(id) - active_style="class='active'" - else - active_style = "" - end - if child.expanded? - open = "class='open'" - else - open = "" - end + + node.children.sort! { |a, b| (a.prefLabel || a.id).downcase <=> (b.prefLabel || b.id).downcase } + node.children.each do |child| + active_style = child.id.eql?(id) ? "class='active'" : '' + open = child.expanded? ? "class='open'" : '' # This fake root will be present at the root of "flat" ontologies, we need to keep the id intact - li_id = child.id.eql?("bp_fake_root") ? "bp_fake_root" : short_uuid + li_id = child.id.eql?('bp_fake_root') ? 'bp_fake_root' : short_uuid - if child.id.eql?("bp_fake_root") + if child.id.eql?('bp_fake_root') string << "
  • #{child.prefLabel}
  • " else icons = child.relation_icon(node) - string << "
  • #{child.prefLabel({use_html: true})} #{icons}" + string << "
  • #{child.prefLabel({use_html: true})} #{icons}" + if child.hasChildren && !child.expanded? - string << "" + string << "" elsif child.expanded? - string << "" + string << '' end - string << "
  • " + string << '' end end @@ -111,11 +75,11 @@ def build_tree(node, string, id) end def loading_spinner(padding = false, include_text = true) - loading_text = include_text ? " loading..." : "" + loading_text = include_text ? ' loading...' : '' if padding - raw('
    ' + image_tag("spinners/spinner_000000_16px.gif", style: "vertical-align: text-bottom;") + loading_text + '
    ') + raw('
    ' + image_tag('spinners/spinner_000000_16px.gif', style: 'vertical-align: text-bottom;') + loading_text + '
    ') else - raw(image_tag("spinners/spinner_000000_16px.gif", style: "vertical-align: text-bottom;") + loading_text) + raw(image_tag('spinners/spinner_000000_16px.gif', style: 'vertical-align: text-bottom;') + loading_text) end end @@ -125,30 +89,11 @@ def short_uuid rand(36**8).to_s(36) end - def help_icon(link, html_attribs = {}) - html_attribs["title"] ||= "Help" - attribs = [] - html_attribs.each {|k,v| attribs << "#{k.to_s}='#{v}'"} - return <<-BLOCK - - - - BLOCK - end - - def anonymous_user - # - # TODO: Fix and failures from removing 'DataAccess' call here. - # - #user = DataAccess.getUser($ANONYMOUS_USER) - user ||= User.new({"id" => 0}) - end - def render_advanced_picker(custom_ontologies = nil, selected_ontologies = [], align_to_dom_id = nil) selected_ontologies ||= [] init_ontology_picker(custom_ontologies, selected_ontologies) - render :partial => "shared/ontology_picker_advanced", :locals => { - :custom_ontologies => custom_ontologies, :selected_ontologies => selected_ontologies, :align_to_dom_id => align_to_dom_id + render partial: 'shared/ontology_picker_advanced', locals: { + custom_ontologies: custom_ontologies, selected_ontologies: selected_ontologies, align_to_dom_id: align_to_dom_id } end @@ -167,93 +112,86 @@ def init_ontology_picker_single end def get_ontologies_data(ontologies = nil) - ontologies ||= LinkedData::Client::Models::Ontology.all(include: "acronym,name") + ontologies ||= LinkedData::Client::Models::Ontology.all(include: 'acronym,name') @onts_for_select = [] @onts_acronym_map = {} @onts_uri2acronym_map = {} ontologies.each do |ont| - # TODO: ontologies parameter may be a list of ontology models (not ontology submission models): - # ont.acronym instead of ont.ontology.acronym - # ont.name instead of ont.ontology.name - # ont.id instead of ont.ontology.id - # TODO: annotator passes in 'custom_ontologies' to the ontologies parameter. - next if ( ont.acronym.nil? or ont.acronym.empty? ) + next if ont.acronym.blank? + acronym = ont.acronym name = ont.name - #id = ont.id # ontology URI - abbreviation = acronym.empty? ? "" : "(#{acronym})" + abbreviation = acronym.empty? ? '' : "(#{acronym})" ont_label = "#{name.strip} #{abbreviation}" - #@onts_for_select << [ont_label, id] # using the URI crashes the UI checkbox selection behavior. @onts_for_select << [ont_label, acronym] @onts_acronym_map[ont_label] = acronym - @onts_uri2acronym_map[ont.id] = acronym # required in ontologies_to_acronyms + @onts_uri2acronym_map[ont.id] = acronym end - @onts_for_select.sort! { |a,b| a[0].downcase <=> b[0].downcase } + @onts_for_select.sort! { |a, b| a[0].downcase <=> b[0].downcase } @onts_for_js = @onts_acronym_map.to_json end def categories_for_select - # This method is called in the search index page. get_ontologies_data get_categories_data - return @categories_for_select + @categories_for_select end def get_categories_data @categories_for_select = [] @categories_map = {} - categories = LinkedData::Client::Models::Category.all(include: "name,ontologies") + categories = LinkedData::Client::Models::Category.all(include: 'name,ontologies') categories.each do |c| - @categories_for_select << [ c.name, c.id ] - @categories_map[c.id] = ontologies_to_acronyms(c.ontologies) # c.ontologies is a list of URIs + @categories_for_select << [c.name, c.id] + @categories_map[c.id] = ontologies_to_acronyms(c.ontologies) end - @categories_for_select.sort! { |a,b| a[0].downcase <=> b[0].downcase } + @categories_for_select.sort! { |a, b| a[0].downcase <=> b[0].downcase } @categories_for_js = @categories_map.to_json end def get_groups_data @groups_map = {} @groups_for_select = [] - groups = LinkedData::Client::Models::Group.all(include: "acronym,name,ontologies") + groups = LinkedData::Client::Models::Group.all(include: 'acronym,name,ontologies') groups.each do |g| - next if ( g.acronym.nil? or g.acronym.empty? ) - @groups_for_select << [ g.name + " (#{g.acronym})", g.acronym ] - @groups_map[g.acronym] = ontologies_to_acronyms(g.ontologies) # g.ontologies is a list of URIs + next if g.acronym.blank? + + @groups_for_select << [g.name + " (#{g.acronym})", g.acronym] + @groups_map[g.acronym] = ontologies_to_acronyms(g.ontologies) end - @groups_for_select.sort! { |a,b| a[0].downcase <=> b[0].downcase } + @groups_for_select.sort! { |a, b| a[0].downcase <=> b[0].downcase } @groups_for_js = @groups_map.to_json end def ontologies_to_acronyms(ontologyIDs) acronyms = [] ontologyIDs.each do |id| - acronyms << @onts_uri2acronym_map[id] # hash generated in get_ontologies_data + acronyms << @onts_uri2acronym_map[id] end - return acronyms.compact # remove nil values from any failures to convert ontology URI to acronym + acronyms.compact end def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true end - def truncate_with_more(text, options = {}) - length ||= options[:length] ||= 30 - trailing_text ||= options[:trailing_text] ||= " ... " - link_more ||= options[:link_more] ||= "[more]" - link_less ||= options[:link_less] ||= "[less]" - more_text = " #{link_more}#{text} #{link_less}" - more = text.length > length ? more_text : "" - output = "#{truncate(text, :length => length, :omission => trailing_text)}" + more + "" + def link_last_part(url) + return '' if url.nil? + + if url.include?('#') + url.split('#').last + else + url.split('/').last + end end def subscribe_ontology_button(ontology_id, user = nil) user = session[:user] if user.nil? if user.nil? - # subscribe button must redirect to login return sanitize("Subscribe") end - # Init subscribe button parameters. - sub_text = "Subscribe" + + sub_text = 'Subscribe' params = "data-bp_ontology_id='#{ontology_id}' data-bp_is_subbed='false' data-bp_user_id='#{user.id}'" begin # Try to create an intelligent subscribe button. @@ -262,17 +200,15 @@ def subscribe_ontology_button(ontology_id, user = nil) else ont = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_id).first end - subscribed = subscribed_to_ontology?(ont.acronym, user) # application_helper - sub_text = subscribed ? "Unsubscribe" : "Subscribe" + subscribed = subscribed_to_ontology?(ont.acronym, user) + sub_text = subscribed ? 'Unsubscribe' : 'Subscribe' params = "data-bp_ontology_id='#{ont.acronym}' data-bp_is_subbed='#{subscribed}' data-bp_user_id='#{user.id}'" rescue # pass, fallback init done above begin block to scope parameters beyond the begin/rescue block end - # TODO: modify/copy CSS for notes_sub_error => subscribe_error - # TODO: modify/copy CSS for subscribe_to_notes => subscribe_to_ontology spinner = '' error = "" - return "#{sub_text} #{spinner} #{error}" + "#{sub_text} #{spinner} #{error}" end def subscribed_to_ontology?(ontology_acronym, user) @@ -295,10 +231,11 @@ def ontolobridge_instructions_template(ontology) # http://stackoverflow.com/questions/1293573/rails-smart-text-truncation def smart_truncate(s, opts = {}) - opts = {:words => 20}.merge(opts) + opts = { words: 20 }.merge(opts) if opts[:sentences] - return s.split(/\.(\s|$)+/)[0, opts[:sentences]].map{|s| s.strip}.join('. ') + '. ...' + return s.split(/\.(\s|$)+/)[0, opts[:sentences]].map { |s| s.strip }.join('. ') + '. ...' end + a = s.split(/\s/) # or /[ ]+/ to only split on spaces n = opts[:words] a[0...n].join(' ') + (a.size > n ? '...' : '') @@ -309,11 +246,9 @@ def smart_truncate(s, opts = {}) # => '06/27/2010' def xmldatetime_to_date(xml_date_time_str) require 'date' - d = DateTime.xmlschema( xml_date_time_str ).to_date + d = DateTime.xmlschema(xml_date_time_str).to_date # Return conventional US date format: - return sprintf("%02d/%02d/%4d", d.month, d.day, d.year) - # Or return "yyyy/mm/dd" format with: - #return DateTime.xmlschema( xml_date_time_str ).to_date.to_s + sprintf("%02d/%02d/%4d", d.month, d.day, d.year) end def flash_class(level) @@ -321,47 +256,43 @@ def flash_class(level) 'notice' => 'alert-info', 'success' => 'alert-success', 'error' => 'alert-danger', - 'alert' => 'alert-danger' + 'alert' => 'alert-danger', + 'warning' => 'alert-warning' } bootstrap_alert_class[level] end - ###BEGIN ruby equivalent of JS code in bp_ajax_controller. - ###Note: this code is used in concepts/_details partial. + # NOTE: The following 4 methods (bp_ont_link, bp_class_link, get_link_for_cls_ajax, get_link_for_ont_ajax) are + # the Ruby equivalent of JS code in bp_ajax_controller.js and are used in the concepts/_details partial. def bp_ont_link(ont_acronym) - return "/ontologies/#{ont_acronym}" + "/ontologies/#{ont_acronym}" end def bp_class_link(cls_id, ont_acronym) ontology_path(id: ont_acronym, p: 'classes', conceptid: cls_id) end - def get_link_for_cls_ajax(cls_id, ont_acronym, target=nil) - # Note: bp_ajax_controller.ajax_process_cls will try to resolve class labels. + def get_link_for_cls_ajax(cls_id, ont_acronym, target = nil) + # NOTE: bp_ajax_controller.ajax_process_cls will try to resolve class labels. # Uses 'http' as a more generic attempt to resolve class labels than .include? ont_acronym; the # bp_ajax_controller.ajax_process_cls will try to resolve class labels and # otherwise remove the UNIQUE_SPLIT_STR and the ont_acronym. - if target.nil? - target = "" - else - target = " target='#{target}' " - end - if cls_id.start_with? 'http://' + target = target.nil? ? '' : " target='#{target}' " + + if cls_id.start_with?('http://', 'https://') href_cls = " href='#{bp_class_link(cls_id, ont_acronym)}' " data_cls = " data-cls='#{cls_id}' " data_ont = " data-ont='#{ont_acronym}' " - return "#{cls_id}" + "#{cls_id}" else - return auto_link(cls_id, :all, :target => '_blank') + auto_link(cls_id, :all, target: '_blank') end end + def get_link_for_ont_ajax(ont_acronym) - # ajax call will replace the acronym with an ontology name (triggered by class='ont4ajax') + # Ajax call will replace the acronym with an ontology name (triggered by class='ont4ajax') href_ont = " href='#{bp_ont_link(ont_acronym)}' " data_ont = " data-ont='#{ont_acronym}' " - return "#{ont_acronym}" + "#{ont_acronym}" end - ###END ruby equivalent of JS code in bp_ajax_controller. - - end diff --git a/app/helpers/internationalisation_helper.rb b/app/helpers/internationalisation_helper.rb new file mode 100644 index 0000000000..6bda49f76d --- /dev/null +++ b/app/helpers/internationalisation_helper.rb @@ -0,0 +1,43 @@ +module InternationalisationHelper + + # Implement logic to make the term 'ontology' configurable throughout the portal, + # allowing it to be replaced with the variable $RESOURCE_TERM + def self.t(*args, **kwargs) + return I18n.t(*args, **kwargs) unless $RESOURCE_TERM + + begin + original_translation = I18n.t(*args, **kwargs) + downcase_translation = original_translation.downcase + rescue StandardError => e + return e.message + end + + term = I18n.t("resource_term.ontology") + plural_term = I18n.t("resource_term.ontology_plural") + single_term = I18n.t("resource_term.ontology_single") + resource = I18n.t("resource_term.#{$RESOURCE_TERM}") + resources = I18n.t("resource_term.#{$RESOURCE_TERM}_plural") + a_resource = I18n.t("resource_term.#{$RESOURCE_TERM}_single") + + if downcase_translation.include?(term) && resource + replacement = resource.capitalize + replacement = resource if downcase_translation.include?(term) + if downcase_translation.include?(single_term) + term = single_term + replacement = a_resource + end + original_translation.gsub(term, replacement) + elsif downcase_translation.include?(plural_term) && resources + replacement = resources.capitalize + replacement = resources if downcase_translation.include?(plural_term) + original_translation.gsub(plural_term, replacement) + else + I18n.t(*args, **kwargs) + end + end + + # def t(*args, **kwargs) + # InternationalisationHelper.t(*args, **kwargs) + # end + +end diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb new file mode 100644 index 0000000000..d12d5dd881 --- /dev/null +++ b/app/helpers/multi_languages_helper.rb @@ -0,0 +1,146 @@ +module MultiLanguagesHelper + + def portal_language_help_text + t('language.portal_language_help_text') + end + + def portal_languages + { + en: { badge: nil, disabled: false }, + fr: { badge: 'beta', disabled: false }, + it: { badge: 'coming', disabled: true }, + de: { badge: 'coming', disabled: true } + } + end + + def portal_language_selector + languages = portal_languages + selected_language = portal_lang + selected_language = content_tag(:span, selected_language.upcase, data: { controller: 'tooltip' }, title: portal_language_help_text) + render DropdownButtonComponent.new do |d| + d.header { selected_language } + d.section(divide: false, selected_index: languages.find_index(selected_language)) do |s| + languages.each do |lang, metadata| + s.item do + text = content_tag(:div, class: 'd-flex align-items-center') do + content_tag(:span, render(LanguageFieldComponent.new(value: lang, auto_label: true)), class: 'mr-1') + beta_badge(metadata[:badge]) + end + link_options = { data: { turbo: false } } + + if metadata[:disabled] + link_options[:class] = 'disabled-link' + link_options[:disabled] = 'disabled' + end + + link_to(text, "/locale/#{lang}", link_options) + end + end + + end + end + end + + def search_language_help_text + content_tag(:div, style: 'width: 300px; text-align: center') do + t('language.search_language_help_text') + end + end + + def search_languages + # top ten spoken languages + portal_languages.keys + %w[zh es hi ar bn pt ru ur id] + end + + def language_hash(concept_label, multiple: false) + if concept_label.is_a?(Array) + return concept_label.first unless multiple + return concept_label + end + + return concept_label.to_h.reject { |key, _| %i[links context].include?(key) } if concept_label.is_a?(OpenStruct) + + concept_label + end + + def sorted_labels(labels) + Array(labels).sort_by { |label| label['prefLabel'].is_a?(String) ? label['prefLabel'] : label['prefLabel'].last } + end + + def select_language_label(concept_label, platform_languages = %i[en fr]) + concept_value = nil + + concept = language_hash(concept_label) + + return ['@none', concept] if concept.is_a?(String) + + concept = concept.to_h + + platform_languages.each do |lang| + if concept[lang] + concept_value = [lang, concept[lang]] + break + end + end + + concept_value || concept.to_a.first + end + + def main_language_label(label) + select_language_label(label)&.last + end + + def selected_language_label(label) + language_hash(label).values.first + end + + def content_language_selector(id: 'content_language', name: 'content_language') + languages, selected = content_languages + select_tag(name, options_for_select(languages, selected || 'all'), class: "form-select", + data: { controller: "language-change", 'language-change-section-value': "classes", action: "change->language-change#dispatchLangChangeEvent" }) if languages&.length > 1 + end + + def content_languages(submission = @submission || @submission_latest) + current_lang = request_lang(submission).downcase + submission_lang = submission_languages(submission) + # Transform each language into a select option + submission_lang = submission_lang.map do |lang| + lang = lang.split('/').last.upcase + lang = ISO_639.find(lang.to_s.downcase) + next nil unless lang + [lang.english_name, lang.alpha2] + end.compact + + [submission_lang, current_lang] + end + + def portal_lang + session[:locale] || 'en' + end + + def request_lang(submission = @submission || @submission_latest) + lang = params[:language] || params[:lang] + lang = submission_languages(submission)&.first unless lang + lang = portal_lang unless lang + lang + end + + def lang_code(code_in) + code_out = code_in + case code_in + when 'en' + code_out = 'us' + when 'ar' + code_out = 'sa' + when 'hi' + code_out = 'in' + when 'ur' + code_out = 'pk' + when 'zh' + code_out = 'cn' + when 'ja' + code_out = 'jp' + end + code_out + end + +end diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 88ea877d94..e255fdedd4 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true +require 'iso-639' module OntologiesHelper + + LANGUAGE_FILTERABLE_SECTIONS = %w[classes properties].freeze + def additional_details return '' if $ADDITIONAL_ONTOLOGY_DETAILS.nil? || $ADDITIONAL_ONTOLOGY_DETAILS[@ontology.acronym].nil? @@ -158,4 +162,34 @@ def change_requests_enabled?(ontology_acronym) Rails.configuration.change_request[:ontologies].include? ontology_acronym.to_sym end + + def current_section + (params[:p]) ? params[:p] : 'summary' + end + + def ontology_data_sections + LANGUAGE_FILTERABLE_SECTIONS + end + + def ontology_data_section?(section_title = current_section) + ontology_data_sections.include?(section_title) + end + + def language_selector_tag(name) + content_language_selector(id: name, name: name) + end + + def submission_languages(submission = @submission) + Array(submission&.naturalLanguage).map { |natural_language| natural_language.split('/').last }.compact + end + + def abbreviations_to_languages(abbreviations) + # Use iso-639 gem to convert language codes to their English names + languages = abbreviations.map do |abbr| + language = ISO_639.find_by_code(abbr) || ISO_639.find_by_english_name(abbr) + language ? language.english_name : abbr + end + languages.sort + end + end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000000..25f5fa46fe --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module UsersHelper + def custom_ontology_set_intro_text + tag.div do + concat(tag.p do + concat(tag.span('Customize your display: ', class: 'fw-bold text-muted')) + concat(tag.span("pick the ontologies you want to see and #{$SITE} will hide all other ontologies.", + class: 'text-muted')) + end) + concat(tag.p('Please note: you must be logged in to use this feature', class: 'fst-italic text-muted')) + end + end + + def custom_ontology_set_slice_text + tag.p class: 'mb-5' do + concat('Please visit the ') + concat(link_to('main site', "#{$UI_URL}/account")) + concat(' to modify your custom ontology set') + end + end +end diff --git a/app/javascript/controllers/clipboard_controller.js b/app/javascript/controllers/clipboard_controller.js new file mode 100644 index 0000000000..30320b5e30 --- /dev/null +++ b/app/javascript/controllers/clipboard_controller.js @@ -0,0 +1,16 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="clipboard" +export default class extends Controller { + static targets = ['source', 'copiedIndicator'] + + copy() { + navigator.clipboard.writeText(this.sourceTarget.textContent); + + this.copiedIndicatorTarget.classList.remove('hidden'); + + setTimeout(() => { + this.copiedIndicatorTarget.classList.add('hidden'); + }, 2000); + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 373c3edc28..273754277c 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -3,3 +3,10 @@ // ./bin/rails generate stimulus controllerName import { application } from "./application" + +import LanguageChangeController from "./language_change_controller" +application.register("language-change", LanguageChangeController) + +import ClipboardController from "./clipboard_controller" +application.register("clipboard", ClipboardController) + diff --git a/app/javascript/controllers/language_change_controller.js b/app/javascript/controllers/language_change_controller.js new file mode 100644 index 0000000000..b1fc34aa5f --- /dev/null +++ b/app/javascript/controllers/language_change_controller.js @@ -0,0 +1,47 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="language-change" +export default class extends Controller { + + + connect() { + // can be used for debugging + // console.log(this.element.value); + jQuery(document).data().bp.ont_viewer.lang = this.element.value; + } + + dispatchLangChangeEvent() { + jQuery(document).data().bp.ont_viewer.lang = this.element.value; + + var url = window.location.href; + url = this.removeURLParameter(url, 'lang'); + if (url.indexOf('?') > -1) { + url += '&lang=' + this.element.value; + } else { + url += '?lang=' + this.element.value; + } + jQuery.blockUI({ message: '

    Switching language...

    ', showOverlay: true }); + window.location.href = url; + } + + removeURLParameter(url, parameter) { + //prefer to use l.search if you have a location/link object + var urlparts = url.split('?'); + + if (urlparts.length >= 2) { + var prefix = encodeURIComponent(parameter) + '='; + var pars = urlparts[1].split(/[&;]/g); + + //reverse iteration as may be destructive + for (var i = pars.length; i-- > 0;) { + //idiom for string.startsWith + if (pars[i].lastIndexOf(prefix, 0) !== -1) { + pars.splice(i, 1); + } + } + return urlparts[0] + (pars.length > 0 ? '?' + pars.join('&') : ''); + } + return url; + } + +} diff --git a/app/views/concepts/_child_nodes.html.haml b/app/views/concepts/_child_nodes.html.haml index 79107a3cb3..b734cc5944 100644 --- a/app/views/concepts/_child_nodes.html.haml +++ b/app/views/concepts/_child_nodes.html.haml @@ -1,8 +1,9 @@ - output ="" - for child in @children - icons = child.relation_icon(@concept) - - output << "
  • #{child.prefLabel} #{icons}" + - output << "
  • #{child.prefLabel} #{icons}" + - if child.hasChildren - - output << "" + - output << "" - output << "
  • " = raw output diff --git a/app/views/concepts/_details.html.haml b/app/views/concepts/_details.html.haml index 4bc9657688..cb0ff44494 100644 --- a/app/views/concepts/_details.html.haml +++ b/app/views/concepts/_details.html.haml @@ -25,7 +25,11 @@ %tr %td Preferred Name %td - %p= @concept.prefLabel({:use_html => true}).html_safe + %p + - if @concept.prefLabel({:use_html => false}).to_s.empty? + %div.alert.alert-warning= 'No preferred name provided for selected language' + - else + = @concept.prefLabel({:use_html => true}).html_safe %td %tr %td Synonyms diff --git a/app/views/concepts/_request_term.html.haml b/app/views/concepts/_request_term.html.haml index 7185c38a35..7c3bed4a93 100644 --- a/app/views/concepts/_request_term.html.haml +++ b/app/views/concepts/_request_term.html.haml @@ -36,11 +36,7 @@ var g_prefLabel; jQuery(document).ready(function() { - g_prefLabel = '#{@concept.prefLabel({:use_html => false}).html_safe}'; - - if (!g_prefLabel) { - g_prefLabel = '#{@concept.id}'; - } + g_prefLabel = '#{@concept.prefLabel({:use_html => false}).to_s.empty? ? @concept.id : @concept.prefLabel({:use_html => false}).html_safe}'; var isPopulated = window.localStorage.getItem('request_term_form_populated'); if (isPopulated) { diff --git a/app/views/layouts/_ontology_viewer.html.haml b/app/views/layouts/_ontology_viewer.html.haml index 64f4899d0c..124ecf014e 100644 --- a/app/views/layouts/_ontology_viewer.html.haml +++ b/app/views/layouts/_ontology_viewer.html.haml @@ -36,6 +36,9 @@ jQuery(document).data().bp.ont_viewer.purl_prefix = "#{(Rails.configuration.settings.purl[:enabled] ? Rails.configuration.settings.purl[:prefix]+"/"+@ontology.acronym : '')}"; jQuery(document).data().bp.ont_viewer.concept_name_title = (jQuery(document).data().bp.ont_viewer.concept_name == "") ? "" : " - " + jQuery(document).data().bp.ont_viewer.concept_name; + jQuery(document).data().bp.ont_viewer.lang = "#{request_lang(sub)}"; + jQuery(document).data().bp.ont_viewer.submission_lang = #{submission_languages(sub).to_json.html_safe}; + jQuery(document).data().bp.ont_viewer.lang_sections = #{ontology_data_sections.to_json.html_safe}; -# Modal dialog for creating a new mapping (must reside in a top-level position in the document to display properly). %div#createNewMappingModal{class: "modal fade", tabindex: "-1", "aria-labelledby": "createNewMappingLabel"} @@ -139,6 +142,8 @@ class: "nav-link", type: 'button', role: "tab", data: {bp_ont_page: "widgets", "bs-toggle": "tab", "bs-target": "#ont_widgets_content"}, aria: {controls: "ont_widgets_content", selected: "false"}) + %li{style: "display: #{ontology_data_section? ? 'block' : 'none'}", class: 'nav-item lang-dropdown', role: 'presentation'} + = language_selector_tag(:content_language) %div.card-body -# Tab panes for displaying ontology content sections %div.tab-content{id: "ontology_content"} @@ -160,7 +165,6 @@ %div.tab-pane{id: "ont_widgets_content", role: "tabpanel", aria: {labelledby: "ont-widgets-tab"}} - if content_section.eql?("widgets") = yield - - if Rails.env.appliance? = render partial: "footer_appliance" diff --git a/app/views/login/index.html.erb b/app/views/login/index.html.erb deleted file mode 100644 index 08110c66a8..0000000000 --- a/app/views/login/index.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%@title = "Login"%> - -<%= form_for(:user, :url => {:controller => 'login',:action=>'create'}) do |f| %> -<%unless @errors.nil?%> -
    -Errors On Form - -
    -<%end%> -
    - - - - - - - - - - - - -
    Account Name:<%= text_field 'user', 'username', :size => 25 %>
    Password:<%= password_field 'user','password', :size => 25, :autocomplete => "off" %>
    <%= submit_tag "Login"%>
    - Not Registered Yet? <%=link_to "Sign Up", new_user_path%>

    - <%=link_to "Lost Password", '/lost_pass'%>

    -
    - -<% end %> \ No newline at end of file diff --git a/app/views/login/index.html.haml b/app/views/login/index.html.haml new file mode 100644 index 0000000000..dc5e593b69 --- /dev/null +++ b/app/views/login/index.html.haml @@ -0,0 +1,29 @@ +- @title = "Login" + +%div.signin + = form_with scope: :user, url: login_index_path, class: 'form-signin' do |f| + %h4.mb-4 + = "Log in to #{$SITE}" + + -# TODO: display errors in a standard Rails flash message + - if @errors.present? + %div.enable-lists + Errors on form: + %ul + - for error in @errors + %li= error + + %div.form-floating.mb-3 + = f.text_field(:username, class: 'form-control', placeholder: 'Username', required: true) + = f.label(:username) + %div.form-floating.mb-3 + = f.password_field(:password, class: 'form-control', placeholder: 'Password', required: true, + autocomplete: 'current-password') + = f.label(:password) + %div.d-grid.mb-3 + = submit_tag('Log in', class: 'btn btn-lg btn-primary') + %p.text-muted.mb-1 + = "New #{$SITE} user?" + = link_to('Create account', new_user_path) + %p.text-muted + = link_to('Forgot your password?', lost_pass_path) diff --git a/app/views/login/lost_password.html.erb b/app/views/login/lost_password.html.erb deleted file mode 100644 index dbbf4338eb..0000000000 --- a/app/views/login/lost_password.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%= form_for(:user , :url=>{:controller=>'login',:action=>'send_pass'}) do%> - -
    - - - - - - - - - - - -
    Email: *<%=text_field 'user', :email%>
    Account Name: *<%=text_field 'user', :account_name%>
    - <%=submit_tag "Email Password Reset Instructions"%> -
    -
    * required
    -
    - - -<%end%> \ No newline at end of file diff --git a/app/views/login/lost_password.html.haml b/app/views/login/lost_password.html.haml new file mode 100644 index 0000000000..d40b2be6e9 --- /dev/null +++ b/app/views/login/lost_password.html.haml @@ -0,0 +1,13 @@ +%div.password-reset + = form_with scope: :user , url: login_send_pass_path do |f| + %h3 Forgot your password? + %p.text-muted.mb-5 + = "Enter your email and #{$SITE} username to receive a password reset link." + %div.mb-3 + = f.label :email, 'Email', class: 'form-label' + = f.email_field :email, class: 'form-control' + %div.mb-3 + = f.label :account_name, 'Username', class: 'form-label' + = f.text_field :account_name, class: 'form-control' + %div + = submit_tag 'Reset password', class: 'btn btn-primary' diff --git a/app/views/ontologies/_metadata.html.haml b/app/views/ontologies/_metadata.html.haml index aa5c44a6dc..b166fda198 100644 --- a/app/views/ontologies/_metadata.html.haml +++ b/app/views/ontologies/_metadata.html.haml @@ -49,6 +49,10 @@ %tr %td Groups %td= groups.map {|g| groups_hash[g].name}.sort.join(", ") + - unless @submission_latest.naturalLanguage.empty? + %tr + %td Language + %td= abbreviations_to_languages(@submission_latest.naturalLanguage).join(", ") = raw additional_details -# Submissions pane diff --git a/app/views/ontologies/_treeview.html.haml b/app/views/ontologies/_treeview.html.haml index 3640b54510..20db396a8b 100644 --- a/app/views/ontologies/_treeview.html.haml +++ b/app/views/ontologies/_treeview.html.haml @@ -2,4 +2,4 @@ %ul.simpleTree %li.root %ul - = draw_tree(@root, @concept.id) # application_helper::draw_tree + = draw_tree(@root, @concept.id, @submission) # application_helper::draw_tree diff --git a/app/views/passwords/edit.html.haml b/app/views/passwords/edit.html.haml new file mode 100644 index 0000000000..83b776e7d8 --- /dev/null +++ b/app/views/passwords/edit.html.haml @@ -0,0 +1,18 @@ +%div.container + %div.edit-user-info + = form_with url: password_path, method: 'patch', class: 'my-4' do |f| + - if @errors.present? + %div{class: 'alert alert-danger mb-4'} + - for error in @errors + %div= error + %h2{class: 'pb-2 border-bottom'} Change password + = tag.p "Enter a new password for your #{$SITE} account (#{@user.username})", class: 'text-muted mb-4' + %div.mb-3 + = f.label :password, 'New password', class: 'form-label' + = f.password_field :password, class: 'form-control', required: 'true' + %div.mb-4 + = f.label :password_confirmation, 'Confirm new password', class: 'form-label' + = f.password_field :password_confirmation, class: 'form-control', required: 'true' + %div + = submit_tag('Update', class: 'btn btn-primary me-1') + = link_to 'Cancel', user_path(@user.username), class: 'btn btn-primary' diff --git a/app/views/search/_concept_preview.html.erb b/app/views/search/_concept_preview.html.erb deleted file mode 100644 index 5d8cdc1fe1..0000000000 --- a/app/views/search/_concept_preview.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
    -Preview of <%=link_to "#{@concept.label_html},#{@concept.id}",uri_url(:ontology=>@ontology,:conceptid=>@concept.id),:target=>"_new"%> - -
    - - -<%@concept.properties.each_pair{|key,value| %> - - - - - -<% } %> - -
    <%="#{remove_owl_notation(key)}:"%><%="#{value}"%>
    - -
    \ No newline at end of file diff --git a/app/views/search/_concepts.html.erb b/app/views/search/_concepts.html.erb deleted file mode 100644 index b0593ccee0..0000000000 --- a/app/views/search/_concepts.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%if @concepts.size > 0%> -
    -
    - -
    - -
    - -
    Comment
    <%=text_area(:mapping, :comment, :size => "30x10")%>
    -
    - -
    - <%= select("mapping", "directionality", { "Bidirectional" => "bidirectional", "Unidirectional" => "unidirectional" })%>

    - <%= submit_tag "Create", :class=>'blueButton'%>    -
    -<%else%> -

    No Results Found

    -<%end%> \ No newline at end of file diff --git a/app/views/search/_results.html.erb b/app/views/search/_results.html.erb deleted file mode 100644 index 314707ad38..0000000000 --- a/app/views/search/_results.html.erb +++ /dev/null @@ -1,69 +0,0 @@ - -<%if @results.size > 0%> - - <%if params[:page].to_i > 1 %> - << Previous Page - - <%end%> - Page <%=params[:page] || 1%> of <%=@pages%> - <%if !params[:page].to_i.eql?(@pages.to_i)%> - Next Page >> - - <%end%> - - - - - - - - - - - - - - <%for result in @results%> - <%begin - #catch exceptions and dont draw row - row = " - - - - "%> - <%=row%> - <%rescue - end - %> - <%end%> - - - - - -
    - <%if params[:page].to_i >1 %> << Previous Page <%end%> Page <%=params[:page] || 1%> of <%=@pages%> <%if !params[:page].to_i.eql?(@pages.to_i)%> Next Page >><%end%> -
    Class NameFound InOntology
    #{link_to highlight(result[:contents],@keyword), uri_url(:ontology => result[:ontologyVersionId], :conceptid => result[:conceptIdShort]), :onclick => "YAHOO.tabwait.container.wait.show();"}#{result[:recordType].titleize.gsub("Record Type","")}#{link_to result[:ontologyDisplayLabel], ontology_path(:id=>result[:ontologyVersionId])}
    - <%if params[:page].to_i >1 %> << Previous Page <%end%> Page <%=params[:page] || 1%> of <%=@pages%> <%if !params[:page].to_i.eql?(@pages.to_i)%> Next Page >><%end%> -
    -<%else%> -

    No Results Found

    -<%end%> diff --git a/app/views/users/_form.html.haml b/app/views/users/_form.html.haml deleted file mode 100644 index a3306597fe..0000000000 --- a/app/views/users/_form.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -%table.form{:width => "70%"} - %tr - %th First Name: - %td.top= text_field :user, :firstName, value: @user.firstname - %tr - %th Last Name: - %td= text_field :user, :lastName, value: @user.lastname - %tr - %th Account Name:* - %td= text_field :user, :username, value: @user.username - %tr - %th Email Address:* - %td= text_field :user, :email, value: @user.email - %tr - %th Re-enter Email Address:* - %td= text_field :user, :email_confirmation, value: @user.email_confirmation - %tr - %th Password:* - %td= password_field :user, :password, :autocomplete => "off" - %tr - %th Re-enter Password:* - %td= password_field :user, :password_confirmation, :autocomplete => "off" - - if using_captcha? - %tr - %th - %td= recaptcha_tags - - unless $ANNOUNCE_LIST.nil? || $ANNOUNCE_LIST.empty? - %tr - %td{:colspan => "2", :style => "text-align: right;"} - %div{:style => "float: right; margin-left: 10px; position: relative; top: 5px;"} - %input#user_register_mail_list{:checked => "checked", :name => "user[register_mail_list]", :type => "checkbox", :value => "1"}/ - %div{:style => "overflow: hidden; line-height: 12px;"} - %label{:for => "user_register_mail_list"} - Register for the #{$SITE} announcements email list - %br/ - %span{:style => "font-size: x-small;"} (approximately two emails per month -- confirmation via email required) - %tr - %td{:colspan => "2", :style => "text-align: right;"}= submit_tag "Create" diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml index cd3f31a335..a9476331d9 100644 --- a/app/views/users/edit.html.haml +++ b/app/views/users/edit.html.haml @@ -1,32 +1,5 @@ -:css - .required { - color: black; - } - table.form { - width: auto; - } - table.form td { - width: 500px; - } - :javascript - /* refer to input fields by name for rules */ - /* refer to input fields by id for equalTo */ jQuery(document).ready(function() { - jQuery("#user_info").validate({ - rules: { - "user[email]": { - required: true, - email: true, - }, - "user[password]": "required", - "user[password_confirmation]": { - required: true, - equalTo: "#user_password", - } - } - }) - jQuery('#user_info').on('submit', function (event) { event.preventDefault() let admin_checkbox = jQuery('input[id=user_admin]') @@ -52,94 +25,46 @@ }) }); - (function (jQuery) { - // custom css expression for a case-insensitive contains() - jQuery.expr[':'].Contains = function(a,i,m){ - return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0; - }; - - function listFilter(header, list) { // header is any element, list is an unordered list - // create and add the filter form to the header - var form = jQuery("
    ").attr({"class":"filterform","action":"#"}), - input = jQuery("").attr({"class":"filterinput","type":"text"}); - jQuery(form).append(input).appendTo(header); - - jQuery(input) - .change( function () { - var filter = jQuery(this).val(); - if(filter) { - // this finds all links in a list that contain the input, - // and hide the ones not containing the input while showing the ones that do - jQuery(list).find("a:not(:Contains(" + filter + "))").closest("li").hide(); - jQuery(list).find("a:Contains(" + filter + ")").closest("li").show(); - } else { - jQuery(list).find("li").show(); - } - return false; - }) - .keyup( function () { - // fire the above change event after every letter - jQuery(this).change(); - }); - } - }) - - - -%div{:style => "padding: 1em;"} - = form_for(:user, :url => user_path(@user.username), :html => { :id => "user_info", :method => :put }) do |f| - - unless @errors.nil? - .enable-lists{:style => "color: red; padding: 1em;"} - Errors creating your account: - %ul - - for error in @errors - %li= error - %table.form - - if @user.validate_password.nil? - %tr - %th - First Name - %td.top= text_field :user, :firstName, value: @user.firstName - %tr - %th - Last Name - %td= text_field :user, :lastName, value: @user.lastName - %tr - %th - Email Address * - %td - = text_field :user, :email, value: @user.email - - if session[:user].admin? - %tr - %th - Grant admin privileges - %td - = check_box :user, :admin?, { class: (@user.admin? ? 'admin' : '') } - \   - = hidden_field :user, :username, value: @user.username - - else - = hidden_field :user, :username, value: @user.username - = hidden_field :user, :firstName, value: @user.firstName - = hidden_field :user, :lastName, value: @user.lastName - = hidden_field :user, :email, value: @user.email - %tr - %th - Password: * - %td.top - = password_field :user, :password -    - %tr - %th - Re-enter Password: * - %td - = password_field :user, :password_confirmation -    +%div.container + %div.edit-user-info + = form_with scope: :user, url: user_path(@user.username), method: 'put', id: 'user_info', class: 'my-4' do |f| + - if @errors.present? + .enable-lists + Errors updating account: + %ul + - for error in @errors + %li= error - %tr - %td{:align => "left", :colspan => "2"} - = submit_tag "Update", :class=>"greenbtn btn btn-outline-secondary btn-sm " - \     - - unless params[:password].eql?("true") - = link_to "Change Password", edit_user_path(@user.username, password: true) - -# use @user.username unstead of sessions - %br/ + %h2.mb-4 Edit account + %div.mb-3 + = f.label :firstName, 'First name', class: 'form-label' + = f.text_field :firstName, class: 'form-control', required: 'true' + %div.mb-3 + = f.label :lastName, 'Last name', class: 'form-label' + = f.text_field :lastName, class: 'form-control' + %div.mb-3 + = f.label :username, class: 'form-label' + = f.text_field :username, class: 'form-control', disabled: 'true' + %div.mb-3 + = f.label :email, 'Email address', class: 'form-label' + = f.email_field :email, class: 'form-control', required: 'true' + %div.mb-3 + = f.label :githubId, 'GitHub username', class: 'form-label' + = f.text_field :githubId, class: 'form-control', 'aria-describedby': 'githubUsernameHelp', + pattern: '[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}', + title: 'Characters: only alphanumeric and single hyphens. Length: 1-39' + %div.form-text{id: 'githubUsernameHelp'} + User handle on GitHub, for example: 'octocat' + %div.mb-3 + = f.label :orcidId, 'ORCID iD', class: 'form-label' + = f.text_field :orcidId, class: 'form-control', 'aria-describedby': 'orcidHelp', + pattern: '\d{4}-\d{4}-\d{4}-\d{4}', title: 'Format: XXXX-XXXX-XXXX-XXXX' + %div.form-text{id: 'orcidHelp'} + 16-digit ORCID, for example: 0000-1234-5678-9101 + - if session[:user].admin? + %div.form-check.mb-3 + = f.check_box :admin?, class: (@user.admin? ? 'admin form-check-input' : 'form-check-input') + = f.label :admin, 'Grant admin privileges', class: 'form-check-label' + %div.mb-5 + = submit_tag('Update', class: 'btn btn-primary me-1') + = link_to 'Cancel', user_path(@user.username), class: 'btn btn-primary' diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index 8f09e6e474..de7a11c266 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -1,12 +1,45 @@ - @title = "Register" -%div{:style => "padding: 1em;"} - %h1 - New #{$SITE} Account - = form_for(:user, :url => users_path) do |f| - - unless @errors.nil? - .enable-lists{:style => "color: red; padding: 1em;"} - Errors creating your account: + +%div.signup + = form_with scope: :user, url: users_path, class: 'form-signup' do |f| + %h4.mb-4 Create an account + + -# TODO: display errors in a standard Rails flash message + - if @errors.present? + %div.enable-lists + Errors creating account: %ul - for error in @errors %li= error - = render :partial => 'form', :locals => {:f => f} + + %div.form-floating.mb-3 + = f.text_field(:firstName, class: 'form-control', placeholder: 'First name', required: 'true') + = f.label(:firstName, 'First name', class: 'form-label') + + %div.form-floating.mb-3 + = f.text_field(:lastName, class: 'form-control', placeholder: 'Last name (optional)') + = f.label(:lastName, 'Last name (optional)', class: 'form-label') + + %div.form-floating.mb-3 + = f.text_field(:username, class: 'form-control', placeholder: 'Username', required: 'true') + = f.label(:username, class: 'form-label') + + %div.form-floating.mb-3 + = f.email_field(:email, class: 'form-control', placeholder: 'Email address', required: 'true') + = f.label(:email, class: 'form-label') + + %div.form-floating.mb-3 + = f.password_field(:password, class: 'form-control', placeholder: 'Password', required: 'true', + autocomplete: 'new-password') + = f.label(:password, class: 'form-label') + + - if using_captcha? + %div.mb-4 + = recaptcha_tags + + %div.d-grid + = submit_tag('Sign Up', class: 'btn btn-lg btn-primary btn-block') + + %p.text-muted.mt-2 + Already have an account? + = link_to('Log in', login_index_path) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c4d7866b80..28f42ff7c6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,67 +1,81 @@ - @title = "Account Information" -%div{style: "padding: 1em;"} - %table.form +%div{class: 'container account-info my-4'} + %h4{class: 'pb-2 mb-4 border-bottom'} + Account basics + = tag.small("(#{@user.username})", class: 'text-muted') + + %table.table %tr - %th.top First Name: - %td.top= @user.firstName + %th First name + %td= @user.firstName %tr - %th Last Name: + %th Last name %td= @user.lastName %tr - %th Email Address: + %th Email %td= @user.email %tr - %th Account Name: - %td= @user.username + %th GitHub username + %td= @user.githubId %tr - %td{colspan: "2", style: "text-align: right;"} - = link_to "Edit Information", edit_user_path(url_encode(@user.username)) - %br/ - %span{style: "font-size: large; font-weight: bold;"} - API Key - %span{style: "font-weight: normal; padding-left: 1em; font-size: x-small; color: gray;"} Your API Key can be used to access the NCBO API (REST) Services - %p{style: "padding: .5em 0 0 1em;"} - = session[:user].apikey - %br/ + %th ORCID iD + %td= @user.orcidId + + %div{class: 'd-flex justify-content-end'} + = link_to('Edit account', edit_user_path(@user.username), class: 'btn btn-sm btn-outline-secondary') + = link_to('Change password', edit_password_path, class: 'btn btn-sm btn-outline-secondary ms-2') + -# User's API key + %h4{class: 'pb-2 my-4 border-bottom'} API Key + = tag.p("Your API key can be used to access the #{$SITE} RESTful API and services", class: 'text-muted') + %div{'data-controller': 'clipboard'} + %span{'data-clipboard-target': 'source'}= session[:user].apikey + %div{class: 'd-flex align-items-center mt-2'} + = tag.button 'Copy to clipboard', 'data-action': 'clipboard#copy', class: 'btn btn-sm btn-outline-secondary' + %span{'data-clipboard-target': 'copiedIndicator', class: 'hidden text-success fw-semibold ms-2'} Copied! - - unless @user.subscription.nil? || @user.subscription.empty? - %h2 Subscriptions - %table + -# User's subscriptions + - unless @user.subscription.blank? + %h4{class: 'pb-2 mt-5 mb-4 border-bottom'} Subscriptions + %table.table + %thead.table-light + %tr + %th Ontology + %th Subscription type + %th   - @user.subscription.each do |subscription| - ont = (!subscription[:ontology].nil? ? subscription[:ontology].split('/').last: nil) # ensure we get the acronym - type = (!subscription[:notification_type].nil? ? subscription[:notification_type].downcase : nil) - %tr{style: "padding: 5px;"} - %td{style: "padding: 5px;"} - %a{href: "/ontologies/#{ont}"}= ont - %td{style: "padding: 5px;"} - - if type == "notes" - %a{href: "/ontologies/#{ont}?p=notes"}= type + %tr + %td + = link_to(ont, ontology_path(ont)) + %td + - if type == 'notes' + = link_to(type, ontology_path(ont, p: 'notes')) - else = type - %td{style: "padding: 5px;"} - .subscribe_to_ontology{style: "float: left;"} + %td + .subscribe_to_ontology = raw subscribe_ontology_button(ont, @user) - %br/ - - unless @user_projects.nil? || @user_projects.empty? - %h2#user_projects{style: "font-size: large;"} - Projects Created - %ul{style: "padding-left: 1em;"} - - @user_projects.each do |project| + -# Projects this user created + - unless @user_projects.blank? + %h4{class: 'pb-2 mt-5 mb-4 border-bottom'} Projects + = tag.p("Projects you created in #{$SITE}", class: 'text-muted') + %ul.list-unstyled + - @user_projects.each do |p| %li - %a{href: "/projects/#{project.acronym}"}= project.name - %br/ + = link_to(p.name, project_path(p.acronym)) - - unless @admin_ontologies.nil? || @admin_ontologies.empty? - %h2#admin_ontologies{style: "font-size: large;"} - Submitted Ontologies - %ul{style: "padding-left: 1em;"} + -# Ontologies this user administers + - unless @admin_ontologies.blank? + %h4{class: 'pb-2 mt-5 mb-4 border-bottom'} Ontologies + = tag.p("Ontologies you have administrative access to in #{$SITE}", class: 'text-muted') + %ul.list-unstyled - @admin_ontologies.each do |ont| %li - %a{href: "/ontologies/#{ont.acronym}"}= ont.name - %br + = link_to(ont.name, ontology_path(ont.acronym)) :javascript jQuery(document).ready(function(){ @@ -106,33 +120,22 @@ jQuery("#custom_ontologies").hide(); } - %h2#custom_ontology_set{style: "font-size: large;"} - Custom Ontology Set - #custom_ontologies.enable-lists + %h4{class: 'pb-2 mt-5 mb-4 border-bottom'} Custom ontology set + #custom_ontologies - if at_slice? - %p{style: "padding-left: 7px; font-size: 10pt; margin: -3px 0 7px;"} - Please - %a{href: "#{$UI_URL}/account"} visit the main site - to modify your Custom Ontology Set. + = custom_ontology_set_slice_text - else - %p{style: "padding-left: 7px; font-size: 9pt; color: gray; margin: -3px 0 7px;"} - %b - Customize your #{$SITE} display: - Pick the ontologies that you want to see and #{$SITE} will hide all other ontologies.
    - %b - Note: - this feature works only when you are logged in. - %p - %span{style: "font-weight: normal; font-size: 9pt; padding-left: 7px;"} - %a#edit_custom_ontologies{href: "javascript:void(0);"} select ontologies - - if @user_ontologies && !@user_ontologies.empty? - %ul + = custom_ontology_set_intro_text + = button_tag('Select ontologies', type: 'button', id: 'edit_custom_ontologies', class: 'btn btn-primary') + - if @user_ontologies.present? + %ul{class: 'list-unstyled mt-3 mb-5'} - @user_ontologies.each do |ont| - - ont = LinkedData::Client::Models::Ontology.get(ont) + - ont = LinkedData::Client::Models::Ontology.get(ont, include: 'name,acronym') %li #{ont.name} (#{ont.acronym}) - else - %p{style: "padding-left: 7px;"} You haven't picked any ontologies yet + = tag.p("You haven't picked any ontologies yet", class: 'mt-3 mb-5') + #custom_ontologies_picker{style: "left: -9999px; position: absolute;"} = form_tag custom_ontologies_path(url_encode(@user.username)) do - selected = @user.customOntology.map {|o| LinkedData::Client::Models::Ontology.get(o).acronym} diff --git a/config/locales/en.yml b/config/locales/en.yml index 8562532e75..e3187b171e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,4 @@ en: - activerecord: errors: models: @@ -48,7 +47,7 @@ en: find_ontology_placeholder: Start typing ontology name, then choose from list query_placeholder: Enter a class, e.g. Melanoma tagline: the world's most comprehensive repository of biomedical ontologies - title: Welcome to the %{organization} + title: Welcome to the %{organization} welcome: Welcome to %{site}, help: welcome: Welcome to the National Center for Biomedical Ontology’s %{site}. %{site} is a web-based application for accessing and sharing biomedical ontologies. diff --git a/config/routes.rb b/config/routes.rb index f26fb3ad8a..4e126f8bf4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -107,6 +107,8 @@ post '/accounts/:id/custom_ontologies' => 'users#custom_ontologies', :as => :custom_ontologies get '/login_as/:login_as' => 'login#login_as', constraints: { login_as: /[\d\w\.\-\%\+ ]+/ } post '/login/send_pass', to: 'login#send_pass' + get 'password', to: 'passwords#edit', as: :edit_password + patch 'password', to: 'passwords#update' # Search get 'search', to: 'search#index' diff --git a/db/schema.rb b/db/schema.rb index fbc53cdaba..5d5db5ddc7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -28,20 +28,6 @@ t.datetime "updated_at", precision: nil, null: false end - create_table "margin_notes", id: :integer, charset: "utf8", force: :cascade do |t| - t.integer "parent_id" - t.integer "mapping_id" - t.integer "note_type" - t.integer "user_id" - t.integer "ontology_id" - t.integer "ontology_version_id" - t.string "concept_id" - t.string "subject" - t.text "comment" - t.datetime "created_at", precision: nil - t.datetime "updated_at", precision: nil - end - create_table "ontologies", charset: "utf8", force: :cascade do |t| t.string "acronym", null: false t.text "new_term_instructions" diff --git a/yarn.lock b/yarn.lock index 6da874dbfd..7da15e91d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,120 +2,120 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== - -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== - -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== - -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== - -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== - -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== - -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== - -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== - -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== - -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== - -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== - -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== - -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== - -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== - -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== - -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== - -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== - -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== - -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== - -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== - -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== - -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== - -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== "@hotwired/stimulus@^3.2.2": version "3.2.2" @@ -123,17 +123,17 @@ integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== "@hotwired/turbo-rails@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.3.tgz#e60375f4eea4b30ec0cd6d7e3fdb3d6349a2b57b" - integrity sha512-n5B9HdFsNiGJfXFAriCArmvFZyznIh/OriB5ZVAWz4Fsi4oLkpgmJNw5pibBAM7NMQQGN6cfKa/nhZT4LWcqbQ== + version "8.0.4" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.4.tgz#d224f524a9e33fe687cec5d706054eb6fe13fa5b" + integrity sha512-GHCv5+B2VzYZZvMFpg/g9JLx/8pl/8chcubSB7T+Xn1zYOMqAKB6cT80vvWUzxdwfm/2KfaRysfDz+BmvtjFaw== dependencies: - "@hotwired/turbo" "^8.0.3" + "@hotwired/turbo" "^8.0.4" "@rails/actioncable" "^7.0" -"@hotwired/turbo@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.3.tgz#338e07278f4b3c76921328d3c92dbc4831c209d0" - integrity sha512-qLgp7d6JaegKjMToTJahosrFxV3odfSbiekispQ3soOzE5jnU+iEMWlRvYRe/jvy5Q+JWoywtf9j3RD4ikVjIg== +"@hotwired/turbo@^8.0.4": + version "8.0.4" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.4.tgz#5c5361c06a37cdf10dcba4223f1afd0ca1c75091" + integrity sha512-mlZEFUZrJnpfj+g/XeCWWuokvQyN68WvM78JM+0jfSFc98wegm259vCbC1zSllcspRwbgXK31ibehCy5PA78/Q== "@rails/actioncable@^7.0": version "7.1.3" @@ -141,30 +141,30 @@ integrity sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA== esbuild@^0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2"