Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/assets/images/bip-minimal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
@import "pages/about";
@import "pages/account";
@import "pages/activities";
@import "pages/bip-scholar";
@import "pages/collaborations";
@import "pages/collection";
@import "pages/curators";
Expand Down
110 changes: 110 additions & 0 deletions app/assets/stylesheets/pages/bip-scholar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
.bip-scholar-embed {
margin: 6px 0;
display: block;

.bip-scholar-badge {
* { box-sizing: border-box; }

display: inline-flex;
align-items: center;
gap: 0;
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 8px 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
cursor: pointer;
font-family: 'Nunito', sans-serif;
position: relative;
transition: box-shadow 0.2s;
text-decoration: none !important; /* Force override global links */
color: inherit !important;

&:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.14); }
}

.bip-scholar-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 10px;
min-width: 54px;

& + .bip-scholar-stat { border-left: 1px solid #e8e8e8; }
}

.bip-scholar-stat--group-sep {
border-left: 2px solid #c8c8c8 !important;
margin-left: 2px;
}

.bip-scholar-stat-value {
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.2;
white-space: nowrap;
}

.bip-scholar-stat-label {
font-size: 10px;
font-weight: 600;
color: #9e9e9e;
text-transform: uppercase;
letter-spacing: 0.03em;
margin-top: 2px;
white-space: nowrap;
}

.bip-scholar-stat-icon {
font-size: 11px;
margin-right: 3px;
color: #757575;
}

.bip-scholar-badge--compact {
.bip-scholar-stat {
min-width: auto;
padding: 0 8px;
}
.bip-scholar-stat-value { font-size: 14px; }
.bip-scholar-stat-icon {
font-size: 12px;
margin-right: 5px;
}
}

.bip-scholar-logo {
display: flex;
align-items: center;
padding-right: 12px;
margin-right: 2px;
border-right: 1px solid #e8e8e8;
font-weight: bold;
color: #1a1a1a;
}

.bip-scholar-no-profile {
display: inline-flex;
align-items: center;
gap: 7px;
background: #fafafa;
border: 1px dashed #d0d0d0;
border-radius: 10px;
padding: 7px 14px;
font-family: 'Nunito', sans-serif;
font-size: 12px;
color: #aaa;

a {
color: #2e7d32;
font-weight: 600;
text-decoration: none;
opacity: 0.8;
&:hover {
text-decoration: underline;
opacity: 1;
}
}
}
}
47 changes: 47 additions & 0 deletions app/helpers/bip_scholar_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module BipScholarHelper
require 'net/http'
require 'cgi'
require 'json'

def self.fetch_score(orcid)
return nil if orcid.blank?

# Cache in Redis for 24 hours to prevent blocking TeSS web threads
Rails.cache.fetch("bip_scholar_score_#{orcid}", expires_in: 24.hours, skip_nil: true) do # `skip_nil: true` to avoid caching nil and then make the widget disappear
uri = URI("https://bip-api.imsi.athenarc.gr/scholar/scores/#{CGI.escape(orcid)}")

# Use timeout to prevent hanging connections
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 3
http.open_timeout = 2

response = http.get(uri.request_uri)

response.is_a?(Net::HTTPSuccess) ? JSON.parse(response.body) : nil
rescue StandardError => e
Rails.logger.error("BIP!Scholar API Error for #{orcid}: #{e.message}")
nil
end
end

def format_scholar_number(num)
num.present? ? number_with_delimiter(num) : "—"
end

def parse_work_types(data)
wt = data['work_types_num']
if wt.is_a?(Array)
wt
elsif wt.is_a?(Hash)
[
wt['papers'],
wt['dataset'] || wt['datasets'],
wt['software'],
wt['other']
]
else
[nil, nil, nil, nil]
end
end
end
59 changes: 59 additions & 0 deletions app/views/common/_bip_scholar_infographics.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<%
# for the `layout` and `empty_mode` parameters, see https://github.com/athenarc/bip-plugin/tree/main
data = BipScholarHelper.fetch_score(orcid)
is_compact = local_assigns.fetch(:layout, 'default') == 'compact' # compact allows to hide most of the words, and shows only icons and numbers
empty_mode = local_assigns.fetch(:empty_mode, 'default') # empty_mode hides BIP!Scholar widget if no ORCID on BIP!Scholar is found
%>

<div class="bip-scholar-embed" data-orcid="<%= orcid %>" <%= 'hidden' if data.nil? && empty_mode == 'silent' %>>
<% if data.nil? %>
<% unless empty_mode == 'silent' %>
<div class="bip-scholar-no-profile">
<i class="fa fa-circle-info" aria-hidden="true"></i>
No BIP! Scholar profile found for this ORCID.
<%= link_to "Learn more ↗", "https://bip.imsi.athenarc.gr/scholar", target: "_blank", rel: "noopener" %>
</div>
<% end %>
<% else %>
<%
wt = parse_work_types(data)
pct = data.dig('openness', 'open_percentage') ? "#{data['openness']['open_percentage'].to_i}%" : "—"
age = data['academic_age'] ? "#{data['academic_age']} yrs" : "—"
profile_url = "https://bip.imsi.athenarc.gr/scholar/profile/#{CGI.escape(orcid)}"

stats = [
{ icon: "file-text-o", value: format_scholar_number(wt[0]), label: "Publications" },
{ icon: "database", value: format_scholar_number(wt[1]), label: "Datasets" },
{ icon: "code", value: format_scholar_number(wt[2]), label: "Software" },
{ icon: "archive", value: format_scholar_number(wt[3]), label: "Other" },
{ icon: "quote-left", value: format_scholar_number(data['citations_num']), label: "Citations", sep: true },
{ icon: "fire", value: format_scholar_number(data['popular_works_count']), label: "Popular" },
{ icon: "university", value: format_scholar_number(data['influential_works_count']), label: "Influential" },
{ icon: "line-chart", value: format_scholar_number(data['h_index']), label: "h-index" },
{ icon: "hourglass", value: age, label: "Acad. Age", sep: true },
{ icon: "unlock-alt", value: pct, label: "Open Access", sep: true }
]
%>

<%= link_to profile_url, target: "_blank", rel: "noopener", class: "bip-scholar-badge#{' bip-scholar-badge--compact' if is_compact}" do %>
<div class="bip-scholar-logo">
<%= image_tag "bip-minimal.svg", alt: "BIP! Scholar", style: "height: 30px; width: auto; display: block;" %>
</div>

<% stats.each do |stat| %>
<div class="bip-scholar-stat <%= 'bip-scholar-stat--group-sep' if stat[:sep] %>" title="<%= stat[:label] %>">
<span class="bip-scholar-stat-value">
<i class="fa fa-<%= stat[:icon] %> bip-scholar-stat-icon" aria-hidden="true"></i>
<% if is_compact %>
<span class="sr-only"><%= stat[:label] %>: </span>
<% end %>
<%= stat[:value] %>
</span>
<% unless is_compact %>
<span class="bip-scholar-stat-label"><%= stat[:label] %></span>
<% end %>
</div>
<% end %>
<% end %>
<% end %>
</div>
3 changes: 3 additions & 0 deletions app/views/trainers/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
<div class="url-wrap">
<%= orcid_link(@trainer, class: 'h5', track: true) %>
</div>

<!-- BIP!Scholar Profile -->
<%= render 'common/bip_scholar_infographics', orcid: @trainer.orcid, layout: 'default', empty_mode: 'default' %>
<% end %>

<!-- Field: description -->
Expand Down
60 changes: 60 additions & 0 deletions test/controllers/trainers_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,64 @@ class TrainersControllerTest < ActionController::TestCase
assert_not_nil external_resource
assert_equal external_resource.url, json_event['mentions'].first['url']
end

test 'should show bip-scholar embed div when trainers feature is enabled' do
with_settings(feature: { trainers: true }) do
get :show, params: { id: @trainer }
assert_response :success
assert_select 'div.bip-scholar-embed[data-orcid="' + @trainer.orcid + '"]'
end
end

test 'should not show bip-scholar embed div when trainers feature is disabled' do
with_settings(feature: { trainers: false }) do
assert_raises ActionController::RoutingError do
get :show, params: { id: @trainer }
end
end
end

test 'should show default fallback message when no profile found and empty_mode is default' do
BipScholarHelper.stub :fetch_score, nil do
get :show, params: { id: @trainer }
assert_response :success

assert_select 'div.bip-scholar-no-profile', text: /No BIP! Scholar profile found/
end
end

test 'should show bip-scholar embed in compact layout' do
fake_data = {
'work_types_num' => [12, 3, 4, 1],
'citations_num' => 5,
'popular_works_count' => 1,
'influential_works_count' => 2,
'h_index' => 3,
'academic_age' => 10,
'openness' => { 'open_percentage' => 80 }
}
BipScholarHelper.stub :fetch_score, fake_data do
html = @controller.render_to_string(
partial: 'common/bip_scholar_infographics',
locals: { orcid: @trainer.orcid, layout: 'compact', empty_mode: 'default' }
)
doc = Nokogiri::HTML(html)

assert_equal 1, doc.css('a.bip-scholar-badge.bip-scholar-badge--compact').size
end
end

test 'should show nothing when empty_mode is silent' do
BipScholarHelper.stub :fetch_score, nil do
# Simulates passing empty_mode: 'silent' in view partial render
html = @controller.render_to_string(
partial: 'common/bip_scholar_infographics',
locals: { orcid: @trainer.orcid, layout: 'default', empty_mode: 'silent' }
)
doc = Nokogiri::HTML(html)

assert_equal 0, doc.css('div.bip-scholar-no-profile').size
assert_equal 0, doc.css('a.bip-scholar-badge').size
end
end
end