theoj 0.0.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86f78e023b1c26cfcd13eea306fcb1ee89a862a300e43e173437655fddcc53b9
4
- data.tar.gz: 43c48ae2941a5f72076f048a58315f861aedddd1708a5886b437b1858739553d
3
+ metadata.gz: 984f9feb0bdcfeaf7d9533aa94c09dde7877ca2381e75e60b0f05d73cb390eb7
4
+ data.tar.gz: 749c5b08e99824f720c5ed4e820d45b7d2b772c345f820d4332393376b215512
5
5
  SHA512:
6
- metadata.gz: 1cba25b3701a0f52f40317e74df287db75a4f79944183a13bd1ea23567e51b010cdaa2f6d707aaedaa6e5c2ac30119c1dd7eab3f83e0d107a4d048d247e9ee80
7
- data.tar.gz: 592c523fb973d4f7c57044b451d17038b5a891aa0d534f4081180c4409f5da81ccebd31686c4f98151fbdeea465f61ea2e58df08aa4b1c2770d3af13a71d72d1
6
+ metadata.gz: 5fa3741bb127abcfb1c590c197fa8ea9631b510624b25b615ccfc60131cdf7ce68af2d400ab16f339cc6705a5aae1129403ac0c1d24e41e04209704bdfbcb076
7
+ data.tar.gz: 246621ddee2c0495eccad79723727205b724ec36b4b401b24ae60f22c1e1cb7a8ece320ab2ccefb6a5074e4423a4eabcbf401272d40e137b0eb831bf0dafd662
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0 (2021-10-20)
4
+
5
+ - Added method to Journal to create paper_id from issue_id
6
+ - Added method to Journal to get a DOI based on a paper id
7
+ - Added languages to Paper
8
+ - Added authors info to Paper
9
+ - Author object
10
+ - Added ORCID validation
11
+ - Added Submission object, grouping a paper, a review issue and a journal
12
+ - Added paper depositing
13
+
3
14
  ## 0.0.3 (2021-10-08)
4
15
 
5
16
  - Added metadata methods to Paper
data/lib/theoj/author.rb CHANGED
@@ -1,16 +1,94 @@
1
+ require "nameable"
2
+
1
3
  module Theoj
2
4
  class Author
3
5
  attr_accessor :name
4
6
  attr_accessor :orcid
5
7
  attr_accessor :affiliation
6
8
 
7
- def initialize(name, orcid, affiliation)
8
- @name = name
9
- @orcid = orcid
10
- @affiliation = affiliation
9
+ AUTHOR_FOOTNOTE_REGEX = /^[^\^]*/
10
+
11
+ # Initialized with authors & affiliations block in the YAML header from an Open Journal paper
12
+ # e.g. https://joss.readthedocs.io/en/latest/submitting.html#example-paper-and-bibliography
13
+ def initialize(name, orcid, index, affiliations_hash)
14
+ parse_name name
15
+ @orcid = validate_orcid orcid
16
+ @affiliation = build_affiliation_string(index, affiliations_hash)
17
+ end
18
+
19
+ def given_name
20
+ @parsed_name.first
21
+ end
22
+
23
+ def middle_name
24
+ @parsed_name.middle
25
+ end
26
+
27
+ def last_name
28
+ @parsed_name.last
29
+ end
30
+
31
+ def initials
32
+ [@parsed_name.first, @parsed_name.middle].compact.map {|v| v[0] + "."} * ' '
33
+ end
34
+
35
+ def to_h
36
+ {
37
+ given_name: given_name,
38
+ middle_name: middle_name,
39
+ last_name: last_name,
40
+ orcid: orcid,
41
+ affiliation: affiliation
42
+ }
43
+ end
44
+
45
+ private
46
+
47
+ def parse_name(author_name)
48
+ @parsed_name = Nameable::Latin.new.parse(strip_footnotes(author_name))
49
+ @name = @parsed_name.to_nameable
50
+ end
51
+
52
+ # Input: Arfon Smith^[Corresponding author: arfon@example.com]
53
+ # Output: Arfon Smith
54
+ def strip_footnotes(author_name)
55
+ author_name.to_s[AUTHOR_FOOTNOTE_REGEX]
56
+ end
57
+
58
+ def validate_orcid(author_orcid)
59
+ return nil if author_orcid.to_s.strip.empty?
60
+
61
+ validator = Theoj::Orcid.new(author_orcid)
62
+ if validator.valid?
63
+ return author_orcid.strip
64
+ else
65
+ raise "Problem with ORCID (#{author_orcid}) for #{self.name}. #{validator.error}"
66
+ end
11
67
  end
12
68
 
13
- def validate_orcid
69
+ # Takes the author affiliation index and a hash of all affiliations and
70
+ # associates them. Then builds the author affiliation string
71
+ def build_affiliation_string(index, affiliations_hash)
72
+ return nil if index.nil? # Some authors don't have an affiliation
73
+
74
+ # If multiple affiliations, parse each one and build the affiliation string
75
+ author_affiliations = []
76
+
77
+ # Turn YAML keys into strings so that mixed integer and string affiliations work
78
+ affiliations_hash.transform_keys!(&:to_s)
79
+
80
+ affiliations = index.to_s.split(',').map(&:strip)
81
+
82
+ # Raise if we can't parse the string, might be because of this bug :-(
83
+ # https://bugs.ruby-lang.org/issues/12451
84
+ affiliations.each do |a|
85
+ raise "Problem with affiliations for #{self.name}, perhaps the " +
86
+ "affiliations index need quoting?" unless affiliations_hash.has_key?(a)
87
+
88
+ author_affiliations << affiliations_hash[a].strip
89
+ end
90
+
91
+ author_affiliations.join(', ')
14
92
  end
15
93
 
16
94
  end
data/lib/theoj/git.rb CHANGED
@@ -1,5 +1,5 @@
1
- require 'open3'
2
- require 'fileutils'
1
+ require "open3"
2
+ require "fileutils"
3
3
 
4
4
  module Theoj
5
5
  module Git
data/lib/theoj/journal.rb CHANGED
@@ -25,6 +25,15 @@ module Theoj
25
25
  data[:current_issue] || (1 + ((Time.new.year * 12 + Time.new.month) - (launch_year * 12 + launch_month)))
26
26
  end
27
27
 
28
+ def paper_id_from_issue(review_issue_id)
29
+ id = "%05d" % review_issue_id
30
+ "#{@alias}.#{id}"
31
+ end
32
+
33
+ def paper_doi_for_id(paper_id)
34
+ "#@doi_prefix/#{paper_id}"
35
+ end
36
+
28
37
  private
29
38
 
30
39
  def set_data(custom_data)
@@ -6,8 +6,9 @@ module Theoj
6
6
  name: "Journal of Open Source Software",
7
7
  alias: "joss",
8
8
  launch_date: "2016-05-05",
9
- paper_repository: "openjournals/joss-papers",
10
- reviews_repository: "openjournals/joss-reviews"
9
+ papers_repository: "openjournals/joss-papers",
10
+ reviews_repository: "openjournals/joss-reviews",
11
+ deposit_url: "https://joss.theoj.org/papers/api_deposit"
11
12
  },
12
13
  jose: {
13
14
  doi_prefix: "10.21105",
@@ -15,8 +16,9 @@ module Theoj
15
16
  name: "Journal of Open Source Education",
16
17
  alias: "jose",
17
18
  launch_date: "2018-03-07",
18
- paper_repository: "openjournals/jose-papers",
19
- reviews_repository: "openjournals/jose-reviews"
19
+ papers_repository: "openjournals/jose-papers",
20
+ reviews_repository: "openjournals/jose-reviews",
21
+ deposit_url: "https://joss.theoj.org/papers/api_deposit"
20
22
  }
21
23
  }
22
24
  end
@@ -0,0 +1,96 @@
1
+ module Theoj
2
+ class Orcid
3
+ attr_reader :orcid, :error
4
+
5
+ def initialize(orcid)
6
+ @orcid = orcid.strip
7
+ @error = nil
8
+ end
9
+
10
+ def valid?
11
+ @error = nil
12
+ return false unless check_structure
13
+ return false unless check_length
14
+ return false unless check_chars
15
+
16
+ return false unless correct_checksum?
17
+
18
+ true
19
+ end
20
+
21
+ def packed_orcid
22
+ orcid.gsub('-', '')
23
+ end
24
+
25
+ private
26
+
27
+ # Returns the last character of the string
28
+ def checksum_char
29
+ packed_orcid[-1]
30
+ end
31
+
32
+ def first_11
33
+ packed_orcid.chop
34
+ end
35
+
36
+ def check_structure
37
+ groups = orcid.split('-')
38
+ if groups.size == 4
39
+ return true
40
+ else
41
+ @error = "ORCID looks malformed"
42
+ return false
43
+ end
44
+ end
45
+
46
+ def check_length
47
+ if packed_orcid.length == 16
48
+ return true
49
+ else
50
+ @error = "ORCID looks to be the wrong length"
51
+ return false
52
+ end
53
+ end
54
+
55
+ def check_chars
56
+ valid = true
57
+ first_11.each_char do |c|
58
+ if !numeric?(c)
59
+ @error = "Invalid ORCID digit (#{c})"
60
+ valid = false
61
+ end
62
+ end
63
+
64
+ return valid
65
+ end
66
+
67
+ def correct_checksum?
68
+ validate_against = checksum_char.to_i
69
+ validate_against = 10 if (checksum_char == "X" || checksum_char == "x")
70
+
71
+ if checksum == validate_against
72
+ return true
73
+ else
74
+ @error = "Invalid ORCID"
75
+ return false
76
+ end
77
+ end
78
+
79
+ # https://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
80
+ def checksum
81
+ total = 0
82
+ first_11.each_char do |c|
83
+ total = (total + c.to_i) * 2
84
+ end
85
+
86
+ remainder = total % 11
87
+ result = (12 - remainder) % 11
88
+ end
89
+
90
+
91
+ def numeric?(s)
92
+ Float(s) != nil rescue false
93
+ end
94
+
95
+ end
96
+ end
data/lib/theoj/paper.rb CHANGED
@@ -1,5 +1,7 @@
1
- require 'find'
2
- require 'yaml'
1
+ require "find"
2
+ require "yaml"
3
+ require "rugged"
4
+ require "linguist"
3
5
 
4
6
  module Theoj
5
7
  class Paper
@@ -19,22 +21,18 @@ module Theoj
19
21
  end
20
22
 
21
23
  def authors
22
- []
24
+ @authors ||= parse_authors
23
25
  end
24
26
 
25
- def self.find_paper_path(search_path)
26
- paper_path = nil
27
+ def citation_author
28
+ surname = authors.first.last_name
29
+ initials = authors.first.initials
27
30
 
28
- if Dir.exist? search_path
29
- Find.find(search_path).each do |path|
30
- if path =~ /paper\.tex$|paper\.md$/
31
- paper_path = path
32
- break
33
- end
34
- end
31
+ if authors.size > 1
32
+ return "#{surname} et al."
33
+ else
34
+ return "#{surname}, #{initials}"
35
35
  end
36
-
37
- paper_path
38
36
  end
39
37
 
40
38
  def title
@@ -49,6 +47,10 @@ module Theoj
49
47
  @paper_metadata["date"]
50
48
  end
51
49
 
50
+ def languages
51
+ @languages ||= detect_languages
52
+ end
53
+
52
54
  def bibliography_path
53
55
  @paper_metadata["bibliography"]
54
56
  end
@@ -61,6 +63,21 @@ module Theoj
61
63
  FileUtils.rm_rf(local_path) if Dir.exist?(local_path)
62
64
  end
63
65
 
66
+ def self.find_paper_path(search_path)
67
+ paper_path = nil
68
+
69
+ if Dir.exist? search_path
70
+ Find.find(search_path).each do |path|
71
+ if path =~ /paper\.tex$|paper\.md$/
72
+ paper_path = path
73
+ break
74
+ end
75
+ end
76
+ end
77
+
78
+ paper_path
79
+ end
80
+
64
81
  def self.from_repo(repository_url, branch = "")
65
82
  Paper.new(repository_url, branch, nil)
66
83
  end
@@ -95,6 +112,44 @@ module Theoj
95
112
  end
96
113
  end
97
114
 
115
+ def parse_authors
116
+ parsed_authors = []
117
+ authors_metadata = @paper_metadata['authors']
118
+ affiliations_metadata = parse_affiliations(@paper_metadata['affiliations'])
119
+
120
+ # Loop through the authors block and build up the affiliation
121
+ authors_metadata.each do |author|
122
+ affiliation_index = author['affiliation']
123
+ failure "Author (#{author['name']}) is missing affiliation" if affiliation_index.nil?
124
+ begin
125
+ parsed_author = Author.new(author['name'], author['orcid'], affiliation_index, affiliations_metadata)
126
+ rescue Exception => e
127
+ failure(e.message)
128
+ end
129
+ parsed_authors << parsed_author
130
+ end
131
+
132
+ parsed_authors
133
+ end
134
+
135
+ def parse_affiliations(affliations_yaml)
136
+ affliations_metadata = {}
137
+
138
+ affliations_yaml.each do |affiliation|
139
+ affliations_metadata[affiliation['index']] = affiliation['name']
140
+ end
141
+
142
+ affliations_metadata
143
+ end
144
+
145
+ def detect_languages
146
+ repo = Rugged::Repository.discover(paper_path)
147
+ project = Linguist::Repository.new(repo, repo.head.target_id)
148
+
149
+ # Take top five languages from Linguist
150
+ project.languages.keys.take(5)
151
+ end
152
+
98
153
  def failure(msg)
99
154
  cleanup
100
155
  raise(msg)
@@ -0,0 +1,70 @@
1
+ require "json"
2
+ require "base64"
3
+ require "faraday"
4
+
5
+ module Theoj
6
+ class Submission
7
+ attr_accessor :journal
8
+ attr_accessor :review_issue
9
+ attr_accessor :paper
10
+
11
+ def initialize(journal, review_issue, paper=nil)
12
+ @journal = journal
13
+ @review_issue = review_issue
14
+ @paper = paper || @review_issue.paper
15
+ end
16
+
17
+ # Create the payload to use to post for depositing with Open Journals
18
+ def deposit_payload
19
+ {
20
+ id: review_issue.issue_id,
21
+ metadata: Base64.encode64(metadata_payload),
22
+ doi: paper_doi,
23
+ archive_doi: review_issue.archive,
24
+ citation_string: citation_string,
25
+ title: paper.title
26
+ }
27
+ end
28
+
29
+ # Create a metadata json payload
30
+ def metadata_payload
31
+ metadata = {
32
+ paper: {
33
+ title: paper.title,
34
+ tags: paper.tags,
35
+ languages: paper.languages,
36
+ authors: paper.authors.collect { |a| a.to_h },
37
+ doi: paper_doi,
38
+ archive_doi: review_issue.archive,
39
+ repository_address: review_issue.target_repository,
40
+ editor: review_issue.editor,
41
+ reviewers: review_issue.reviewers.collect(&:strip),
42
+ volume: journal.current_volume,
43
+ issue: journal.current_issue,
44
+ year: journal.current_year,
45
+ page: review_issue.issue_id,
46
+ }
47
+ }
48
+
49
+ metadata.to_json
50
+ end
51
+
52
+ def deposit!(secret)
53
+ parameters = deposit_payload.merge(secret: secret)
54
+ Faraday.post(journal.data[:deposit_url], parameters.to_json, {"Content-Type" => "application/json"})
55
+ end
56
+
57
+ def citation_string
58
+ paper_year = Time.now.strftime('%Y')
59
+ "#{paper.citation_author}, (#{paper_year}). #{paper.title}. #{journal.name}, #{journal.current_volume}(#{journal.current_issue}), #{review_issue.issue_id}, https://doi.org/#{paper_doi}"
60
+ end
61
+
62
+ def paper_id
63
+ journal.paper_id_from_issue(review_issue.issue_id)
64
+ end
65
+
66
+ def paper_doi
67
+ journal.paper_doi_for_id(paper_id)
68
+ end
69
+ end
70
+ end
data/lib/theoj/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Theoj
2
- VERSION = "0.0.3"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/theoj.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require_relative "theoj/version"
2
2
  require_relative "theoj/git"
3
3
  require_relative "theoj/github"
4
+ require_relative "theoj/orcid"
5
+ require_relative "theoj/submission"
4
6
  require_relative "theoj/journal"
5
7
  require_relative "theoj/review_issue"
6
8
  require_relative "theoj/paper"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theoj
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juanjo Bazán
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-08 00:00:00.000000000 Z
11
+ date: 2021-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: octokit
@@ -24,6 +24,62 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.21'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: openjournals-nameable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: github-linguist
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rugged
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
27
83
  - !ruby/object:Gem::Dependency
28
84
  name: rake
29
85
  requirement: !ruby/object:Gem::Requirement
@@ -68,8 +124,10 @@ files:
68
124
  - lib/theoj/github.rb
69
125
  - lib/theoj/journal.rb
70
126
  - lib/theoj/journals_data.rb
127
+ - lib/theoj/orcid.rb
71
128
  - lib/theoj/paper.rb
72
129
  - lib/theoj/review_issue.rb
130
+ - lib/theoj/submission.rb
73
131
  - lib/theoj/version.rb
74
132
  homepage: http://github.com/xuanxu/theoj
75
133
  licenses: