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.
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
@@ -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 things
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) { method(:xml_payload) }
29
- profile_creation_service = options.fetch(:profile_creation_service) { default_profile_creation_service }
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 ||= Orcid::Remote::ProfileCreationService.new do |on|
35
- on.success {|orcid_profile_id| handle_profile_creation_response(orcid_profile_id) }
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
- if context.orcid_profile_id?
42
- context.errors.add(:base, "#{context.class} ID=#{context.to_param} already has an assigned :orcid_profile_id #{context.orcid_profile_id.inspect}")
43
- return false
44
- end
45
-
46
- if user_orcid_profile = Orcid.profile_for(context.user)
47
- context.errors.add(:base, "#{context.class} ID=#{context.to_param}'s associated user #{context.user.to_param} already has an assigned :orcid_profile_id #{user_orcid_profile.to_param}")
48
- return false
49
- end
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
- true
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 -> http://support.orcid.org/knowledgebase/articles/177522-create-an-id-technical-developer
55
- # NOTE: This one was true at 2014-02-06:14:55 -> http://support.orcid.org/knowledgebase/articles/162412-tutorial-create-a-new-record-using-curl
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
@@ -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
- "artistic-performance","book-chapter","book-review","book","conference-abstract","conference-paper","conference-poster","data-set","dictionary-entry","disclosure","dissertation","edited-book","encyclopedia-entry","invention","journal-article","journal-issue","lecture-speech","license","magazine-article","manual","newsletter-article","newspaper-article","online-resource","other","patent","registered-copyright","report","research-technique","research-tool","spin-off-company","standards-and-policy","supervised-student-publication","technical-standard","test","translation","trademark","website","working-paper",
7
- ].freeze
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 ==(comparison_object)
26
- super || comparison_object.instance_of?(self.class) &&
63
+ def ==(other)
64
+ super ||
65
+ other.instance_of?(self.class) &&
27
66
  id.present? &&
28
- comparison_object.id == id
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::Remote
3
- # Responsible for minting a new ORCID for the given payload.
4
- class ProfileCreationService < Orcid::Remote::Service
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
- def self.call(payload, config = {}, &callback_config)
7
- new(config, &callback_config).call(payload)
8
- end
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
- attr_reader :token, :path, :headers
11
- def initialize(config = {}, &callback_config)
12
- super(&callback_config)
13
- @token = config.fetch(:token) { Orcid.client_credentials_token('/orcid-profile/create') }
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
- def call(payload)
19
- response = deliver(payload)
20
- parse(response)
21
- end
25
+ protected
22
26
 
23
- protected
24
- def deliver(body)
25
- token.post(path, body: body, headers: headers)
26
- end
27
+ def deliver(body)
28
+ token.post(path, body: body, headers: headers)
29
+ end
27
30
 
28
- def parse(response)
29
- uri = URI.parse(response.headers.fetch(:location))
30
- if orcid_profile_id = uri.path.sub(/\A\//, "").split("/").first
31
- callback(:success, orcid_profile_id)
32
- orcid_profile_id
33
- else
34
- callback(:failure)
35
- false
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
- def default_headers
40
- { "Accept" => 'application/xml', 'Content-Type'=>'application/vdn.orcid+xml' }
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