orcid 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|