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
@@ -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