roboneuro 0.1.1
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 +7 -0
- data/.DS_Store +0 -0
- data/.env-example +12 -0
- data/.env.test +8 -0
- data/.github/workflows/docker-image.yml +63 -0
- data/.github/workflows/tests.yml +28 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +135 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/Rakefile +13 -0
- data/bin/whedon +131 -0
- data/fixtures/latex_paper/paper.tex +0 -0
- data/fixtures/paper/10.21105.jcon.00017.crossref.xml +88 -0
- data/fixtures/paper/crossref-metadata.yaml +34 -0
- data/fixtures/paper/paper-bib.md +27 -0
- data/fixtures/paper/paper-single-author.md +24 -0
- data/fixtures/paper/paper-with-harder-names.md +27 -0
- data/fixtures/paper/paper-with-missing-affiliations.md +21 -0
- data/fixtures/paper/paper.bib +65 -0
- data/fixtures/paper/paper.html +14 -0
- data/fixtures/paper/paper.md +27 -0
- data/fixtures/paper/paper.pdf +0 -0
- data/fixtures/paper/paper.xml +28 -0
- data/fixtures/paper/paper_with_missing_title.md +26 -0
- data/fixtures/review_body.txt +48 -0
- data/fixtures/test_paper/nested/paper.bib +0 -0
- data/fixtures/test_paper/nested/paper.md +0 -0
- data/fixtures/test_paper/paper.bib +0 -0
- data/fixtures/test_paper/paper.md +0 -0
- data/fixtures/vcr_cassettes/joss-lookup.yml +58 -0
- data/fixtures/vcr_cassettes/review.yml +237 -0
- data/fixtures/vcr_cassettes/reviews.yml +270 -0
- data/lib/whedon/auditor.rb +39 -0
- data/lib/whedon/author.rb +81 -0
- data/lib/whedon/bibtex_parser.rb +103 -0
- data/lib/whedon/compilers.rb +379 -0
- data/lib/whedon/github.rb +12 -0
- data/lib/whedon/orcid_validator.rb +83 -0
- data/lib/whedon/processor.rb +178 -0
- data/lib/whedon/review.rb +25 -0
- data/lib/whedon/reviews.rb +29 -0
- data/lib/whedon/version.rb +3 -0
- data/lib/whedon.rb +387 -0
- data/paperdraft.Dockerfile +48 -0
- data/pkg/roboneuro-0.1.0.gem +0 -0
- data/resources/.DS_Store +0 -0
- data/resources/NeuroLibre/.DS_Store +0 -0
- data/resources/NeuroLibre/aas-logo.png +0 -0
- data/resources/NeuroLibre/apa.csl +1919 -0
- data/resources/NeuroLibre/defaults.yaml +14 -0
- data/resources/NeuroLibre/latex.template +541 -0
- data/resources/NeuroLibre/logo.png +0 -0
- data/resources/NeuroLibre/logo_link.png +0 -0
- data/resources/apa.csl +1919 -0
- data/resources/crossref.template +89 -0
- data/resources/docker-defaults.yaml +42 -0
- data/resources/docker-entrypoint.sh +37 -0
- data/resources/jats.csl +204 -0
- data/resources/jats.template +105 -0
- data/resources/jose/apa.csl +1919 -0
- data/resources/jose/defaults.yaml +14 -0
- data/resources/jose/latex.template +486 -0
- data/resources/jose/logo.png +0 -0
- data/resources/joss/aas-logo.png +0 -0
- data/resources/joss/apa.csl +1919 -0
- data/resources/joss/defaults.yaml +14 -0
- data/resources/joss/latex.template +525 -0
- data/resources/joss/logo.png +0 -0
- data/resources/latex.template +485 -0
- data/resources/time.lua +8 -0
- data/roboneuro.gemspec +39 -0
- data/spec/auditor_spec.rb +22 -0
- data/spec/author_spec.rb +17 -0
- data/spec/bibtex_spec.rb +31 -0
- data/spec/orcid_validator_spec.rb +33 -0
- data/spec/processor_spec.rb +66 -0
- data/spec/review_spec.rb +12 -0
- data/spec/reviews_spec.rb +18 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/whedon_spec.rb +93 -0
- metadata +386 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
module Whedon
|
2
|
+
class Author
|
3
|
+
require 'nameable'
|
4
|
+
|
5
|
+
attr_accessor :name, :affiliation, :orcid
|
6
|
+
|
7
|
+
AUTHOR_FOOTNOTE_REGEX = /^[^\^]*/
|
8
|
+
|
9
|
+
# Initialized with authors & affiliations block in the YAML header from
|
10
|
+
# a JOSS paper e.g. http://joss.theoj.org/about#paper_structure
|
11
|
+
#
|
12
|
+
def initialize(name, orcid, index, affiliations_yaml)
|
13
|
+
name = strip_footnotes(name)
|
14
|
+
@parsed_name = Nameable::Latin.new.parse(name)
|
15
|
+
@name = @parsed_name.to_nameable
|
16
|
+
@orcid = orcid
|
17
|
+
@affiliation = build_affiliation_string(index, affiliations_yaml)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
{
|
22
|
+
:given_name => given_name,
|
23
|
+
:middle_name => @parsed_name.middle,
|
24
|
+
:last_name => last_name,
|
25
|
+
:orcid => orcid,
|
26
|
+
:affiliation => affiliation.strip
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Input: Arfon Smith^[Corresponding author: arfon@example.com]
|
31
|
+
# Output: Arfon Smith
|
32
|
+
def strip_footnotes(name)
|
33
|
+
name[AUTHOR_FOOTNOTE_REGEX]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Use the Nameable gem to return last name
|
37
|
+
def last_name
|
38
|
+
@parsed_name.last
|
39
|
+
end
|
40
|
+
|
41
|
+
# Use the Nameable gem to return first name
|
42
|
+
def given_name
|
43
|
+
@parsed_name.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def initials
|
47
|
+
[@parsed_name.first, @parsed_name.middle].compact.map {|v| v[0]}.zip(['.', '.', '.']).map(&:join) * ' '
|
48
|
+
end
|
49
|
+
|
50
|
+
# Takes the author affiliation index and a hash of all affiliations and
|
51
|
+
# associates them. Then builds (and assigns) the author affiliation string.
|
52
|
+
def build_affiliation_string(index, affiliations_yaml)
|
53
|
+
# Some people have two affiliations, if this is the case then we need
|
54
|
+
# to parse each one and build the affiliation string.
|
55
|
+
author_affiliations = []
|
56
|
+
|
57
|
+
return nil if index.nil? # Some authors don't have an affiliation
|
58
|
+
|
59
|
+
affiliations = if index.to_s.include?(',')
|
60
|
+
index.split(',').map { |a| a.strip }
|
61
|
+
else
|
62
|
+
[ index.to_s ]
|
63
|
+
end
|
64
|
+
|
65
|
+
# We need to turn all of the affiliation YAML keys into strings
|
66
|
+
# So that mixed integer and string affiliations work
|
67
|
+
affiliations_yaml = affiliations_yaml.inject({}) {|hash, (key, val)| hash.merge(key.to_s => val) }
|
68
|
+
|
69
|
+
# Raise if we can't parse the string, might be because of this bug :-(
|
70
|
+
# https://bugs.ruby-lang.org/issues/12451
|
71
|
+
affiliations.each do |a|
|
72
|
+
raise "Problem with affiliations for #{self.name}, perhaps the \
|
73
|
+
affiliations index need quoting?" unless affiliations_yaml.has_key?(a)
|
74
|
+
|
75
|
+
author_affiliations << affiliations_yaml[a]
|
76
|
+
end
|
77
|
+
|
78
|
+
return author_affiliations.join(', ')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# Whedon::Bibtex is used to generate the citation string used in the Crossref
|
2
|
+
# metadata. It uses the bibtex RubyGem.
|
3
|
+
|
4
|
+
require 'bibtex'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
# => bib = Whedon::BibtexParser.new('paper.bib').generate_citations
|
9
|
+
module Whedon
|
10
|
+
class BibtexParser
|
11
|
+
# Initialize the Bibtex generator
|
12
|
+
# Takes a path to bibtex file
|
13
|
+
def initialize(bib_file)
|
14
|
+
@bib_file = bib_file
|
15
|
+
@ref_count = 1
|
16
|
+
@citation_string = ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def bibtex_keys
|
20
|
+
entries = BibTeX.open(@bib_file, :filter => :latex)
|
21
|
+
|
22
|
+
keys = []
|
23
|
+
entries.each do |entry|
|
24
|
+
next if entry.comment?
|
25
|
+
next if entry.preamble?
|
26
|
+
|
27
|
+
keys << "@#{entry.key}"
|
28
|
+
end
|
29
|
+
|
30
|
+
return keys
|
31
|
+
end
|
32
|
+
|
33
|
+
# Generates the <citations></citations> XML block for Crossref
|
34
|
+
# Returns an XML fragment <citations></citations> with or
|
35
|
+
# without citations within
|
36
|
+
# TODO: should probably use Ruby builder templates here
|
37
|
+
def generate_citations(citations=nil)
|
38
|
+
entries = BibTeX.open(@bib_file, :filter => :latex)
|
39
|
+
|
40
|
+
if entries.empty?
|
41
|
+
@citation_string = ""
|
42
|
+
else
|
43
|
+
entries.each do |entry|
|
44
|
+
next if entry.comment?
|
45
|
+
next if entry.preamble?
|
46
|
+
|
47
|
+
if citations
|
48
|
+
next unless citations.include?("@#{entry.key}")
|
49
|
+
end
|
50
|
+
@citation_string << make_citation(entry)
|
51
|
+
@ref_count += 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# This Nokogiri step is simply to pretty-print the XML of the citations
|
56
|
+
doc = Nokogiri::XML("<citation_list>#{@citation_string}</citation_list>")
|
57
|
+
return doc.root.to_xml
|
58
|
+
end
|
59
|
+
|
60
|
+
# Chooses what sort of citation to make based upon whether there is a DOI
|
61
|
+
# present in the bibtex entry
|
62
|
+
def make_citation(entry)
|
63
|
+
if entry.has_field?('doi') && !entry.doi.empty?
|
64
|
+
return doi_citation(entry)
|
65
|
+
else
|
66
|
+
return general_citation(entry)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a simple <citation> XML snippet with the DOI
|
71
|
+
def doi_citation(entry)
|
72
|
+
# Crossref DOIs need to be strings like 10.21105/joss.01461 rather
|
73
|
+
# than https://doi.org/10.21105/joss.01461
|
74
|
+
bare_doi = entry.doi.to_s[/\b(10[.][0-9]{4,}(?:[.][0-9]+)*\/(?:(?!["&\'<>])\S)+)\b/]
|
75
|
+
|
76
|
+
# Check for shortDOI formatted DOIs http://shortdoi.org
|
77
|
+
if bare_doi.nil?
|
78
|
+
bare_doi = entry.doi.to_s[/\b(10\/[a-bA-z0-9]+)\b/]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sometimes there are weird characters in the DOI. This escapes
|
82
|
+
escaped_doi = bare_doi.encode(:xml => :text)
|
83
|
+
"<citation key=\"ref#{@ref_count}\"><doi>#{escaped_doi}</doi></citation>"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a more complex <citation> XML snippet with keys for each of the
|
87
|
+
# bibtex fields
|
88
|
+
def general_citation(entry)
|
89
|
+
citation = "<citation key=\"ref#{@ref_count}\"><unstructured_citation>"
|
90
|
+
values = []
|
91
|
+
entry.each_pair do |name, value|
|
92
|
+
# Ultimately we should call entry.to_citeproce here to parse this properly.
|
93
|
+
# https://github.com/inukshuk/bibtex-ruby/pull/139/files
|
94
|
+
value = value.gsub('\urlhttp', 'http')
|
95
|
+
values << value.encode(:xml => :text)
|
96
|
+
end
|
97
|
+
citation << values.join(', ')
|
98
|
+
citation << "</unstructured_citation></citation>"
|
99
|
+
|
100
|
+
return citation
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,379 @@
|
|
1
|
+
# This module has methods to compile PDFs and Crossref XML depending upon
|
2
|
+
# the content type of the paper (Markdown or LaTeX)
|
3
|
+
module Compilers
|
4
|
+
require 'date'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
# Generate the paper PDF
|
8
|
+
# Optionally pass in a custom branch name as first param
|
9
|
+
def generate_pdf(custom_branch=nil, draft=true, paper_issue=nil, paper_volume=nil, paper_year=nil)
|
10
|
+
if paper.latex_source?
|
11
|
+
pdf_from_latex(custom_branch, draft, paper_issue, paper_volume, paper_year)
|
12
|
+
elsif paper.markdown_source?
|
13
|
+
pdf_from_markdown(custom_branch, draft, paper_issue, paper_volume, paper_year)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_crossref(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
18
|
+
if paper.latex_source?
|
19
|
+
crossref_from_latex(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
20
|
+
elsif paper.markdown_source?
|
21
|
+
crossref_from_markdown(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_jats(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
26
|
+
if paper.latex_source?
|
27
|
+
jats_from_latex(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
28
|
+
elsif paper.markdown_source?
|
29
|
+
jats_from_markdown(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pdf_from_latex(custom_branch=nil, draft=true, paper_issue=nil, paper_volume=nil, paper_year=nil)
|
34
|
+
# Optionally pass a custom branch name
|
35
|
+
`cd #{paper.directory} && git checkout #{custom_branch} --quiet` if custom_branch
|
36
|
+
|
37
|
+
metadata = YAML.load_file("#{paper.directory}/paper.yml")
|
38
|
+
|
39
|
+
for k in ["title", "authors", "affiliations", "keywords"]
|
40
|
+
raise "Key #{k} not present in metadata" unless metadata.keys().include?(k)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Remove everything that shouldn't be there before processing
|
44
|
+
|
45
|
+
`cd #{paper.directory} && rm -f *.aux \
|
46
|
+
&& rm -f *.blg && rm -f *.fls && rm -f *.log\
|
47
|
+
&& rm -f *.fdb_latexmk`
|
48
|
+
|
49
|
+
open("#{paper.directory}/header.tex", 'w') do |f|
|
50
|
+
f << "% **************GENERATED FILE, DO NOT EDIT**************\n\n"
|
51
|
+
f << "\\title{#{metadata["title"]}}\n\n"
|
52
|
+
for auth in metadata["authors"]
|
53
|
+
f << "\\author[#{auth["affiliation"]}]{#{auth["name"]}}\n"
|
54
|
+
end
|
55
|
+
for aff in metadata["affiliations"]
|
56
|
+
f << "\\affil[#{aff["index"]}]{#{aff["name"]}}\n"
|
57
|
+
end
|
58
|
+
f << "\n\\keywords{"
|
59
|
+
for i in 0...metadata["keywords"].length-1
|
60
|
+
f << "#{metadata["keywords"][i]}, "
|
61
|
+
end
|
62
|
+
f << metadata["keywords"].last
|
63
|
+
f << "}\n"
|
64
|
+
|
65
|
+
# draft mode by default.
|
66
|
+
if draft
|
67
|
+
f << "\\usepackage{draftwatermark}\n\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
`cd #{paper.directory} && latexmk -f -bibtex -pdf paper.tex`
|
72
|
+
|
73
|
+
if File.exists?("#{paper.directory}/paper.pdf")
|
74
|
+
`mv #{paper.directory}/paper.pdf #{paper.directory}/#{paper.filename_doi}.pdf`
|
75
|
+
puts "#{paper.directory}/#{paper.filename_doi}.pdf"
|
76
|
+
else
|
77
|
+
abort("Looks like we failed to compile the PDF")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def generate_issue(date)
|
82
|
+
parsed = Date.parse(date)
|
83
|
+
return 1 + ((parsed.year * 12 + parsed.month) - (Time.parse(ENV['JOURNAL_LAUNCH_DATE']).year * 12 + Time.parse(ENV['JOURNAL_LAUNCH_DATE']).month))
|
84
|
+
end
|
85
|
+
|
86
|
+
def generate_volume(date)
|
87
|
+
parsed = Date.parse(date)
|
88
|
+
return parsed.year - (Date.parse(ENV['JOURNAL_LAUNCH_DATE']).year - 1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def generate_year(date)
|
92
|
+
parsed = Date.parse(date)
|
93
|
+
return parsed.year
|
94
|
+
end
|
95
|
+
|
96
|
+
def generate_month(date)
|
97
|
+
parsed = Date.parse(date)
|
98
|
+
return parsed.month
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_day(date)
|
102
|
+
parsed = Date.parse(date)
|
103
|
+
return parsed.day
|
104
|
+
end
|
105
|
+
|
106
|
+
def pdf_from_markdown(custom_branch=nil, draft=true, paper_issue=nil, paper_volume=nil, paper_year=nil)
|
107
|
+
latex_template_path = "#{Whedon.resources}/#{ENV['JOURNAL_ALIAS']}/latex.template"
|
108
|
+
csl_file = "#{Whedon.resources}/#{ENV['JOURNAL_ALIAS']}/apa.csl"
|
109
|
+
|
110
|
+
url = "#{ENV['JOURNAL_URL']}/papers/lookup/#{@review_issue_id}"
|
111
|
+
response = RestClient.get(url)
|
112
|
+
parsed = JSON.parse(response)
|
113
|
+
submitted = parsed['submitted']
|
114
|
+
published = parsed['accepted']
|
115
|
+
|
116
|
+
# TODO - remove this once JOSE has their editors hooked up in the system
|
117
|
+
if ENV['JOURNAL_ALIAS'] == "joss" && !paper.editor.nil?
|
118
|
+
editor_lookup_url = "#{ENV['JOURNAL_URL']}/editors/lookup/#{paper.editor}"
|
119
|
+
response = RestClient.get(editor_lookup_url)
|
120
|
+
parsed = JSON.parse(response)
|
121
|
+
editor_name = parsed['name']
|
122
|
+
editor_url = parsed['url']
|
123
|
+
else
|
124
|
+
editor_name = "Pending Editor"
|
125
|
+
editor_url = "http://example.com"
|
126
|
+
end
|
127
|
+
|
128
|
+
# If we have already published the paper then overwrite the year, volume, issue
|
129
|
+
if published
|
130
|
+
paper_year = generate_year(published)
|
131
|
+
paper_issue = generate_issue(published)
|
132
|
+
paper_volume = generate_volume(published)
|
133
|
+
else
|
134
|
+
published = Time.now.strftime('%d %B %Y')
|
135
|
+
paper_year ||= @current_year
|
136
|
+
paper_issue ||= @current_issue
|
137
|
+
paper_volume ||= @current_volume
|
138
|
+
end
|
139
|
+
|
140
|
+
# Optionally pass a custom branch name
|
141
|
+
`cd #{paper.directory} && git checkout #{custom_branch} --quiet` if custom_branch
|
142
|
+
|
143
|
+
metadata = {
|
144
|
+
"repository" => repository_address,
|
145
|
+
"archive_doi" => archive_doi,
|
146
|
+
"paper_url" => paper.pdf_url,
|
147
|
+
"journal_name" => ENV['JOURNAL_NAME'],
|
148
|
+
"review_issue_url" => paper.review_issue_url,
|
149
|
+
"issue" => paper_issue,
|
150
|
+
"volume" => paper_volume,
|
151
|
+
"page" => paper.review_issue_id,
|
152
|
+
"logo_path" => "#{Whedon.resources}/#{ENV['JOURNAL_ALIAS']}/logo.png",
|
153
|
+
"aas_logo_path" => "#{Whedon.resources}/#{ENV['JOURNAL_ALIAS']}/aas-logo.png",
|
154
|
+
"year" => paper_year,
|
155
|
+
"submitted" => submitted,
|
156
|
+
"published" => published,
|
157
|
+
"formatted_doi" => paper.formatted_doi,
|
158
|
+
"citation_author" => paper.citation_author,
|
159
|
+
"editor_name" => editor_name,
|
160
|
+
"reviewers" => paper.reviewers_without_handles,
|
161
|
+
"link-citations" => true
|
162
|
+
}
|
163
|
+
|
164
|
+
metadata.merge!({"draft" => true}) if draft
|
165
|
+
|
166
|
+
File.open("#{paper.directory}/markdown-metadata.yaml", 'w') { |file| file.write(metadata.to_yaml) }
|
167
|
+
|
168
|
+
`cd #{paper.directory} && pandoc \
|
169
|
+
-V repository="#{repository_address}" \
|
170
|
+
-V archive_doi="#{archive_doi}" \
|
171
|
+
-V review_issue_url="#{paper.review_issue_url}" \
|
172
|
+
-V editor_url="#{editor_url}" \
|
173
|
+
-V graphics="true" \
|
174
|
+
-o #{paper.filename_doi}.pdf -V geometry:margin=1in \
|
175
|
+
--pdf-engine=xelatex \
|
176
|
+
--citeproc #{File.basename(paper.paper_path)} \
|
177
|
+
--from markdown+autolink_bare_uris \
|
178
|
+
--csl=#{csl_file} \
|
179
|
+
--template #{latex_template_path} \
|
180
|
+
--metadata-file=markdown-metadata.yaml`
|
181
|
+
|
182
|
+
if File.exists?("#{paper.directory}/#{paper.filename_doi}.pdf")
|
183
|
+
puts "#{paper.directory}/#{paper.filename_doi}.pdf"
|
184
|
+
else
|
185
|
+
abort("Looks like we failed to compile the PDF")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def crossref_from_latex(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
190
|
+
cross_ref_template_path = "#{Whedon.resources}/crossref.template"
|
191
|
+
bibtex = Whedon::BibtexParser.new(paper.bibtex_path)
|
192
|
+
|
193
|
+
# Pass the citations that are actually in the paper to the CrossRef
|
194
|
+
# citations generator.
|
195
|
+
|
196
|
+
citations_in_paper = File.read(paper.paper_path).scan(/(?<=\\cite\{)\w+/)
|
197
|
+
# FIXME
|
198
|
+
# Because of the way citations are handled in Pandoc, we need to prepend and @
|
199
|
+
# to the front of each of the citation strings.
|
200
|
+
citations_in_paper = citations_in_paper.map {|c| c.prepend("@")}
|
201
|
+
|
202
|
+
citations = bibtex.generate_citations(citations_in_paper)
|
203
|
+
authors = paper.crossref_authors
|
204
|
+
# TODO fix this when we update the DOI URLs
|
205
|
+
# crossref_doi = archive_doi.gsub("http://dx.doi.org/", '')
|
206
|
+
|
207
|
+
paper_day ||= Time.now.strftime('%d')
|
208
|
+
paper_month ||= Time.now.strftime('%m')
|
209
|
+
paper_year ||= @current_year
|
210
|
+
paper_issue ||= @current_issue
|
211
|
+
paper_volume ||= @current_volume
|
212
|
+
|
213
|
+
`cd #{paper.directory} && pandoc \
|
214
|
+
-V timestamp=#{Time.now.strftime('%Y%m%d%H%M%S')} \
|
215
|
+
-V doi_batch_id=#{generate_doi_batch_id} \
|
216
|
+
-V formatted_doi=#{paper.formatted_doi} \
|
217
|
+
-V archive_doi=#{archive_doi} \
|
218
|
+
-V review_issue_url=#{paper.review_issue_url} \
|
219
|
+
-V paper_url=#{paper.pdf_url} \
|
220
|
+
-V joss_resource_url=#{paper.joss_resource_url} \
|
221
|
+
-V journal_alias=#{ENV['JOURNAL_ALIAS']} \
|
222
|
+
-V journal_abbrev_title=#{ENV['JOURNAL_ALIAS'].upcase} \
|
223
|
+
-V journal_url=#{ENV['JOURNAL_URL']} \
|
224
|
+
-V journal_name='#{ENV['JOURNAL_NAME']}' \
|
225
|
+
-V journal_issn=#{ENV['JOURNAL_ISSN']} \
|
226
|
+
-V citations='#{citations}' \
|
227
|
+
-V authors='#{authors}' \
|
228
|
+
-V month=#{paper_month} \
|
229
|
+
-V day=#{paper_day} \
|
230
|
+
-V year=#{paper_year} \
|
231
|
+
-V issue=#{paper_issue} \
|
232
|
+
-V volume=#{paper_volume} \
|
233
|
+
-V page=#{paper.review_issue_id} \
|
234
|
+
-V title='#{paper.plain_title}' \
|
235
|
+
-f markdown #{File.basename(paper.paper_path)} -o #{paper.filename_doi}.crossref.xml \
|
236
|
+
--template #{cross_ref_template_path}`
|
237
|
+
|
238
|
+
if File.exists?("#{paper.directory}/#{paper.filename_doi}.crossref.xml")
|
239
|
+
"#{paper.directory}/#{paper.filename_doi}.crossref.xml"
|
240
|
+
else
|
241
|
+
abort("Looks like we failed to compile the Crossref XML")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def crossref_from_markdown(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
246
|
+
cross_ref_template_path = "#{Whedon.resources}/crossref.template"
|
247
|
+
bibtex = Whedon::BibtexParser.new(paper.bibtex_path)
|
248
|
+
|
249
|
+
# Pass the citations that are actually in the paper to the CrossRef
|
250
|
+
# citations generator.
|
251
|
+
|
252
|
+
citations_in_paper = File.read(paper.paper_path).scan(/@[\w|\-|:|_|\/|\+]+/)
|
253
|
+
citations = bibtex.generate_citations(citations_in_paper)
|
254
|
+
authors = paper.crossref_authors
|
255
|
+
# TODO fix this when we update the DOI URLs
|
256
|
+
# crossref_doi = archive_doi.gsub("http://dx.doi.org/", '')
|
257
|
+
|
258
|
+
url = "#{ENV['JOURNAL_URL']}/papers/lookup/#{@review_issue_id}"
|
259
|
+
response = RestClient.get(url)
|
260
|
+
parsed = JSON.parse(response)
|
261
|
+
submitted = parsed['submitted']
|
262
|
+
published = parsed['accepted']
|
263
|
+
|
264
|
+
# If we have already published the paper then overwrite the year, volume, issue
|
265
|
+
if published
|
266
|
+
paper_day = generate_day(published)
|
267
|
+
paper_month = generate_month(published)
|
268
|
+
paper_year = generate_year(published)
|
269
|
+
paper_issue = generate_issue(published)
|
270
|
+
paper_volume = generate_volume(published)
|
271
|
+
else
|
272
|
+
paper_issue ||= @current_issue
|
273
|
+
paper_volume ||= @current_volume
|
274
|
+
paper_day ||= Time.now.strftime('%d')
|
275
|
+
paper_month ||= Time.now.strftime('%m')
|
276
|
+
paper_year ||= Time.now.strftime('%Y')
|
277
|
+
end
|
278
|
+
|
279
|
+
metadata = {
|
280
|
+
"timestamp" => Time.now.strftime('%Y%m%d%H%M%S'),
|
281
|
+
"doi_batch_id" => generate_doi_batch_id,
|
282
|
+
"formatted_doi" => paper.formatted_doi,
|
283
|
+
"archive_doi" => archive_doi,
|
284
|
+
"review_issue_url" => paper.review_issue_url,
|
285
|
+
"paper_url" => paper.pdf_url,
|
286
|
+
"joss_resource_url" => paper.joss_resource_url,
|
287
|
+
"journal_alias" => ENV['JOURNAL_ALIAS'],
|
288
|
+
"journal_abbrev_title" => ENV['JOURNAL_ALIAS'].upcase,
|
289
|
+
"journal_url" => ENV['JOURNAL_URL'],
|
290
|
+
"journal_name" => ENV['JOURNAL_NAME'],
|
291
|
+
"journal_issn"=> ENV['JOURNAL_ISSN'],
|
292
|
+
"month" => paper_month,
|
293
|
+
"day" => paper_day,
|
294
|
+
"year" => paper_year,
|
295
|
+
"issue" => paper_issue,
|
296
|
+
"volume" => paper_volume,
|
297
|
+
"page" => paper.review_issue_id,
|
298
|
+
"title" => paper.plain_title,
|
299
|
+
"crossref_authors" => authors,
|
300
|
+
"citations" => citations
|
301
|
+
}
|
302
|
+
|
303
|
+
File.open("#{paper.directory}/crossref-metadata.yaml", 'w') { |file| file.write(metadata.to_yaml) }
|
304
|
+
|
305
|
+
`cd #{paper.directory} && pandoc \
|
306
|
+
-V title="#{paper.plain_title}" \
|
307
|
+
-f markdown #{File.basename(paper.paper_path)} -o #{paper.filename_doi}.crossref.xml \
|
308
|
+
--template #{cross_ref_template_path} \
|
309
|
+
--metadata-file=crossref-metadata.yaml`
|
310
|
+
|
311
|
+
if File.exists?("#{paper.directory}/#{paper.filename_doi}.crossref.xml")
|
312
|
+
doc = Nokogiri::XML(File.open("#{paper.directory}/#{paper.filename_doi}.crossref.xml", "r"), &:noblanks)
|
313
|
+
File.open("#{paper.directory}/#{paper.filename_doi}.crossref.xml", 'w') {|f| f.write(doc.to_xml(:indent => 2))}
|
314
|
+
"#{paper.directory}/#{paper.filename_doi}.crossref.xml"
|
315
|
+
else
|
316
|
+
abort("Looks like we failed to compile the Crossref XML")
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def jats_from_latex(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
321
|
+
"JATS from LaTeX"
|
322
|
+
end
|
323
|
+
|
324
|
+
def jats_from_markdown(paper_issue=nil, paper_volume=nil, paper_year=nil, paper_month=nil, paper_day=nil)
|
325
|
+
latex_template_path = "#{Whedon.resources}/latex.template"
|
326
|
+
jats_template_path = "#{Whedon.resources}/jats.template"
|
327
|
+
csl_file = "#{Whedon.resources}/jats.csl"
|
328
|
+
|
329
|
+
|
330
|
+
url = "#{ENV['JOURNAL_URL']}/papers/lookup/#{@review_issue_id}"
|
331
|
+
response = RestClient.get(url)
|
332
|
+
parsed = JSON.parse(response)
|
333
|
+
submitted = parsed['submitted']
|
334
|
+
published = parsed['accepted']
|
335
|
+
|
336
|
+
# If we have already published the paper then overwrite the year, volume, issue
|
337
|
+
if published
|
338
|
+
paper_day = Date.parse(published).strftime('%d')
|
339
|
+
paper_year = generate_year(published)
|
340
|
+
paper_issue = generate_issue(published)
|
341
|
+
paper_volume = generate_volume(published)
|
342
|
+
else
|
343
|
+
paper_day ||= Time.now.strftime('%d')
|
344
|
+
paper_month ||= Time.now.strftime('%m')
|
345
|
+
paper_year ||= Time.now.strftime('%Y')
|
346
|
+
end
|
347
|
+
|
348
|
+
# TODO: may eventually want to swap out the latex template
|
349
|
+
`cd #{paper.directory} && pandoc \
|
350
|
+
-V repository="#{repository_address}" \
|
351
|
+
-V archive_doi="#{archive_doi}" \
|
352
|
+
-V paper_url="#{paper.pdf_url}" \
|
353
|
+
-V journal_name='#{ENV['JOURNAL_NAME']}' \
|
354
|
+
-V journal_issn=#{ENV['JOURNAL_ISSN']} \
|
355
|
+
-V journal_abbrev_title=#{ENV['JOURNAL_ALIAS']} \
|
356
|
+
-V graphics="true" \
|
357
|
+
-V issue="#{paper_issue}" \
|
358
|
+
-V volume="#{paper_volume}" \
|
359
|
+
-V page="#{paper.review_issue_id}" \
|
360
|
+
-V logo_path="#{Whedon.resources}/#{ENV['JOURNAL_ALIAS']}-logo.png" \
|
361
|
+
-V month=#{paper_month} \
|
362
|
+
-V day=#{paper_day} \
|
363
|
+
-V year="#{paper_year}" \
|
364
|
+
-V jats_authors='#{paper.jats_authors}' \
|
365
|
+
-V jats_affiliations='#{paper.jats_affiliations}' \
|
366
|
+
-t jats \
|
367
|
+
-s \
|
368
|
+
--citeproc \
|
369
|
+
-o #{paper.filename_doi}.jats.xml \
|
370
|
+
#{File.basename(paper.paper_path)} \
|
371
|
+
--template #{jats_template_path}`
|
372
|
+
|
373
|
+
if File.exists?("#{paper.directory}/#{paper.filename_doi}.jats.xml")
|
374
|
+
"#{paper.directory}/#{paper.filename_doi}.jats.xml"
|
375
|
+
else
|
376
|
+
abort("Looks like we failed to compile the JATS XML")
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# How we connect to GitHub.
|
2
|
+
|
3
|
+
module GitHub
|
4
|
+
# Authenticated Octokit
|
5
|
+
# TODO remove license preview media type when this ships
|
6
|
+
MEDIA_TYPE = "application/vnd.github.drax-preview+json"
|
7
|
+
|
8
|
+
def client
|
9
|
+
@client ||= Octokit::Client.new(:access_token => ENV['GH_TOKEN'],
|
10
|
+
:default_media_type => MEDIA_TYPE)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class String
|
2
|
+
def numeric?
|
3
|
+
Float(self) != nil rescue false
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module Whedon
|
8
|
+
class OrcidValidator
|
9
|
+
attr_reader :orcid
|
10
|
+
|
11
|
+
def initialize(orcid)
|
12
|
+
@orcid = orcid.strip
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns true or false
|
16
|
+
def validate
|
17
|
+
return false unless check_structure
|
18
|
+
return false unless check_length
|
19
|
+
return false unless check_chars
|
20
|
+
|
21
|
+
if checksum_char == "X" || checksum_char == "x"
|
22
|
+
return checksum == 10
|
23
|
+
else
|
24
|
+
return checksum == checksum_char.to_i
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def packed_orcid
|
29
|
+
orcid.gsub('-', '')
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the last character of the string
|
33
|
+
def checksum_char
|
34
|
+
packed_orcid[-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def first_11
|
38
|
+
packed_orcid.chop
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_structure
|
42
|
+
groups = orcid.split('-')
|
43
|
+
if groups.size == 4
|
44
|
+
return true
|
45
|
+
else
|
46
|
+
warn("ORCID looks malformed") and return false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def check_length
|
51
|
+
if packed_orcid.length == 16
|
52
|
+
return true
|
53
|
+
else
|
54
|
+
warn("ORCID looks to be the wrong length") and return false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_chars
|
59
|
+
valid = true
|
60
|
+
first_11.each_char do |c|
|
61
|
+
if !c.numeric?
|
62
|
+
warn("Invalid ORDIC digit (#{c})")
|
63
|
+
valid = false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return valid
|
68
|
+
end
|
69
|
+
|
70
|
+
# https://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
|
71
|
+
def checksum
|
72
|
+
total = 0
|
73
|
+
first_11.each_char do |c|
|
74
|
+
total = (total + c.to_i) * 2
|
75
|
+
end
|
76
|
+
|
77
|
+
remainder = total % 11
|
78
|
+
result = (12 - remainder) % 11
|
79
|
+
|
80
|
+
return result
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|