• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / pdc_describe / 7b9a4d12-cc2d-49d9-aa32-045f102a18fb

pending completion
7b9a4d12-cc2d-49d9-aa32-045f102a18fb

Pull #654

circleci

Hector Correa
Added tests to validate new properties
Pull Request #654: Funder information

18 of 18 new or added lines in 3 files covered. (100.0%)

1719 of 1737 relevant lines covered (98.96%)

126.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.65
/app/models/pdc_serialization/datacite.rb
1
# frozen_string_literal: true
2
module PDCSerialization
1✔
3
  # The common use for this class is:
4
  #
5
  #   work = Work.find(123)
6
  #   datacite_xml = PDCSerialization::Datacite.new_from_work(work).to_xml
7
  #
8
  # For testing purposes you can also quickly get an XML serialization with the helper:
9
  #
10
  #   datacite_xml = PDCSerialization::Datacite.skeleton_datacite_xml(...)
11
  #
12
  # You can also pass a PDCMetadata::Resource which is useful to test with a more
13
  # complex dataset without saving the work to the database:
14
  #
15
  #   json = {...}.to_json
16
  #   resource = PDCMetadata::Resource.new_from_json(json)
17
  #   datacite_xml = PDCSerialization::Datacite.new_from_work_resource(resource).to_xml
18
  #
19
  # For information
20
  #   Datacite schema: https://support.datacite.org/docs/datacite-metadata-schema-v44-properties-overview
21
  #   Datacite mapping gem: https://github.com/CDLUC3/datacite-mapping
22
  #
23
  # rubocop:disable Metrics/ClassLength
24
  class Datacite
1✔
25
    attr_reader :mapping, :errors
1✔
26

27
    def initialize(mapping)
1✔
28
      @mapping = mapping
43✔
29
      @errors = []
43✔
30
    end
31

32
    # Returns the XML serialization for the Datacite record
33
    # Note that the actual XML serialization is done by the datacite-mapping gem.
34
    def to_xml
1✔
35
      @mapping.write_xml
34✔
36
    end
37

38
    # Validate this DataCite XML serialization against the official DataCite schema
39
    # By default we validate against DataCite 4.4. This will shift over time as new
40
    # versions of the datacite schema are released.
41
    # @return [Boolean]
42
    def valid?
1✔
43
      @errors = []
8✔
44
      datacite_xml = Nokogiri::XML(to_xml)
8✔
45
      schema_location = Rails.root.join("config", "schema")
8✔
46
      Dir.chdir(schema_location) do
8✔
47
        xsd = Nokogiri::XML::Schema(File.read("datacite_4_4.xsd"))
8✔
48
        xsd.validate(datacite_xml).each do |error|
8✔
49
          @errors << error
×
50
        end
51
      end
52
      return true if @errors.empty?
8✔
53
      false
×
54
    end
55

56
    ##
57
    # Returns the XML serialization for a valid Datacite skeleton record based on a few required values.
58
    # Useful for early in the workflow when we don't have much data yet and for testing.
59
    #
60
    # @param [String] identifier, e.g "10.1234/567"
61
    # @param [String] title
62
    # @param [String] creator
63
    # @param [String] publisher
64
    # @param [String] publication_year
65
    # @param [String] resource_type
66
    def self.skeleton_datacite_xml(identifier:, title:, creator:, publisher:, publication_year:, resource_type:)
1✔
67
      mapping = ::Datacite::Mapping::Resource.new(
1✔
68
        identifier: ::Datacite::Mapping::Identifier.new(value: identifier),
69
        creators: [] << ::Datacite::Mapping::Creator.new(name: creator),
70
        titles: [] << ::Datacite::Mapping::Title.new(value: title),
71
        publisher: ::Datacite::Mapping::Publisher.new(value: publisher),
72
        publication_year: publication_year,
73
        resource_type: datacite_resource_type(resource_type)
74
      )
75
      mapping.write_xml
1✔
76
    end
77

78
    ##
79
    # Creates a PDCSerialization::Datacite object from a Work
80
    def self.new_from_work(work)
1✔
81
      new_from_work_resource(work.resource)
9✔
82
    end
83

84
    ##
85
    # Creates a PDCSerialization::Datacite object from a PDCMetadata::Resource
86
    # rubocop: disable Metrics/MethodLength
87
    def self.new_from_work_resource(resource)
1✔
88
      mapping = ::Datacite::Mapping::Resource.new(
43✔
89
        identifier: ::Datacite::Mapping::Identifier.new(value: resource.doi),
90
        creators: creators_from_work_resource(resource.creators),
91
        contributors: contributors_from_work_resource(resource.contributors),
92
        descriptions: descriptions_from_work_resource(resource.description),
93
        titles: titles_from_work_resource(resource.titles),
94
        publisher: ::Datacite::Mapping::Publisher.new(value: resource.publisher),
95
        publication_year: resource.publication_year,
96
        resource_type: datacite_resource_type(resource.resource_type),
97
        related_identifiers: related_identifiers_from_work_resource(resource),
98
        rights_list: rights_from_work_resource(resource),
99
        version: resource.version_number,
100
        funding_references: funding_reference_from_work_resource(resource)
101
      )
102
      Datacite.new(mapping)
43✔
103
    end
104
    # rubocop: enable Metrics/MethodLength
105

106
    class << self
1✔
107
      ##
108
      # rubocop:disable Metrics/MethodLength
109
      # rubocop:disable Metrics/CyclomaticComplexity
110
      # Returns the appropriate Datacite::Resource::ResourceType for a given string
111
      # @param [String] resource_type
112
      def datacite_resource_type(resource_type)
1✔
113
        resource_type_general = case resource_type.downcase
59✔
114
                                when "dataset"
115
                                  ::Datacite::Mapping::ResourceTypeGeneral::DATASET
45✔
116
                                when "audiovisual"
117
                                  ::Datacite::Mapping::ResourceTypeGeneral::AUDIOVISUAL
1✔
118
                                when "collection"
119
                                  ::Datacite::Mapping::ResourceTypeGeneral::COLLECTION
1✔
120
                                when "datapaper"
121
                                  ::Datacite::Mapping::ResourceTypeGeneral::DATA_PAPER
1✔
122
                                when "event"
123
                                  ::Datacite::Mapping::ResourceTypeGeneral::EVENT
1✔
124
                                when "image"
125
                                  ::Datacite::Mapping::ResourceTypeGeneral::IMAGE
1✔
126
                                when "interactiveresource"
127
                                  ::Datacite::Mapping::ResourceTypeGeneral::INTERACTIVE_RESOURCE
1✔
128
                                when "model"
129
                                  ::Datacite::Mapping::ResourceTypeGeneral::MODEL
1✔
130
                                when "physicalobject"
131
                                  ::Datacite::Mapping::ResourceTypeGeneral::PHYSICAL_OBJECT
1✔
132
                                when "service"
133
                                  ::Datacite::Mapping::ResourceTypeGeneral::SERVICE
1✔
134
                                when "software"
135
                                  ::Datacite::Mapping::ResourceTypeGeneral::SOFTWARE
1✔
136
                                when "sound"
137
                                  ::Datacite::Mapping::ResourceTypeGeneral::SOUND
1✔
138
                                when "text"
139
                                  ::Datacite::Mapping::ResourceTypeGeneral::TEXT
1✔
140
                                when "workflow"
141
                                  ::Datacite::Mapping::ResourceTypeGeneral::WORKFLOW
1✔
142
                                else
143
                                  ::Datacite::Mapping::ResourceTypeGeneral::OTHER
1✔
144
                                end
145
        ::Datacite::Mapping::ResourceType.new(resource_type_general: resource_type_general)
59✔
146
      end
147
      # rubocop:enable Metrics/MethodLength
148
      # rubocop:enable Metrics/CyclomaticComplexity
149

150
      ##
151
      # rubocop:disable Metrics/MethodLength
152
      # rubocop:disable Metrics/CyclomaticComplexity
153
      # Returns the appropriate Datacite::Resource::ContributorType for a given string
154
      # @param [String] type
155
      def datacite_contributor_type(type)
1✔
156
        case type
22✔
157
        when "DataCollector"
158
          ::Datacite::Mapping::ContributorType::DATA_COLLECTOR
1✔
159
        when "DataCurator"
160
          ::Datacite::Mapping::ContributorType::DATA_CURATOR
1✔
161
        when "DataManager"
162
          ::Datacite::Mapping::ContributorType::DATA_MANAGER
1✔
163
        when "Distributor"
164
          ::Datacite::Mapping::ContributorType::DISTRIBUTOR
1✔
165
        when "Editor"
166
          ::Datacite::Mapping::ContributorType::EDITOR
1✔
167
        when "HostingInstitution"
168
          ::Datacite::Mapping::ContributorType::HOSTING_INSTITUTION
1✔
169
        when "Producer"
170
          ::Datacite::Mapping::ContributorType::PRODUCER
1✔
171
        when "ProjectLeader"
172
          ::Datacite::Mapping::ContributorType::PROJECT_LEADER
2✔
173
        when "ProjectManager"
174
          ::Datacite::Mapping::ContributorType::PROJECT_MANAGER
1✔
175
        when "ProjectMember"
176
          ::Datacite::Mapping::ContributorType::PROJECT_MEMBER
1✔
177
        when "RegistrationAgency"
178
          ::Datacite::Mapping::ContributorType::REGISTRATION_AGENCY
1✔
179
        when "RegistrationAuthority"
180
          ::Datacite::Mapping::ContributorType::REGISTRATION_AUTHORITY
1✔
181
        when "RelatedPerson"
182
          ::Datacite::Mapping::ContributorType::RELATED_PERSON
1✔
183
        when "Researcher"
184
          ::Datacite::Mapping::ContributorType::RESEARCHER
1✔
185
        when "ResearchGroup"
186
          ::Datacite::Mapping::ContributorType::RESEARCH_GROUP
1✔
187
        when "RightsHolder"
188
          ::Datacite::Mapping::ContributorType::RIGHTS_HOLDER
1✔
189
        when "Sponsor"
190
          ::Datacite::Mapping::ContributorType::SPONSOR
1✔
191
        when "Supervisor"
192
          ::Datacite::Mapping::ContributorType::SUPERVISOR
1✔
193
        when "WorkPackageLeader"
194
          ::Datacite::Mapping::ContributorType::WORK_PACKAGE_LEADER
1✔
195
        else
196
          ::Datacite::Mapping::ContributorType::OTHER
2✔
197
        end
198
      end
199
      # rubocop:enable Metrics/MethodLength
200
      # rubocop:enable Metrics/CyclomaticComplexity
201

202
      private
1✔
203

204
        def creators_from_work_resource(creators)
1✔
205
          creators.sort_by(&:sequence).map do |creator|
43✔
206
            ::Datacite::Mapping::Creator.new(
114✔
207
              name: creator.value,
208
              given_name: creator.given_name,
209
              family_name: creator.family_name,
210
              identifier: name_identifier_from_identifier(creator.identifier),
211
              affiliations: nil
212
            )
213
          end
214
        end
215

216
        def contributors_from_work_resource(contributors)
1✔
217
          contributors.sort_by(&:sequence).map do |contributor|
43✔
218
            ::Datacite::Mapping::Contributor.new(
2✔
219
              name: contributor.value,
220
              identifier: name_identifier_from_identifier(contributor.identifier),
221
              affiliations: nil,
222
              type: datacite_contributor_type(contributor.type)
223
            )
224
          end
225
        end
226

227
        ##
228
        # We are deliberately not differentiating between "abstract", "methods" and other kinds of descriptions.
229
        # We are instead putting all description into the same field and classifying it as "OTHER".
230
        def descriptions_from_work_resource(description)
1✔
231
          return [] if description.blank?
43✔
232
          [] << ::Datacite::Mapping::Description.new(type: ::Datacite::Mapping::DescriptionType::OTHER, value: description)
43✔
233
        end
234

235
        def name_identifier_from_identifier(identifier)
1✔
236
          return nil if identifier.nil?
116✔
237
          ::Datacite::Mapping::NameIdentifier.new(
1✔
238
            scheme: identifier.scheme,
239
            scheme_uri: identifier.scheme_uri,
240
            value: identifier.value
241
          )
242
        end
243

244
        def titles_from_work_resource(titles)
1✔
245
          titles.map do |title|
43✔
246
            if title.main?
46✔
247
              ::Datacite::Mapping::Title.new(value: title.title)
42✔
248
            elsif title.title_type == "Subtitle"
4✔
249
              ::Datacite::Mapping::Title.new(value: title.title, type: ::Datacite::Mapping::TitleType::SUBTITLE)
1✔
250
            elsif title.title_type == "AlternativeTitle"
3✔
251
              ::Datacite::Mapping::Title.new(value: title.title, type: ::Datacite::Mapping::TitleType::ALTERNATIVE_TITLE)
2✔
252
            elsif title.title_type == "TranslatedTitle"
1✔
253
              ::Datacite::Mapping::Title.new(value: title.title, type: ::Datacite::Mapping::TitleType::TRANSLATED_TITLE)
1✔
254
            end
255
          end.compact
256
        end
257

258
        ##
259
        # Add related identifiers from various locations in the metadata.
260
        # @param [PDCMetadata::Resource] resource
261
        # @return [<::Datacite::Mapping::RelatedIdentifier>]
262
        def related_identifiers_from_work_resource(resource)
1✔
263
          related_identifiers = []
43✔
264
          related_identifiers = related_identifiers.union(extract_ark_as_related_identfier(resource))
43✔
265
          related_identifiers = related_identifiers.union(extract_related_objects(resource))
43✔
266
          related_identifiers
43✔
267
        end
268

269
        def extract_ark_as_related_identfier(resource)
1✔
270
          related_ids = []
43✔
271
          if resource.ark.present?
43✔
272
            related_ids << ::Datacite::Mapping::RelatedIdentifier.new(
14✔
273
              relation_type: ::Datacite::Mapping::RelationType::IS_IDENTICAL_TO,
274
              value: resource.ark,
275
              identifier_type: ::Datacite::Mapping::RelatedIdentifierType::ARK
276
            )
277
          end
278
          related_ids
43✔
279
        end
280

281
        def extract_related_objects(resource)
1✔
282
          related_objects = []
43✔
283
          resource.related_objects.each do |ro|
43✔
284
            related_objects << ::Datacite::Mapping::RelatedIdentifier.new(
4✔
285
              relation_type: ::Datacite::Mapping::RelationType.find_by_key(ro.relation_type.to_sym),
286
              value: ro.related_identifier,
287
              identifier_type: ::Datacite::Mapping::RelatedIdentifierType.find_by_key(ro.related_identifier_type.to_sym)
288
            )
289
          end
290
          related_objects
43✔
291
        end
292

293
        def rights_from_work_resource(resource)
1✔
294
          rights = []
43✔
295
          if resource.rights.present?
43✔
296
            rights << ::Datacite::Mapping::Rights.new(
33✔
297
              value: resource.rights.name,
298
              uri: resource.rights.uri,
299
              identifier: resource.rights.identifier
300
            )
301
          end
302
          rights
43✔
303
        end
304

305
        def funding_reference_from_work_resource(resource)
1✔
306
          return nil unless resource.award_number.present? && resource.funder_name.present?
43✔
307
          award = ::Datacite::Mapping::AwardNumber.new(uri: resource.award_uri, value: resource.award_number)
×
308
          funding_reference = ::Datacite::Mapping::FundingReference.new(name: resource.funder_name, award_number: award)
×
309
          [funding_reference]
×
310
        end
311
      end
312
  end
313
  # rubocop:enable Metrics/ClassLength
314
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc