orcid 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +10 -0
- data/.hound.yml +793 -0
- data/.travis.yml +14 -0
- data/Gemfile +14 -0
- data/README.md +107 -6
- data/Rakefile +17 -9
- data/app/assets/images/orcid/.keep +0 -0
- data/app/controllers/orcid/application_controller.rb +13 -4
- data/app/controllers/orcid/profile_connections_controller.rb +34 -6
- data/app/controllers/orcid/profile_requests_controller.rb +18 -15
- data/app/models/orcid/profile.rb +15 -5
- data/app/models/orcid/profile_connection.rb +20 -20
- data/app/models/orcid/profile_request.rb +39 -20
- data/app/models/orcid/work.rb +45 -55
- data/app/models/orcid/work/xml_parser.rb +45 -0
- data/app/models/orcid/work/xml_renderer.rb +29 -0
- data/app/services/orcid/remote/profile_creation_service.rb +40 -32
- data/app/services/orcid/remote/profile_query_service.rb +82 -0
- data/app/services/orcid/remote/profile_query_service/query_parameter_builder.rb +43 -0
- data/app/services/orcid/remote/profile_query_service/search_response.rb +31 -0
- data/app/services/orcid/remote/service.rb +16 -13
- data/app/services/orcid/remote/work_service.rb +58 -43
- data/app/templates/orcid/work.template.v1.1.xml.erb +32 -1
- data/app/views/orcid/profile_connections/_orcid_connector.html.erb +14 -0
- data/app/views/orcid/profile_connections/new.html.erb +4 -4
- data/bin/rails +8 -0
- data/config/{application.yml → application.yml.example} +3 -13
- data/config/locales/orcid.en.yml +5 -1
- data/config/routes.rb +4 -2
- data/lib/generators/orcid/install/install_generator.rb +46 -7
- data/lib/orcid.rb +23 -11
- data/lib/orcid/configuration.rb +32 -13
- data/lib/orcid/configuration/provider.rb +27 -13
- data/lib/orcid/engine.rb +20 -4
- data/lib/orcid/exceptions.rb +33 -4
- data/lib/orcid/named_callbacks.rb +3 -1
- data/lib/orcid/spec_support.rb +19 -9
- data/lib/orcid/version.rb +1 -1
- data/lib/tasks/orcid_tasks.rake +3 -3
- data/orcid.gemspec +51 -0
- data/rubocop.txt +1164 -0
- data/script/fast_specs +22 -0
- data/spec/controllers/orcid/profile_connections_controller_spec.rb +101 -0
- data/spec/controllers/orcid/profile_requests_controller_spec.rb +116 -0
- data/spec/factories/orcid_profile_requests.rb +11 -0
- data/spec/factories/users.rb +9 -0
- data/spec/fast_helper.rb +12 -0
- data/spec/features/batch_profile_spec.rb +31 -0
- data/spec/features/non_ui_based_interactions_spec.rb +117 -0
- data/spec/features/profile_connection_feature_spec.rb +19 -0
- data/spec/features/public_api_query_spec.rb +36 -0
- data/spec/fixtures/orcid_works.xml +55 -0
- data/spec/lib/orcid/configuration/provider_spec.rb +40 -0
- data/spec/lib/orcid/configuration_spec.rb +38 -0
- data/spec/lib/orcid/named_callbacks_spec.rb +28 -0
- data/spec/lib/orcid_spec.rb +97 -0
- data/spec/models/orcid/profile_connection_spec.rb +81 -0
- data/spec/models/orcid/profile_request_spec.rb +131 -0
- data/spec/models/orcid/profile_spec.rb +76 -0
- data/spec/models/orcid/work/xml_parser_spec.rb +40 -0
- data/spec/models/orcid/work/xml_renderer_spec.rb +18 -0
- data/spec/models/orcid/work_spec.rb +53 -0
- data/spec/services/orcid/remote/profile_creation_service_spec.rb +40 -0
- data/spec/services/orcid/remote/profile_query_service/query_parameter_builder_spec.rb +44 -0
- data/spec/services/orcid/remote/profile_query_service/search_response_spec.rb +14 -0
- data/spec/services/orcid/remote/profile_query_service_spec.rb +118 -0
- data/spec/services/orcid/remote/service_spec.rb +26 -0
- data/spec/services/orcid/remote/work_service_spec.rb +44 -0
- data/spec/spec_helper.rb +99 -0
- data/spec/support/non_orcid_models.rb +11 -0
- data/spec/support/stub_callback.rb +25 -0
- data/spec/test_app_templates/Gemfile.extra +3 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +36 -0
- data/spec/views/orcid/profile_connections/new.html.erb_spec.rb +25 -0
- data/spec/views/orcid/profile_requests/new.html.erb_spec.rb +23 -0
- metadata +119 -29
- data/app/runners/orcid/profile_lookup_runner.rb +0 -33
- data/app/runners/orcid/runner.rb +0 -15
- data/app/services/orcid/remote/profile_lookup_service.rb +0 -56
- data/app/services/orcid/remote/profile_lookup_service/search_response.rb +0 -23
- data/lib/orcid/query_parameter_builder.rb +0 -38
@@ -4,7 +4,6 @@ module Orcid
|
|
4
4
|
# * submitting a request for an ORCID Profile
|
5
5
|
# * handling the response for the ORCID Profile creation
|
6
6
|
class ProfileRequest < ActiveRecord::Base
|
7
|
-
|
8
7
|
def self.find_by_user(user)
|
9
8
|
where(user: user).first
|
10
9
|
end
|
@@ -20,39 +19,60 @@ module Orcid
|
|
20
19
|
belongs_to :user
|
21
20
|
|
22
21
|
def run(options = {})
|
23
|
-
# Why dependency injection? Because this is going to be a plugin, and
|
24
|
-
# can't possibly be simple.
|
22
|
+
# Why dependency injection? Because this is going to be a plugin, and
|
23
|
+
# things can't possibly be simple. I also found it easier to test the
|
24
|
+
# #run method with these injected dependencies
|
25
25
|
validator = options.fetch(:validator) { method(:validate_before_run) }
|
26
26
|
return false unless validator.call(self)
|
27
27
|
|
28
|
-
payload_xml_builder = options.fetch(:payload_xml_builder)
|
29
|
-
|
28
|
+
payload_xml_builder = options.fetch(:payload_xml_builder) do
|
29
|
+
method(:xml_payload)
|
30
|
+
end
|
31
|
+
profile_creation_service = options.fetch(:profile_creation_service) do
|
32
|
+
default_profile_creation_service
|
33
|
+
end
|
30
34
|
profile_creation_service.call(payload_xml_builder.call(attributes))
|
31
35
|
end
|
32
36
|
|
33
37
|
def default_profile_creation_service
|
34
|
-
@default_profile_creation_service ||=
|
35
|
-
|
38
|
+
@default_profile_creation_service ||= begin
|
39
|
+
Orcid::Remote::ProfileCreationService.new do |on|
|
40
|
+
on.success do |orcid_profile_id|
|
41
|
+
handle_profile_creation_response(orcid_profile_id)
|
42
|
+
end
|
43
|
+
end
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
39
47
|
def validate_before_run(context = self)
|
48
|
+
validate_profile_id_is_unassigned(context) &&
|
49
|
+
validate_user_does_not_have_profile(context)
|
50
|
+
end
|
40
51
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
52
|
+
def validate_user_does_not_have_profile(context)
|
53
|
+
user_orcid_profile = Orcid.profile_for(context.user)
|
54
|
+
return true unless user_orcid_profile
|
55
|
+
message = "#{context.class} ID=#{context.to_param}'s associated user" \
|
56
|
+
" #{context.user.to_param} already has an assigned :orcid_profile_id" \
|
57
|
+
" #{user_orcid_profile.to_param}"
|
58
|
+
context.errors.add(:base, message)
|
59
|
+
false
|
60
|
+
end
|
61
|
+
private :validate_user_does_not_have_profile
|
50
62
|
|
51
|
-
|
63
|
+
def validate_profile_id_is_unassigned(context)
|
64
|
+
return true unless context.orcid_profile_id?
|
65
|
+
message = "#{context.class} ID=#{context.to_param} already has an" \
|
66
|
+
" assigned :orcid_profile_id #{context.orcid_profile_id.inspect}"
|
67
|
+
context.errors.add(:base, message)
|
68
|
+
false
|
52
69
|
end
|
70
|
+
private :validate_profile_id_is_unassigned
|
53
71
|
|
54
|
-
# NOTE: This one lies ->
|
55
|
-
#
|
72
|
+
# NOTE: This one lies ->
|
73
|
+
# http://support.orcid.org/knowledgebase/articles/177522-create-an-id-technical-developer
|
74
|
+
# NOTE: This one was true at 2014-02-06:14:55 ->
|
75
|
+
# http://support.orcid.org/knowledgebase/articles/162412-tutorial-create-a-new-record-using-curl
|
56
76
|
def xml_payload(input = attributes)
|
57
77
|
attrs = input.with_indifferent_access
|
58
78
|
returning_value = <<-XML_TEMPLATE
|
@@ -83,6 +103,5 @@ module Orcid
|
|
83
103
|
Orcid.connect_user_and_orcid_profile(user, orcid_profile_id)
|
84
104
|
end
|
85
105
|
end
|
86
|
-
|
87
106
|
end
|
88
107
|
end
|
data/app/models/orcid/work.rb
CHANGED
@@ -2,9 +2,28 @@ module Orcid
|
|
2
2
|
# A well-defined data structure that coordinates with its :template in order
|
3
3
|
# to generate XML that can be POSTed/PUT as an Orcid Work.
|
4
4
|
class Work
|
5
|
-
VALID_WORK_TYPES =
|
6
|
-
|
7
|
-
|
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
|
8
27
|
|
9
28
|
include Virtus.model
|
10
29
|
include ActiveModel::Validations
|
@@ -16,16 +35,36 @@ module Orcid
|
|
16
35
|
attribute :work_type, String
|
17
36
|
validates :work_type, presence: true, inclusion: { in: VALID_WORK_TYPES }
|
18
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
|
19
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
|
20
58
|
|
21
59
|
def to_xml
|
22
60
|
XmlRenderer.call(self)
|
23
61
|
end
|
24
62
|
|
25
|
-
def ==(
|
26
|
-
super ||
|
63
|
+
def ==(other)
|
64
|
+
super ||
|
65
|
+
other.instance_of?(self.class) &&
|
27
66
|
id.present? &&
|
28
|
-
|
67
|
+
other.id == id
|
29
68
|
end
|
30
69
|
|
31
70
|
def id
|
@@ -37,54 +76,5 @@ module Orcid
|
|
37
76
|
nil
|
38
77
|
end
|
39
78
|
end
|
40
|
-
|
41
|
-
class XmlRenderer
|
42
|
-
def self.call(works, options = {})
|
43
|
-
new(works, options).call
|
44
|
-
end
|
45
|
-
|
46
|
-
attr_reader :works, :template
|
47
|
-
def initialize(works, options = {})
|
48
|
-
self.works = works
|
49
|
-
@template = options.fetch(:template_path) { Orcid::Engine.root.join('app/templates/orcid/work.template.v1.1.xml.erb').read }
|
50
|
-
end
|
51
|
-
|
52
|
-
def call
|
53
|
-
ERB.new(template).result(binding)
|
54
|
-
end
|
55
|
-
|
56
|
-
protected
|
57
|
-
def works=(thing)
|
58
|
-
@works = Array(thing)
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
class XmlParser
|
64
|
-
def self.call(xml)
|
65
|
-
new(xml).call
|
66
|
-
end
|
67
|
-
|
68
|
-
attr_reader :xml
|
69
|
-
def initialize(xml)
|
70
|
-
@xml = xml
|
71
|
-
end
|
72
|
-
|
73
|
-
def call
|
74
|
-
document = Nokogiri::XML.parse(xml)
|
75
|
-
document.css('orcid-works orcid-work').collect do |node|
|
76
|
-
transform(node)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
def transform(node)
|
82
|
-
Orcid::Work.new.tap do |work|
|
83
|
-
work.put_code = node.attributes.fetch("put-code").value
|
84
|
-
work.title = node.css('work-title title').text
|
85
|
-
work.work_type = node.css('work-type').text
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
79
|
end
|
90
80
|
end
|
@@ -0,0 +1,45 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
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, options = {})
|
6
|
+
new(works, options).call
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :works, :template
|
10
|
+
def initialize(works, options = {})
|
11
|
+
self.works = works
|
12
|
+
@template = options.fetch(:template_path) do
|
13
|
+
template_name = 'app/templates/orcid/work.template.v1.1.xml.erb'
|
14
|
+
Orcid::Engine.root.join(template_name).read
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
ERB.new(template).result(binding)
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def works=(thing)
|
25
|
+
@works = Array.wrap(thing)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,43 +1,51 @@
|
|
1
1
|
require 'orcid/remote/service'
|
2
|
-
module Orcid
|
3
|
-
|
4
|
-
|
2
|
+
module Orcid
|
3
|
+
module Remote
|
4
|
+
# Responsible for minting a new ORCID for the given payload.
|
5
|
+
class ProfileCreationService < Orcid::Remote::Service
|
6
|
+
def self.call(payload, config = {}, &callback_config)
|
7
|
+
new(config, &callback_config).call(payload)
|
8
|
+
end
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
10
|
+
attr_reader :token, :path, :headers
|
11
|
+
def initialize(config = {}, &callback_config)
|
12
|
+
super(&callback_config)
|
13
|
+
@token = config.fetch(:token) do
|
14
|
+
Orcid.client_credentials_token('/orcid-profile/create')
|
15
|
+
end
|
16
|
+
@path = config.fetch(:path) { 'v1.1/orcid-profile' }
|
17
|
+
@headers = config.fetch(:headers) { default_headers }
|
18
|
+
end
|
9
19
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@path = config.fetch(:path) { "v1.1/orcid-profile" }
|
15
|
-
@headers = config.fetch(:headers) { default_headers }
|
16
|
-
end
|
20
|
+
def call(payload)
|
21
|
+
response = deliver(payload)
|
22
|
+
parse(response)
|
23
|
+
end
|
17
24
|
|
18
|
-
|
19
|
-
response = deliver(payload)
|
20
|
-
parse(response)
|
21
|
-
end
|
25
|
+
protected
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
+
def deliver(body)
|
28
|
+
token.post(path, body: body, headers: headers)
|
29
|
+
end
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
def parse(response)
|
32
|
+
uri = URI.parse(response.headers.fetch(:location))
|
33
|
+
orcid_profile_id = uri.path.sub(/\A\//, '').split('/').first
|
34
|
+
if orcid_profile_id
|
35
|
+
callback(:success, orcid_profile_id)
|
36
|
+
orcid_profile_id
|
37
|
+
else
|
38
|
+
callback(:failure)
|
39
|
+
false
|
40
|
+
end
|
36
41
|
end
|
37
|
-
end
|
38
42
|
|
39
|
-
|
40
|
-
|
43
|
+
def default_headers
|
44
|
+
{
|
45
|
+
'Accept' => 'application/xml',
|
46
|
+
'Content-Type' => 'application/vdn.orcid+xml'
|
47
|
+
}
|
48
|
+
end
|
41
49
|
end
|
42
50
|
end
|
43
51
|
end
|
@@ -0,0 +1,82 @@
|
|
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
|
+
def initialize(config = {}, &callbacks)
|
13
|
+
super(&callbacks)
|
14
|
+
@query_builder = config.fetch(:query_parameter_builder) { QueryParameterBuilder }
|
15
|
+
@token = config.fetch(:token) { default_token }
|
16
|
+
@response_builder = config.fetch(:response_builder) { SearchResponse }
|
17
|
+
@path = config.fetch(:path) { 'v1.1/search/orcid-bio/' }
|
18
|
+
@headers = config.fetch(:headers) { default_headers }
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(input)
|
22
|
+
parameters = query_builder.call(input)
|
23
|
+
response = deliver(parameters)
|
24
|
+
parsed_response = parse(response.body)
|
25
|
+
issue_callbacks(parsed_response)
|
26
|
+
parsed_response
|
27
|
+
end
|
28
|
+
alias_method :search, :call
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def default_token
|
33
|
+
Orcid.client_credentials_token('/read-public')
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_headers
|
37
|
+
{
|
38
|
+
:accept => 'application/orcid+json',
|
39
|
+
'Content-Type' => 'application/orcid+xml'
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def issue_callbacks(search_results)
|
44
|
+
if search_results.any?
|
45
|
+
callback(:found, search_results)
|
46
|
+
else
|
47
|
+
callback(:not_found)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :host, :access_token
|
52
|
+
def deliver(parameters)
|
53
|
+
token.get(path, headers: headers, params: parameters)
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse(document)
|
57
|
+
json = JSON.parse(document)
|
58
|
+
|
59
|
+
json.fetch('orcid-search-results').fetch('orcid-search-result')
|
60
|
+
.each_with_object([]) do |result, returning_value|
|
61
|
+
profile = result.fetch('orcid-profile')
|
62
|
+
identifier = profile.fetch('orcid-identifier').fetch('path')
|
63
|
+
orcid_bio = profile.fetch('orcid-bio')
|
64
|
+
given_names = orcid_bio.fetch('personal-details').fetch('given-names').fetch('value')
|
65
|
+
family_name = orcid_bio.fetch('personal-details').fetch('family-name').fetch('value')
|
66
|
+
emails = []
|
67
|
+
contact_details = orcid_bio['contact-details']
|
68
|
+
if contact_details
|
69
|
+
emails = (contact_details['email'] || []).map {|email| email.fetch('value') }
|
70
|
+
end
|
71
|
+
label = "#{given_names} #{family_name}"
|
72
|
+
label << ' (' << emails.join(', ') << ')' if emails.any?
|
73
|
+
label << " [ORCID: #{identifier}]"
|
74
|
+
biography = ''
|
75
|
+
biography = orcid_bio['biography']['value'] if orcid_bio['biography']
|
76
|
+
returning_value << response_builder.new('id' => identifier, 'label' => label, 'biography' => biography)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|