├── .coveralls.yml ├── app ├── assets │ ├── images │ │ └── orcid │ │ │ └── .keep │ ├── stylesheets │ │ └── orcid │ │ │ └── application.css │ └── javascripts │ │ └── orcid │ │ └── application.js ├── services │ └── orcid │ │ ├── remote.rb │ │ └── remote │ │ ├── service.rb │ │ ├── profile_query_service │ │ ├── search_response.rb │ │ ├── query_parameter_builder.rb │ │ └── response_parser.rb │ │ ├── profile_query_service.rb │ │ ├── profile_creation_service.rb │ │ └── work_service.rb ├── views │ ├── orcid │ │ └── profile_connections │ │ │ ├── _orcid_disconnector.html.erb │ │ │ ├── _authenticated_connection.html.erb │ │ │ ├── _pending_connection.html.erb │ │ │ ├── _orcid_connector.html.erb │ │ │ ├── new.html.erb │ │ │ └── _options_to_connect_orcid_profile.html.erb │ └── layouts │ │ └── orcid │ │ └── application.html.erb ├── helpers │ └── orcid │ │ └── on_demand_url_helper.rb ├── models │ └── orcid │ │ ├── work │ │ ├── xml_renderer.rb │ │ └── xml_parser.rb │ │ ├── profile_request.rb │ │ ├── profile.rb │ │ ├── work.rb │ │ ├── profile_status.rb │ │ └── profile_connection.rb ├── controllers │ └── orcid │ │ ├── create_profile_controller.rb │ │ ├── application_controller.rb │ │ └── profile_connections_controller.rb └── templates │ └── orcid │ └── work.template.v1.2.xml.erb ├── lib ├── orcid │ ├── version.rb │ ├── engine.rb │ ├── named_callbacks.rb │ ├── configuration.rb │ ├── configuration │ │ └── provider.rb │ ├── exceptions.rb │ └── spec_support.rb ├── generators │ └── orcid │ │ └── install │ │ ├── templates │ │ └── orcid_initializer.rb.erb │ │ └── install_generator.rb ├── orcid.rb └── tasks │ └── orcid_tasks.rake ├── spec ├── test_app_templates │ ├── Gemfile.extra │ └── lib │ │ └── generators │ │ └── test_app_generator.rb ├── factories │ ├── users.rb │ └── orcid_profile_requests.rb ├── support │ ├── non_orcid_models.rb │ └── stub_callback.rb ├── fast_helper.rb ├── controllers │ └── orcid │ │ ├── application_controller_spec.rb │ │ ├── create_profile_controller_spec.rb │ │ └── profile_connections_controller_spec.rb ├── routing │ └── orcid │ │ └── profile_request_routing_spec.rb ├── models │ └── orcid │ │ ├── work │ │ ├── xml_renderer_spec.rb │ │ └── xml_parser_spec.rb │ │ ├── profile_request_spec.rb │ │ ├── work_spec.rb │ │ ├── profile_spec.rb │ │ ├── profile_connection_spec.rb │ │ └── profile_status_spec.rb ├── views │ └── orcid │ │ └── profile_connections │ │ ├── _options_to_connect_orcid_profile.html.erb_spec.rb │ │ ├── _authenticated_connection.html.erb_spec.rb │ │ ├── _pending_connection.html.erb_spec.rb │ │ ├── new.html.erb_spec.rb │ │ └── _orcid_connector.html.erb_spec.rb ├── features │ ├── profile_connection_feature_spec.rb │ ├── orcid_work_query_spec.rb │ ├── batch_profile_spec.rb │ ├── public_api_query_spec.rb │ └── non_ui_based_interactions_spec.rb ├── services │ └── orcid │ │ └── remote │ │ ├── profile_query_service │ │ ├── search_response_spec.rb │ │ ├── query_parameter_builder_spec.rb │ │ └── response_parser_spec.rb │ │ ├── service_spec.rb │ │ ├── work_service_spec.rb │ │ ├── profile_query_service_spec.rb │ │ └── profile_creation_service_spec.rb ├── lib │ ├── orcid │ │ ├── exceptions_spec.rb │ │ ├── named_callbacks_spec.rb │ │ ├── configuration_spec.rb │ │ └── configuration │ │ │ └── provider_spec.rb │ └── orcid_spec.rb ├── fixtures │ ├── orcid-remote-profile_query_service-response_parser │ │ ├── single-response-with-orcid-valid-profile.json │ │ └── multiple-responses-without-valid-response.json │ └── orcid_works.xml └── spec_helper.rb ├── .gitignore ├── .mailmap ├── db └── migrate │ ├── 20140205185339_update_orcid_profile_requests.rb │ └── 20140205185338_create_orcid_profile_requests.rb ├── bin └── rails ├── gemfiles ├── rails4.1.gemfile └── rails4.0.gemfile ├── config ├── routes.rb ├── application.yml.example └── locales │ └── orcid.en.yml ├── .travis.yml ├── script └── fast_specs ├── Gemfile ├── LICENSE ├── Rakefile ├── orcid.gemspec ├── CONTRIBUTING.md ├── README.md └── .hound.yml /.coveralls.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/orcid/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/orcid/version.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | VERSION = '0.9.1' 3 | end 4 | -------------------------------------------------------------------------------- /app/services/orcid/remote.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | module Remote 3 | end 4 | end -------------------------------------------------------------------------------- /spec/test_app_templates/Gemfile.extra: -------------------------------------------------------------------------------- 1 | # extra gems to load into the test app go here 2 | gem 'figaro' 3 | gem 'devise-multi_auth' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/internal 5 | Gemfile.lock 6 | tmp/ 7 | config/application.yml 8 | tags 9 | .tags* 10 | coverage/* -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Dan Brubaker Horst 2 | Glen Horton 3 | Carolyn Cole cam156 -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | # Read about factories at https://github.com/thoughtbot/factory_girl 2 | 3 | FactoryGirl.define do 4 | factory :user do 5 | email 'test@test.com' 6 | password '12345678' 7 | password_confirmation '12345678' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/non_orcid_models.rb: -------------------------------------------------------------------------------- 1 | module NonOrcid 2 | class Article 3 | attr_accessor :title 4 | 5 | def initialize(attributes = {}) 6 | attributes.each do |key, value| 7 | send("#{key}=", value) if respond_to?("#{key}=") 8 | end 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /app/views/orcid/profile_connections/_orcid_disconnector.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to "Disconnect from ORCID record", orcid.disconnect_path, class: 'btn btn-primary', data: { confirm: "Are you sure you want to disconnect your ORCID record? (You can re-connect later)"} %> 3 |
-------------------------------------------------------------------------------- /db/migrate/20140205185339_update_orcid_profile_requests.rb: -------------------------------------------------------------------------------- 1 | class UpdateOrcidProfileRequests < ActiveRecord::Migration 2 | def change 3 | add_column :orcid_profile_requests, :response_text, :text 4 | add_column :orcid_profile_requests, :response_status, :string, index: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/layouts/orcid/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Orcid 5 | <%= stylesheet_link_tag "orcid/application", media: "all" %> 6 | <%= javascript_include_tag "orcid/application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/orcid/engine', __FILE__) 6 | 7 | require 'rails/all' 8 | require 'rails/engine/commands' 9 | -------------------------------------------------------------------------------- /gemfiles/rails4.1.gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | file = File.expand_path("../../Gemfile", __FILE__) 4 | 5 | if File.exists?(file) 6 | puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` 7 | instance_eval File.read(file) 8 | end 9 | gem 'sass', '~> 3.2.15' 10 | gem 'sprockets', '~> 2.11.0' 11 | 12 | gem 'rails', '4.1.0' 13 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Orcid::Engine.routes.draw do 2 | scope module: 'orcid' do 3 | resource :profile_request, only: [:show, :new, :create, :destroy] 4 | resources :profile_connections, only: [:new, :create, :index] 5 | 6 | get 'create_orcid', to: 'create_profile#create' 7 | get "disconnect", to: "profile_connections#destroy" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /gemfiles/rails4.0.gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | file = File.expand_path("../../Gemfile", __FILE__) 4 | 5 | if File.exists?(file) 6 | puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` 7 | instance_eval File.read(file) 8 | end 9 | 10 | gem 'sass', '~> 3.2.15' 11 | gem 'sprockets', '~> 2.11.0' 12 | 13 | gem 'rails', '4.0.3' 14 | -------------------------------------------------------------------------------- /spec/fast_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/given' 2 | Dir[File.expand_path("../../app/*", __FILE__)].each do |dir| 3 | $LOAD_PATH << dir 4 | end 5 | $LOAD_PATH << File.expand_path("../../lib", __FILE__) 6 | require File.expand_path('../support/stub_callback', __FILE__) 7 | 8 | unless defined?(require_dependency) 9 | def require_dependency(*files) 10 | require *files 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - "2.1.3" 5 | - "2.0.0" 6 | 7 | gemfile: 8 | - gemfiles/rails4.1.gemfile 9 | - gemfiles/rails4.0.gemfile 10 | 11 | env: 12 | global: 13 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 14 | 15 | script: 'COVERAGE=true rake spec:travis' 16 | 17 | bundler_args: --without headless debug 18 | 19 | before_install: 20 | - gem install bundler 21 | -------------------------------------------------------------------------------- /app/views/orcid/profile_connections/_authenticated_connection.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= t('orcid.views.authenticated_connection', orcid_profile_url: Orcid.url_for_orcid_id(authenticated_connection.orcid_profile_id), orcid_profile_id: authenticated_connection.orcid_profile_id).html_safe %> 3 | <%= render partial: 'orcid/profile_connections/orcid_disconnector' %> 4 |
-------------------------------------------------------------------------------- /spec/factories/orcid_profile_requests.rb: -------------------------------------------------------------------------------- 1 | # Read about factories at https://github.com/thoughtbot/factory_girl 2 | 3 | FactoryGirl.define do 4 | factory :orcid_profile_request, :class => 'Orcid::ProfileRequest' do 5 | association :user, strategy: :create 6 | given_names 'All of the Names' 7 | family_name 'Under-the-sun' 8 | primary_email 'all-of-the-names@underthesun.com' 9 | primary_email_confirmation 'all-of-the-names@underthesun.com' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /script/fast_specs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -w 2 | 3 | require 'rake' 4 | 5 | # A helper function for answering: Does this spec run fast? 6 | module Fast 7 | module_function 8 | def spec?(fn) 9 | open(fn) { |f| f.gets =~ /fast_helper/ } 10 | end 11 | end 12 | 13 | fast_specs = FileList['spec/**/*_spec.rb'].select do |fn| 14 | Fast.spec?(fn) 15 | end 16 | 17 | if fast_specs.any? 18 | system "rspec #{fast_specs}" 19 | else 20 | puts 'Unable to find any fast specs' 21 | exit(-1) 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20140205185338_create_orcid_profile_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateOrcidProfileRequests < ActiveRecord::Migration 2 | def change 3 | create_table :orcid_profile_requests do |t| 4 | t.integer :user_id, unique: true, index: true, null: false 5 | t.string :given_names, null: false 6 | t.string :family_name, null: false 7 | t.string :primary_email, null: false 8 | t.string :orcid_profile_id, unique: true, index: true 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/orcid/engine.rb: -------------------------------------------------------------------------------- 1 | # The namespace for all things related to Orcid integration 2 | module Orcid 3 | 4 | # While not an isolated namespace engine 5 | # @See http://guides.rubyonrails.org/engines.html 6 | class Engine < ::Rails::Engine 7 | engine_name 'orcid' 8 | 9 | initializer 'orcid.initializers' do |app| 10 | app.config.paths.add 'app/services', eager_load: true 11 | app.config.autoload_paths += %W( 12 | #{config.root}/app/services 13 | ) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/stub_callback.rb: -------------------------------------------------------------------------------- 1 | class StubCallback 2 | def invoke(*args) 3 | @invoked_args = args 4 | end 5 | 6 | def invoked(*args) 7 | @invoked_args 8 | end 9 | 10 | def configure(*names) 11 | lambda do |on| 12 | configure_name(on, :success) 13 | configure_name(on, :failure) 14 | names.each do |name| 15 | configure_name(on, name) 16 | end 17 | end 18 | end 19 | 20 | private 21 | 22 | def configure_name(on, name) 23 | on.send(name) { |*args| invoke(name, *args) } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/views/orcid/profile_connections/_pending_connection.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= t( 3 | 'orcid.views.pending_connection', 4 | orcid_profile_url: Orcid.url_for_orcid_id(pending_connection.orcid_profile_id), 5 | orcid_profile_id: pending_connection.orcid_profile_id, 6 | application_orcid_authorization_href: user_omniauth_authorize_path(provider: 'orcid'), 7 | application_orcid_authorization_text: 'sign into this application' 8 | ).html_safe 9 | %> 10 | <%= render partial: 'orcid/profile_connections/orcid_disconnector' %> 11 |
-------------------------------------------------------------------------------- /spec/controllers/orcid/application_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Orcid::ApplicationController, type: :controller do 4 | context '#path_for' do 5 | it 'yields when the provided symbol is not a method' do 6 | path_for = controller.path_for(:__obviously_missing_method__, '123') { |arg| "/abc/#{arg}" } 7 | expect(path_for).to eq('/abc/123') 8 | end 9 | 10 | it 'calls the named method' do 11 | path_for = controller.path_for(:to_s) { "/abc/#{arg}" } 12 | expect(path_for).to eq(controller.to_s) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/orcid/on_demand_url_helper.rb: -------------------------------------------------------------------------------- 1 | module Orcid::OnDemandUrlHelper 2 | def on_demand_url(user) 3 | connector_params = { 4 | client_id: ENV['ORCID_APP_ID'], 5 | response_type: 'code', 6 | scope: Orcid.provider.authentication_scope, 7 | redirect_uri: orcid.create_orcid_url, 8 | family_names: (user.last_name if user.respond_to? :last_name), 9 | given_names: (user.first_name if user.respond_to? :first_name), 10 | email: user.email 11 | } 12 | 13 | "#{Orcid.provider.authorize_url}?#{connector_params.to_query}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/routing/orcid/profile_request_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Routes for Orcid::ProfileRequest' do 4 | routes { Orcid::Engine.routes } 5 | let(:persisted_profile) { Orcid::ProfileRequest.new(id: 2) } 6 | it 'generates a conventional URL' do 7 | expect(profile_request_path). 8 | to(eq('/orcid/profile_request')) 9 | end 10 | 11 | it 'treats the input profile_request as the :format parameter' do 12 | expect(profile_request_path(persisted_profile)). 13 | to(eq("/orcid/profile_request.#{persisted_profile.to_param}")) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/services/orcid/remote/service.rb: -------------------------------------------------------------------------------- 1 | require 'orcid/named_callbacks' 2 | module Orcid 3 | module Remote 4 | # An abstract service class, responsible for making remote calls and 5 | # issuing a callback. 6 | class Service 7 | def initialize 8 | @callbacks = Orcid::NamedCallbacks.new 9 | yield(@callbacks) if block_given? 10 | end 11 | 12 | def call 13 | fail NotImplementedError, ("Define #{self.class}#call") 14 | end 15 | 16 | def callback(name, *args) 17 | @callbacks.call(name, *args) 18 | args 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/orcid/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /spec/controllers/orcid/create_profile_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe CreateProfileController do 5 | 6 | let(:user) { mock_model('User') } 7 | 8 | context '#create' do 9 | before { sign_in(user) } 10 | 11 | it 'should contstruct an http post request' do 12 | stub_request(:post, "https://api.sandbox.orcid.org/oauth/token"). 13 | to_return(:status => 200, :body => "", :headers => {}) 14 | 15 | post :create, use_route: :orcid 16 | expect(response).to redirect_to(user_omniauth_authorize_path(:orcid)) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/models/orcid/work/xml_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe Work::XmlRenderer do 5 | let(:work) { Work.new(title: 'Hello', work_type: 'journal-article') } 6 | subject { described_class.new(work) } 7 | 8 | context '#call' do 9 | it 'should return an XML document' do 10 | rendered = subject.call 11 | expect(rendered).to have_tag('orcid-profile orcid-activities orcid-works orcid-work') do 12 | with_tag('work-title title', text: work.title) 13 | with_tag('work-type', text: work.work_type) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/views/orcid/profile_connections/_options_to_connect_orcid_profile.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'orcid/profile_connections/_options_to_connect_orcid_profile.html.erb', type: :view do 4 | let(:default_search_text) { '' } 5 | let(:user) {FactoryGirl.create(User)} 6 | 7 | it 'renders a form' do 8 | allow(view).to receive(:current_user).and_return(user) 9 | render 10 | expect(rendered).to( 11 | have_tag('.options-to-connect-orcid-profile') do 12 | with_tag( 13 | 'a', 14 | with: { id: "connect-orcid-link"} 15 | ) 16 | end 17 | ) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/views/orcid/profile_connections/_authenticated_connection.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'orcid/profile_connections/_authenticated_connection.html.erb' do 4 | Given(:profile) { double('Profile', orcid_profile_id: orcid_profile_id) } 5 | Given(:orcid_profile_id) { '123-456' } 6 | 7 | When(:rendered) do 8 | render( 9 | partial: 'orcid/profile_connections/authenticated_connection', 10 | object: profile 11 | ) 12 | end 13 | 14 | Then do 15 | expect(rendered).to have_tag('.authenticated-connection') do 16 | with_tag('a.orcid-profile-id', text: orcid_profile_id) 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/features/profile_connection_feature_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe 'connect to a publicly visible profile', requires_net_connect: true do 5 | around(:each) do |example| 6 | WebMock.allow_net_connect! 7 | example.run 8 | WebMock.disable_net_connect! 9 | end 10 | 11 | Given(:user) { FactoryGirl.create(:user) } 12 | Given(:text) { '"Jeremy Friesen"' } 13 | Given(:profile_connect) { ProfileConnection.new(user: user, text: text) } 14 | 15 | When(:profile_candidates) { profile_connect.orcid_profile_candidates } 16 | 17 | Then { expect(profile_candidates.count).to be > 1 } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/assets/javascripts/orcid/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /lib/orcid/named_callbacks.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # Inspired by Jim Weirich's NamedCallbacks 3 | # https://github.com/jimweirich/wyriki/blob/master/spec/runners/named_callbacks_spec.rb#L1-L28 4 | class NamedCallbacks 5 | def initialize 6 | @callbacks = {} 7 | end 8 | 9 | # Note this very specific implementation of #method_missing will raise 10 | # errors on non-zero method arity. 11 | def method_missing(callback_name, &block) 12 | @callbacks[callback_name] = block 13 | end 14 | 15 | def call(callback_name, *args) 16 | name = callback_name.to_sym 17 | cb = @callbacks[name] 18 | cb ? cb.call(*args) : true 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/views/orcid/profile_connections/_pending_connection.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'orcid/profile_connections/_pending_connection.html.erb' do 4 | Given(:profile) { double('Profile', orcid_profile_id: orcid_profile_id) } 5 | Given(:orcid_profile_id) { '123-456' } 6 | 7 | When(:rendered) do 8 | render( 9 | partial: 'orcid/profile_connections/pending_connection', 10 | object: profile 11 | ) 12 | end 13 | 14 | Then do 15 | expect(rendered).to have_tag('.pending-connection') do 16 | with_tag('a.orcid-profile-id', text: orcid_profile_id) 17 | with_tag('a.find-out-more') 18 | with_tag('a.signin-via-orcid') 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in orcid.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec path: File.expand_path('..', __FILE__) 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'debugger' 15 | gem 'coveralls', require: false 16 | gem 'execjs' 17 | gem 'therubyracer', platforms: :ruby 18 | gem 'byebug', require: false 19 | 20 | -------------------------------------------------------------------------------- /config/application.yml.example: -------------------------------------------------------------------------------- 1 | # Add account credentials and API keys here. 2 | # See http://railsapps.github.io/rails-environment-variables.html 3 | # This file should be listed in .gitignore to keep your settings secret! 4 | # Each entry sets a local environment variable and overrides ENV variables in the Unix shell. 5 | # For example, setting: 6 | # GMAIL_USERNAME: Your_Gmail_Username 7 | # makes 'Your_Gmail_Username' available as ENV["GMAIL_USERNAME"] 8 | 9 | # From http://support.orcid.org/knowledgebase/articles/162412-tutorial-create-a-new-profile-using-curl 10 | ORCID_APP_ID: 0000-0000-0000-0000 11 | ORCID_APP_SECRET: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 12 | 13 | ORCID_CLAIMED_PROFILE_ID: 0000-0000-0000-0000 14 | ORCID_CLAIMED_PROFILE_PASSWORD: password1A 15 | -------------------------------------------------------------------------------- /spec/features/orcid_work_query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'orcid work query', requires_net_connect: true do 4 | around(:each) do |example| 5 | WebMock.allow_net_connect! 6 | example.run 7 | WebMock.disable_net_connect! 8 | end 9 | Given(:token) { Orcid.client_credentials_token('/read-public') } 10 | 11 | # This profile exists on the Sandbox. But for how long? Who knows. 12 | Given(:orcid_profile_id) { '0000-0002-1117-8571' } 13 | Given(:remote_work_service) { Orcid::Remote::WorkService.new(orcid_profile_id, method: :get, token: token) } 14 | Given(:remote_work_document) { remote_work_service.call } 15 | When(:works) { Orcid::Work::XmlParser.call(remote_work_document) } 16 | Then { expect(works.size).to_not be 0 } 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/orcid/install/templates/orcid_initializer.rb.erb: -------------------------------------------------------------------------------- 1 | Orcid.configure do |config| 2 | # # Configure your Orcid Client Application. See URL below for more 3 | # # information 4 | # # http://support.orcid.org/knowledgebase/articles/116739-register-a-client-application 5 | # config.provider.id = "Your app's Orcid Client ID" 6 | # config.provider.secret = "Your app's Orcid Client Secret" 7 | 8 | 9 | # # Configure how your applications models will be mapped to an Orcid Work so 10 | # # that your application can append the work to an Orcid Profile. 11 | # config.register_mapping_to_orcid_work( 12 | # :article, 13 | # [ 14 | # [:title, :title], 15 | # [lambda{|article| article.publishers.join("; ")}, :publishers] 16 | # ] 17 | # ) 18 | end 19 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/profile_query_service/search_response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'orcid/remote/profile_query_service/search_response' 3 | 4 | module Orcid::Remote 5 | describe ProfileQueryService::SearchResponse do 6 | Given(:attributes) { {id: 'Hello', label: 'World', junk: 'JUNK!', biography: "Extended Biography"} } 7 | Given(:search_response) { described_class.new(attributes) } 8 | Then { expect(search_response.id).to eq(attributes[:id]) } 9 | And { expect(search_response.biography).to eq(attributes[:biography]) } 10 | And { expect(search_response.label).to eq(attributes[:label]) } 11 | And { expect(search_response.orcid_profile_id).to eq(attributes[:id]) } 12 | And { expect{search_response.junk }.to raise_error } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # 3 | # Copyright 2014 University of Notre Dame 4 | # 5 | # Additional copyright may be held by others, as reflected in the commit log 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | -------------------------------------------------------------------------------- /spec/views/orcid/profile_connections/new.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'orcid/profile_connections/new.html.erb' do 4 | let(:profile_connection) { Orcid::ProfileConnection.new } 5 | it 'renders a form' do 6 | view.stub(:profile_connection).and_return(profile_connection) 7 | render 8 | expect(rendered).to( 9 | have_tag( 10 | 'form.search-form', 11 | with: { action: orcid.new_profile_connection_path, method: :get } 12 | ) 13 | ) do 14 | with_tag('fieldset') do 15 | profile_connection.available_query_attribute_names.each do |field_name| 16 | with_tag( 17 | 'input', 18 | with: { name: "profile_connection[#{field_name}]", type: 'search' } 19 | ) 20 | end 21 | end 22 | with_tag('button', with: { type: 'submit' }) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/orcid/work/xml_renderer.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | class Work 3 | # Responsible for transforming a Work into an Orcid Work XML document 4 | class XmlRenderer 5 | def self.call(works, collaborators = {}) 6 | new(works, collaborators).call 7 | end 8 | 9 | attr_reader :works, :template 10 | def initialize(works, collaborators = {}) 11 | self.works = works 12 | @template = collaborators.fetch(:template) { default_template } 13 | end 14 | 15 | def call 16 | ERB.new(template).result(binding) 17 | end 18 | 19 | protected 20 | 21 | def works=(thing) 22 | @works = Array.wrap(thing) 23 | end 24 | 25 | def default_template 26 | template_name = 'app/templates/orcid/work.template.v1.2.xml.erb' 27 | Orcid::Engine.root.join(template_name).read 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/orcid/exceptions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'orcid/exceptions' 3 | 4 | module Orcid 5 | describe BaseError do 6 | it { should be_a_kind_of RuntimeError } 7 | end 8 | 9 | describe ProfileRequestStateError do 10 | subject { described_class } 11 | its(:superclass) { should be BaseError } 12 | end 13 | 14 | describe MissingUserForProfileRequest do 15 | subject { described_class } 16 | its(:superclass) { should be BaseError } 17 | end 18 | 19 | describe ConfigurationError do 20 | subject { described_class } 21 | its(:superclass) { should be BaseError } 22 | end 23 | 24 | describe RemoteServiceError do 25 | subject { described_class } 26 | its(:superclass) { should be BaseError } 27 | end 28 | 29 | describe ProfileRequestMethodExpectedError do 30 | subject { described_class } 31 | its(:superclass) { should be BaseError } 32 | end 33 | end -------------------------------------------------------------------------------- /app/controllers/orcid/create_profile_controller.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | module Orcid 3 | class CreateProfileController < Orcid::ApplicationController 4 | respond_to :html 5 | before_filter :authenticate_user! 6 | 7 | def create 8 | uri = URI.parse(Orcid.provider.token_url) 9 | 10 | request = Net::HTTP::Post.new(uri) 11 | request["Accept"] = "application/json" 12 | request.set_form_data( "client_id" => ENV['ORCID_APP_ID'], 13 | "client_secret" => ENV['ORCID_APP_SECRET'], 14 | "grant_type" => "authorization_code", 15 | "code" => params[:code], 16 | "redirect_uri" => config.application_root_url ) 17 | response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http| 18 | http.request(request) 19 | end 20 | 21 | redirect_to(user_omniauth_authorize_path(:orcid)) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/services/orcid/remote/profile_query_service/search_response.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'orcid/remote/profile_query_service' 2 | module Orcid 3 | module Remote 4 | class ProfileQueryService 5 | # A search response against Orcid. This should implement the Questioning 6 | # Authority query response object interface. 7 | class SearchResponse 8 | delegate :[], :has_key?, :key?, :fetch, to: :@records 9 | def initialize(attributes = {}) 10 | @attributes = attributes.with_indifferent_access 11 | end 12 | 13 | def id 14 | @attributes.fetch(:id) 15 | end 16 | 17 | def orcid_profile_id 18 | @attributes.fetch(:id) 19 | end 20 | 21 | def label 22 | @attributes.fetch(:label) 23 | end 24 | 25 | def biography 26 | @attributes.fetch(:biography) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/orcid/profile_connections/_orcid_connector.html.erb: -------------------------------------------------------------------------------- 1 | <% defined?(status_processor) || status_processor = Orcid::ProfileStatus.method(:for) %> 2 |
3 |

<%= link_to t('orcid.verbose_name'), Orcid.provider.host_url, target: :_blank%>

4 | <% status_processor.call(current_user) do |on|%> 5 | <% on.authenticated_connection do |profile| %> 6 | <%= render partial: 'orcid/profile_connections/authenticated_connection', object: profile %> 7 | <% end %> 8 | <% on.pending_connection do |profile| %> 9 | <%= render partial: 'orcid/profile_connections/pending_connection', object: profile %> 10 | <% end %> 11 | <% on.unknown do %> 12 | <% defined?(default_search_text) || default_search_text = '' %> 13 | <%= render template: 'orcid/profile_connections/_options_to_connect_orcid_profile', locals: { default_search_text: default_search_text } %> 14 | <% end %> 15 | <% end %> 16 |
17 | -------------------------------------------------------------------------------- /spec/lib/orcid/named_callbacks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require './lib/orcid/named_callbacks' 3 | 4 | module Orcid 5 | describe NamedCallbacks do 6 | Given(:named_callback) { NamedCallbacks.new } 7 | Given(:context) { [ ] } 8 | Given { named_callback.my_named_callback { |*a| context.replace(a) } } 9 | 10 | describe "with a named callback" do 11 | Given(:callback_name) { :my_named_callback } 12 | When { named_callback.call(callback_name, 'a',:hello) } 13 | Then { context == ['a', :hello] } 14 | end 15 | 16 | describe "with a named callback called by a string" do 17 | Given(:callback_name) { 'my_named_callback' } 18 | When { named_callback.call(callback_name, 'a',:hello) } 19 | Then { context == ['a', :hello] } 20 | end 21 | 22 | describe "with a undeclared callback" do 23 | When(:result) { named_callback.call(:undeclared_callback, 1, 2, 3) } 24 | Then { result } 25 | Then { context == [] } 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/features/batch_profile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'batch profile behavior', requires_net_connect: true do 4 | around(:each) do |example| 5 | WebMock.allow_net_connect! 6 | example.run 7 | WebMock.disable_net_connect! 8 | end 9 | 10 | Given(:runner) { 11 | lambda { |person| 12 | Orcid::Remote::ProfileQueryService.new {|on| 13 | on.found {|results| person.found(results) } 14 | on.not_found { person.not_found } 15 | }.call(email: person.email) 16 | } 17 | } 18 | Given(:person) { 19 | double('Person', email: email, found: true, not_found: true) 20 | } 21 | context 'with existing email' do 22 | Given(:email) { 'jeremy.n.friesen@gmail.com' } 23 | When { runner.call(person) } 24 | Then { person.should have_received(:found) } 25 | end 26 | context 'without an existing email' do 27 | Given(:email) { 'nobody@nowhere.zorg' } 28 | When { runner.call(person) } 29 | Then { person.should have_received(:not_found) } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/orcid/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # The foundation for Orcid controllers. A few helpful accessors. 3 | class ApplicationController < Orcid.parent_controller.constantize 4 | # Providing a mechanism for overrding the default path in an implementing 5 | # application 6 | def path_for(named_path, *args) 7 | return send(named_path, *args).to_s if respond_to?(named_path) 8 | yield(*args) 9 | end 10 | 11 | private 12 | 13 | def redirecting_because_user_has_connected_orcid_profile 14 | if orcid_profile 15 | flash[:notice] = I18n.t( 16 | 'orcid.requests.messages.previously_connected_profile', 17 | orcid_profile_id: orcid_profile.orcid_profile_id 18 | ) 19 | redirect_to path_for(:orcid_settings_path) { main_app.root_path } 20 | return true 21 | else 22 | return false 23 | end 24 | end 25 | 26 | def orcid_profile 27 | @orcid_profile ||= Orcid.profile_for(current_user) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'orcid/remote/service' 3 | 4 | module Orcid::Remote 5 | describe Service do 6 | Given(:context) { double(invoked: true) } 7 | Given(:runner) { 8 | described_class.new { |on| 9 | on.found { |a,b| context.invoked('FOUND', a, b) } 10 | } 11 | } 12 | 13 | describe 'calling defined callback' do 14 | When(:result) { runner.callback(:found, :first, :second) } 15 | Then { context.should have_received(:invoked).with('FOUND', :first, :second) } 16 | Then { result == [:first, :second] } 17 | end 18 | 19 | describe 'calling undefined callback' do 20 | When(:result) { runner.callback(:missing, :first, :second) } 21 | Then { context.should_not have_received(:invoked) } 22 | Then { result == [:first, :second] } 23 | end 24 | 25 | describe 'call' do 26 | When(:response) { runner.call } 27 | Then { expect(response).to have_failed(NotImplementedError) } 28 | end 29 | 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/orcid/profile_connections/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= simple_form_for(profile_connection, as: :profile_connection, url: orcid.new_profile_connection_path, method: :get, html: {class: 'search-form'}) do |f| %> 2 | <%= field_set_tag(t('orcid.views.profile_connections.fieldsets.search_orcid_profiles')) do %> 3 | <% profile_connection.available_query_attribute_names.each do |field_name| %> 4 | <%= f.input field_name, as: :search %> 5 | <% end %> 6 | <% end %> 7 | 10 | <% end %> 11 | 12 | <% profile_connection.with_orcid_profile_candidates do |candidates| %> 13 | <%= simple_form_for(profile_connection, as: :profile_connection, url: orcid.profile_connections_path,) do |f| %> 14 | <%= field_set_tag(t('orcid.views.profile_connections.fieldsets.select_an_orcid_profile')) do %> 15 | <%= f.collection_radio_buttons :orcid_profile_id, candidates, :id, :label, item_wrapper_class: 'radio' %> 16 | <% end %> 17 | <%= f.submit class: 'btn btn-default' %> 18 | <% end %> 19 | <% end %> 20 | -------------------------------------------------------------------------------- /spec/lib/orcid/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe Configuration do 5 | 6 | subject { described_class.new } 7 | 8 | its(:parent_controller) { should be_an_instance_of String } 9 | its(:provider) { should be_an_instance_of Configuration::Provider } 10 | its(:authentication_model) { should eq Devise::MultiAuth::Authentication } 11 | 12 | its(:mapper) { should respond_to :map } 13 | its(:mapper) { should respond_to :configure } 14 | 15 | context 'mapping to an Orcid::Work' do 16 | let(:legend) { 17 | [ 18 | [:title, :title], 19 | [lambda{|*| 'spaghetti'}, :work_type] 20 | ] 21 | } 22 | let(:title) { 'Hello World' } 23 | let(:article) { NonOrcid::Article.new(title: title)} 24 | before(:each) do 25 | subject.register_mapping_to_orcid_work('non_orcid/article', legend) 26 | end 27 | 28 | it 'should configure the mapper' do 29 | orcid_work = subject.mapper.map(article, target: 'orcid/work') 30 | expect(orcid_work.work_type).to eq('spaghetti') 31 | expect(orcid_work.title).to eq(title) 32 | expect(orcid_work).to be_an_instance_of(Orcid::Work) 33 | end 34 | 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/test_app_templates/lib/generators/test_app_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | 3 | class TestAppGenerator < Rails::Generators::Base 4 | source_root "spec/test_app_templates" 5 | 6 | def create_application_yml 7 | application_yml_file = File.expand_path("../../../../config/application.yml", __FILE__) 8 | application_yml_example_file = File.expand_path("../../../../config/application.yml.example", __FILE__) 9 | if File.exist?(application_yml_file) 10 | create_link 'config/application.yml', application_yml_file, symbolic:true 11 | else 12 | create_link 'config/application.yml', application_yml_example_file, symbolic:true 13 | end 14 | end 15 | 16 | def run_install_oricd 17 | generate 'orcid:install --devise --skip_application_yml ' 18 | end 19 | 20 | def create_shims 21 | create_file 'app/assets/javascripts/jquery.js' 22 | create_file 'app/assets/javascripts/jquery_ujs.js' 23 | create_file 'app/assets/javascripts/turbolinks.js' 24 | 25 | end 26 | 27 | def insert_home_route 28 | route 'root :to => "application#index"' 29 | content = %( 30 | def index 31 | render text: 'This page is left intentionally blank' 32 | end 33 | ) 34 | inject_into_file 'app/controllers/application_controller.rb', content, after: '< ActionController::Base' 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/orcid/configuration.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # Responsible for exposing the customization mechanism 3 | class Configuration 4 | attr_reader :mapper 5 | def initialize(collaborators = {}) 6 | @mapper = collaborators.fetch(:mapper) { default_mapper } 7 | @provider = collaborators.fetch(:provider) { default_provider } 8 | @authentication_model = collaborators.fetch(:authentication_model) { default_authenticaton_model } 9 | @parent_controller = collaborators.fetch(:parent_controller) { default_parent_controller } 10 | end 11 | 12 | attr_accessor :provider 13 | attr_accessor :authentication_model 14 | attr_accessor :parent_controller 15 | 16 | def register_mapping_to_orcid_work(source_type, legend) 17 | mapper.configure do |config| 18 | config.register(source: source_type, target: 'orcid/work', legend: legend) 19 | end 20 | end 21 | 22 | private 23 | 24 | def default_parent_controller 25 | '::ApplicationController' 26 | end 27 | 28 | def default_mapper 29 | require 'mappy' 30 | ::Mappy 31 | end 32 | 33 | def default_provider 34 | require 'orcid/configuration/provider' 35 | Provider.new 36 | end 37 | 38 | def default_authenticaton_model 39 | require 'devise-multi_auth' 40 | ::Devise::MultiAuth::Authentication 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/models/orcid/profile_request.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # Responsible for: 3 | # * acknowledging that an ORCID Profile was requested 4 | # * submitting a request for an ORCID Profile 5 | # * handling the response for the ORCID Profile creation 6 | class ProfileRequest < ActiveRecord::Base 7 | ERROR_STATUS = 'error'.freeze 8 | def self.find_by_user(user) 9 | where(user: user).first 10 | end 11 | 12 | self.table_name = :orcid_profile_requests 13 | 14 | alias_attribute :email, :primary_email 15 | validates :user_id, presence: true, uniqueness: true 16 | validates :given_names, presence: true 17 | validates :family_name, presence: true 18 | validates :primary_email, presence: true, email: true, confirmation: true 19 | 20 | belongs_to :user 21 | 22 | def successful_profile_creation(orcid_profile_id) 23 | self.class.transaction do 24 | update_column(:orcid_profile_id, orcid_profile_id) 25 | Orcid.connect_user_and_orcid_profile(user, orcid_profile_id) 26 | end 27 | end 28 | 29 | def error_on_profile_creation(error_message) 30 | update_column(:response_text, error_message) 31 | update_column(:response_status, ERROR_STATUS) 32 | end 33 | 34 | alias_attribute :error_message, :response_text 35 | 36 | def error_on_profile_creation? 37 | error_message.present? || response_status == ERROR_STATUS 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/features/public_api_query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'public api query', requires_net_connect: true do 4 | around(:each) do |example| 5 | WebMock.allow_net_connect! 6 | example.run 7 | WebMock.disable_net_connect! 8 | end 9 | 10 | Given(:runner) { 11 | Orcid::Remote::ProfileQueryService.new 12 | } 13 | context 'with simple query' do 14 | Given(:parameters) { { email: 'jeremy.n.friesen@gmail.com' } } 15 | When(:result) { runner.call(parameters) } 16 | Then { expect(result.size).to eq(1) } 17 | end 18 | 19 | context 'with bad query' do 20 | Given(:parameters) { { hobomancer: 'jeremy.n.friesen@gmail.com' } } 21 | When(:result) { runner.call(parameters) } 22 | Then { expect(result).to have_failed(OAuth2::Error) } 23 | end 24 | 25 | context 'with a text query' do 26 | Given(:parameters) { { text: '"Jeremy Friesen"' } } 27 | When(:result) { runner.call(parameters) } 28 | Then { expect(result.size).to be > 0 } 29 | end 30 | 31 | context 'with bogus text query' do 32 | Given(:parameters) { { text: 'verybogustextthatyoushouldnotfind' } } 33 | When(:result) { runner.call(parameters) } 34 | Then { expect(result.size).to eq 0 } 35 | end 36 | 37 | context 'with a compound text query' do 38 | Given(:parameters) { { email: "nobody@gmail.com", text: '"Jeremy+Friesen"' } } 39 | When(:result) { runner.call(parameters) } 40 | Then { expect(result.size).to eq 0 } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/models/orcid/work/xml_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe Work::XmlParser do 5 | let(:xml) { fixture_file('orcid_works.xml').read } 6 | 7 | context '.call' do 8 | subject { described_class.call(xml) } 9 | its(:size) { should eq 2 } 10 | its(:first) { should be_an_instance_of Orcid::Work } 11 | its(:last) { should be_an_instance_of Orcid::Work } 12 | 13 | context 'first element' do 14 | subject { described_class.call(xml).first } 15 | 16 | its(:title) { should eq "Another Test Drive" } 17 | its(:put_code) { should eq "303475" } 18 | its(:work_type) { should eq "test" } 19 | its(:journal_title) { should_not be_present } 20 | its(:short_description) { should_not be_present } 21 | its(:citation_type) { should_not be_present } 22 | its(:citation) { should_not be_present } 23 | its(:publication_month) { should_not be_present } 24 | its(:publication_year) { should_not be_present } 25 | its(:url) { should_not be_present } 26 | its(:language_code) { should_not be_present } 27 | its(:country) { should_not be_present } 28 | end 29 | 30 | context 'last element' do 31 | subject { described_class.call(xml).last } 32 | 33 | its(:title) { should eq "Test Driven Orcid Integration" } 34 | its(:put_code) { should eq "303474" } 35 | its(:work_type) { should eq "test" } 36 | end 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /app/services/orcid/remote/profile_query_service/query_parameter_builder.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'orcid/remote/profile_query_service' 2 | module Orcid 3 | module Remote 4 | class ProfileQueryService 5 | # http://support.orcid.org/knowledgebase/articles/132354-searching-with-the-public-api 6 | module QueryParameterBuilder 7 | 8 | module_function 9 | # Responsible for converting an arbitrary query string to the acceptable 10 | # Orcid query format. 11 | # 12 | # @TODO - Note this is likely not correct, but is providing the singular 13 | # point of entry 14 | def call(input = {}) 15 | params = {} 16 | q_params = [] 17 | text_params = [] 18 | input.each do |key, value| 19 | next if value.nil? || value.to_s.strip == '' 20 | case key.to_s 21 | when 'start', 'row' 22 | params[key] = value 23 | when 'q', 'text' 24 | text_params << "#{value}" 25 | else 26 | q_params << "#{key.to_s.gsub('_', '-')}:#{value}" 27 | end 28 | end 29 | 30 | case text_params.size 31 | when 0; then nil 32 | when 1 33 | q_params << "text:#{text_params.first}" 34 | else 35 | q_params << "text:((#{text_params.join(') AND (')}))" 36 | end 37 | params[:q] = q_params.join(' AND ') 38 | params 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/views/orcid/profile_connections/_options_to_connect_orcid_profile.html.erb: -------------------------------------------------------------------------------- 1 | <% profile_connection = Orcid::ProfileConnection.new %> 2 | <% default_search_text = '' unless defined?(default_search_text) %> 3 |
4 |

5 | > 6 | 7 | Create or Connect your ORCID iD 8 | 9 |

10 |

11 | <%= link_to t('orcid/profile_connection.look_up_your_existing_orcid', scope: 'helpers.label', target: :_blank), orcid.new_profile_connection_path(profile_connection:{text: default_search_text}) %> 12 |

13 |

14 | <%= form_for(profile_connection, as: :profile_connection, url: orcid.profile_connections_path, method: :post) do |f| %> 15 | <%# Note the `scope: 'helpers.label'` option is the default for the label, but I want to call those specifically out %> 16 | <%= f.label :orcid_profile_id, t('orcid/profile_connection.orcid_profile_id', scope: 'helpers.label') %> 17 | <%= f.text_field :orcid_profile_id, class:"orcid-input" %> 18 | 21 | <% end %> 22 |

23 |
-------------------------------------------------------------------------------- /app/models/orcid/work/xml_parser.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | class Work 3 | # Responsible for taking an Orcid Work and extracting the value/text from 4 | # the document and reifying an Orcid::Work object. 5 | class XmlParser 6 | def self.call(xml) 7 | new(xml).call 8 | end 9 | 10 | attr_reader :xml 11 | def initialize(xml) 12 | @xml = xml 13 | end 14 | 15 | def call 16 | document.css('orcid-works orcid-work').map do |node| 17 | transform(node) 18 | end 19 | end 20 | 21 | private 22 | 23 | def document 24 | @document ||= Nokogiri::XML.parse(xml) 25 | end 26 | 27 | def transform(node) 28 | Work.new.tap do |work| 29 | work.put_code = node.attributes.fetch('put-code').value 30 | work.title = node.css('work-title title').text 31 | work.work_type = node.css('work-type').text 32 | work.journal_title = node.css('journal-title').text 33 | work.short_description = node.css('short-description').text 34 | work.citation_type = node.css('work-citation work-citation-type').text 35 | work.citation = node.css('work-citation citation').text 36 | work.publication_year = node.css('publication-date year').text 37 | work.publication_month = node.css('publication-date month').text 38 | work.url = node.css('url').text 39 | work.language_code = node.css('language_code').text 40 | work.country = node.css('country').text 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/profile_query_service/query_parameter_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'orcid/remote/profile_query_service/query_parameter_builder' 3 | 4 | module Orcid::Remote 5 | 6 | describe ProfileQueryService::QueryParameterBuilder do 7 | When(:response) { described_class.call(input) } 8 | context 'single word input' do 9 | Given(:input) { { text: 'Hello', email: 'jeremy.n.friesen@gmail.com' } } 10 | Then { expect(response).to eq(q: "email:#{input[:email]} AND text:#{input[:text]}") } 11 | end 12 | 13 | context 'empty string and nil' do 14 | Given(:input) { { text: '' , email: nil } } 15 | Then { expect(response).to eq(q: '') } 16 | end 17 | 18 | context 'start or row' do 19 | Given(:input) { { start: '1' , row: '2' } } 20 | Then { expect(response).to eq(q: '', start: '1', row: '2') } 21 | end 22 | 23 | context 'multi-word named input' do 24 | Given(:input) { { other_names: %("Tim O'Connor" -"Oak"), email: 'jeremy.n.friesen@gmail.com' } } 25 | Then { expect(response).to eq(q: "other-names:#{input[:other_names]} AND email:#{input[:email]}") } 26 | end 27 | 28 | context 'q is provided along with other params' do 29 | Given(:input) { { q: %("Tim O'Connor" -"Oak"), email: 'jeremy.n.friesen@gmail.com' } } 30 | Then { expect(response).to eq(q: "email:#{input[:email]} AND text:#{input[:q]}") } 31 | end 32 | 33 | context 'q is provided with text params' do 34 | Given(:input) { { q: %("Tim O'Connor" -"Oak"), text: 'jeremy.n.friesen@gmail.com' } } 35 | Then { expect(response).to eq(q: "text:((#{input[:q]}) AND (#{input[:text]}))") } 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/profile_query_service/response_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'spec_helper' 3 | require 'orcid/remote/profile_query_service/response_parser' 4 | 5 | module Orcid 6 | module Remote 7 | class ProfileQueryService 8 | describe ResponseParser do 9 | context '.call' do 10 | Given(:response_builder) { OpenStruct } 11 | Given(:logger) { double(warn: true) } 12 | Given(:document) do 13 | File.read(fixture_file(File.join('orcid-remote-profile_query_service-response_parser',response_filename))) 14 | end 15 | Given(:subject) { described_class.new(response_builder: response_builder, logger: logger) } 16 | 17 | context 'happy path' do 18 | let(:response_filename) { 'single-response-with-orcid-valid-profile.json' } 19 | When(:response) { subject.call(document) } 20 | Then do 21 | response.should eq( 22 | [ 23 | response_builder.new( 24 | id: "MY-ORCID-PROFILE-ID", 25 | label: "Corwin Amber (MY-ORCID-EMAIL) [ORCID: MY-ORCID-PROFILE-ID]", 26 | biography:"MY-ORCID-BIOGRAPHY" 27 | ) 28 | ] 29 | ) 30 | end 31 | end 32 | 33 | context 'unhappy path' do 34 | let(:response_filename) { 'multiple-responses-without-valid-response.json' } 35 | When(:response) { subject.call(document) } 36 | Then { response.should eq [] } 37 | And { logger.should have_received(:warn).at_least(1).times } 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/work_service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'oauth2/error' 3 | require 'orcid/remote/work_service' 4 | 5 | module Orcid::Remote 6 | describe WorkService do 7 | let(:payload) { %() } 8 | let(:token) { double("Token") } 9 | let(:orcid_profile_id) { '0000-0003-1495-7122' } 10 | let(:request_headers) { { 'Content-Type' => 'application/orcid+xml', 'Accept' => 'application/xml' } } 11 | let(:response) { double("Response", body: 'Body') } 12 | 13 | context '.call' do 14 | let(:token) { double('Token', client: client, token: 'access_token', refresh_token: 'refresh_token')} 15 | let(:client) { double('Client', id: '123', site: 'URL', options: {})} 16 | it 'raises a more helpful message' do 17 | response = double("Response", status: '100', body: 'body') 18 | response.stub(:error=) 19 | response.stub(:parsed) 20 | token.should_receive(:request).and_raise(OAuth2::Error.new(response)) 21 | 22 | expect { 23 | described_class.call(orcid_profile_id, token: token) 24 | }.to raise_error(Orcid::RemoteServiceError) 25 | end 26 | it 'instantiates and calls underlying instance' do 27 | token.should_receive(:request). 28 | with(:post, "v1.2/#{orcid_profile_id}/orcid-works/", body: payload, headers: request_headers). 29 | and_return(response) 30 | 31 | expect( 32 | described_class.call( 33 | orcid_profile_id, 34 | body: payload, 35 | request_method: :post, 36 | token: token, 37 | headers: request_headers 38 | ) 39 | ).to eq(response.body) 40 | end 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | Bundler::GemHelper.install_tasks 8 | 9 | 10 | begin 11 | APP_RAKEFILE = File.expand_path('../spec/internal/Rakefile', __FILE__) 12 | load 'rails/tasks/engine.rake' 13 | rescue LoadError 14 | puts "Unable to load all app tasks for #{APP_RAKEFILE}" 15 | end 16 | 17 | require 'engine_cart/rake_task' 18 | require 'rspec/core/rake_task' 19 | 20 | namespace :spec do 21 | RSpec::Core::RakeTask.new(:all) do 22 | ENV['COVERAGE'] = 'true' 23 | end 24 | 25 | desc 'Only run specs that do not require net connect' 26 | RSpec::Core::RakeTask.new(:offline) do |t| 27 | t.rspec_opts = '--tag ~requires_net_connect' 28 | end 29 | 30 | desc 'Only run specs that require net connect' 31 | RSpec::Core::RakeTask.new(:online) do |t| 32 | t.rspec_opts = '--tag requires_net_connect' 33 | end 34 | 35 | desc 'Run the Jenkins CI specs' 36 | task :jenkins do 37 | ENV['RAILS_ENV'] = 'test' 38 | ENV['SPEC_OPTS'] = '--profile 20' 39 | Rake::Task['engine_cart:clean'].invoke 40 | Rake::Task['engine_cart:generate'].invoke 41 | Rake::Task['spec:all'].invoke 42 | end 43 | 44 | desc 'Run the Travis CI specs' 45 | task :travis do 46 | ENV['RAILS_ENV'] = 'test' 47 | ENV['SPEC_OPTS'] = '--profile 20' 48 | ENV['ORCID_APP_ID'] = 'bleck' 49 | ENV['ORCID_APP_SECRET'] = 'bleck' 50 | Rake::Task['engine_cart:clean'].invoke 51 | Rake::Task['engine_cart:generate'].invoke 52 | Rake::Task['spec:offline'].invoke 53 | end 54 | end 55 | 56 | begin 57 | Rake::Task['default'].clear 58 | rescue RuntimeError 59 | # This isn't a big deal if we don't have a default 60 | end 61 | 62 | Rake::Task['spec'].clear 63 | 64 | task spec: 'spec:offline' 65 | task default: 'spec:travis' 66 | -------------------------------------------------------------------------------- /app/services/orcid/remote/profile_query_service.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'json' 2 | require 'orcid/remote/service' 3 | module Orcid 4 | module Remote 5 | # Responsible for querying Orcid to find various ORCiDs 6 | class ProfileQueryService < Orcid::Remote::Service 7 | def self.call(query, config = {}, &callbacks) 8 | new(config, &callbacks).call(query) 9 | end 10 | 11 | attr_reader :token, :path, :headers, :response_builder, :query_builder 12 | attr_reader :parser 13 | def initialize(config = {}, &callbacks) 14 | super(&callbacks) 15 | @query_builder = config.fetch(:query_parameter_builder) { QueryParameterBuilder } 16 | @token = config.fetch(:token) { default_token } 17 | @parser = config.fetch(:parser) { ResponseParser } 18 | @path = config.fetch(:path) { 'v1.2/search/orcid-bio/' } 19 | @headers = config.fetch(:headers) { default_headers } 20 | end 21 | 22 | def call(input) 23 | parameters = query_builder.call(input) 24 | response = deliver(parameters) 25 | parsed_response = parse(response.body) 26 | issue_callbacks(parsed_response) 27 | parsed_response 28 | end 29 | alias_method :search, :call 30 | 31 | protected 32 | 33 | def default_token 34 | Orcid.client_credentials_token('/read-public') 35 | end 36 | 37 | def default_headers 38 | { :accept => 'application/orcid+json', 'Content-Type' => 'application/orcid+xml' } 39 | end 40 | 41 | def issue_callbacks(search_results) 42 | if Array.wrap(search_results).any?(&:present?) 43 | callback(:found, search_results) 44 | else 45 | callback(:not_found) 46 | end 47 | end 48 | 49 | attr_reader :host, :access_token 50 | def deliver(parameters) 51 | token.get(path, headers: headers, params: parameters) 52 | end 53 | 54 | def parse(document) 55 | parser.call(document) 56 | end 57 | 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/lib/orcid/configuration/provider_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'orcid/configuration/provider' 3 | 4 | module Orcid 5 | describe Configuration::Provider do 6 | 7 | let(:storage) { 8 | { 9 | 'ORCID_APP_AUTHENTICATION_SCOPE' => '_APP_AUTHENTICATION_SCOPE', 10 | 'ORCID_SITE_URL' => '_SITE_URL', 11 | 'ORCID_TOKEN_URL' => '_TOKEN_URL', 12 | 'ORCID_REMOTE_SIGNIN_URL' => '_REMOTE_SIGNIN_URL', 13 | 'ORCID_AUTHORIZE_URL' => '_AUTHORIZE_URL', 14 | 'ORCID_APP_ID' => '_APP_ID', 15 | 'ORCID_APP_SECRET' => '_APP_SECRET', 16 | 'ORCID_HOST_URL' => '_HOST_URL', 17 | } 18 | } 19 | 20 | subject { described_class.new(storage) } 21 | 22 | its(:authentication_scope) { should eq storage.fetch('ORCID_APP_AUTHENTICATION_SCOPE') } 23 | its(:site_url) { should eq storage.fetch('ORCID_SITE_URL') } 24 | its(:token_url) { should eq storage.fetch('ORCID_TOKEN_URL') } 25 | its(:signin_via_json_url) { should eq storage.fetch('ORCID_REMOTE_SIGNIN_URL') } 26 | its(:host_url) { should eq storage.fetch('ORCID_HOST_URL') } 27 | its(:authorize_url) { should eq storage.fetch('ORCID_AUTHORIZE_URL') } 28 | its(:id) { should eq storage.fetch('ORCID_APP_ID') } 29 | its(:secret) { should eq storage.fetch('ORCID_APP_SECRET') } 30 | 31 | context 'with an empty ENV' do 32 | Given(:provider) { described_class.new({}) } 33 | Then { expect(provider.authentication_scope).to be_an_instance_of(String) } 34 | And { expect(provider.site_url).to be_an_instance_of(String) } 35 | And { expect(provider.token_url).to be_an_instance_of(String) } 36 | And { expect(provider.signin_via_json_url).to be_an_instance_of(String) } 37 | And { expect(provider.host_url).to be_an_instance_of(String) } 38 | And { expect(provider.authorize_url).to be_an_instance_of(String) } 39 | And { expect { provider.id }.to raise_error Orcid::ConfigurationError } 40 | And { expect { provider.secret }.to raise_error Orcid::ConfigurationError } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/models/orcid/profile_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe ProfileRequest do 5 | let(:orcid_profile_id) { '0000-0001-8025-637X'} 6 | let(:user) { FactoryGirl.create(:user) } 7 | let(:attributes) { 8 | { 9 | user: user, 10 | given_names: 'Daffy', 11 | family_name: 'Duck', 12 | primary_email: 'daffy@duck.com' 13 | } 14 | } 15 | subject { described_class.new(attributes) } 16 | 17 | context '#find_by_user' do 18 | let!(:profile_request) { FactoryGirl.create(:orcid_profile_request) } 19 | it 'returns the profile request' do 20 | expect(described_class.find_by_user(profile_request.user)).to eq(profile_request) 21 | end 22 | 23 | it 'to return nil' do 24 | other_user = FactoryGirl.build(:user) 25 | expect(described_class.find_by_user(other_user)).to be_nil 26 | end 27 | 28 | end 29 | 30 | context '#successful_profile_creation' do 31 | it 'should update profile request' do 32 | # Don't want to hit the database 33 | subject.should_receive(:update_column).with(:orcid_profile_id, orcid_profile_id) 34 | Orcid.should_receive(:connect_user_and_orcid_profile).with(user, orcid_profile_id) 35 | 36 | subject.successful_profile_creation(orcid_profile_id) 37 | end 38 | end 39 | 40 | context '#error_on_profile_creation' do 41 | it 'should update profile request' do 42 | error_message = '123' 43 | # Don't want to hit the database 44 | subject.should_receive(:update_column).with(:response_text, error_message) 45 | subject.should_receive(:update_column).with(:response_status, ProfileRequest::ERROR_STATUS) 46 | subject.error_on_profile_creation(error_message) 47 | end 48 | end 49 | 50 | context '#error_on_profile_creation?' do 51 | it 'should be true if there is a response text' do 52 | subject.response_status = ProfileRequest::ERROR_STATUS 53 | expect(subject.error_on_profile_creation?).to be_truthy 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/services/orcid/remote/profile_creation_service.rb: -------------------------------------------------------------------------------- 1 | require 'orcid/remote/service' 2 | require 'oauth2/error' 3 | require 'nokogiri' 4 | module Orcid 5 | module Remote 6 | # Responsible for minting a new ORCID for the given payload. 7 | class ProfileCreationService < Orcid::Remote::Service 8 | def self.call(payload, config = {}, &callback_config) 9 | new(config, &callback_config).call(payload) 10 | end 11 | 12 | attr_reader :token, :path, :headers 13 | def initialize(config = {}, &callback_config) 14 | super(&callback_config) 15 | @token = config.fetch(:token) { default_token } 16 | @path = config.fetch(:path) { 'v1.1/orcid-profile' } 17 | @headers = config.fetch(:headers) { default_headers } 18 | end 19 | 20 | def call(payload) 21 | response = deliver(payload) 22 | parse(response) 23 | rescue ::OAuth2::Error => e 24 | parse_exception(e) 25 | end 26 | 27 | protected 28 | 29 | def deliver(body) 30 | token.post(path, body: body, headers: headers) 31 | end 32 | 33 | def parse(response) 34 | uri = URI.parse(response.headers.fetch(:location)) 35 | orcid_profile_id = uri.path.sub(/\A\//, '').split('/').first 36 | if orcid_profile_id 37 | callback(:success, orcid_profile_id) 38 | orcid_profile_id 39 | else 40 | callback(:failure) 41 | false 42 | end 43 | end 44 | 45 | def parse_exception(exception) 46 | doc = Nokogiri::XML.parse(exception.response.body) 47 | error_text = doc.css('error-desc').text 48 | if error_text.to_s.size > 0 49 | callback(:orcid_validation_error, error_text) 50 | false 51 | else 52 | fail exception 53 | end 54 | end 55 | 56 | def default_headers 57 | { 'Accept' => 'application/xml', 'Content-Type' => 'application/vdn.orcid+xml' } 58 | end 59 | 60 | def default_token 61 | Orcid.client_credentials_token('/orcid-profile/create') 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/profile_query_service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'orcid/remote/profile_query_service' 3 | require 'ostruct' 4 | 5 | module Orcid::Remote 6 | describe ProfileQueryService do 7 | Given(:parser) { double('Parser', call: parsed_response)} 8 | Given(:config) { 9 | { 10 | token: token, 11 | parser: parser, 12 | query_parameter_builder: query_parameter_builder 13 | } 14 | } 15 | Given(:query_parameter_builder) { double('Query Builder') } 16 | Given(:response) { double("Response", body: 'Response Body') } 17 | Given(:token) { double("Token") } 18 | Given(:parameters) { double("Parameters") } 19 | Given(:normalized_parameters) { double("Normalized Parameters") } 20 | Given(:callback) { StubCallback.new } 21 | Given(:callback_config) { callback.configure(:found, :not_found) } 22 | Given(:parsed_response) { 'HELLO WORLD!' } 23 | 24 | context '.call' do 25 | context 'with at least one found' do 26 | before(:each) do 27 | query_parameter_builder.should_receive(:call).with(parameters).and_return(normalized_parameters) 28 | token.should_receive(:get).with(kind_of(String), headers: kind_of(Hash), params: normalized_parameters).and_return(response) 29 | end 30 | When(:result) { described_class.call(parameters, config, &callback_config) } 31 | Then { expect(result).to eq(parsed_response) } 32 | And { expect(callback.invoked).to eq [:found, parsed_response] } 33 | end 34 | 35 | context 'with no objects found' do 36 | before(:each) do 37 | query_parameter_builder.should_receive(:call).with(parameters).and_return(normalized_parameters) 38 | token.should_receive(:get).with(kind_of(String), headers: kind_of(Hash), params: normalized_parameters).and_return(response) 39 | end 40 | When(:parsed_response) { '' } 41 | When(:result) { described_class.call(parameters, config, &callback_config) } 42 | Then { expect(result).to eq(parsed_response) } 43 | And { expect(callback.invoked).to eq [:not_found] } 44 | 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/fixtures/orcid-remote-profile_query_service-response_parser/single-response-with-orcid-valid-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "message-version": "1.1", 3 | "orcid-search-results": { 4 | "orcid-search-result": [ 5 | { 6 | "relevancy-score": { 7 | "value": 14.298138 8 | }, 9 | "orcid-profile": { 10 | "orcid": null, 11 | "orcid-identifier": { 12 | "value": null, 13 | "uri": "http://orcid.org/MY-ORCID-PROFILE-ID", 14 | "path": "MY-ORCID-PROFILE-ID", 15 | "host": "orcid.org" 16 | }, 17 | "orcid-bio": { 18 | "personal-details": { 19 | "given-names": { 20 | "value": "Corwin" 21 | }, 22 | "family-name": { 23 | "value": "Amber" 24 | } 25 | }, 26 | "biography": { 27 | "value": "MY-ORCID-BIOGRAPHY", 28 | "visibility": null 29 | }, 30 | "contact-details": { 31 | "email": [ 32 | { 33 | "value": "MY-ORCID-EMAIL", 34 | "primary": true, 35 | "current": true, 36 | "verified": true, 37 | "visibility": null, 38 | "source": null 39 | } 40 | ], 41 | "address": { 42 | "country": { 43 | "value": "US", 44 | "visibility": null 45 | } 46 | } 47 | }, 48 | "keywords": { 49 | "keyword": [ 50 | { 51 | "value": "Lord of Amber" 52 | } 53 | ], 54 | "visibility": null 55 | }, 56 | "delegation": null, 57 | "applications": null, 58 | "scope": null 59 | }, 60 | "orcid-activities": { 61 | "affiliations": null 62 | }, 63 | "type": null, 64 | "group-type": null, 65 | "client-type": null 66 | } 67 | } 68 | ], 69 | "num-found": 1 70 | } 71 | } -------------------------------------------------------------------------------- /app/services/orcid/remote/work_service.rb: -------------------------------------------------------------------------------- 1 | require 'orcid/exceptions' 2 | module Orcid 3 | module Remote 4 | # Responsible for interacting with the Orcid works endpoint 5 | class WorkService 6 | def self.call(orcid_profile_id, options = {}) 7 | new(orcid_profile_id, options).call 8 | end 9 | 10 | attr_reader( 11 | :headers, :token, :orcid_profile_id, :body, :request_method, :path 12 | ) 13 | def initialize(orcid_profile_id, options = {}) 14 | @orcid_profile_id = orcid_profile_id 15 | @request_method = options.fetch(:request_method) { :get } 16 | @body = options.fetch(:body) { '' } 17 | @token = options.fetch(:token) { default_token } 18 | @headers = options.fetch(:headers) { default_headers } 19 | @path = options.fetch(:path) { default_path } 20 | end 21 | 22 | # :post will append works to the Orcid Profile 23 | # :put will replace the existing Orcid Profile works with the payload 24 | # :get will retrieve the Orcid Profile 25 | # http://support.orcid.org/knowledgebase/articles/177528-add-works-technical-developer 26 | def call 27 | response = deliver 28 | response.body 29 | end 30 | 31 | protected 32 | 33 | def deliver 34 | token.request(request_method, path, body: body, headers: headers) 35 | rescue OAuth2::Error => e 36 | handle_oauth_error(e) 37 | end 38 | 39 | def handle_oauth_error(e) 40 | fail Orcid::RemoteServiceError, 41 | response_body: e.response.body, 42 | response_status: e.response.status, 43 | client: token.client, 44 | token: token, 45 | request_method: request_method, 46 | request_path: path, 47 | request_body: body, 48 | request_headers: headers 49 | end 50 | 51 | def default_token 52 | Orcid.access_token_for(orcid_profile_id) 53 | end 54 | 55 | def default_headers 56 | { 'Accept' => 'application/xml', 'Content-Type' => 'application/orcid+xml' } 57 | end 58 | 59 | def default_path 60 | "v1.2/#{orcid_profile_id}/orcid-works/" 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/models/orcid/profile.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # Provides a container around an Orcid Profile and its relation to the Orcid 3 | # Works. 4 | class Profile 5 | attr_reader :orcid_profile_id, :mapper, :remote_service, :xml_renderer, :xml_parser 6 | private :mapper, :remote_service, :xml_renderer, :xml_parser 7 | def initialize(orcid_profile_id, collaborators = {}) 8 | @orcid_profile_id = orcid_profile_id 9 | @mapper = collaborators.fetch(:mapper) { default_mapper } 10 | @remote_service = collaborators.fetch(:remote_service) { default_remote_service } 11 | @xml_renderer = collaborators.fetch(:xml_renderer) { default_xml_renderer } 12 | @xml_parser = collaborators.fetch(:xml_parser) { default_xml_parser } 13 | end 14 | 15 | # Answers the question: Has the user been authenticated via the ORCID 16 | # system. 17 | # 18 | # @TODO - Extract this to the Orcid::ProfileStatus object. As the method 19 | # is referenced via a controller, this can easily be moved. 20 | def verified_authentication? 21 | Orcid.authenticated_orcid?(orcid_profile_id) 22 | end 23 | 24 | def remote_works(options = {}) 25 | @remote_works = nil if options.fetch(:force, false) 26 | @remote_works ||= begin 27 | response = remote_service.call(orcid_profile_id, request_method: :get) 28 | xml_parser.call(response) 29 | end 30 | end 31 | 32 | def append_new_work(*works) 33 | request_work_changes_via(:post, *works) 34 | end 35 | 36 | def replace_works_with(*works) 37 | request_work_changes_via(:put, *works) 38 | end 39 | 40 | protected 41 | 42 | def request_work_changes_via(request_method, *works) 43 | orcid_works = normalize_work(*works) 44 | xml = xml_renderer.call(orcid_works) 45 | remote_service.call(orcid_profile_id, request_method: request_method, body: xml) 46 | end 47 | 48 | def default_mapper 49 | Orcid.mapper 50 | end 51 | 52 | def default_remote_service 53 | Orcid::Remote::WorkService 54 | end 55 | 56 | def default_xml_renderer 57 | Orcid::Work::XmlRenderer 58 | end 59 | 60 | def default_xml_parser 61 | Orcid::Work::XmlParser 62 | end 63 | 64 | # Note: We can handle 65 | def normalize_work(*works) 66 | Array.wrap(works).map do |work| 67 | mapper.map(work, target: 'orcid/work') 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/orcid/configuration/provider.rb: -------------------------------------------------------------------------------- 1 | require 'orcid/exceptions' 2 | module Orcid 3 | class Configuration 4 | # Responsible for negotiating the retrieval of Orcid provider information. 5 | # Especially important given that you have private auth keys. 6 | # Also given that you may want to request against the sandbox versus the 7 | # production Orcid service. 8 | class Provider 9 | attr_reader :store 10 | def initialize(store = ::ENV) 11 | @store = store 12 | end 13 | 14 | # See http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3 for 15 | # how to formulate scopes 16 | attr_writer :authentication_scope 17 | def authentication_scope 18 | @authentication_scope ||= 19 | store.fetch('ORCID_APP_AUTHENTICATION_SCOPE') do 20 | '/read-limited /activities/update' 21 | end 22 | end 23 | 24 | attr_writer :site_url 25 | def site_url 26 | @site_url ||= store.fetch('ORCID_SITE_URL') do 27 | 'http://api.sandbox.orcid.org' 28 | end 29 | end 30 | 31 | attr_writer :token_url 32 | def token_url 33 | @token_url ||= store.fetch('ORCID_TOKEN_URL') do 34 | 'https://api.sandbox.orcid.org/oauth/token' 35 | end 36 | end 37 | 38 | attr_writer :signin_via_json_url 39 | def signin_via_json_url 40 | @signin_via_json_url ||= store.fetch('ORCID_REMOTE_SIGNIN_URL') do 41 | 'https://sandbox.orcid.org/signin/auth.json' 42 | end 43 | end 44 | 45 | attr_writer :host_url 46 | def host_url 47 | @host_url ||= store.fetch('ORCID_HOST_URL') do 48 | uri = URI.parse(signin_via_json_url) 49 | "#{uri.scheme}://#{uri.host}" 50 | end 51 | end 52 | 53 | attr_writer :authorize_url 54 | def authorize_url 55 | @authorize_url ||= store.fetch('ORCID_AUTHORIZE_URL') do 56 | 'https://sandbox.orcid.org/oauth/authorize' 57 | end 58 | end 59 | 60 | attr_writer :id 61 | def id 62 | @id ||= store.fetch('ORCID_APP_ID') 63 | rescue KeyError 64 | raise ConfigurationError, 'ORCID_APP_ID' 65 | end 66 | 67 | attr_writer :secret 68 | def secret 69 | @secret ||= store.fetch('ORCID_APP_SECRET') 70 | rescue KeyError 71 | raise ConfigurationError, 'ORCID_APP_SECRET' 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/views/orcid/profile_connections/_orcid_connector.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'orcid/profile_connections/_orcid_connector.html.erb', type: :view do 4 | let(:default_search_text) { 'hello' } 5 | let(:current_user) { double('User') } 6 | let(:status_processor) { double('Processor') } 7 | let(:user) {FactoryGirl.create(User)} 8 | let(:handler) do 9 | double( 10 | 'Handler', 11 | profile_request_pending: true, 12 | unknown: true, 13 | authenticated_connection: true, 14 | pending_connection: true, 15 | profile_request_in_error: true 16 | ) 17 | end 18 | def render_with_params 19 | allow(view).to receive(:current_user).and_return(user) 20 | render( 21 | partial: 'orcid/profile_connections/orcid_connector', 22 | locals: { default_search_text: default_search_text, status_processor: status_processor, current_user: current_user } 23 | ) 24 | end 25 | 26 | before do 27 | status_processor.should_receive(:call).with(current_user).and_yield(handler) 28 | end 29 | context 'with :unknown status' do 30 | it 'renders the options to connect orcid profile' do 31 | expect(handler).to receive(:unknown).and_yield 32 | render_with_params 33 | 34 | expect(view).to render_template(partial: 'orcid/profile_connections/_options_to_connect_orcid_profile') 35 | expect(rendered).to have_tag('.orcid-connector') 36 | end 37 | end 38 | context 'with :authenticated_connection status' do 39 | let(:profile) { double('Profile', orcid_profile_id: '123-456') } 40 | it 'renders the options to view the authenticated connection' do 41 | expect(handler).to receive(:authenticated_connection).and_yield(profile) 42 | render_with_params 43 | 44 | expect(view).to render_template(partial: 'orcid/profile_connections/_authenticated_connection') 45 | expect(rendered).to have_tag('.orcid-connector') 46 | end 47 | end 48 | context 'with :pending_connection status' do 49 | let(:profile) { double('Profile', orcid_profile_id: '123-456') } 50 | it 'renders the options to view the authenticated connection' do 51 | expect(handler).to receive(:pending_connection).and_yield(profile) 52 | render_with_params 53 | 54 | expect(view).to render_template(partial: 'orcid/profile_connections/_pending_connection') 55 | expect(rendered).to have_tag('.orcid-connector') 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/fixtures/orcid_works.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.1 4 | 5 | 6 | http://sandbox.orcid.org/0000-0002-1117-8571 7 | 0000-0002-1117-8571 8 | sandbox.orcid.org 9 | 10 | 11 | en 12 | 13 | 14 | API 15 | 2014-02-14T15:32:15.453Z 16 | 2014-02-14T15:31:12.816Z 17 | 2014-02-18T20:23:40.472Z 18 | true 19 | 20 | 21 | http://sandbox.orcid.org/0000-0002-6683-6607 22 | 0000-0002-6683-6607 23 | sandbox.orcid.org 24 | 25 | Hydra ORCID Integrator 26 | 27 | 28 | 29 | 30 | 31 | 32 | Another Test Drive 33 | 34 | test 35 | 36 | http://sandbox.orcid.org/0000-0002-6683-6607 37 | 0000-0002-6683-6607 38 | sandbox.orcid.org 39 | 40 | 41 | 42 | 43 | Test Driven Orcid Integration 44 | 45 | test 46 | 47 | http://sandbox.orcid.org/0000-0002-6683-6607 48 | 0000-0002-6683-6607 49 | sandbox.orcid.org 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /spec/models/orcid/work_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe Work do 5 | let(:attributes) { 6 | { 7 | title: 'Hello', 8 | work_type: 'journal-article', 9 | put_code: '1234', 10 | external_identifiers: [ {type: 'doi', identifier: 'abc-123' }] 11 | } 12 | } 13 | subject { described_class.new(attributes) } 14 | 15 | its(:title) { should eq attributes[:title] } 16 | its(:subtitle) { should eq nil } 17 | its(:work_type) { should eq attributes[:work_type] } 18 | its(:put_code) { should eq attributes[:put_code] } 19 | its(:external_identifiers) { should be_an_instance_of(Array) } 20 | its(:valid?) { should eq true } 21 | 22 | context '#==' do 23 | context 'differing objects' do 24 | it 'should not be ==' do 25 | expect(subject == 'other').to eq(false) 26 | end 27 | end 28 | context 'same classes but different objects' do 29 | it 'should not be ==' do 30 | other = described_class.new 31 | expect(subject == other).to eq(false) 32 | end 33 | end 34 | context 'same classes with same put code' do 35 | it 'should be ==' do 36 | other = described_class.new(put_code: 123) 37 | subject.put_code = 123 38 | expect(subject == other).to eq(true) 39 | end 40 | end 41 | end 42 | 43 | context '#id' do 44 | context 'with put_code' do 45 | subject { described_class.new(put_code: '123') } 46 | its(:id) { should eq subject.put_code} 47 | end 48 | context 'with title and work type' do 49 | subject { described_class.new(title: 'Title', work_type: 'journal-article') } 50 | its(:id) { should eq [subject.title, subject.work_type]} 51 | end 52 | 53 | context 'without title, work type, and put_code' do 54 | subject { described_class.new } 55 | its(:id) { should eq nil } 56 | end 57 | end 58 | 59 | context '#to_xml' do 60 | it 'should return an XML document' do 61 | rendered = subject.to_xml 62 | expect(rendered).to have_tag('orcid-profile orcid-activities orcid-works orcid-work') do 63 | with_tag('work-title title', text: subject.title) 64 | with_tag('work-type', text: subject.work_type) 65 | with_tag('work-external-identifiers work-external-identifier', count: 1) do 66 | with_tag('work-external-identifier-type', text: 'doi') 67 | with_tag('work-external-identifier-id', text: 'abc-123') 68 | end 69 | end 70 | end 71 | end 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /app/models/orcid/work.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # A well-defined data structure that coordinates with its :template in order 3 | # to generate XML that can be POSTed/PUT as an Orcid Work. 4 | class Work 5 | VALID_WORK_TYPES = 6 | %w(artistic-performance book-chapter book-review book 7 | conference-abstract conference-paper conference-poster 8 | data-set dictionary-entry disclosure dissertation 9 | edited-book encyclopedia-entry invention journal-article 10 | journal-issue lecture-speech license magazine-article 11 | manual newsletter-article newspaper-article online-resource 12 | other patent registered-copyright report research-technique 13 | research-tool spin-off-company standards-and-policy 14 | supervised-student-publication technical-standard test 15 | translation trademark website working-paper 16 | ).freeze 17 | 18 | # An Orcid Work's external identifier is not represented in a single 19 | # attribute. 20 | class ExternalIdentifier 21 | include Virtus.value_object 22 | values do 23 | attribute :type, String 24 | attribute :identifier, String 25 | end 26 | end 27 | 28 | include Virtus.model 29 | include ActiveModel::Validations 30 | extend ActiveModel::Naming 31 | 32 | attribute :title, String 33 | validates :title, presence: true 34 | 35 | attribute :work_type, String 36 | validates :work_type, presence: true, inclusion: { in: VALID_WORK_TYPES } 37 | 38 | attribute :subtitle, String 39 | attribute :journal_title, String 40 | attribute :short_description, String 41 | attribute :citation_type, String 42 | attribute :citation, String 43 | attribute :publication_year, Integer 44 | attribute :publication_month, Integer 45 | attribute :url, String 46 | attribute :language_code, String 47 | attribute :country, String 48 | attribute :put_code, String 49 | attribute :external_identifiers, Array[ExternalIdentifier] 50 | 51 | def work_citation? 52 | citation_type.present? || citation.present? 53 | end 54 | 55 | def publication_date? 56 | publication_year.present? || publication_month.present? 57 | end 58 | 59 | def to_xml 60 | XmlRenderer.call(self) 61 | end 62 | 63 | def ==(other) 64 | super || 65 | other.instance_of?(self.class) && 66 | id.present? && 67 | other.id == id 68 | end 69 | 70 | def id 71 | if put_code.present? 72 | put_code 73 | elsif title.present? && work_type.present? 74 | [title, work_type] 75 | else 76 | nil 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /app/templates/orcid/work.template.v1.2.xml.erb: -------------------------------------------------------------------------------- 1 | 2 | 5 | 1.2 6 | 7 | 8 | <% works.each do |work| %> 9 | 10 | 11 | <%= work.title %> 12 | <% if work.subtitle.present? %><%= work.subtitle %><% end %> 13 | 14 | <% if work.journal_title.present? %><%= work.journal_title %><% end %> 15 | <% if work.short_description.present? %><%= work.short_description %><% end %> 16 | <% if work.work_citation? %> 17 | <%= work.citation_type %> 18 | <%= work.citation %> 19 | <% end %> 20 | <%= work.work_type %> 21 | <% if work.publication_date? %> 22 | <%= work.publication_year %> 23 | <% if work.publication_month.present? %><%= work.publication_month %><% end %> 24 | <% end %> 25 | <% if work.external_identifiers.present? %> 26 | <% work.external_identifiers.each do |external_identifier| %> 27 | <%= external_identifier.type %> 28 | <%= external_identifier.identifier %> 29 | <% end %> 30 | <% end %> 31 | <% if work.url.present? %><%= work.url %><% end %> 32 | 43 | <% if work.language_code.present? %><%= work.language_code %><% end %> 44 | <% if work.country.present? %><%= work.country %><% end %> 45 | 46 | <% end %> 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /orcid.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require 'orcid/version' 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = 'orcid' 9 | s.version = Orcid::VERSION 10 | s.authors = [ 11 | 'Jeremy Friesen' 12 | ] 13 | s.email = [ 14 | 'jeremy.n.friesen@gmail.com' 15 | ] 16 | s.homepage = 'https://github.com/projecthydra-labs/orcid' 17 | s.metadata = { 18 | 'source' => 'https://github.com/projecthydra-labs/orcid', 19 | 'issue_tracker' => 'https://github.com/projecthydra-labs/orcid/issues' 20 | } 21 | s.summary = 'A Rails engine for orcid.org integration.' 22 | s.description = 'A Rails engine for orcid.org integration.' 23 | 24 | s.files = `git ls-files -z`.split("\x0") 25 | # Deliberately removing bin executables as it appears to relate to 26 | # https://github.com/cbeer/engine_cart/issues/9 27 | s.executables = s.executables = s.files.grep(%r{^bin/}) do |f| 28 | f == 'bin/rails' ? nil : File.basename(f) 29 | end.compact 30 | s.test_files = s.files.grep(/^(test|spec|features)\//) 31 | s.require_paths = ['lib'] 32 | 33 | s.add_dependency 'nokogiri', '1.6.8' 34 | s.add_dependency 'railties', '~> 4.0' 35 | s.add_dependency 'figaro' 36 | s.add_dependency 'devise-multi_auth', '~> 0.1' 37 | s.add_dependency 'omniauth-orcid', '0.6' 38 | s.add_dependency 'mappy' 39 | s.add_dependency 'virtus' 40 | s.add_dependency 'email_validator' 41 | s.add_dependency 'simple_form' 42 | s.add_dependency 'omniauth-oauth2', '< 1.4' 43 | s.add_dependency 'hashie', '3.4.6' 44 | 45 | s.add_development_dependency 'sqlite3' 46 | s.add_development_dependency 'engine_cart' 47 | s.add_development_dependency 'rspec-rails', '~> 2.99' 48 | s.add_development_dependency 'database_cleaner' 49 | s.add_development_dependency 'factory_girl' 50 | s.add_development_dependency 'rspec-html-matchers', '~> 0.5.0' 51 | s.add_development_dependency 'capybara' 52 | s.add_development_dependency 'capybara-webkit' 53 | s.add_development_dependency 'headless' 54 | s.add_development_dependency 'webmock' 55 | s.add_development_dependency 'simplecov' 56 | s.add_development_dependency 'rest_client' 57 | s.add_development_dependency 'rspec-given' 58 | s.add_development_dependency 'rspec', '~>2.99' 59 | s.add_development_dependency 'rspec-mocks', '~>2.99' 60 | s.add_development_dependency 'rspec-core', '~>2.99' 61 | s.add_development_dependency 'rspec-expectations', '~>2.99' 62 | s.add_development_dependency 'rspec-its' 63 | s.add_development_dependency 'rspec-activemodel-mocks' 64 | s.add_development_dependency 'rake', '11.2.2' 65 | end 66 | -------------------------------------------------------------------------------- /app/models/orcid/profile_status.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # Responsible for determining a given user's orcid profile state as it 3 | # pertains to the parent application. 4 | # 5 | # @TODO - There are quite a few locations where the state related behavior 6 | # has leaked out (i.e. the Orcid::ProfileConnectionsController and Orcid:: 7 | # ProfileRequestsController) 8 | # 9 | # ProfileStatus.status 10 | # **:authenticated_connection** - User has authenticated against the Orcid 11 | # remote system 12 | # **:pending_connection** - User has indicated there is a connection, but has 13 | # not authenticated against the Orcid remote system 14 | # **:profile_request_pending** - User has requested a profile be created on 15 | # their behalf 16 | # **:unknown** - None of the above 17 | class ProfileStatus 18 | def self.for(user, collaborators = {}, &block) 19 | new(user, collaborators, &block).status 20 | end 21 | 22 | attr_reader :user, :profile_finder, :request_finder, :callback_handler 23 | 24 | def initialize(user, collaborators = {}) 25 | @user = user 26 | @profile_finder = collaborators.fetch(:profile_finder) { default_profile_finder } 27 | @request_finder = collaborators.fetch(:request_finder) { default_request_finder } 28 | @callback_handler = collaborators.fetch(:callback_handler) { default_callback_handler } 29 | yield(callback_handler) if block_given? 30 | end 31 | 32 | def status 33 | return callback(:unknown) if user.nil? 34 | profile = profile_finder.call(user) 35 | if profile 36 | if profile.verified_authentication? 37 | return callback(:authenticated_connection, profile) 38 | else 39 | return callback(:pending_connection, profile) 40 | end 41 | else 42 | request = request_finder.call(user) 43 | if request 44 | if request.error_on_profile_creation? 45 | return callback(:profile_request_in_error, request) 46 | else 47 | return callback(:profile_request_pending, request) 48 | end 49 | else 50 | return callback(:unknown) 51 | end 52 | end 53 | end 54 | 55 | private 56 | 57 | def callback(name, *args) 58 | callback_handler.call(name, *args) 59 | name 60 | end 61 | 62 | def default_callback_handler 63 | require 'orcid/named_callbacks' 64 | NamedCallbacks.new 65 | end 66 | 67 | def default_profile_finder 68 | require 'orcid' 69 | Orcid.method(:profile_for) 70 | end 71 | 72 | def default_request_finder 73 | require 'orcid/profile_request' 74 | ProfileRequest.method(:find_by_user) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/models/orcid/profile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe Profile do 5 | let(:orcid_profile_id) { '0001-0002-0003-0004' } 6 | let(:remote_service) { double('Service') } 7 | let(:mapper) { double("Mapper") } 8 | let(:non_orcid_work) { double("A non-ORCID Work") } 9 | let(:orcid_work) { double("Orcid::Work") } 10 | let(:xml_renderer) { double("Renderer") } 11 | let(:xml_parser) { double("Parser") } 12 | let(:xml) { double("XML Payload")} 13 | 14 | subject { 15 | described_class.new( 16 | orcid_profile_id, 17 | mapper: mapper, 18 | remote_service: remote_service, 19 | xml_renderer: xml_renderer, 20 | xml_parser: xml_parser 21 | ) 22 | } 23 | 24 | def should_map(source, target) 25 | mapper.should_receive(:map).with(source, target: 'orcid/work').and_return(target) 26 | end 27 | 28 | context '#remote_works' do 29 | let(:parsed_object) { double("Parsed Object")} 30 | let(:response_body) { double("XML Response") } 31 | it 'should parse the response body' do 32 | xml_parser.should_receive(:call).with(response_body).and_return(parsed_object) 33 | remote_service.should_receive(:call).with(orcid_profile_id, request_method: :get).and_return(response_body) 34 | 35 | expect(subject.remote_works).to eq(parsed_object) 36 | end 37 | end 38 | 39 | 40 | context '#append_new_work' do 41 | it 'should transform the input work to xml and deliver to the remote_service' do 42 | xml_renderer.should_receive(:call).with([orcid_work]).and_return(xml) 43 | remote_service.should_receive(:call).with(orcid_profile_id, body: xml, request_method: :post) 44 | 45 | should_map(non_orcid_work, orcid_work) 46 | 47 | subject.append_new_work(non_orcid_work) 48 | end 49 | end 50 | 51 | context '#replace_works_with' do 52 | it 'should transform the input work to xml and deliver to the remote_service' do 53 | xml_renderer.should_receive(:call).with([orcid_work]).and_return(xml) 54 | remote_service.should_receive(:call).with(orcid_profile_id, body: xml, request_method: :put) 55 | 56 | should_map(non_orcid_work, orcid_work) 57 | 58 | subject.replace_works_with(non_orcid_work) 59 | end 60 | end 61 | 62 | context '#verified_authentication' do 63 | it 'should not be authorized' do 64 | subject.verified_authentication?.should eq false 65 | end 66 | end 67 | 68 | context '#verified_authentication' do 69 | it 'should be authorized' do 70 | Orcid.stub(:access_token_for).and_return(Object.new()) 71 | 72 | subject.verified_authentication?.should eq true 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/orcid/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | 3 | # Intended to be the base for all exceptions raised by the Orcid gem. 4 | class BaseError < RuntimeError 5 | end 6 | 7 | class ProfileRequestStateError < BaseError 8 | def initialize(user) 9 | super("Unexpected Orcid::ProfileRequest state for #{user.class} ID=#{user.to_param}.") 10 | end 11 | end 12 | 13 | class ProfileRequestMethodExpectedError < BaseError 14 | def initialize(request, method_name) 15 | super("Expected #{request.inspect} to respond to :#{method_name}.") 16 | end 17 | end 18 | 19 | class MissingUserForProfileRequest < BaseError 20 | def initialize(request) 21 | super("#{request.class} ID=#{request.to_param} is not associated with a :user.") 22 | end 23 | end 24 | 25 | class ConfigurationError < BaseError 26 | def initialize(key_name) 27 | super("Unable to find #{key_name.inspect} in configuration storage.") 28 | end 29 | end 30 | 31 | # Because in trouble shooting what all goes into this remote call, 32 | # you may very well want all of this. 33 | class RemoteServiceError < BaseError 34 | def initialize(options) 35 | text = [] 36 | text << "-- Client --" 37 | append_client_options(options[:client], text) 38 | append_token(options[:token], text) 39 | append_request(options, text) 40 | append_response(options, text) 41 | super(text.join("\n")) 42 | end 43 | 44 | private 45 | 46 | def append_client_options(client, text) 47 | if client 48 | text << "id:\n\t#{client.id.inspect}" 49 | text << "site:\n\t#{client.site.inspect}" 50 | text << "options:\n\t#{client.options.inspect}" 51 | if defined?(Orcid.provider) 52 | text << "scopes:\n\t#{Orcid.provider.authentication_scope}" 53 | end 54 | end 55 | text 56 | end 57 | 58 | def append_token(token, text) 59 | text << "\n-- Token --" 60 | if token 61 | text << "access_token:\n\t#{token.token.inspect}" 62 | text << "refresh_token:\n\t#{token.refresh_token.inspect}" 63 | end 64 | text 65 | end 66 | 67 | def append_request(options, text) 68 | text << "\n-- Request --" 69 | text << "path:\n\t#{options[:request_path].inspect}" if options[:request_path] 70 | text << "headers:\n\t#{options[:request_headers].inspect}" if options[:request_headers] 71 | text << "body:\n\t#{options[:request_body]}" if options[:request_body] 72 | text 73 | end 74 | 75 | def append_response(options, text) 76 | text << "\n-- Response --" 77 | text << "status:\n\t#{options[:response_status].inspect}" if options[:response_status] 78 | text << "body:\n\t#{options[:response_body]}" if options[:response_body] 79 | text 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/generators/orcid/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | 3 | module Orcid 4 | # If you want to quickly add Orcid integration into your application. 5 | # This assumes the use of the ubiqutous Devise gem. 6 | class InstallGenerator < Rails::Generators::Base 7 | source_root File.expand_path('../templates', __FILE__) 8 | 9 | class_option :devise, default: false, type: :boolean 10 | class_option :skip_application_yml, default: false, type: :boolean 11 | 12 | def create_application_yml 13 | unless options[:skip_application_yml] 14 | create_file 'config/application.yml' do 15 | orcid_app_id = ask('What is your Orcid Client ID?') 16 | orcid_app_secret = ask('What is your Orcid Client Secret?') 17 | [ 18 | '', 19 | "ORCID_APP_ID: #{orcid_app_id}", 20 | "ORCID_APP_SECRET: #{orcid_app_secret}", 21 | '' 22 | ].join("\n") 23 | end 24 | end 25 | end 26 | 27 | def install_devise_multi_auth 28 | if options[:devise] 29 | generate 'devise:multi_auth:install --install_devise' 30 | else 31 | generate 'devise:multi_auth:install' 32 | end 33 | end 34 | 35 | def copy_locale 36 | copy_file( 37 | '../../../../../config/locales/orcid.en.yml', 38 | 'config/locales/orcid.en.yml' 39 | ) 40 | end 41 | 42 | def install_migrations 43 | rake 'orcid:install:migrations' 44 | end 45 | 46 | def update_devise_user 47 | config_code = ', :omniauthable, :omniauth_providers => [:orcid]' 48 | insert_into_file( 49 | 'app/models/user.rb', 50 | config_code, after: /:validatable/, verbose: false 51 | ) 52 | end 53 | 54 | def update_devise_omniauth_provider 55 | init_code = %( 56 | config.omniauth(:orcid, Orcid.provider.id, Orcid.provider.secret, 57 | scope: Orcid.provider.authentication_scope, 58 | client_options: { 59 | site: Orcid.provider.site_url, 60 | authorize_url: Orcid.provider.authorize_url, 61 | token_url: Orcid.provider.token_url 62 | } 63 | ) 64 | ) 65 | insert_into_file( 66 | 'config/initializers/devise.rb', 67 | init_code, after: /Devise\.setup.*$/, verbose: true 68 | ) 69 | end 70 | 71 | def mount_orcid_engine 72 | route 'mount Orcid::Engine => "/orcid"' 73 | end 74 | 75 | def migrate_the_database 76 | rake 'db:migrate' 77 | end 78 | 79 | def install_initializer 80 | template( 81 | 'orcid_initializer.rb.erb', 82 | 'config/initializers/orcid_initializer.rb' 83 | ) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /app/models/orcid/profile_connection.rb: -------------------------------------------------------------------------------- 1 | require 'virtus' 2 | require 'active_model' 3 | module Orcid 4 | # Responsible for connecting an authenticated user to the ORCID profile that 5 | # the user searched for and selected. 6 | class ProfileConnection 7 | include Virtus.model 8 | include ActiveModel::Validations 9 | include ActiveModel::Conversion 10 | extend ActiveModel::Naming 11 | 12 | # See: http://support.orcid.org/knowledgebase/articles/132354-tutorial-searching-with-the-api 13 | class_attribute :available_query_attribute_names 14 | self.available_query_attribute_names = [:text] 15 | 16 | available_query_attribute_names.each do |attribute_name| 17 | attribute attribute_name 18 | end 19 | 20 | attribute :orcid_profile_id 21 | attribute :user 22 | 23 | validates :user, presence: true 24 | validates :orcid_profile_id, presence: true 25 | 26 | def save 27 | valid? ? persister.call(user, orcid_profile_id) : false 28 | end 29 | 30 | def persisted? 31 | false 32 | end 33 | 34 | attr_writer :persister 35 | def persister 36 | @persister ||= default_persister 37 | end 38 | private :persister 39 | 40 | def default_persister 41 | require 'orcid' 42 | Orcid.method(:connect_user_and_orcid_profile) 43 | end 44 | 45 | attr_writer :profile_query_service 46 | def profile_query_service 47 | @profile_query_service ||= default_profile_query_service 48 | end 49 | private :profile_query_service 50 | 51 | def default_profile_query_service 52 | Remote::ProfileQueryService.new do |on| 53 | on.found { |results| self.orcid_profile_candidates = results } 54 | on.not_found { self.orcid_profile_candidates = [] } 55 | end 56 | end 57 | private :default_profile_query_service 58 | 59 | def with_orcid_profile_candidates 60 | yield(orcid_profile_candidates) if query_requested? 61 | end 62 | 63 | attr_writer :orcid_profile_candidates 64 | private :orcid_profile_candidates= 65 | def orcid_profile_candidates 66 | @orcid_profile_candidates || lookup_profile_candidates 67 | end 68 | 69 | def lookup_profile_candidates 70 | profile_query_service.call(query_attributes) if query_requested? 71 | end 72 | private :lookup_profile_candidates 73 | 74 | def query_requested? 75 | available_query_attribute_names.any? do |attribute_name| 76 | attributes[attribute_name].present? 77 | end 78 | end 79 | private :query_requested? 80 | 81 | def query_attributes 82 | available_query_attribute_names.each_with_object({}) do |name, mem| 83 | orcid_formatted_name = convert_attribute_name_to_orcid_format(name) 84 | mem[orcid_formatted_name] = attributes.fetch(name) 85 | mem 86 | end 87 | end 88 | 89 | def convert_attribute_name_to_orcid_format(name) 90 | name.to_s.gsub(/_+/, '-') 91 | end 92 | private :convert_attribute_name_to_orcid_format 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /app/controllers/orcid/profile_connections_controller.rb: -------------------------------------------------------------------------------- 1 | module Orcid 2 | # Responsible for negotiating a user request Profile Creation. 3 | # 4 | # @TODO - Leverage the Orcid::ProfileStatus object instead of the really 5 | # chatty method names. The method names are fine, but the knowledge of 6 | # Orcid::ProfileStatus is encapsulated in that class. 7 | class ProfileConnectionsController < Orcid::ApplicationController 8 | respond_to :html 9 | before_filter :authenticate_user! 10 | 11 | def index 12 | redirecting_because_user_does_not_have_a_connected_orcid_profile || 13 | redirecting_because_user_must_verify_their_connected_profile || 14 | redirecting_because_user_has_verified_their_connected_profile 15 | end 16 | 17 | def new 18 | return false if redirecting_because_user_has_connected_orcid_profile 19 | assign_attributes(new_profile_connection) 20 | respond_with(orcid, new_profile_connection) 21 | end 22 | 23 | def create 24 | return false if redirecting_because_user_has_connected_orcid_profile 25 | assign_attributes(new_profile_connection) 26 | create_profile_connection(new_profile_connection) 27 | respond_with(orcid, new_profile_connection) 28 | end 29 | 30 | SUCCESS_NOTICE = "You have been disconnected from your ORCID record." 31 | def destroy 32 | Orcid.disconnect_user_and_orcid_profile(current_user) 33 | redirect_to root_path, notice: SUCCESS_NOTICE 34 | end 35 | 36 | protected 37 | 38 | attr_reader :profile_connection 39 | helper_method :profile_connection 40 | 41 | def assign_attributes(profile_connection) 42 | profile_connection.attributes = profile_connection_params 43 | profile_connection.user = current_user 44 | end 45 | 46 | def profile_connection_params 47 | params[:profile_connection] || {} 48 | end 49 | 50 | def create_profile_connection(profile_connection) 51 | profile_connection.save 52 | end 53 | 54 | def new_profile_connection 55 | @profile_connection ||= begin 56 | Orcid::ProfileConnection.new(params[:profile_connection]) 57 | end 58 | end 59 | 60 | def redirecting_because_user_does_not_have_a_connected_orcid_profile 61 | return false if orcid_profile 62 | flash[:notice] = I18n.t( 63 | 'orcid.connections.messages.profile_connection_not_found' 64 | ) 65 | redirect_to orcid.new_profile_connection_path 66 | end 67 | 68 | def redirecting_because_user_must_verify_their_connected_profile 69 | return false unless orcid_profile 70 | return false if orcid_profile.verified_authentication? 71 | 72 | redirect_to user_omniauth_authorize_url('orcid') 73 | end 74 | 75 | def redirecting_because_user_has_verified_their_connected_profile 76 | orcid_profile = Orcid.profile_for(current_user) 77 | notice = I18n.t( 78 | 'orcid.connections.messages.verified_profile_connection_exists', 79 | orcid_profile_id: orcid_profile.orcid_profile_id 80 | ) 81 | location = path_for(:orcid_settings_path) { main_app.root_path } 82 | redirect_to(location, flash: { notice: notice }) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /app/services/orcid/remote/profile_query_service/response_parser.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'orcid/remote/profile_query_service' 2 | module Orcid 3 | module Remote 4 | class ProfileQueryService 5 | # Responsible for parsing a response document 6 | class ResponseParser 7 | 8 | # A convenience method to expose entry into the ResponseParser function 9 | def self.call(document, collaborators = {}) 10 | new(collaborators).call(document) 11 | end 12 | 13 | attr_reader :response_builder, :logger 14 | private :response_builder, :logger 15 | def initialize(collaborators = {}) 16 | @response_builder = collaborators.fetch(:response_builder) { default_response_builder } 17 | @logger = collaborators.fetch(:logger) { default_logger } 18 | end 19 | 20 | def call(document) 21 | json = JSON.parse(document) 22 | json.fetch('orcid-search-results').fetch('orcid-search-result') 23 | .each_with_object([]) do |result, returning_value| 24 | profile = result.fetch('orcid-profile') 25 | begin 26 | identifier = extract_identifier(profile) 27 | label = extract_label(identifier, profile) 28 | biography = extract_biography(profile) 29 | returning_value << response_builder.new('id' => identifier, 'label' => label, 'biography' => biography) 30 | rescue KeyError => e 31 | logger.warn("Unexpected ORCID JSON Response, part of the response has been ignored.\tException Encountered:#{e.class}\t#{e}") 32 | end 33 | returning_value 34 | end 35 | end 36 | 37 | private 38 | def extract_identifier(profile) 39 | profile.fetch('orcid-identifier').fetch('path') 40 | end 41 | 42 | def extract_label(identifier, profile) 43 | orcid_bio = profile.fetch('orcid-bio') 44 | given_names = orcid_bio.fetch('personal-details').fetch('given-names').fetch('value') 45 | # family name is not a required field on orcid record 46 | family_name = orcid_bio.try(:[], 'personal-details').try(:[], 'family-name').try(:[], 'value') 47 | emails = [] 48 | contact_details = orcid_bio['contact-details'] 49 | if contact_details 50 | emails = (contact_details['email'] || []).map {|email| email.fetch('value') } 51 | end 52 | label = "#{given_names} #{family_name}" 53 | label << ' (' << emails.join(', ') << ')' if emails.any? 54 | label << " [ORCID: #{identifier}]" 55 | label << " Look Up Orcid Profile" 56 | label.html_safe 57 | end 58 | 59 | def extract_biography(profile) 60 | orcid_bio = profile.fetch('orcid-bio') 61 | if orcid_bio['biography'] 62 | orcid_bio['biography'].fetch('value') 63 | else 64 | '' 65 | end 66 | end 67 | 68 | def default_logger 69 | Rails.logger 70 | end 71 | 72 | def default_response_builder 73 | SearchResponse 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/services/orcid/remote/profile_creation_service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'orcid/remote/profile_creation_service' 3 | 4 | module Orcid::Remote 5 | describe ProfileCreationService do 6 | Given(:payload) { %() } 7 | Given(:config) { {token: token, headers: request_headers, path: 'path/to/somewhere' } } 8 | Given(:token) { double("Token", post: response) } 9 | Given(:minted_orcid) { '0000-0001-8025-637X' } 10 | Given(:request_headers) { 11 | { 'Content-Type' => 'application/vdn.orcid+xml', 'Accept' => 'application/xml' } 12 | } 13 | Given(:callback) { StubCallback.new } 14 | Given(:callback_config) { callback.configure(:orcid_validation_error) } 15 | 16 | Given(:response) { 17 | double("Response", headers: { location: File.join("/", minted_orcid, "orcid-profile") }) 18 | } 19 | 20 | 21 | context 'with orcid created' do 22 | Given(:response) { 23 | double("Response", headers: { location: File.join("/", minted_orcid, "orcid-profile") }) 24 | } 25 | When(:returned_value) { described_class.call(payload, config, &callback_config) } 26 | Then { returned_value.should eq(minted_orcid)} 27 | And { expect(callback.invoked).to eq [:success, minted_orcid] } 28 | And { token.should have_received(:post).with(config.fetch(:path), body: payload, headers: request_headers)} 29 | end 30 | 31 | context 'with orcid not created' do 32 | Given(:response) { 33 | double("Response", headers: { location: "" }) 34 | } 35 | When(:returned_value) { described_class.call(payload, config, &callback_config) } 36 | Then { returned_value.should eq(false)} 37 | And { expect(callback.invoked).to eq [:failure] } 38 | And { token.should have_received(:post).with(config.fetch(:path), body: payload, headers: request_headers)} 39 | end 40 | 41 | context 'with an orcid validation error' do 42 | before { token.should_receive(:post).and_raise(error) } 43 | Given(:token) { double('Token') } 44 | Given(:error_description) { 'My special error' } 45 | Given(:response) do 46 | double( 47 | 'Response', 48 | :body => "#{error_description}", 49 | :parsed => true, 50 | :error= => true 51 | ) 52 | end 53 | Given(:error) { ::OAuth2::Error.new(response) } 54 | When(:returned_value) { described_class.call(payload, config, &callback_config) } 55 | Then { returned_value.should eq(false) } 56 | And { expect(callback.invoked).to eq [:orcid_validation_error, error_description] } 57 | end 58 | 59 | context 'with a remote error that is not an orcid validation error' do 60 | before { token.should_receive(:post).and_raise(error) } 61 | Given(:token) { double('Token') } 62 | Given(:response) do 63 | double( 64 | 'Response', 65 | :body => 'Danger! Problem! Help!', 66 | :parsed => true, 67 | :error= => true 68 | ) 69 | end 70 | Given(:error) { ::OAuth2::Error.new(response) } 71 | When(:returned_value) { described_class.call(payload, config, &callback_config) } 72 | Then { expect(returned_value).to have_failed } 73 | And { expect(callback.invoked).to be_nil } 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/orcid.rb: -------------------------------------------------------------------------------- 1 | require 'orcid/engine' if defined?(Rails) 2 | require 'orcid/configuration' 3 | require 'orcid/exceptions' 4 | require 'figaro' 5 | require 'mappy' 6 | require 'devise_multi_auth' 7 | require 'virtus' 8 | require 'omniauth-orcid' 9 | require 'email_validator' 10 | require 'simple_form' 11 | 12 | # The namespace for all things related to Orcid integration 13 | module Orcid 14 | class << self 15 | attr_writer :configuration 16 | 17 | def configuration 18 | @configuration ||= Configuration.new 19 | end 20 | end 21 | 22 | module_function 23 | def configure 24 | yield(configuration) 25 | end 26 | 27 | def mapper 28 | configuration.mapper 29 | end 30 | 31 | def provider 32 | configuration.provider 33 | end 34 | 35 | def parent_controller 36 | configuration.parent_controller 37 | end 38 | 39 | def authentication_model 40 | configuration.authentication_model 41 | end 42 | 43 | def connect_user_and_orcid_profile(user, orcid_profile_id) 44 | authentication_model.create!( 45 | provider: 'orcid', uid: orcid_profile_id, user: user 46 | ) 47 | end 48 | 49 | def access_token_for(orcid_profile_id, collaborators = {}) 50 | client = collaborators.fetch(:client) { oauth_client } 51 | tokenizer = collaborators.fetch(:tokenizer) { authentication_model } 52 | tokenizer.to_access_token( 53 | uid: orcid_profile_id, provider: 'orcid', client: client 54 | ) 55 | end 56 | 57 | # Returns true if the person with the given ORCID has already obtained an 58 | # ORCID access token by authenticating via ORCID. 59 | def authenticated_orcid?(orcid_profile_id) 60 | Orcid.access_token_for(orcid_profile_id).present? 61 | rescue Devise::MultiAuth::AccessTokenError 62 | return false 63 | end 64 | 65 | def disconnect_user_and_orcid_profile(user) 66 | authentication_model.where(provider: 'orcid', user: user).destroy_all 67 | Orcid::ProfileRequest.where(user: user).destroy_all 68 | true 69 | end 70 | 71 | def profile_for(user) 72 | auth = authentication_model.where(provider: 'orcid', user: user).first 73 | auth && Orcid::Profile.new(auth.uid) 74 | end 75 | 76 | def enqueue(object) 77 | object.run 78 | end 79 | 80 | def url_for_orcid_id(orcid_profile_id) 81 | File.join(provider.host_url, orcid_profile_id) 82 | end 83 | 84 | def oauth_client 85 | # passing the site: option as Orcid's Sandbox has an invalid certificate 86 | # for the api.sandbox.orcid.org 87 | @oauth_client ||= Devise::MultiAuth.oauth_client_for( 88 | 'orcid', options: { site: provider.site_url } 89 | ) 90 | end 91 | 92 | def client_credentials_token(scope, collaborators = {}) 93 | tokenizer = collaborators.fetch(:tokenizer) { oauth_client.client_credentials } 94 | tokenizer.get_token(scope: scope) 95 | end 96 | 97 | # As per an isolated_namespace Rails engine. 98 | # But the isolated namespace creates issues. 99 | # @api private 100 | def table_name_prefix 101 | 'orcid_' 102 | end 103 | 104 | # Because I am not using isolate_namespace for Orcid::Engine 105 | # I need this for the application router to find the appropriate routes. 106 | # @api private 107 | def use_relative_model_naming? 108 | true 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /config/locales/orcid.en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | orcid: 24 | requests: 25 | messages: 26 | existing_request_not_found: "Unable to find an existing orcid Profile request. Go ahead and create one." 27 | existing_request: "You have already submitted an ORCID Profile request." 28 | previously_connected_profile: "You have already connected to an ORCID Profile (%{orcid_profile_id})." 29 | profile_request_created: "Your ORCID profile request has been submitted. Check your email for more information from ORCID." 30 | profile_request_created: "Your ORCID profile request has been submitted. Check your email for more information from ORCID." 31 | profile_request_destroyed: "Your ORCID profile request has been cancelled." 32 | connections: 33 | messages: 34 | profile_connection_not_found: "Unable to find an existing ORCID Profile connection." 35 | verified_profile_connection_exists: "You have already connected and verified your ORCID Profile (%{orcid_profile_id})." 36 | verbose_name: Open Researcher and Contributor ID (ORCID) 37 | views: 38 | profile_connections: 39 | fieldsets: 40 | search_orcid_profiles: "Search ORCID Profiles" 41 | select_an_orcid_profile: "Select an ORCID Profile" 42 | buttons: 43 | search: 'Search' 44 | authenticated_connection: "

Your ORCID (%{orcid_profile_id}) has been authenticated for this application.

" 45 | profile_request_pending: "

We are processing your ORCID Profile Request. It was submitted ago.

" 46 | pending_connection: > 47 |

You have an ORCID (%{orcid_profile_id}).

48 |

However, your ORCID has not been verified by this system. There are a few possibilities:

49 | 53 | helpers: 54 | label: 55 | orcid/profile_connection: 56 | orcid_profile_id: "Enter your existing ORCID (####-####-####-####)" 57 | create_an_orcid: Create an ORCID 58 | look_up_your_existing_orcid: Look up your existing ORCID 59 | connect_button_text: Connect 60 | profile_request_destroy: Cancel your ORCID request 61 | -------------------------------------------------------------------------------- /spec/models/orcid/profile_connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fast_helper' 2 | require 'orcid/profile_connection' 3 | 4 | # :nodoc: 5 | module Orcid 6 | describe ProfileConnection do 7 | let(:text) { 'test@hello.com' } 8 | let(:dois) { '123' } 9 | let(:user) { double('User') } 10 | let(:profile_query_service) { double('Profile Lookup Service') } 11 | let(:persister) { double('Persister') } 12 | 13 | subject do 14 | Orcid::ProfileConnection.new(text: text, user: user).tap do |pc| 15 | pc.persister = persister 16 | pc.profile_query_service = profile_query_service 17 | end 18 | end 19 | 20 | its(:default_persister) { should respond_to(:call) } 21 | its(:text) { should eq text } 22 | its(:to_model) { should eq subject } 23 | its(:user) { should eq user } 24 | its(:persisted?) { should eq false } 25 | its(:orcid_profile_id) { should be_nil } 26 | 27 | context '.available_query_attribute_names' do 28 | subject { Orcid::ProfileConnection.new.available_query_attribute_names } 29 | it { should include(:text) } 30 | end 31 | 32 | context '#query_attributes' do 33 | subject do 34 | Orcid::ProfileConnection.new( 35 | text: text, user: user, digital_object_ids: dois 36 | ) 37 | end 38 | its(:query_attributes) do 39 | should eq( 40 | 'text' => text 41 | ) 42 | end 43 | end 44 | 45 | context '#query_requested?' do 46 | context 'with no attributes' do 47 | subject { Orcid::ProfileConnection.new } 48 | its(:query_requested?) { should eq false } 49 | end 50 | context 'with attribute set' do 51 | subject { Orcid::ProfileConnection.new(text: text, user: user) } 52 | its(:query_requested?) { should eq true } 53 | end 54 | end 55 | 56 | context '#save' do 57 | let(:orcid_profile_id) { '1234-5678' } 58 | 59 | it 'should call the persister when valid' do 60 | subject.orcid_profile_id = orcid_profile_id 61 | persister.should_receive(:call). 62 | with(user, orcid_profile_id). 63 | and_return(:persisted) 64 | 65 | expect(subject.save).to eq(:persisted) 66 | end 67 | 68 | it 'should NOT call the persister and add errors when not valid' do 69 | subject.user = nil 70 | subject.orcid_profile_id = nil 71 | 72 | expect { subject.save }.to change { subject.errors.count }.by(2) 73 | end 74 | end 75 | 76 | context '#with_orcid_profile_candidates' do 77 | context 'with an text' do 78 | 79 | it 'should yield the query response' do 80 | subject.text = text 81 | 82 | profile_query_service. 83 | should_receive(:call). 84 | with(subject.query_attributes). 85 | and_return(:query_response) 86 | 87 | expect { |b| subject.with_orcid_profile_candidates(&b) }. 88 | to yield_with_args(:query_response) 89 | end 90 | end 91 | 92 | context 'without an text' do 93 | it 'should not yield' do 94 | subject.text = nil 95 | profile_query_service.stub(:call).and_return(:query_response) 96 | 97 | expect { |b| subject.with_orcid_profile_candidates(&b) }. 98 | to_not yield_control 99 | end 100 | end 101 | 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/models/orcid/profile_status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'orcid/profile_status' 3 | 4 | module Orcid 5 | describe ProfileStatus do 6 | Given(:user) { nil } 7 | Given(:profile_finder) { double('ProfileFinder') } 8 | Given(:request_finder) { double('RequestFinder') } 9 | Given(:callback) { StubCallback.new } 10 | Given(:callback_config) do 11 | callback.configure( 12 | :unknown, 13 | :authenticated_connection, 14 | :pending_connection, 15 | :profile_request_pending, 16 | :profile_request_in_error 17 | ) 18 | end 19 | Given(:subject) do 20 | described_class.new(user, profile_finder: profile_finder, request_finder: request_finder, &callback_config) 21 | end 22 | 23 | context '.for' do 24 | Given(:user) { nil } 25 | When(:response) { described_class.for(user, &callback_config) } 26 | Then { expect(response).to eq :unknown } 27 | And { expect(callback.invoked).to eq [:unknown] } 28 | end 29 | 30 | context '#status' do 31 | context 'user is nil' do 32 | Given(:user) { nil } 33 | When(:status) { subject.status } 34 | Then { expect(status).to eq :unknown } 35 | end 36 | 37 | context 'user is not nil' do 38 | Given(:user) { double('User') } 39 | Given(:profile_finder) { double('ProfileFinder', call: nil) } 40 | Given(:request_finder) { double('RequestFinder', call: nil) } 41 | context 'and has a profile' do 42 | Given(:profile_finder) { double('ProfileFinder', call: profile) } 43 | context 'that they have remotely authenticated' do 44 | Given(:profile) { double('Profile', verified_authentication?: true) } 45 | When(:status) { subject.status } 46 | Then { expect(status).to eq :authenticated_connection } 47 | And { expect(callback.invoked).to eq [:authenticated_connection, profile] } 48 | end 49 | context 'that they have not remotely authenticated' do 50 | Given(:profile) { double('Profile', verified_authentication?: false) } 51 | When(:status) { subject.status } 52 | Then { expect(status).to eq :pending_connection } 53 | And { expect(callback.invoked).to eq [:pending_connection, profile] } 54 | end 55 | end 56 | 57 | context 'and does not have a profile' do 58 | context 'but has submitted a request' do 59 | Given(:request) { double('ProfileRequest', :error_on_profile_creation? => error_on_creation) } 60 | Given(:request_finder) { double('RequestFinder', call: request) } 61 | 62 | context "and there weren't problems with the request" do 63 | Given(:error_on_creation) { false } 64 | When(:status) { subject.status } 65 | Then { expect(status).to eq :profile_request_pending } 66 | And { expect(callback.invoked).to eq [:profile_request_pending, request] } 67 | end 68 | 69 | context "and there were problems with the request" do 70 | Given(:error_on_creation) { true } 71 | When(:status) { subject.status } 72 | Then { expect(status).to eq :profile_request_in_error } 73 | And { expect(callback.invoked).to eq [:profile_request_in_error, request] } 74 | end 75 | end 76 | context 'user does not have a request' do 77 | When(:status) { subject.status } 78 | Then { expect(status).to eq :unknown } 79 | And { expect(callback.invoked).to eq [:unknown] } 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | 3 | # Normally loaded via application config; I want the production code to 4 | # choke when app_id and app_secret is not set. 5 | ENV["RAILS_ENV"] ||= 'test' 6 | if ENV['COVERAGE'] 7 | require 'simplecov' 8 | SimpleCov.start 'rails' 9 | SimpleCov.command_name "spec" 10 | end 11 | 12 | if ENV['TRAVIS'] 13 | require 'coveralls' 14 | Coveralls.wear!('rails') 15 | end 16 | 17 | require 'figaro' # must declare before the application loads 18 | require 'engine_cart' 19 | require 'omniauth-orcid' 20 | require File.expand_path("../../.internal_test_app/config/environment.rb", __FILE__) 21 | 22 | EngineCart.load_application! 23 | 24 | require 'orcid/spec_support' 25 | require 'rspec/rails' 26 | require 'rspec/autorun' 27 | require 'rspec/given' 28 | require 'rspec/active_model/mocks' 29 | require 'rspec/its' 30 | require 'database_cleaner' 31 | require 'factory_girl' 32 | require 'rspec-html-matchers' 33 | require 'webmock/rspec' 34 | require 'capybara' 35 | require 'capybara-webkit' 36 | require 'headless' 37 | 38 | Capybara.register_driver :webkit do |app| 39 | Capybara::Webkit::Driver.new(app, :ignore_ssl_errors => true) 40 | end 41 | 42 | Capybara.javascript_driver = :webkit 43 | 44 | if ENV['TRAVIS'] || ENV['JENKINS'] 45 | headless = Headless.new 46 | headless.start 47 | end 48 | 49 | # Requires supporting ruby files with custom matchers and macros, etc, 50 | # in spec/support/ and its subdirectories. 51 | Dir[File.expand_path("../support/**/*.rb",__FILE__)].each {|f| require f} 52 | Dir[File.expand_path("../factories/**/*.rb",__FILE__)].each {|f| require f} 53 | 54 | # From https://github.com/plataformatec/devise/wiki/How-To:-Stub-authentication-in-controller-specs 55 | module ControllerHelpers 56 | def sign_in(user = double('user')) 57 | if user.nil? 58 | request.env['warden'].stub(:authenticate!). 59 | and_throw(:warden, {:scope => :user}) 60 | controller.stub :current_user => nil 61 | else 62 | request.env['warden'].stub :authenticate! => user 63 | controller.stub :current_user => user 64 | end 65 | end 66 | 67 | def main_app 68 | controller.main_app 69 | end 70 | 71 | def orcid 72 | controller.orcid 73 | end 74 | 75 | end 76 | 77 | module FixtureFiles 78 | def fixture_file(path) 79 | Pathname.new(File.expand_path(File.join("../fixtures", path), __FILE__)) 80 | end 81 | end 82 | 83 | RSpec.configure do |config| 84 | config.include Devise::TestHelpers, type: :controller 85 | config.include ControllerHelpers, type: :controller 86 | config.include FixtureFiles 87 | 88 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 89 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 90 | 91 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 92 | # examples within a transaction, remove the following line or assign false 93 | # instead of true. 94 | config.use_transactional_fixtures = true 95 | 96 | # If true, the base class of anonymous controllers will be inferred 97 | # automatically. This will be the default behavior in future versions of 98 | # rspec-rails. 99 | config.infer_base_class_for_anonymous_controllers = false 100 | 101 | config.infer_spec_type_from_file_location! 102 | 103 | # Run specs in random order to surface order dependencies. If you find an 104 | # order dependency and want to debug it, you can fix the order by providing 105 | # the seed, which is printed after each run. 106 | # --seed 1234 107 | config.order = "random" 108 | 109 | config.before(:suite) do 110 | DatabaseCleaner.strategy = :truncation 111 | end 112 | config.before(:each) do 113 | OmniAuth.config.test_mode = true 114 | DatabaseCleaner.start 115 | end 116 | config.after(:each) do 117 | DatabaseCleaner.clean 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/tasks/orcid_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :orcid do 2 | namespace :batch do 3 | desc 'Given the input: CSV query for existing Orcids and report to output: CSV' 4 | task :query => [:environment, :init, :query_runner, :run] 5 | 6 | task :query_runner do 7 | @runner = lambda { |person| 8 | puts "Processing: #{person.email}" 9 | Orcid::Remote::ProfileLookupRunner.new {|on| 10 | on.found {|results| 11 | person.found(results) 12 | puts "\tfound" if verbose 13 | } 14 | on.not_found { 15 | person.not_found 16 | puts "\tnot found" if verbose 17 | } 18 | }.call(email: person.email) 19 | } 20 | end 21 | 22 | desc "Given the input: CSV query for existing Orcids and if not found create new ones recording output: CSV" 23 | task :create => [:environment, :init, :create_runner, :run] 24 | 25 | task :create_runner do 26 | @creation_service = lambda {|person| 27 | puts "Creating Profile for: #{person.email}" 28 | profile_creation_service = Orcid::Remote::ProfileCreationService.new do |on| 29 | on.success {|orcid_profile_id| 30 | person.orcid_profile_id = orcid_profile_id 31 | puts "\tcreated #{orcid_profile_id}" if verbose 32 | } 33 | end 34 | profile_request = Orcid::ProfileRequest.new(person.attributes) 35 | profile_request.run( 36 | validator: lambda{|*| true}, 37 | profile_creation_service: profile_creation_service 38 | ) 39 | } 40 | @runner = lambda { |person| 41 | puts "Processing: #{person.email}" 42 | Orcid::Remote::ProfileQueryService.new {|on| 43 | on.found {|results| 44 | person.found(results) 45 | puts "\tfound" if verbose 46 | } 47 | on.not_found { 48 | person.not_found 49 | puts "\tnot found" if verbose 50 | @creation_service.call(person) 51 | } 52 | }.call(email: person.email) 53 | } 54 | end 55 | 56 | 57 | task :run do 58 | if defined?(WebMock) 59 | WebMock.allow_net_connect! 60 | end 61 | input_file = ENV.fetch('input') { './tmp/orcid_input.csv' } 62 | output_file = ENV.fetch('output') { './tmp/orcid_output.csv' } 63 | 64 | require 'csv' 65 | CSV.open(output_file, 'wb+') do |output| 66 | output << @person_builder.to_header_row 67 | CSV.foreach(input_file, headers: true, header_converters: [lambda{|col| col.strip }]) do |input| 68 | person = @person_builder.new(input.to_hash) 69 | @runner.call(person) 70 | output << person.to_output_row 71 | end 72 | end 73 | end 74 | 75 | task :init do 76 | module Orcid::Batch 77 | class PersonRecord 78 | def self.to_header_row 79 | ['email', 'given_names', 'family_name', 'existing_orcids', 'created_orcid', 'queried_at'] 80 | end 81 | attr_reader :email, :given_names, :family_name, :existing_orcids 82 | attr_accessor :created_orcid 83 | def initialize(row) 84 | @email = row.fetch('email') 85 | @given_names = row['given_names'] 86 | @family_name = row['family_name'] 87 | @existing_orcids = nil 88 | end 89 | 90 | def attributes 91 | { email: email, given_names: given_names, family_name: family_name } 92 | end 93 | 94 | def found(existing_orcids) 95 | @existing_orcids = Array.wrap(existing_orcids).collect(&:orcid_profile_id).join("; ") 96 | end 97 | 98 | def to_output_row 99 | [email, given_names, family_name, existing_orcids, created_orcid, Time.now] 100 | end 101 | 102 | def not_found 103 | @existing_orcids = 'null' 104 | end 105 | 106 | end 107 | end 108 | @person_builder = Orcid::Batch::PersonRecord 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/lib/orcid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Orcid do 4 | let(:user) { FactoryGirl.create(:user) } 5 | let(:orcid_profile_id) { '0001-0002-0003-0004' } 6 | subject { described_class } 7 | its(:provider) { should respond_to :id } 8 | its(:provider) { should respond_to :secret } 9 | its(:mapper) { should respond_to :map } 10 | its(:use_relative_model_naming?) { should eq true } 11 | its(:table_name_prefix) { should be_an_instance_of String } 12 | 13 | context '.authentication_model' do 14 | subject { Orcid.authentication_model } 15 | it { should respond_to :to_access_token } 16 | it { should respond_to :create! } 17 | it { should respond_to :count } 18 | it { should respond_to :where } 19 | end 20 | 21 | context '.oauth_client' do 22 | subject { Orcid.oauth_client } 23 | its(:client_credentials) { should respond_to :get_token } 24 | end 25 | 26 | context '.configure' do 27 | it 'should yield a configuration' do 28 | expect{|b| Orcid.configure(&b) }.to yield_with_args(Orcid::Configuration) 29 | end 30 | end 31 | 32 | context '.profile_for' do 33 | it 'should return nil if none is found' do 34 | expect(Orcid.profile_for(user)).to eq(nil) 35 | end 36 | 37 | it 'should return an Orcid::Profile if the user has an orcid authentication' do 38 | Orcid.connect_user_and_orcid_profile(user,orcid_profile_id) 39 | expect(Orcid.profile_for(user).orcid_profile_id).to eq(orcid_profile_id) 40 | end 41 | end 42 | 43 | context '.client_credentials_token' do 44 | let(:tokenizer) { double('Tokenizer') } 45 | let(:scope) { '/my-scope' } 46 | let(:token) { double('Token') } 47 | 48 | it 'should request the scoped token from the tokenizer' do 49 | tokenizer.should_receive(:get_token).with(scope: scope).and_return(token) 50 | expect(Orcid.client_credentials_token(scope, tokenizer: tokenizer)).to eq(token) 51 | end 52 | end 53 | 54 | context '.connect_user_and_orcid_profile' do 55 | 56 | it 'changes the authentication count' do 57 | expect { 58 | Orcid.connect_user_and_orcid_profile(user, orcid_profile_id) 59 | }.to change(Orcid.authentication_model, :count).by(1) 60 | end 61 | end 62 | 63 | context '.disconnect_user_and_orcid_profile' do 64 | it 'changes the authentication count' do 65 | Orcid.connect_user_and_orcid_profile(user, orcid_profile_id) 66 | expect { Orcid.disconnect_user_and_orcid_profile(user) }. 67 | to change(Orcid.authentication_model, :count).by(-1) 68 | end 69 | 70 | it 'deletes any profile request' do 71 | Orcid::ProfileRequest.create!( 72 | user: user, given_names: 'Hello', family_name: 'World', 73 | primary_email: 'hello@world.com', primary_email_confirmation: 'hello@world.com' 74 | ) 75 | expect { Orcid.disconnect_user_and_orcid_profile(user) }. 76 | to change(Orcid::ProfileRequest, :count).by(-1) 77 | end 78 | end 79 | 80 | context '.access_token_for' do 81 | let(:client) { double("Client")} 82 | let(:token) { double('Token') } 83 | let(:tokenizer) { double("Tokenizer") } 84 | 85 | it 'delegates to .authentication' do 86 | tokenizer.should_receive(:to_access_token).with(provider: 'orcid', uid: orcid_profile_id, client: client).and_return(token) 87 | expect(Orcid.access_token_for(orcid_profile_id, client: client, tokenizer: tokenizer)).to eq(token) 88 | end 89 | end 90 | 91 | context '.enqueue' do 92 | let(:object) { double } 93 | 94 | it 'should #run the object' do 95 | object.should_receive(:run) 96 | Orcid.enqueue(object) 97 | end 98 | end 99 | 100 | context '#authenticated_orcid' do 101 | it 'should not be authenticated' do 102 | Orcid.authenticated_orcid?(orcid_profile_id).should eq false 103 | end 104 | end 105 | 106 | context '.url_for_orcid_id' do 107 | it 'should render a valid uri' do 108 | profile_id = '123-456' 109 | uri = URI.parse(Orcid.url_for_orcid_id(profile_id)) 110 | expect(uri.path).to eq('/123-456') 111 | end 112 | end 113 | 114 | =begin 115 | context '#authenticated_orcid' do 116 | it 'should be authenticated' do 117 | Orcid.should_receive(:authenticated_orcid).with(orcid_profile_id).and_return(true) 118 | Orcid.authenticated_orcid?(orcid_profile_id).should eq true 119 | end 120 | end 121 | =end 122 | 123 | end 124 | -------------------------------------------------------------------------------- /spec/controllers/orcid/profile_connections_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Orcid 4 | describe ProfileConnectionsController do 5 | def self.it_prompts_unauthenticated_users_for_signin(method, action) 6 | context 'unauthenticated user' do 7 | it 'should redirect for sign in' do 8 | send(method, action, use_route: :orcid) 9 | expect(response).to redirect_to(main_app.new_user_session_path) 10 | end 11 | end 12 | end 13 | 14 | def self.it_redirects_if_user_has_previously_connected_to_orcid_profile(method, action) 15 | context 'user has existing orcid_profile' do 16 | it 'should redirect to home_path' do 17 | sign_in(user) 18 | orcid_profile = double("Orcid::Profile", orcid_profile_id: '1234-5678-0001-0002') 19 | Orcid.should_receive(:profile_for).with(user).and_return(orcid_profile) 20 | 21 | send(method, action, use_route: :orcid) 22 | 23 | expect(response).to redirect_to(main_app.root_path) 24 | expect(flash[:notice]).to eq( 25 | I18n.t("orcid.requests.messages.previously_connected_profile", orcid_profile_id: orcid_profile.orcid_profile_id) 26 | ) 27 | end 28 | end 29 | end 30 | 31 | let(:user) { mock_model('User') } 32 | before(:each) do 33 | Orcid.stub(:profile_for).and_return(nil) 34 | end 35 | 36 | context 'GET #index' do 37 | it_prompts_unauthenticated_users_for_signin(:get, :index) 38 | it 'redirects because the user does not have a connected orcid profile' do 39 | sign_in(user) 40 | Orcid.should_receive(:profile_for).with(user).and_return(nil) 41 | get :index, use_route: :orcid 42 | expect(flash[:notice]).to eq(I18n.t("orcid.connections.messages.profile_connection_not_found")) 43 | expect(response).to redirect_to(orcid.new_profile_connection_path) 44 | end 45 | 46 | it 'redirects user has an orcid profile but it is not verified' do 47 | sign_in(user) 48 | orcid_profile = double("Orcid::Profile", orcid_profile_id: '1234-5678-0001-0002', verified_authentication?: false) 49 | Orcid.stub(:profile_for).with(user).and_return(orcid_profile) 50 | 51 | get :index, use_route: :orcid 52 | expect(response).to redirect_to(user_omniauth_authorize_url("orcid")) 53 | expect(orcid_profile).to have_received(:verified_authentication?) 54 | end 55 | 56 | it 'redirects to configured location if user orcid profile is connected and verified' do 57 | orcid_profile = double("Orcid::Profile", orcid_profile_id: '1234-5678-0001-0002', verified_authentication?: true) 58 | Orcid.stub(:profile_for).with(user).and_return(orcid_profile) 59 | 60 | sign_in(user) 61 | get :index, use_route: :orcid 62 | expect(response).to redirect_to('/') 63 | expect(flash[:notice]).to eq(I18n.t("orcid.connections.messages.verified_profile_connection_exists", orcid_profile_id: orcid_profile.orcid_profile_id)) 64 | end 65 | end 66 | 67 | context 'GET #new' do 68 | it_prompts_unauthenticated_users_for_signin(:get, :new) 69 | it_redirects_if_user_has_previously_connected_to_orcid_profile(:get, :new) 70 | 71 | context 'authenticated and authorized user' do 72 | before { sign_in(user) } 73 | 74 | it 'should render a profile request form' do 75 | get :new, use_route: :orcid 76 | expect(response).to be_success 77 | expect(assigns(:profile_connection)).to be_an_instance_of(Orcid::ProfileConnection) 78 | expect(response).to render_template('new') 79 | end 80 | end 81 | end 82 | 83 | context 'POST #create' do 84 | it_prompts_unauthenticated_users_for_signin(:post, :create) 85 | it_redirects_if_user_has_previously_connected_to_orcid_profile(:post, :create) 86 | 87 | context 'authenticated and authorized user' do 88 | let(:orcid_profile_id) {'0000-0001-8025-637X'} 89 | before { sign_in(user) } 90 | 91 | it 'should render a profile request form' do 92 | Orcid::ProfileConnection.any_instance.should_receive(:save) 93 | 94 | post :create, profile_connection: { orcid_profile_id: orcid_profile_id }, use_route: :orcid 95 | expect(assigns(:profile_connection)).to be_an_instance_of(Orcid::ProfileConnection) 96 | expect(response).to redirect_to(orcid.profile_connections_path) 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/orcid/spec_support.rb: -------------------------------------------------------------------------------- 1 | require 'rest_client' 2 | 3 | # This follows the instructions from: 4 | # http://support.orcid.org/knowledgebase/articles/179969-methods-to-generate-an-access-token-for-testing#curl 5 | class RequestSandboxAuthorizationCode 6 | 7 | def self.call(options = {}) 8 | new(options).call 9 | end 10 | 11 | attr_reader :cookies, :access_scope, :authorize_url, :login_url 12 | attr_reader :oauth_redirect_uri, :orcid_client_id, :authorization_code, :orcid_client_secret 13 | attr_reader :orcid_profile_id, :password 14 | 15 | def initialize(options = {}) 16 | @orcid_client_id = options.fetch(:orcid_client_id) { Orcid.provider.id } 17 | @orcid_client_secret = options.fetch(:orcid_client_secret) { Orcid.provider.secret } 18 | @login_url = options.fetch(:login_url) { Orcid.provider.signin_via_json_url } 19 | @authorize_url = options.fetch(:authorize_url) { Orcid.provider.authorize_url } 20 | @oauth_redirect_uri = options.fetch(:oauth_redirect_uri) { 'https://developers.google.com/oauthplayground' } 21 | @access_scope = options.fetch(:scope) { Orcid.provider.authentication_scope } 22 | @orcid_profile_id = options.fetch(:orcid_profile_id) { ENV['ORCID_CLAIMED_PROFILE_ID'] } 23 | @password = options.fetch(:password) { ENV['ORCID_CLAIMED_PROFILE_PASSWORD'] } 24 | end 25 | 26 | def call 27 | puts "Attempting to login to orcid { PROFILE_ID: '#{orcid_profile_id}', PASSWORD: '#{password}' }" 28 | login_to_orcid 29 | request_authorization 30 | request_authorization_code 31 | end 32 | 33 | attr_writer :cookies 34 | private :cookies 35 | 36 | private 37 | 38 | def custom_authorization_url 39 | authorize_url.sub('oauth/authorize', 'oauth/custom/authorize.json') 40 | end 41 | 42 | def resource_options 43 | { ssl_version: :SSLv23 }.tap {|options| 44 | options[:headers] = { cookies: cookies } if cookies 45 | } 46 | end 47 | 48 | def login_to_orcid 49 | resource = RestClient::Resource.new(login_url, resource_options) 50 | response = resource.post({ userId: orcid_profile_id, password: password }) 51 | if JSON.parse(response)['success'] 52 | self.cookies = response.cookies 53 | else 54 | fail "Response not successful: \n#{response}" 55 | end 56 | end 57 | 58 | def request_authorization 59 | parameters = { 60 | client_id: orcid_client_id, 61 | response_type: 'code', 62 | scope: access_scope, 63 | redirect_uri: oauth_redirect_uri 64 | } 65 | resource = RestClient::Resource.new("#{authorize_url}?#{parameters.to_query}", resource_options) 66 | response = resource.get 67 | response 68 | end 69 | 70 | def request_authorization_code 71 | options = resource_options 72 | options[:headers] ||= {} 73 | options[:headers][:content_type] = :json 74 | options[:headers][:accept] = :json 75 | resource = RestClient::Resource.new(custom_authorization_url, options) 76 | response = resource.post(authorization_code_payload.to_json) 77 | json = JSON.parse(response) 78 | redirected_to = json.fetch('redirectUri').fetch('value') 79 | uri = URI.parse(redirected_to) 80 | CGI.parse(uri.query).fetch('code').first 81 | rescue RestClient::Exception => e 82 | File.open("/Users/jfriesen/Desktop/orcid.html", 'w+') {|f| f.puts e.response.body.force_encoding('UTF-8') } 83 | $stderr.puts "Response Code: #{e.response.code}\n\tCookies: #{cookies.inspect}\n\tAuthorizeURL: #{authorize_url.inspect}" 84 | raise e 85 | end 86 | 87 | def authorization_code_payload 88 | { 89 | "errors" => [], 90 | "userName" => { 91 | "errors" => [], 92 | "value" => "", 93 | "required" => true, 94 | "getRequiredMessage" => nil 95 | }, 96 | "password" => { 97 | "errors" => [], 98 | "value" => "", 99 | "required" => true, 100 | "getRequiredMessage" => nil 101 | }, 102 | "clientId" => { 103 | "errors" => [], 104 | "value" => "#{orcid_client_id}", 105 | "required" => true, 106 | "getRequiredMessage" => nil 107 | }, 108 | "redirectUri" => { 109 | "errors" => [], 110 | "value" => "=#{URI.escape(oauth_redirect_uri)}", 111 | "required" => true, 112 | "getRequiredMessage" => nil 113 | }, 114 | "scope" => { 115 | "errors" => [], 116 | "value" => "#{access_scope}", 117 | "required" => true, 118 | "getRequiredMessage" => nil 119 | }, 120 | "responseType" => { 121 | "errors" => [], 122 | "value" => "code", 123 | "required" => true, 124 | "getRequiredMessage" => nil 125 | }, 126 | "approved" => true, 127 | "persistentTokenEnabled" => false 128 | } 129 | end 130 | 131 | end 132 | -------------------------------------------------------------------------------- /spec/features/non_ui_based_interactions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'non-UI based interactions' , requires_net_connect: true do 4 | around(:each) do |example| 5 | Mappy.configure {|b|} 6 | WebMock.allow_net_connect! 7 | 8 | example.run 9 | WebMock.disable_net_connect! 10 | Mappy.reset! 11 | end 12 | let(:work) { Orcid::Work.new(title: "Test Driven Orcid Integration", work_type: 'test') } 13 | let(:user) { FactoryGirl.create(:user) } 14 | let(:orcid_profile_password) { 'password1A' } 15 | 16 | context 'issue a profile request', api_abusive: true do 17 | 18 | # Because either ORCID or Mailinator are blocking some emails. 19 | let(:random_valid_email_prefix) { (0...24).map { (65 + rand(26)).chr }.join.downcase } 20 | let(:email) { "#{random_valid_email_prefix}@mailinator.com" } 21 | let(:profile_request) { 22 | FactoryGirl.create(:orcid_profile_request, user: user, primary_email: email, primary_email_confirmation: email) 23 | } 24 | let(:profile_request_coordinator) { Orcid::ProfileRequestCoordinator.new(profile_request)} 25 | 26 | before(:each) do 27 | # Making sure things are properly setup 28 | expect(profile_request.orcid_profile_id).to be_nil 29 | end 30 | 31 | if ENV['MAILINATOR_API_KEY'] 32 | it 'creates a profile' do 33 | profile_request_coordinator.call 34 | profile_request.reload 35 | 36 | orcid_profile_id = profile_request.orcid_profile_id 37 | 38 | expect(orcid_profile_id).to match(/\w{4}-\w{4}-\w{4}-\w{4}/) 39 | 40 | claim_the_orcid!(random_valid_email_prefix) 41 | 42 | authenticate_the_orcid!(orcid_profile_id, orcid_profile_password) 43 | 44 | orcid_profile = Orcid::Profile.new(orcid_profile_id) 45 | 46 | orcid_profile.append_new_work(work) 47 | 48 | expect(orcid_profile.remote_works(force: true).count).to eq(1) 49 | end 50 | end 51 | 52 | end 53 | 54 | context 'appending a work to an already claimed and authorize orcid', requires_net_connect: true do 55 | let(:orcid_profile_id) { ENV.fetch('ORCID_CLAIMED_PROFILE_ID')} 56 | let(:orcid_profile_password) { ENV.fetch('ORCID_CLAIMED_PROFILE_PASSWORD')} 57 | 58 | before(:each) do 59 | expect(work).to be_valid 60 | end 61 | 62 | subject { Orcid::Profile.new(orcid_profile_id) } 63 | it 'should increment orcid works' do 64 | authenticate_the_orcid!(orcid_profile_id, orcid_profile_password) 65 | replacement_work = Orcid::Work.new(title: "Test Driven Orcid Integration", work_type: 'test') 66 | appended_work = Orcid::Work.new(title: "Another Test Drive", work_type: 'test') 67 | 68 | subject.replace_works_with(replacement_work) 69 | 70 | expect { 71 | subject.append_new_work(appended_work) 72 | }.to change { subject.remote_works(force: true).count }.by(1) 73 | 74 | end 75 | end 76 | 77 | # Extract this method as a proper helper 78 | def authenticate_the_orcid!(orcid_profile_id, orcid_profile_password) 79 | code = RequestSandboxAuthorizationCode.call(orcid_profile_id: orcid_profile_id, password: orcid_profile_password) 80 | token = Orcid.oauth_client.auth_code.get_token(code) 81 | normalized_token = {provider: 'orcid', uid: orcid_profile_id, credentials: {token: token.token, refresh_token: token.refresh_token }} 82 | Devise::MultiAuth::CaptureSuccessfulExternalAuthentication.call(user, normalized_token) 83 | end 84 | 85 | def claim_the_orcid!(email_prefix) 86 | $stdout.puts "Claiming an ORCID. This could take a while." 87 | api_token = ENV.fetch('MAILINATOR_API_KEY') 88 | 89 | mailbox_uri = "https://api.mailinator.com/api/inbox?to=#{email_prefix}&token=#{api_token}" 90 | 91 | orcid_messages = [] 92 | mailinator_requests(mailbox_uri) do |response| 93 | orcid_messages = response['messages'].select {|m| m['from'] =~ /\.orcid\.org\Z/ } 94 | !!orcid_messages.first 95 | end 96 | 97 | orcid_message = orcid_messages.first 98 | raise "Unable to retrieve email for #{email_prefix}@mailinator.com" unless orcid_message 99 | 100 | message_uri = "https://api.mailinator.com/api/email?msgid=#{orcid_message.fetch('id')}&token=#{api_token}" 101 | claim_uri = nil 102 | mailinator_requests(message_uri) do |response| 103 | bodies = response.fetch('data').fetch('parts').map { |part| part.fetch('body') } 104 | bodies.each do |body| 105 | if body =~ %r{(https://sandbox.orcid.org/claim/[\w\?=]+)} 106 | claim_uri = $1.strip 107 | break 108 | end 109 | end 110 | claim_uri 111 | end 112 | 113 | # I have the href for the claim 114 | uri = URI.parse(claim_uri) 115 | Capybara.current_driver = :webkit 116 | Capybara.run_server = false 117 | Capybara.app_host = "#{uri.scheme}://#{uri.host}" 118 | 119 | visit("#{uri.path}?#{uri.query}") 120 | page.all('input').each do |input| 121 | case input[:name] 122 | when 'password' then input.set(orcid_profile_password) 123 | when 'confirmPassword' then input.set(orcid_profile_password) 124 | when 'acceptTermsAndConditions' then input.click 125 | end 126 | end 127 | page.all('button').find {|i| i.text == 'Claim' }.click 128 | sleep(5) # Because claiming your orcid could be slow 129 | end 130 | 131 | def mailinator_requests(uri) 132 | base_sleep_duration = ENV.fetch('MAILINATOR_SECONDS_TO_RETRY', 120).to_i 133 | retry_attempts = ENV.fetch('MAILINATOR_RETRY_ATTEMPTS', 6).to_i 134 | $stdout.print "\n=-=-= Fetching the mail from #{uri}" 135 | (0...retry_attempts).each do |attempt| 136 | sleep_duration = base_sleep_duration * (attempt +1) 137 | $stdout.print "\n=-=-= Connecting to Mailinator. Attempt #{attempt+1}\n\tWaiting #{sleep_duration} seconds to connect: " 138 | $stdout.flush 139 | (0...sleep_duration).each do |second| 140 | $stdout.print 'z' if (second % 5 == 0) 141 | $stdout.flush 142 | sleep(1) 143 | end 144 | resource = RestClient::Resource.new(uri, ssl_version: :SSLv23) 145 | response = JSON.parse(resource.get(format: :json)) 146 | if yield(response) 147 | $stdout.print "\n=-=-= Success on attempt #{attempt+1}. Moving on." 148 | $stdout.flush 149 | break 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /spec/fixtures/orcid-remote-profile_query_service-response_parser/multiple-responses-without-valid-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message-version": "1.1", 3 | "orcid-search-results": { 4 | "orcid-search-result": [ 5 | { 6 | "relevancy-score": { 7 | "value": 0.016482107 8 | }, 9 | "orcid-profile": { 10 | "orcid": null, 11 | "orcid-bio": { 12 | "personal-details": { 13 | "given-names": { 14 | "value": "Reserved For Claim" 15 | } 16 | }, 17 | "keywords": null, 18 | "delegation": null, 19 | "applications": null, 20 | "scope": null 21 | }, 22 | "orcid-activities": { 23 | "affiliations": null 24 | }, 25 | "type": null, 26 | "group-type": null, 27 | "client-type": null 28 | } 29 | }, 30 | { 31 | "relevancy-score": { 32 | "value": 0.016482107 33 | }, 34 | "orcid-profile": { 35 | "orcid": null, 36 | "orcid-bio": { 37 | "personal-details": { 38 | "given-names": { 39 | "value": "Reserved For Claim" 40 | } 41 | }, 42 | "keywords": null, 43 | "delegation": null, 44 | "applications": null, 45 | "scope": null 46 | }, 47 | "orcid-activities": { 48 | "affiliations": null 49 | }, 50 | "type": null, 51 | "group-type": null, 52 | "client-type": null 53 | } 54 | }, 55 | { 56 | "relevancy-score": { 57 | "value": 0.016482107 58 | }, 59 | "orcid-profile": { 60 | "orcid": null, 61 | "orcid-bio": { 62 | "personal-details": { 63 | "given-names": { 64 | "value": "Reserved For Claim" 65 | } 66 | }, 67 | "keywords": null, 68 | "delegation": null, 69 | "applications": null, 70 | "scope": null 71 | }, 72 | "orcid-activities": { 73 | "affiliations": null 74 | }, 75 | "type": null, 76 | "group-type": null, 77 | "client-type": null 78 | } 79 | }, 80 | { 81 | "relevancy-score": { 82 | "value": 0.016482107 83 | }, 84 | "orcid-profile": { 85 | "orcid": null, 86 | "orcid-bio": { 87 | "personal-details": { 88 | "given-names": { 89 | "value": "Reserved For Claim" 90 | } 91 | }, 92 | "keywords": null, 93 | "delegation": null, 94 | "applications": null, 95 | "scope": null 96 | }, 97 | "orcid-activities": { 98 | "affiliations": null 99 | }, 100 | "type": null, 101 | "group-type": null, 102 | "client-type": null 103 | } 104 | }, 105 | { 106 | "relevancy-score": { 107 | "value": 0.016482107 108 | }, 109 | "orcid-profile": { 110 | "orcid": null, 111 | "orcid-bio": { 112 | "personal-details": { 113 | "given-names": { 114 | "value": "Reserved For Claim" 115 | } 116 | }, 117 | "keywords": null, 118 | "delegation": null, 119 | "applications": null, 120 | "scope": null 121 | }, 122 | "orcid-activities": { 123 | "affiliations": null 124 | }, 125 | "type": null, 126 | "group-type": null, 127 | "client-type": null 128 | } 129 | }, 130 | { 131 | "relevancy-score": { 132 | "value": 0.016482107 133 | }, 134 | "orcid-profile": { 135 | "orcid": null, 136 | "orcid-bio": { 137 | "personal-details": { 138 | "given-names": { 139 | "value": "Reserved For Claim" 140 | } 141 | }, 142 | "keywords": null, 143 | "delegation": null, 144 | "applications": null, 145 | "scope": null 146 | }, 147 | "orcid-activities": { 148 | "affiliations": null 149 | }, 150 | "type": null, 151 | "group-type": null, 152 | "client-type": null 153 | } 154 | }, 155 | { 156 | "relevancy-score": { 157 | "value": 0.016482107 158 | }, 159 | "orcid-profile": { 160 | "orcid": null, 161 | "orcid-bio": { 162 | "personal-details": { 163 | "given-names": { 164 | "value": "Reserved For Claim" 165 | } 166 | }, 167 | "keywords": null, 168 | "delegation": null, 169 | "applications": null, 170 | "scope": null 171 | }, 172 | "orcid-activities": { 173 | "affiliations": null 174 | }, 175 | "type": null, 176 | "group-type": null, 177 | "client-type": null 178 | } 179 | }, 180 | { 181 | "relevancy-score": { 182 | "value": 0.016482107 183 | }, 184 | "orcid-profile": { 185 | "orcid": null, 186 | "orcid-bio": { 187 | "personal-details": { 188 | "given-names": { 189 | "value": "Reserved For Claim" 190 | } 191 | }, 192 | "keywords": null, 193 | "delegation": null, 194 | "applications": null, 195 | "scope": null 196 | }, 197 | "orcid-activities": { 198 | "affiliations": null 199 | }, 200 | "type": null, 201 | "group-type": null, 202 | "client-type": null 203 | } 204 | }, 205 | { 206 | "relevancy-score": { 207 | "value": 0.016482107 208 | }, 209 | "orcid-profile": { 210 | "orcid": null, 211 | "orcid-bio": { 212 | "personal-details": { 213 | "given-names": { 214 | "value": "Reserved For Claim" 215 | } 216 | }, 217 | "keywords": null, 218 | "delegation": null, 219 | "applications": null, 220 | "scope": null 221 | }, 222 | "orcid-activities": { 223 | "affiliations": null 224 | }, 225 | "type": null, 226 | "group-type": null, 227 | "client-type": null 228 | } 229 | }, 230 | { 231 | "relevancy-score": { 232 | "value": 0.016482107 233 | }, 234 | "orcid-profile": { 235 | "orcid": null, 236 | "orcid-bio": { 237 | "personal-details": { 238 | "given-names": { 239 | "value": "Reserved For Claim" 240 | } 241 | }, 242 | "keywords": null, 243 | "delegation": null, 244 | "applications": null, 245 | "scope": null 246 | }, 247 | "orcid-activities": { 248 | "affiliations": null 249 | }, 250 | "type": null, 251 | "group-type": null, 252 | "client-type": null 253 | } 254 | } 255 | ], 256 | "num-found": 0 257 | } 258 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We want your help to make Project Hydra great. 4 | There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. 5 | 6 | ## Code of Conduct 7 | 8 | The Hydra community is dedicated to providing a welcoming and positive experience for all its 9 | members, whether they are at a formal gathering, in a social setting, or taking part in activities 10 | online. Please see our [Code of Conduct](https://wiki.duraspace.org/display/hydra/Code+of+Conduct) 11 | for more information. 12 | 13 | ## Hydra Project Intellectual Property Licensing and Ownership 14 | 15 | All code contributors must have an Individual Contributor License Agreement (iCLA) on file with the Hydra Project Steering Group. 16 | If the contributor works for an institution, the institution must have a Corporate Contributor License Agreement (cCLA) on file. 17 | 18 | https://wiki.duraspace.org/display/hydra/Hydra+Project+Intellectual+Property+Licensing+and+Ownership 19 | 20 | You should also add yourself to the `CONTRIBUTORS.md` file in the root of the project. 21 | 22 | ## Contribution Tasks 23 | 24 | * Reporting Issues 25 | * Making Changes 26 | * Documenting Code 27 | * Committing Changes 28 | * Submitting Changes 29 | * Reviewing and Merging Changes 30 | 31 | ### Reporting Issues 32 | 33 | * Make sure you have a [GitHub account](https://github.com/signup/free) 34 | * Submit a [Github issue](./issues) by: 35 | * Clearly describing the issue 36 | * Provide a descriptive summary 37 | * Explain the expected behavior 38 | * Explain the actual behavior 39 | * Provide steps to reproduce the actual behavior 40 | 41 | ### Making Changes 42 | 43 | * Fork the repository on GitHub 44 | * Create a topic branch from where you want to base your work. 45 | * This is usually the master branch. 46 | * To quickly create a topic branch based on master; `git branch fix/master/my_contribution master` 47 | * Then checkout the new branch with `git checkout fix/master/my_contribution`. 48 | * Please avoid working directly on the `master` branch. 49 | * You may find the [hub suite of commands](https://github.com/defunkt/hub) helpful 50 | * Make sure you have added sufficient tests and documentation for your changes. 51 | * Test functionality with RSpec; est features / UI with Capybara. 52 | * Run _all_ the tests to assure nothing else was accidentally broken. 53 | 54 | ### Documenting Code 55 | 56 | * All new public methods, modules, and classes should include inline documentation in [YARD](http://yardoc.org/). 57 | * Documentation should seek to answer the question "why does this code exist?" 58 | * Document private / protected methods as desired. 59 | * If you are working in a file with no prior documentation, do try to document as you gain understanding of the code. 60 | * If you don't know exactly what a bit of code does, it is extra likely that it needs to be documented. Take a stab at it and ask for feedback in your pull request. You can use the 'blame' button on GitHub to identify the original developer of the code and @mention them in your comment. 61 | * This work greatly increases the usability of the code base and supports the on-ramping of new committers. 62 | * We will all be understanding of one another's time constraints in this area. 63 | * YARD examples: 64 | * [Hydra::Works::RemoveGenericFile](https://github.com/projecthydra-labs/hydra-works/blob/master/lib/hydra/works/services/generic_work/remove_generic_file.rb) 65 | * [ActiveTriples::LocalName::Minter](https://github.com/ActiveTriples/active_triples-local_name/blob/master/lib/active_triples/local_name/minter.rb) 66 | * [Getting started with YARD](http://www.rubydoc.info/gems/yard/file/docs/GettingStarted.md) 67 | 68 | ### Committing changes 69 | 70 | * Make commits of logical units. 71 | * Your commit should include a high level description of your work in HISTORY.textile 72 | * Check for unnecessary whitespace with `git diff --check` before committing. 73 | * Make sure your commit messages are [well formed](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 74 | * If you created an issue, you can close it by including "Closes #issue" in your commit message. See [Github's blog post for more details](https://github.com/blog/1386-closing-issues-via-commit-messages) 75 | 76 | ``` 77 | Present tense short summary (50 characters or less) 78 | 79 | More detailed description, if necessary. It should be wrapped to 72 80 | characters. Try to be as descriptive as you can, even if you think that 81 | the commit content is obvious, it may not be obvious to others. You 82 | should add such description also if it's already present in bug tracker, 83 | it should not be necessary to visit a webpage to check the history. 84 | 85 | Include Closes # when relavent. 86 | 87 | Description can have multiple paragraphs and you can use code examples 88 | inside, just indent it with 4 spaces: 89 | 90 | class PostsController 91 | def index 92 | respond_to do |wants| 93 | wants.html { render 'index' } 94 | end 95 | end 96 | end 97 | 98 | You can also add bullet points: 99 | 100 | - you can use dashes or asterisks 101 | 102 | - also, try to indent next line of a point for readability, if it's too 103 | long to fit in 72 characters 104 | ``` 105 | 106 | ### Submitting Changes 107 | 108 | * Read the article ["Using Pull Requests"](https://help.github.com/articles/using-pull-requests) on GitHub. 109 | * Make sure your branch is up to date with its parent branch (i.e. master) 110 | * `git checkout master` 111 | * `git pull --rebase` 112 | * `git checkout ` 113 | * `git rebase master` 114 | * It is a good idea to run your tests again. 115 | * If you've made more than one commit take a moment to consider whether squashing commits together would help improve their logical grouping. 116 | * [Detailed Walkthrough of One Pull Request per Commit](http://ndlib.github.io/practices/one-commit-per-pull-request/) 117 | * `git rebase --interactive master` ([See Github help](https://help.github.com/articles/interactive-rebase)) 118 | * Squashing your branch's changes into one commit is "good form" and helps the person merging your request to see everything that is going on. 119 | * Push your changes to a topic branch in your fork of the repository. 120 | * Submit a pull request from your fork to the project. 121 | 122 | ### Reviewing and Merging Changes 123 | 124 | We adopted [Github's Pull Request Review](https://help.github.com/articles/about-pull-request-reviews/) for our repositories. 125 | Common checks that may occur in our repositories: 126 | 127 | 1. Travis CI - where our automated tests are running 128 | 2. Hound CI - where we check for style violations 129 | 3. Approval Required - Github enforces at least one person approve a pull request. Also, all reviewers that have chimed in must approve. 130 | 4. CodeClimate - is our code remaining healthy (at least according to static code analysis) 131 | 132 | If one or more of the required checks failed (or are incomplete), the code should not be merged (and the UI will not allow it). If all of the checks have passed, then anyone on the project (including the pull request submitter) may merge the code. 133 | 134 | *Example: Carolyn submits a pull request, Justin reviews the pull request and approves. However, Justin is still waiting on other checks (Travis CI is usually the culprit), so he does not merge the pull request. Eventually, all of the checks pass. At this point, Carolyn or anyone else may merge the pull request.* 135 | 136 | #### Things to Consider When Reviewing 137 | 138 | First, the person contributing the code is putting themselves out there. Be mindful of what you say in a review. 139 | 140 | * Ask clarifying questions 141 | * State your understanding and expectations 142 | * Provide example code or alternate solutions, and explain why 143 | 144 | This is your chance for a mentoring moment of another developer. Take time to give an honest and thorough review of what has changed. Things to consider: 145 | 146 | * Does the commit message explain what is going on? 147 | * Does the code changes have tests? _Not all changes need new tests, some changes are refactors_ 148 | * Do new or changed methods, modules, and classes have documentation? 149 | * Does the commit contain more than it should? Are two separate concerns being addressed in one commit? 150 | * Does the description of the new/changed specs match your understanding of what the spec is doing? 151 | 152 | If you are uncertain, bring other contributors into the conversation by assigning them as a reviewer. 153 | 154 | # Additional Resources 155 | 156 | * [General GitHub documentation](http://help.github.com/) 157 | * [GitHub pull request documentation](http://help.github.com/send-pull-requests/) 158 | * [Pro Git](http://git-scm.com/book) is both a free and excellent book about Git. 159 | * [A Git Config for Contributing](http://ndlib.github.io/practices/my-typical-per-project-git-config/) 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orcid 2 | 3 | [![Version](https://badge.fury.io/rb/orcid.png)](http://badge.fury.io/rb/orcid) 4 | [![Build Status](https://travis-ci.org/projecthydra-labs/orcid.png?branch=master)](https://travis-ci.org/projecthydra-labs/orcid) 5 | [![Code Climate](https://codeclimate.com/github/projecthydra-labs/orcid.png)](https://codeclimate.com/github/projecthydra-labs/orcid) 6 | [![Coverage Status](https://img.shields.io/coveralls/projecthydra-labs/orcid.svg)](https://coveralls.io/r/projecthydra-labs/orcid) 7 | [![Documentation Status](http://inch-ci.org/github/projecthydra-labs/orcid.svg?branch=master)](http://inch-ci.org/github/projecthydra-labs/orcid) 8 | [![API Docs](http://img.shields.io/badge/API-docs-blue.svg)](http://rubydoc.info/gems/orcid/0.8.0/frames) 9 | [![APACHE 2 License](http://img.shields.io/badge/APACHE2-license-blue.svg)](./LICENSE) 10 | [![Contributing Guidelines](http://img.shields.io/badge/CONTRIBUTING-Guidelines-blue.svg)](./CONTRIBUTING.md) 11 | 12 | A [Rails Engine](https://guides.rubyonrails.org/engines.html) for integrating with [Orcid](https://orcid.org). It leverages the [Devise MultiAuth plugin](https://rubygems.org/gems/devise-multi_auth) for negotiating the interaction with [orcid.org](https://orcid.org). 13 | 14 | **Note: While this is part of ProjectHydra, this gem is not dependent on any of the Hydra components.** 15 | 16 | ## Features 17 | 18 | Associate ORCID with your user account for the application by: 19 | 20 | * Creating an ORCID 21 | * Looking up and associating with an existing ORCID 22 | * Providing an ORCID to directly associate with your account 23 | 24 | Authentication 25 | 26 | * Using OAuth2, you can use orcid.org as one of your authentication mechanisms 27 | 28 | Interacting with ORCID Profile Works: 29 | 30 | **The functionality exists, but it will be a bit bumpy to implement.** 31 | **Plans are to improve the integration with Version 1.0.0 of the Orcid gem.** 32 | 33 | * Query for your Orcid Profile's works 34 | * Append one or more works to your Orcid Profile 35 | * Replace your Orcid Profile works with one or more works 36 | 37 | ## Getting Started with the Orcid gem 38 | 39 | To fully interact with the Orcid remote services, you will need to [register your ORCID application profile](#registering-for-an-orcid-application-profile). 40 | 41 | * [Installation](#installation) 42 | * [Using the Orcid widget in your application](#using-the-orcid-widget-in-your-application) 43 | * [Registering for an ORCID application profile](#registering-for-an-orcid-application-profile) 44 | * [Setting up your own ORCIDs in the ORCID Development Sandbox](#setting-up-your-own-orcids-in-the-orcid-development-sandbox) 45 | * [Running the tests](#running-the-tests) 46 | * [Versioning](#versioning) 47 | * [Contributing to this gem](./CONTRIBUTING.md) 48 | 49 | ## Installation 50 | 51 | Add this line to your application's Gemfile: 52 | 53 | ```ruby 54 | gem 'orcid' 55 | ``` 56 | 57 | And then execute: 58 | 59 | ```console 60 | $ bundle 61 | ``` 62 | 63 | If bundle fails, you may need to [install Qt](https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit). 64 | 65 | And then install by running the following: 66 | 67 | ```console 68 | $ rails generate orcid:install 69 | ``` 70 | 71 | *Note: It will prompt you for your Orcid application secrets.* 72 | 73 | 74 | ### Caveat 75 | 76 | You will also need to **update your devise configuration** in your routes. 77 | 78 | From something like this: 79 | 80 | ```ruby 81 | devise_for :users 82 | ``` 83 | 84 | To: 85 | 86 | ```ruby 87 | devise_for :users, controllers: { omniauth_callbacks: 'devise/multi_auth/omniauth_callbacks' } 88 | ``` 89 | 90 | You may find it helpful to review the help text, as there are a few options for the generator. 91 | 92 | ```console 93 | $ rails generate orcid:install -h 94 | ``` 95 | 96 | ## Using the Orcid widget in your application 97 | 98 | In order to facilitate integration of this ORCID gem into your application, a widget has been provided to offer these functions: 99 | 100 | 1. Enter a known ORCID and connect to the ORCID repository. 101 | 1. Look up the ORCID of the current user of your application. 102 | 1. Create an ORCID to be associated with the current user of your application. 103 | 104 | The widget is contained in the partial `app/views/orcid/profile_connections/_orcid_connector.html.erb`. 105 | 106 | An example use of the partial is shown below. 107 | 108 | ```ruby 109 | # The `if defined?(Orcid)` could be viewed as a courtesy. 110 | # Don't attempt to render this partial if the Orcid gem is not being used. 111 | if defined?(Orcid) 112 | <%= render partial: 'orcid/profile_connections/orcid_connector', locals: {default_search_text: current_user.name } %> 113 | end 114 | ``` 115 | 116 | **To customize the labels, review the `./config/locales/orcid.en.yml` file.** 117 | 118 | ## Registering for an ORCID application profile 119 | 120 | Your application which will interface with ORCID must be registered with ORCID. Note that you will want to register your production 121 | application separately from the development sandbox. 122 | 123 | 1. Go to http://support.orcid.org/knowledgebase/articles/116739-register-a-client-application 124 | 1. Read the information on the entire page, in particular, the 'Filling in the client application form' and 'About Redirect URIs' sections. 125 | 1. Click on 'register a client application', http://orcid.org/organizations/integrators/create-client-application 126 | 1. There you will be given a choice of registering for the Development Sandbox or the Production Registry. 127 | 1. Fill in the other information as appropriate for your organization. If you are doing development, select Development Sandbox. 128 | 1. For the URL of the home page of your application, you must use an https:// URL. If you are going to be doing development work locally 129 | on your own machine where your application's server will run, enter https://localhost:3000 for the URL of your home page (or as appropriate 130 | to your local development environment). See **NOTE: Application home page URL** below. 131 | 1. You must enter at least one Redirect URI, which should be https://localhost:3000/users/auth/orcid/callback 132 | 1. Another suggested Redirect URI is https://developers.google.com/oauthplayground 133 | 134 | Within a day or so, you will receive an email with an attached xml file containing the client-id and client-secret which must be used in the application.yml 135 | file discussed below. 136 | 137 | ### NOTE: Application home page URL 138 | You must enter the same URL for the application home page on the form as you would enter into your browser. For example, if you specify "https://localhost:3000" on 139 | the ORCID registration form, then you MUST invoke your application via the browser with "https://localhost:3000" in order for all of the ORCID functionality to work. 140 | 141 | For development work in particular, there are multiple ways to specify the local machine: 127.0.0.1, ::1, 192.168.1.1, and localhost. It is strongly recommended that you use 'localhost' 142 | in the ORCID form's URL for your application and when invoking your application from the browser rather than using any IP address for your local machine. 143 | 144 | ## Setting up your own ORCIDs in the ORCID Development Sandbox 145 | 146 | [Read more about the ORCID Sandbox](http://support.orcid.org/knowledgebase/articles/166623-about-the-orcid-sandbox). 147 | 148 | 1. Register two ORCID users: https://sandbox-1.orcid.org/register (make sure to use @mailinator.com as your email) 149 | Save the email addresses, orcid ids, and passwords for editing the application.yml later. 150 | 1. Go to mailinator (http://mailinator.com/) and claim 1 ORCID by clicking the verify link in the email. 151 | 1. Go to the ORCID sandbox https://sandbox.orcid.org, log in and click on *Account Settings* (https://sandbox.orcid.org/account). On the Account Settings page, 152 | click on Email and select the little icon with the group of heads to make your Primary Email publicly accessible. 153 | 154 | ## Setting up the config/application.yml file 155 | Customize the sample application.yml file by first copying it to config/application.yml and opening it for editing. 156 | 157 | ```console 158 | $ cp config/application.yml.sample config/application.yml 159 | ``` 160 | 161 | ## Running the tests 162 | 163 | Run `rake` to generate the dummy app and run the offline tests. 164 | 165 | To run the online tests, you'll need ORCID application credentials: 166 | 167 | 1. Register for an ORCID app. See **Registering for an ORCID application profile** above. (This may take several days to complete.) 168 | 1. Register two ORCID users in the ORCID Development Sandbox. See **Setting up your own ORCIDs in the ORCID Development Sandbox** above. 169 | 1. Update the application.yml with your information. See **Setting up the config/application.yml file** above. 170 | 171 | Run the online tests with 172 | 173 | ```console 174 | $ rake spec:online 175 | ``` 176 | 177 | ### Running ALL of the Tests 178 | 179 | Not all of the tests run. There are a few very long running tests that run. 180 | 181 | ```console 182 | $ ORCID_APP_ID= \ 183 | ORCID_APP_SECRET= \ 184 | MAILINATOR_API_KEY= \ 185 | ORCID_CLAIMED_PROFILE_ID= \ 186 | ORCID_CLAIMED_PROFILE_PASSWORD= \ 187 | bundle exec rake spec:jenkins 188 | ``` 189 | 190 | By setting all of the above environment variables, you will run tests that will: 191 | 192 | * Create an Orcid, then claim it, and authenticate with your application via ORCID 193 | 194 | ## Versioning 195 | 196 | **Orcid** uses [Semantic Versioning 2.0.0](http://semver.org/) -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## Releasing the hounds in your local environment. 3 | ## 4 | ## Setup: 5 | ## $ gem install rubocop 6 | ## 7 | ## Run: 8 | ## $ rubocop ./path/to/file ./or/path/to/directory -c ./.hound.yml 9 | ## 10 | ################################################################################ 11 | 12 | ################################################################################ 13 | ## 14 | ## Custom options for Orcid 15 | ## These options were plucked from below and modified accordingly. 16 | ## 17 | ################################################################################ 18 | AllCops: 19 | Include: 20 | - Rakefile 21 | - config.ru 22 | Exclude: 23 | - db/**/* 24 | - config/**/* 25 | - lib/**/version.rb 26 | - spec/internal/**/* 27 | - spec/*_helper.rb 28 | 29 | MethodLength: 30 | Max: 10 31 | Description: 'Avoid methods longer than 10 lines of code.' 32 | CountComments: false 33 | Enabled: true 34 | 35 | LineLength: 36 | Description: 'Limit lines to 140 characters.' 37 | Max: 140 38 | Enabled: true 39 | 40 | ClassLength: 41 | Max: 100 42 | Description: 'Avoid classes longer than 100 lines of code.' 43 | CountComments: false 44 | Enabled: true 45 | 46 | ################################################################################ 47 | ## 48 | ## Below this banner, the configuration options were copied from the following 49 | ## URL: 50 | ## https://raw.githubusercontent.com/bbatsov/rubocop/master/config/enabled.yml 51 | ## 52 | ################################################################################ 53 | 54 | # These are all the cops that are enabled in the default configuration. 55 | 56 | AccessModifierIndentation: 57 | Description: Check indentation of private/protected visibility modifiers. 58 | Enabled: true 59 | 60 | AccessorMethodName: 61 | Description: Check the naming of accessor methods for get_/set_. 62 | Enabled: true 63 | 64 | Alias: 65 | Description: 'Use alias_method instead of alias.' 66 | Enabled: true 67 | 68 | AlignArray: 69 | Description: >- 70 | Align the elements of an array literal if they span more than 71 | one line. 72 | Enabled: true 73 | 74 | AlignHash: 75 | Description: >- 76 | Align the elements of a hash literal if they span more than 77 | one line. 78 | Enabled: true 79 | 80 | AlignParameters: 81 | Description: >- 82 | Align the parameters of a method call if they span more 83 | than one line. 84 | Enabled: true 85 | 86 | AndOr: 87 | Description: 'Use &&/|| instead of and/or.' 88 | Enabled: true 89 | 90 | ArrayJoin: 91 | Description: 'Use Array#join instead of Array#*.' 92 | Enabled: true 93 | 94 | AsciiComments: 95 | Description: 'Use only ascii symbols in comments.' 96 | Enabled: true 97 | 98 | AsciiIdentifiers: 99 | Description: 'Use only ascii symbols in identifiers.' 100 | Enabled: true 101 | 102 | Attr: 103 | Description: 'Checks for uses of Module#attr.' 104 | Enabled: true 105 | 106 | BeginBlock: 107 | Description: 'Avoid the use of BEGIN blocks.' 108 | Enabled: true 109 | 110 | BlockComments: 111 | Description: 'Do not use block comments.' 112 | Enabled: true 113 | 114 | BlockNesting: 115 | Description: 'Avoid excessive block nesting' 116 | Enabled: true 117 | 118 | Blocks: 119 | Description: >- 120 | Avoid using {...} for multi-line blocks (multiline chaining is 121 | always ugly). 122 | Prefer {...} over do...end for single-line blocks. 123 | Enabled: true 124 | 125 | BracesAroundHashParameters: 126 | Description: 'Enforce braces style inside hash parameters.' 127 | Enabled: true 128 | 129 | CaseEquality: 130 | Description: 'Avoid explicit use of the case equality operator(===).' 131 | Enabled: true 132 | 133 | CaseIndentation: 134 | Description: 'Indentation of when in a case/when/[else/]end.' 135 | Enabled: true 136 | 137 | CharacterLiteral: 138 | Description: 'Checks for uses of character literals.' 139 | Enabled: true 140 | 141 | ClassAndModuleCamelCase: 142 | Description: 'Use CamelCase for classes and modules.' 143 | Enabled: true 144 | 145 | ClassAndModuleChildren: 146 | Description: 'Checks style of children classes and modules.' 147 | Enabled: true 148 | 149 | ClassMethods: 150 | Description: 'Use self when defining module/class methods.' 151 | Enabled: true 152 | 153 | ClassVars: 154 | Description: 'Avoid the use of class variables.' 155 | Enabled: true 156 | 157 | CollectionMethods: 158 | Description: 'Preferred collection methods.' 159 | Enabled: true 160 | 161 | ColonMethodCall: 162 | Description: 'Do not use :: for method call.' 163 | Enabled: true 164 | 165 | CommentAnnotation: 166 | Description: >- 167 | Checks formatting of special comments 168 | (TODO, FIXME, OPTIMIZE, HACK, REVIEW). 169 | Enabled: true 170 | 171 | CommentIndentation: 172 | Description: 'Indentation of comments.' 173 | Enabled: true 174 | 175 | ConstantName: 176 | Description: 'Constants should use SCREAMING_SNAKE_CASE.' 177 | Enabled: true 178 | 179 | CyclomaticComplexity: 180 | Description: 'Avoid complex methods.' 181 | Enabled: true 182 | 183 | DefWithParentheses: 184 | Description: 'Use def with parentheses when there are arguments.' 185 | Enabled: true 186 | 187 | Delegate: 188 | Description: 'Prefer delegate method for delegations.' 189 | Enabled: false 190 | 191 | DeprecatedHashMethods: 192 | Description: 'Checks for use of deprecated Hash methods.' 193 | Enabled: true 194 | 195 | Documentation: 196 | Description: 'Document classes and non-namespace modules.' 197 | Enabled: true 198 | Exclude: 199 | - spec/**/* 200 | - lib/**/version.rb 201 | 202 | DotPosition: 203 | Description: 'Checks the position of the dot in multi-line method calls.' 204 | EnforcedStyle: trailing 205 | Enabled: true 206 | 207 | DoubleNegation: 208 | Description: 'Checks for uses of double negation (!!).' 209 | Enabled: true 210 | 211 | EachWithObject: 212 | Description: 'Prefer `each_with_object` over `inject` or `reduce`.' 213 | Enabled: true 214 | 215 | EmptyLineBetweenDefs: 216 | Description: 'Use empty lines between defs.' 217 | Enabled: true 218 | 219 | EmptyLines: 220 | Description: "Don't use several empty lines in a row." 221 | Enabled: true 222 | 223 | EmptyLinesAroundAccessModifier: 224 | Description: "Keep blank lines around access modifiers." 225 | Enabled: true 226 | 227 | EmptyLinesAroundBody: 228 | Description: "Keeps track of empty lines around expression bodies." 229 | Enabled: true 230 | 231 | EmptyLiteral: 232 | Description: 'Prefer literals to Array.new/Hash.new/String.new.' 233 | Enabled: true 234 | 235 | Encoding: 236 | Description: 'Use UTF-8 as the source file encoding.' 237 | Enabled: true 238 | 239 | EndBlock: 240 | Description: 'Avoid the use of END blocks.' 241 | Enabled: true 242 | 243 | EndOfLine: 244 | Description: 'Use Unix-style line endings.' 245 | Enabled: true 246 | 247 | EvenOdd: 248 | Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' 249 | Enabled: true 250 | 251 | FileName: 252 | Description: 'Use snake_case for source file names.' 253 | Enabled: false 254 | 255 | FlipFlop: 256 | Description: 'Checks for flip flops' 257 | Enabled: true 258 | 259 | For: 260 | Description: 'Checks use of for or each in multiline loops.' 261 | Enabled: true 262 | 263 | FormatString: 264 | Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' 265 | Enabled: true 266 | 267 | GlobalVars: 268 | Description: 'Do not introduce global variables.' 269 | Enabled: true 270 | 271 | HashSyntax: 272 | Description: >- 273 | Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax 274 | { :a => 1, :b => 2 }. 275 | Enabled: true 276 | 277 | IfUnlessModifier: 278 | Description: >- 279 | Favor modifier if/unless usage when you have a 280 | single-line body. 281 | Enabled: true 282 | 283 | IfWithSemicolon: 284 | Description: 'Never use if x; .... Use the ternary operator instead.' 285 | Enabled: true 286 | 287 | IndentationConsistency: 288 | Description: 'Keep indentation straight.' 289 | Enabled: true 290 | 291 | IndentationWidth: 292 | Description: 'Use 2 spaces for indentation.' 293 | Enabled: true 294 | 295 | IndentArray: 296 | Description: >- 297 | Checks the indentation of the first element in an array 298 | literal. 299 | Enabled: true 300 | 301 | IndentHash: 302 | Description: 'Checks the indentation of the first key in a hash literal.' 303 | Enabled: true 304 | 305 | Lambda: 306 | Description: 'Use the new lambda literal syntax for single-line blocks.' 307 | Enabled: true 308 | 309 | LambdaCall: 310 | Description: 'Use lambda.call(...) instead of lambda.(...).' 311 | Enabled: true 312 | 313 | LeadingCommentSpace: 314 | Description: 'Comments should start with a space.' 315 | Enabled: true 316 | 317 | LineEndConcatenation: 318 | Description: >- 319 | Use \ instead of + or << to concatenate two string literals at 320 | line end. 321 | Enabled: true 322 | 323 | MethodCallParentheses: 324 | Description: 'Do not use parentheses for method calls with no arguments.' 325 | Enabled: true 326 | 327 | MethodDefParentheses: 328 | Description: >- 329 | Checks if the method definitions have or don't have 330 | parentheses. 331 | Enabled: true 332 | 333 | MethodName: 334 | Description: 'Use the configured style when naming methods.' 335 | Enabled: true 336 | 337 | ModuleFunction: 338 | Description: 'Checks for usage of `extend self` in modules.' 339 | Enabled: true 340 | 341 | MultilineBlockChain: 342 | Description: 'Avoid multi-line chains of blocks.' 343 | Enabled: true 344 | 345 | MultilineIfThen: 346 | Description: 'Never use then for multi-line if/unless.' 347 | Enabled: true 348 | 349 | MultilineTernaryOperator: 350 | Description: >- 351 | Avoid multi-line ?: (the ternary operator); 352 | use if/unless instead. 353 | Enabled: true 354 | 355 | NegatedIf: 356 | Description: >- 357 | Favor unless over if for negative conditions 358 | (or control flow or). 359 | Enabled: true 360 | 361 | NegatedWhile: 362 | Description: 'Favor until over while for negative conditions.' 363 | Enabled: true 364 | 365 | NestedTernaryOperator: 366 | Description: 'Use one expression per branch in a ternary operator.' 367 | Enabled: true 368 | 369 | Next: 370 | Description: 'Use `next` to skip iteration instead of a condition at the end.' 371 | Enabled: true 372 | 373 | NilComparison: 374 | Description: 'Prefer x.nil? to x == nil.' 375 | Enabled: true 376 | 377 | NonNilCheck: 378 | Description: 'Checks for redundant nil checks.' 379 | Enabled: true 380 | 381 | Not: 382 | Description: 'Use ! instead of not.' 383 | Enabled: true 384 | 385 | NumericLiterals: 386 | Description: >- 387 | Add underscores to large numeric literals to improve their 388 | readability. 389 | Enabled: true 390 | 391 | OneLineConditional: 392 | Description: >- 393 | Favor the ternary operator(?:) over 394 | if/then/else/end constructs. 395 | Enabled: true 396 | 397 | OpMethod: 398 | Description: 'When defining binary operators, name the argument other.' 399 | Enabled: true 400 | 401 | ParameterLists: 402 | Description: 'Avoid parameter lists longer than three or four parameters.' 403 | Enabled: true 404 | 405 | ParenthesesAroundCondition: 406 | Description: >- 407 | Don't use parentheses around the condition of an 408 | if/unless/while. 409 | Enabled: true 410 | 411 | PercentLiteralDelimiters: 412 | Description: 'Use `%`-literal delimiters consistently' 413 | PreferredDelimiters: 414 | '%': () 415 | '%i': () 416 | '%q': () 417 | '%Q': () 418 | '%r': '{}' 419 | '%s': () 420 | '%w': () 421 | '%W': () 422 | '%x': () 423 | Enabled: true 424 | 425 | PerlBackrefs: 426 | Description: 'Avoid Perl-style regex back references.' 427 | Enabled: true 428 | 429 | PredicateName: 430 | Description: 'Check the names of predicate methods.' 431 | Enabled: true 432 | 433 | Proc: 434 | Description: 'Use proc instead of Proc.new.' 435 | Enabled: true 436 | 437 | RaiseArgs: 438 | Description: 'Checks the arguments passed to raise/fail.' 439 | Enabled: true 440 | 441 | RedundantBegin: 442 | Description: "Don't use begin blocks when they are not needed." 443 | Enabled: true 444 | 445 | RedundantException: 446 | Description: "Checks for an obsolete RuntimeException argument in raise/fail." 447 | Enabled: true 448 | 449 | RedundantReturn: 450 | Description: "Don't use return where it's not required." 451 | Enabled: true 452 | 453 | RedundantSelf: 454 | Description: "Don't use self where it's not needed." 455 | Enabled: true 456 | 457 | RegexpLiteral: 458 | Description: >- 459 | Use %r for regular expressions matching more than 460 | `MaxSlashes` '/' characters. 461 | Use %r only for regular expressions matching more than 462 | `MaxSlashes` '/' character. 463 | Enabled: true 464 | 465 | RescueModifier: 466 | Description: 'Avoid using rescue in its modifier form.' 467 | Enabled: true 468 | 469 | SelfAssignment: 470 | Description: 'Checks for places where self-assignment shorthand should have been used.' 471 | Enabled: true 472 | 473 | Semicolon: 474 | Description: "Don't use semicolons to terminate expressions." 475 | Enabled: true 476 | 477 | SignalException: 478 | Description: 'Checks for proper usage of fail and raise.' 479 | Enabled: true 480 | 481 | SingleLineBlockParams: 482 | Description: 'Enforces the names of some block params.' 483 | Enabled: true 484 | 485 | SingleLineMethods: 486 | Description: 'Avoid single-line methods.' 487 | Enabled: true 488 | 489 | SingleSpaceBeforeFirstArg: 490 | Description: >- 491 | Checks that exactly one space is used between a method name 492 | and the first argument for method calls without parentheses. 493 | Enabled: true 494 | 495 | SpaceAfterColon: 496 | Description: 'Use spaces after colons.' 497 | Enabled: true 498 | 499 | SpaceAfterComma: 500 | Description: 'Use spaces after commas.' 501 | Enabled: true 502 | 503 | SpaceAfterControlKeyword: 504 | Description: 'Use spaces after if/elsif/unless/while/until/case/when.' 505 | Enabled: true 506 | 507 | SpaceAfterMethodName: 508 | Description: >- 509 | Never put a space between a method name and the opening 510 | parenthesis in a method definition. 511 | Enabled: true 512 | 513 | SpaceAfterNot: 514 | Description: Tracks redundant space after the ! operator. 515 | Enabled: true 516 | 517 | SpaceAfterSemicolon: 518 | Description: 'Use spaces after semicolons.' 519 | Enabled: true 520 | 521 | SpaceBeforeBlockBraces: 522 | Description: >- 523 | Checks that the left block brace has or doesn't have space 524 | before it. 525 | Enabled: true 526 | 527 | SpaceInsideBlockBraces: 528 | Description: >- 529 | Checks that block braces have or don't have surrounding space. 530 | For blocks taking parameters, checks that the left brace has 531 | or doesn't have trailing space. 532 | Enabled: true 533 | 534 | SpaceAroundEqualsInParameterDefault: 535 | Description: >- 536 | Checks that the equals signs in parameter default assignments 537 | have or don't have surrounding space depending on 538 | configuration. 539 | Enabled: true 540 | 541 | SpaceAroundOperators: 542 | Description: 'Use spaces around operators.' 543 | Enabled: true 544 | 545 | SpaceBeforeModifierKeyword: 546 | Description: 'Put a space before the modifier keyword.' 547 | Enabled: true 548 | 549 | SpaceInsideBrackets: 550 | Description: 'No spaces after [ or before ].' 551 | Enabled: true 552 | 553 | SpaceInsideHashLiteralBraces: 554 | Description: "Use spaces inside hash literal braces - or don't." 555 | Enabled: true 556 | 557 | SpaceInsideParens: 558 | Description: 'No spaces after ( or before ).' 559 | Enabled: true 560 | 561 | SpecialGlobalVars: 562 | Description: 'Avoid Perl-style global variables.' 563 | Enabled: true 564 | 565 | StringLiterals: 566 | Description: 'Checks if uses of quotes match the configured preference.' 567 | Enabled: false 568 | 569 | Tab: 570 | Description: 'No hard tabs.' 571 | Enabled: true 572 | 573 | TrailingBlankLines: 574 | Description: 'Checks trailing blank lines and final newline.' 575 | Enabled: true 576 | 577 | TrailingComma: 578 | Description: 'Checks for trailing comma in parameter lists and literals.' 579 | Enabled: true 580 | 581 | TrailingWhitespace: 582 | Description: 'Avoid trailing whitespace.' 583 | Enabled: true 584 | 585 | TrivialAccessors: 586 | Description: 'Prefer attr_* methods to trivial readers/writers.' 587 | Enabled: true 588 | 589 | UnlessElse: 590 | Description: >- 591 | Never use unless with else. Rewrite these with the positive 592 | case first. 593 | Enabled: true 594 | 595 | UnneededCapitalW: 596 | Description: 'Checks for %W when interpolation is not needed.' 597 | Enabled: true 598 | 599 | VariableInterpolation: 600 | Description: >- 601 | Don't interpolate global, instance and class variables 602 | directly in strings. 603 | Enabled: true 604 | 605 | VariableName: 606 | Description: 'Use the configured style when naming variables.' 607 | Enabled: true 608 | 609 | WhenThen: 610 | Description: 'Use when x then ... for one-line cases.' 611 | Enabled: true 612 | 613 | WhileUntilDo: 614 | Description: 'Checks for redundant do after while or until.' 615 | Enabled: true 616 | 617 | WhileUntilModifier: 618 | Description: >- 619 | Favor modifier while/until usage when you have a 620 | single-line body. 621 | Enabled: true 622 | 623 | WordArray: 624 | Description: 'Use %w or %W for arrays of words.' 625 | Enabled: true 626 | 627 | #################### Lint ################################ 628 | ### Warnings 629 | 630 | AmbiguousOperator: 631 | Description: >- 632 | Checks for ambiguous operators in the first argument of a 633 | method invocation without parentheses. 634 | Enabled: true 635 | 636 | AmbiguousRegexpLiteral: 637 | Description: >- 638 | Checks for ambiguous regexp literals in the first argument of 639 | a method invocation without parenthesis. 640 | Enabled: true 641 | 642 | AssignmentInCondition: 643 | Description: "Don't use assignment in conditions." 644 | Enabled: true 645 | 646 | BlockAlignment: 647 | Description: 'Align block ends correctly.' 648 | Enabled: true 649 | 650 | ConditionPosition: 651 | Description: 'Checks for condition placed in a confusing position relative to the keyword.' 652 | Enabled: true 653 | 654 | Debugger: 655 | Description: 'Check for debugger calls.' 656 | Enabled: true 657 | 658 | DeprecatedClassMethods: 659 | Description: 'Check for deprecated class method calls.' 660 | Enabled: true 661 | 662 | ElseLayout: 663 | Description: 'Check for odd code arrangement in an else block.' 664 | Enabled: true 665 | 666 | EmptyEnsure: 667 | Description: 'Checks for empty ensure block.' 668 | Enabled: true 669 | 670 | EmptyInterpolation: 671 | Description: 'Checks for empty string interpolation.' 672 | Enabled: true 673 | 674 | EndAlignment: 675 | Description: 'Align ends correctly.' 676 | Enabled: true 677 | 678 | EndInMethod: 679 | Description: 'END blocks should not be placed inside method definitions.' 680 | Enabled: true 681 | 682 | EnsureReturn: 683 | Description: 'Never use return in an ensure block.' 684 | Enabled: true 685 | 686 | Eval: 687 | Description: 'The use of eval represents a serious security risk.' 688 | Enabled: true 689 | 690 | GuardClause: 691 | Description: 'Check for conditionals that can be replaced with guard clauses' 692 | Enabled: true 693 | 694 | HandleExceptions: 695 | Description: "Don't suppress exception." 696 | Enabled: true 697 | 698 | InvalidCharacterLiteral: 699 | Description: >- 700 | Checks for invalid character literals with a non-escaped 701 | whitespace character. 702 | Enabled: true 703 | 704 | LiteralInCondition: 705 | Description: 'Checks of literals used in conditions.' 706 | Enabled: true 707 | 708 | LiteralInInterpolation: 709 | Description: 'Checks for literals used in interpolation.' 710 | Enabled: true 711 | 712 | Loop: 713 | Description: >- 714 | Use Kernel#loop with break rather than begin/end/until or 715 | begin/end/while for post-loop tests. 716 | Enabled: true 717 | 718 | ParenthesesAsGroupedExpression: 719 | Description: >- 720 | Checks for method calls with a space before the opening 721 | parenthesis. 722 | Enabled: true 723 | 724 | RequireParentheses: 725 | Description: >- 726 | Use parentheses in the method call to avoid confusion 727 | about precedence. 728 | Enabled: true 729 | 730 | RescueException: 731 | Description: 'Avoid rescuing the Exception class.' 732 | Enabled: true 733 | 734 | ShadowingOuterLocalVariable: 735 | Description: >- 736 | Do not use the same name as outer local variable 737 | for block arguments or block local variables. 738 | Enabled: true 739 | 740 | SpaceBeforeFirstArg: 741 | Description: >- 742 | Put a space between a method name and the first argument 743 | in a method call without parentheses. 744 | Enabled: true 745 | 746 | StringConversionInInterpolation: 747 | Description: 'Checks for Object#to_s usage in string interpolation.' 748 | Enabled: true 749 | 750 | UnderscorePrefixedVariableName: 751 | Description: 'Do not use prefix `_` for a variable that is used.' 752 | Enabled: true 753 | 754 | UnusedBlockArgument: 755 | Description: 'Checks for unused block arguments.' 756 | Enabled: true 757 | 758 | UnusedMethodArgument: 759 | Description: 'Checks for unused method arguments.' 760 | Enabled: true 761 | 762 | UnreachableCode: 763 | Description: 'Unreachable code.' 764 | Enabled: true 765 | 766 | UselessAccessModifier: 767 | Description: 'Checks for useless access modifiers.' 768 | Enabled: true 769 | 770 | UselessAssignment: 771 | Description: 'Checks for useless assignment to a local variable.' 772 | Enabled: true 773 | 774 | UselessComparison: 775 | Description: 'Checks for comparison of something with itself.' 776 | Enabled: true 777 | 778 | UselessElseWithoutRescue: 779 | Description: 'Checks for useless `else` in `begin..end` without `rescue`.' 780 | Enabled: true 781 | 782 | UselessSetterCall: 783 | Description: 'Checks for useless setter call to a local variable.' 784 | Enabled: true 785 | 786 | Void: 787 | Description: 'Possible use of operator/literal/variable in void context.' 788 | Enabled: true 789 | 790 | ##################### Rails ################################## 791 | 792 | ActionFilter: 793 | Description: 'Enforces consistent use of action filter methods.' 794 | Enabled: true 795 | 796 | DefaultScope: 797 | Description: 'Checks if the argument passed to default_scope is a block.' 798 | Enabled: true 799 | 800 | HasAndBelongsToMany: 801 | Description: 'Prefer has_many :through to has_and_belongs_to_many.' 802 | Enabled: true 803 | 804 | Output: 805 | Description: 'Checks for calls to puts, print, etc.' 806 | Enabled: true 807 | 808 | ReadWriteAttribute: 809 | Description: 'Checks for read_attribute(:attr) and write_attribute(:attr, val).' 810 | Enabled: true 811 | 812 | ScopeArgs: 813 | Description: 'Checks the arguments of ActiveRecord scopes.' 814 | Enabled: true 815 | 816 | Validation: 817 | Description: 'Use sexy validations.' 818 | Enabled: true --------------------------------------------------------------------------------