orcid 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -0
  3. data/.hound.yml +793 -0
  4. data/.travis.yml +14 -0
  5. data/Gemfile +14 -0
  6. data/README.md +107 -6
  7. data/Rakefile +17 -9
  8. data/app/assets/images/orcid/.keep +0 -0
  9. data/app/controllers/orcid/application_controller.rb +13 -4
  10. data/app/controllers/orcid/profile_connections_controller.rb +34 -6
  11. data/app/controllers/orcid/profile_requests_controller.rb +18 -15
  12. data/app/models/orcid/profile.rb +15 -5
  13. data/app/models/orcid/profile_connection.rb +20 -20
  14. data/app/models/orcid/profile_request.rb +39 -20
  15. data/app/models/orcid/work.rb +45 -55
  16. data/app/models/orcid/work/xml_parser.rb +45 -0
  17. data/app/models/orcid/work/xml_renderer.rb +29 -0
  18. data/app/services/orcid/remote/profile_creation_service.rb +40 -32
  19. data/app/services/orcid/remote/profile_query_service.rb +82 -0
  20. data/app/services/orcid/remote/profile_query_service/query_parameter_builder.rb +43 -0
  21. data/app/services/orcid/remote/profile_query_service/search_response.rb +31 -0
  22. data/app/services/orcid/remote/service.rb +16 -13
  23. data/app/services/orcid/remote/work_service.rb +58 -43
  24. data/app/templates/orcid/work.template.v1.1.xml.erb +32 -1
  25. data/app/views/orcid/profile_connections/_orcid_connector.html.erb +14 -0
  26. data/app/views/orcid/profile_connections/new.html.erb +4 -4
  27. data/bin/rails +8 -0
  28. data/config/{application.yml → application.yml.example} +3 -13
  29. data/config/locales/orcid.en.yml +5 -1
  30. data/config/routes.rb +4 -2
  31. data/lib/generators/orcid/install/install_generator.rb +46 -7
  32. data/lib/orcid.rb +23 -11
  33. data/lib/orcid/configuration.rb +32 -13
  34. data/lib/orcid/configuration/provider.rb +27 -13
  35. data/lib/orcid/engine.rb +20 -4
  36. data/lib/orcid/exceptions.rb +33 -4
  37. data/lib/orcid/named_callbacks.rb +3 -1
  38. data/lib/orcid/spec_support.rb +19 -9
  39. data/lib/orcid/version.rb +1 -1
  40. data/lib/tasks/orcid_tasks.rake +3 -3
  41. data/orcid.gemspec +51 -0
  42. data/rubocop.txt +1164 -0
  43. data/script/fast_specs +22 -0
  44. data/spec/controllers/orcid/profile_connections_controller_spec.rb +101 -0
  45. data/spec/controllers/orcid/profile_requests_controller_spec.rb +116 -0
  46. data/spec/factories/orcid_profile_requests.rb +11 -0
  47. data/spec/factories/users.rb +9 -0
  48. data/spec/fast_helper.rb +12 -0
  49. data/spec/features/batch_profile_spec.rb +31 -0
  50. data/spec/features/non_ui_based_interactions_spec.rb +117 -0
  51. data/spec/features/profile_connection_feature_spec.rb +19 -0
  52. data/spec/features/public_api_query_spec.rb +36 -0
  53. data/spec/fixtures/orcid_works.xml +55 -0
  54. data/spec/lib/orcid/configuration/provider_spec.rb +40 -0
  55. data/spec/lib/orcid/configuration_spec.rb +38 -0
  56. data/spec/lib/orcid/named_callbacks_spec.rb +28 -0
  57. data/spec/lib/orcid_spec.rb +97 -0
  58. data/spec/models/orcid/profile_connection_spec.rb +81 -0
  59. data/spec/models/orcid/profile_request_spec.rb +131 -0
  60. data/spec/models/orcid/profile_spec.rb +76 -0
  61. data/spec/models/orcid/work/xml_parser_spec.rb +40 -0
  62. data/spec/models/orcid/work/xml_renderer_spec.rb +18 -0
  63. data/spec/models/orcid/work_spec.rb +53 -0
  64. data/spec/services/orcid/remote/profile_creation_service_spec.rb +40 -0
  65. data/spec/services/orcid/remote/profile_query_service/query_parameter_builder_spec.rb +44 -0
  66. data/spec/services/orcid/remote/profile_query_service/search_response_spec.rb +14 -0
  67. data/spec/services/orcid/remote/profile_query_service_spec.rb +118 -0
  68. data/spec/services/orcid/remote/service_spec.rb +26 -0
  69. data/spec/services/orcid/remote/work_service_spec.rb +44 -0
  70. data/spec/spec_helper.rb +99 -0
  71. data/spec/support/non_orcid_models.rb +11 -0
  72. data/spec/support/stub_callback.rb +25 -0
  73. data/spec/test_app_templates/Gemfile.extra +3 -0
  74. data/spec/test_app_templates/lib/generators/test_app_generator.rb +36 -0
  75. data/spec/views/orcid/profile_connections/new.html.erb_spec.rb +25 -0
  76. data/spec/views/orcid/profile_requests/new.html.erb_spec.rb +23 -0
  77. metadata +119 -29
  78. data/app/runners/orcid/profile_lookup_runner.rb +0 -33
  79. data/app/runners/orcid/runner.rb +0 -15
  80. data/app/services/orcid/remote/profile_lookup_service.rb +0 -56
  81. data/app/services/orcid/remote/profile_lookup_service/search_response.rb +0 -23
  82. data/lib/orcid/query_parameter_builder.rb +0 -38
data/lib/orcid.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'orcid/engine'
2
2
  require 'orcid/configuration'
3
3
  require 'orcid/exceptions'
4
-
4
+ require 'figaro'
5
5
  require 'mappy'
6
6
  require 'devise_multi_auth'
7
7
  require 'virtus'
@@ -9,8 +9,8 @@ require 'omniauth-orcid'
9
9
  require 'email_validator'
10
10
  require 'simple_form'
11
11
 
12
+ # The namespace for all things related to Orcid integration
12
13
  module Orcid
13
-
14
14
  class << self
15
15
  attr_writer :configuration
16
16
 
@@ -32,26 +32,39 @@ module Orcid
32
32
  configuration.provider
33
33
  end
34
34
 
35
+ def parent_controller
36
+ configuration.parent_controller
37
+ end
38
+
35
39
  def authentication_model
36
40
  configuration.authentication_model
37
41
  end
38
42
 
39
43
  def connect_user_and_orcid_profile(user, orcid_profile_id)
40
- authentication_model.create!(provider: 'orcid', uid: orcid_profile_id, user: user)
44
+ authentication_model.create!(
45
+ provider: 'orcid', uid: orcid_profile_id, user: user
46
+ )
41
47
  end
42
48
 
43
49
  def access_token_for(orcid_profile_id, options = {})
44
50
  client = options.fetch(:client) { oauth_client }
45
51
  tokenizer = options.fetch(:tokenizer) { authentication_model }
46
- tokenizer.to_access_token(uid: orcid_profile_id, provider: 'orcid', client: client)
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
47
63
  end
48
64
 
49
65
  def profile_for(user)
50
- if auth = authentication_model.where(provider: 'orcid', user: user).first
51
- Orcid::Profile.new(auth.uid)
52
- else
53
- nil
54
- end
66
+ auth = authentication_model.where(provider: 'orcid', user: user).first
67
+ auth && Orcid::Profile.new(auth.uid)
55
68
  end
56
69
 
57
70
  def enqueue(object)
@@ -60,7 +73,7 @@ module Orcid
60
73
 
61
74
  def oauth_client
62
75
  # passing the site: option as Orcid's Sandbox has an invalid certificate
63
- # for the api.sandbox-1.orcid.org
76
+ # for the api.sandbox.orcid.org
64
77
  @oauth_client ||= Devise::MultiAuth.oauth_client_for(
65
78
  'orcid', options: { site: provider.site_url }
66
79
  )
@@ -70,5 +83,4 @@ module Orcid
70
83
  tokenizer = options.fetch(:tokenizer) { oauth_client.client_credentials }
71
84
  tokenizer.get_token(scope: scope)
72
85
  end
73
-
74
86
  end
@@ -1,28 +1,47 @@
1
1
  module Orcid
2
+ # Responsible for exposing the customization mechanism
2
3
  class Configuration
3
4
  attr_reader :mapper
4
5
  def initialize(options = {})
5
- @mapper = options.fetch(:mapper) {
6
- require 'mappy'
7
- ::Mappy
8
- }
9
- @provider = options.fetch(:provider) {
10
- require 'orcid/configuration/provider'
11
- Provider.new
12
- }
13
- @authentication_model = options.fetch(:authentication_model) {
14
- require 'devise-multi_auth'
15
- ::Devise::MultiAuth::Authentication
16
- }
6
+ @mapper = options.fetch(:mapper) { default_mapper }
7
+ @provider = options.fetch(:provider) { default_provider }
8
+ @authentication_model = options.fetch(:authentication_model) do
9
+ default_authenticaton_model
10
+ end
11
+ @parent_controller = options.fetch(:parent_controller) do
12
+ '::ApplicationController'
13
+ end
17
14
  end
18
15
 
19
16
  attr_accessor :provider
20
17
  attr_accessor :authentication_model
18
+ attr_accessor :parent_controller
21
19
 
22
20
  def register_mapping_to_orcid_work(source_type, legend)
23
21
  mapper.configure do |config|
24
- config.register(source: source_type, target: 'orcid/work', legend: legend)
22
+ config.register(
23
+ source: source_type,
24
+ target: 'orcid/work',
25
+ legend: legend
26
+ )
25
27
  end
26
28
  end
29
+
30
+ protected
31
+
32
+ def default_mapper
33
+ require 'mappy'
34
+ ::Mappy
35
+ end
36
+
37
+ def default_provider
38
+ require 'orcid/configuration/provider'
39
+ Provider.new
40
+ end
41
+
42
+ def default_authenticaton_model
43
+ require 'devise-multi_auth'
44
+ ::Devise::MultiAuth::Authentication
45
+ end
27
46
  end
28
47
  end
@@ -1,5 +1,10 @@
1
+ require 'orcid/exceptions'
1
2
  module Orcid
2
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.
3
8
  class Provider
4
9
  attr_reader :store
5
10
  def initialize(store = ::ENV)
@@ -8,43 +13,52 @@ module Orcid
8
13
 
9
14
  attr_writer :authentication_scope
10
15
  def authentication_scope
11
- @authentication_scope ||= store.fetch('ORCID_APP_AUTHENTICATION_SCOPE') {
12
- "/authenticate,/orcid-works/create,/orcid-works/update,/read-public"
13
- }
16
+ @authentication_scope ||=
17
+ store.fetch('ORCID_APP_AUTHENTICATION_SCOPE') do
18
+ '/authenticate,/orcid-works/create,/orcid-works/update,/read-public'
19
+ end
14
20
  end
15
21
 
16
22
  attr_writer :site_url
17
23
  def site_url
18
- @site_url ||= store.fetch('ORCID_SITE_URL') { "http://api.sandbox-1.orcid.org" }
24
+ @site_url ||= store.fetch('ORCID_SITE_URL') do
25
+ 'http://api.sandbox.orcid.org'
26
+ end
19
27
  end
20
28
 
21
29
  attr_writer :token_url
22
30
  def token_url
23
- @token_url ||= store.fetch('ORCID_TOKEN_URL') { "https://api.sandbox-1.orcid.org/oauth/token" }
31
+ @token_url ||= store.fetch('ORCID_TOKEN_URL') do
32
+ 'https://api.sandbox.orcid.org/oauth/token'
33
+ end
24
34
  end
25
35
 
26
36
  attr_writer :signin_via_json_url
27
37
  def signin_via_json_url
28
- @signin_via_json_url ||= store.fetch('ORCID_REMOTE_SIGNIN_URL') { "https://sandbox-1.orcid.org/signin/auth.json" }
38
+ @signin_via_json_url ||= store.fetch('ORCID_REMOTE_SIGNIN_URL') do
39
+ 'https://sandbox.orcid.org/signin/auth.json'
40
+ end
29
41
  end
30
42
 
31
43
  attr_writer :authorize_url
32
44
  def authorize_url
33
- @authorize_url ||= store.fetch('ORCID_AUTHORIZE_URL') { "https://sandbox-1.orcid.org/oauth/authorize" }
45
+ @authorize_url ||= store.fetch('ORCID_AUTHORIZE_URL') do
46
+ 'https://sandbox.orcid.org/oauth/authorize'
47
+ end
34
48
  end
35
49
 
36
50
  attr_writer :id
37
51
  def id
38
- @id ||= store.fetch('ORCID_APP_ID') {
39
- Rails.logger.error('Set your Orcid.provider.id')
40
- }
52
+ @id ||= store.fetch('ORCID_APP_ID')
53
+ rescue KeyError
54
+ raise ConfigurationError, 'ORCID_APP_ID'
41
55
  end
42
56
 
43
57
  attr_writer :secret
44
58
  def secret
45
- @secret ||= store.fetch('ORCID_APP_SECRET') {
46
- Rails.logger.error('Set your Orcid.provider.secret')
47
- }
59
+ @secret ||= store.fetch('ORCID_APP_SECRET')
60
+ rescue KeyError
61
+ raise ConfigurationError, 'ORCID_APP_SECRET'
48
62
  end
49
63
  end
50
64
  end
data/lib/orcid/engine.rb CHANGED
@@ -1,15 +1,31 @@
1
+ # The namespace for all things related to Orcid integration
1
2
  module Orcid
3
+ module_function
4
+
5
+ # As per an isolated_namespace Rails engine.
6
+ # But the isolated namespace creates issues.
7
+ # @api private
8
+ def table_name_prefix
9
+ 'orcid_'
10
+ end
11
+
12
+ # Because I am not using isolate_namespace for Orcid::Engine
13
+ # I need this for the application router to find the appropriate routes.
14
+ # @api private
15
+ def use_relative_model_naming?
16
+ true
17
+ end
18
+
19
+ # While not an isolated namespace engine
20
+ # @See http://guides.rubyonrails.org/engines.html
2
21
  class Engine < ::Rails::Engine
3
- isolate_namespace Orcid
22
+ engine_name 'orcid'
4
23
 
5
24
  initializer 'orcid.initializers' do |app|
6
25
  app.config.paths.add 'app/services', eager_load: true
7
- app.config.paths.add 'app/runners', eager_load: true
8
26
  app.config.autoload_paths += %W(
9
27
  #{config.root}/app/services
10
- #{config.root}/app/runners
11
28
  )
12
29
  end
13
-
14
30
  end
15
31
  end
@@ -1,11 +1,28 @@
1
1
  module Orcid
2
+
3
+ class ConfigurationError < RuntimeError
4
+ def initialize(key_name)
5
+ super("Unable to find #{key_name.inspect} in configuration storage.")
6
+ end
7
+ end
8
+
2
9
  # Because in trouble shooting what all goes into this remote call,
3
10
  # you may very well want all of this.
4
11
  class RemoteServiceError < RuntimeError
5
12
  def initialize(options)
6
13
  text = []
7
14
  text << "-- Client --"
8
- if client = options[:client]
15
+ append_client_options(options[:client], text)
16
+ append_token(options[:token], text)
17
+ append_request(options, text)
18
+ append_response(options, text)
19
+ super(text.join("\n"))
20
+ end
21
+
22
+ private
23
+
24
+ def append_client_options(client, text)
25
+ if client
9
26
  text << "id:\n\t#{client.id.inspect}"
10
27
  text << "site:\n\t#{client.site.inspect}"
11
28
  text << "options:\n\t#{client.options.inspect}"
@@ -13,19 +30,31 @@ module Orcid
13
30
  text << "scopes:\n\t#{Orcid.provider.authentication_scope}"
14
31
  end
15
32
  end
33
+ text
34
+ end
35
+
36
+ def append_token(token, text)
16
37
  text << "\n-- Token --"
17
- if token = options[:token]
38
+ if token
18
39
  text << "access_token:\n\t#{token.token.inspect}"
19
40
  text << "refresh_token:\n\t#{token.refresh_token.inspect}"
20
41
  end
42
+ text
43
+ end
44
+
45
+ def append_request(options, text)
21
46
  text << "\n-- Request --"
22
47
  text << "path:\n\t#{options[:request_path].inspect}" if options[:request_path]
23
48
  text << "headers:\n\t#{options[:request_headers].inspect}" if options[:request_headers]
24
49
  text << "body:\n\t#{options[:request_body]}" if options[:request_body]
50
+ text
51
+ end
52
+
53
+ def append_response(options, text)
25
54
  text << "\n-- Response --"
26
55
  text << "status:\n\t#{options[:response_status].inspect}" if options[:response_status]
27
56
  text << "body:\n\t#{options[:response_body]}" if options[:response_body]
28
- super(text.join("\n"))
57
+ text
29
58
  end
30
59
  end
31
- end
60
+ end
@@ -6,7 +6,9 @@ module Orcid
6
6
  @callbacks = {}
7
7
  end
8
8
 
9
- def method_missing(callback_name, *args, &block)
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)
10
12
  @callbacks[callback_name] = block
11
13
  end
12
14
 
@@ -19,7 +19,7 @@ class RequestSandboxAuthorizationCode
19
19
 
20
20
  def call(options = {})
21
21
  orcid_profile_id = options.fetch(:orcid_profile_id) { ENV['ORCID_CLAIMED_PROFILE_ID'] }
22
- password = options.fetch(:password) { ENV['ORCID_CLAIMED_PROFILE_PASSWORD']}
22
+ password = options.fetch(:password) { ENV['ORCID_CLAIMED_PROFILE_PASSWORD'] }
23
23
 
24
24
  login_to_orcid(orcid_profile_id, password)
25
25
  request_authorization
@@ -30,25 +30,35 @@ class RequestSandboxAuthorizationCode
30
30
  private :cookies
31
31
 
32
32
  private
33
+
33
34
  def login_to_orcid(orcid_profile_id, password)
34
- response = RestClient.post(login_url, userId: orcid_profile_id, password: password )
35
- if ! JSON.parse(response)["success"]
36
- raise "Response not successful: \n#{response}"
37
- else
35
+ response = RestClient.post(
36
+ login_url, userId: orcid_profile_id, password: password
37
+ )
38
+ if JSON.parse(response)['success']
38
39
  self.cookies = response.cookies
40
+ else
41
+ fail "Response not successful: \n#{response}"
39
42
  end
40
43
  end
41
44
 
42
45
  def request_authorization
43
- parameters = { client_id: orcid_client_id, response_type: 'code', scope: access_scope, redirect_uri: oauth_redirect_uri }
44
- RestClient.get(authorize_url, {params: parameters, cookies: cookies})
46
+ parameters = {
47
+ client_id: orcid_client_id,
48
+ response_type: 'code',
49
+ scope: access_scope,
50
+ redirect_uri: oauth_redirect_uri
51
+ }
52
+ RestClient.get(authorize_url, { params: parameters, cookies: cookies })
45
53
  end
46
54
 
47
55
  def request_authorization_code
48
- RestClient.post(authorize_url, {user_oauth_approval: true}, {cookies: cookies})
56
+ RestClient.post(
57
+ authorize_url, { user_oauth_approval: true }, { cookies: cookies }
58
+ )
49
59
  rescue RestClient::Found => e
50
60
  uri = URI.parse(e.response.headers.fetch(:location))
51
- CGI::parse(uri.query).fetch('code').first
61
+ CGI.parse(uri.query).fetch('code').first
52
62
  end
53
63
 
54
64
  end
data/lib/orcid/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Orcid
2
- VERSION = "0.0.4"
2
+ VERSION = '0.1.0'
3
3
  end
@@ -6,7 +6,7 @@ namespace :orcid do
6
6
  task :query_runner do
7
7
  @runner = lambda { |person|
8
8
  puts "Processing: #{person.email}"
9
- Orcid::ProfileLookupRunner.new {|on|
9
+ Orcid::Remote::ProfileLookupRunner.new {|on|
10
10
  on.found {|results|
11
11
  person.found(results)
12
12
  puts "\tfound" if verbose
@@ -39,7 +39,7 @@ namespace :orcid do
39
39
  }
40
40
  @runner = lambda { |person|
41
41
  puts "Processing: #{person.email}"
42
- Orcid::ProfileLookupRunner.new {|on|
42
+ Orcid::Remote::ProfileQueryService.new {|on|
43
43
  on.found {|results|
44
44
  person.found(results)
45
45
  puts "\tfound" if verbose
@@ -92,7 +92,7 @@ namespace :orcid do
92
92
  end
93
93
 
94
94
  def found(existing_orcids)
95
- @existing_orcids = Array(existing_orcids).collect(&:orcid_profile_id).join("; ")
95
+ @existing_orcids = Array.wrap(existing_orcids).collect(&:orcid_profile_id).join("; ")
96
96
  end
97
97
 
98
98
  def to_output_row
data/orcid.gemspec ADDED
@@ -0,0 +1,51 @@
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/jeremyf/orcid'
17
+ s.summary = 'A Rails engine for orcid.org integration.'
18
+ s.description = 'A Rails engine for orcid.org integration.'
19
+
20
+ s.files = `git ls-files -z`.split("\x0")
21
+ # Deliberately removing bin executables as it appears to relate to
22
+ # https://github.com/cbeer/engine_cart/issues/9
23
+ s.executables = s.executables = s.files.grep(%r{^bin/}) do |f|
24
+ f == 'bin/rails' ? nil : File.basename(f)
25
+ end.compact
26
+ s.test_files = s.files.grep(/^(test|spec|features)\//)
27
+ s.require_paths = ['lib']
28
+
29
+ s.add_dependency 'rails', '~> 4.0.3'
30
+ s.add_dependency 'mappy', '~> 0.1.0'
31
+ s.add_dependency 'figaro'
32
+ s.add_dependency 'devise-multi_auth', '~> 0.1.0'
33
+ s.add_dependency 'omniauth-orcid'
34
+ s.add_dependency 'virtus'
35
+ s.add_dependency 'email_validator'
36
+ s.add_dependency 'simple_form'
37
+ s.add_dependency 'byebug'
38
+
39
+ s.add_development_dependency 'sqlite3'
40
+ s.add_development_dependency 'engine_cart'
41
+ s.add_development_dependency 'rspec-rails'
42
+ s.add_development_dependency 'database_cleaner'
43
+ s.add_development_dependency 'factory_girl'
44
+ s.add_development_dependency 'rspec-html-matchers'
45
+ s.add_development_dependency 'capybara'
46
+ s.add_development_dependency 'capybara-webkit'
47
+ s.add_development_dependency 'webmock'
48
+ s.add_development_dependency 'simplecov'
49
+ s.add_development_dependency 'rest_client'
50
+ s.add_development_dependency 'rspec-given'
51
+ end