figshare_api_v2 0.9.5 → 0.9.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.txt +36 -0
- data/lib/authors.rb +16 -13
- data/lib/base.rb +65 -65
- data/lib/figshare_api_v2.rb +19 -18
- data/lib/institutions.rb +80 -80
- data/lib/oai_pmh.rb +4 -2
- data/lib/other.rb +13 -13
- data/lib/private_articles.rb +135 -112
- data/lib/private_collections.rb +87 -84
- data/lib/private_projects.rb +79 -78
- data/lib/public_articles.rb +60 -51
- data/lib/public_collections.rb +50 -42
- data/lib/public_projects.rb +24 -23
- data/lib/upload.rb +53 -48
- metadata +4 -4
data/lib/public_collections.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
module Figshare
|
2
|
-
|
3
2
|
# Figshare public colections api calls
|
4
3
|
#
|
5
4
|
class PublicCollections < Base
|
6
|
-
|
7
5
|
# Requests a list of public collections
|
8
6
|
#
|
9
7
|
# @param institution [Boolean] Just our institution
|
@@ -16,25 +14,30 @@ module Figshare
|
|
16
14
|
# @param order [String] "published_date" Default, "modified_date", "views", "cites", "shares"
|
17
15
|
# @param order_direction [String] "desc" Default, "asc"
|
18
16
|
# @yield [Hash] {id, title, doi, handle, url, published_date}
|
19
|
-
def list(institution: false,
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
def list( institution: false,
|
18
|
+
group_id: nil,
|
19
|
+
published_since: nil,
|
20
|
+
modified_since: nil,
|
21
|
+
resource_doi: nil,
|
22
|
+
doi: nil,
|
23
|
+
handle: nil,
|
24
|
+
order: 'published_date',
|
25
|
+
order_direction: 'desc',
|
26
|
+
&block
|
27
|
+
)
|
25
28
|
args = {}
|
26
|
-
args['institution'] = @institute_id
|
27
|
-
args['group'] = group_id
|
28
|
-
args['resource_doi'] = resource_doi
|
29
|
-
args['doi'] = doi
|
30
|
-
args['handle'] = handle
|
31
|
-
args['published_since'] = published_since
|
32
|
-
args['modified_since'] = modified_since
|
33
|
-
args['order'] = order
|
34
|
-
args['order_direction'] = order_direction
|
29
|
+
args['institution'] = @institute_id unless institution.nil?
|
30
|
+
args['group'] = group_id unless group_id.nil?
|
31
|
+
args['resource_doi'] = resource_doi unless resource_doi.nil?
|
32
|
+
args['doi'] = doi unless doi.nil?
|
33
|
+
args['handle'] = handle unless handle.nil?
|
34
|
+
args['published_since'] = published_since unless published_since.nil?
|
35
|
+
args['modified_since'] = modified_since unless modified_since.nil?
|
36
|
+
args['order'] = order unless order.nil?
|
37
|
+
args['order_direction'] = order_direction unless order_direction.nil?
|
35
38
|
get_paginate(api_query: 'collections', args: args, &block)
|
36
39
|
end
|
37
|
-
|
40
|
+
|
38
41
|
# Search within the public collections
|
39
42
|
#
|
40
43
|
# @param institution [Boolean] Just our institution
|
@@ -47,24 +50,30 @@ module Figshare
|
|
47
50
|
# @param order [String] "published_date" Default, "modified_date", "views", "cites", "shares"
|
48
51
|
# @param order_direction [String] "desc" Default, "asc"
|
49
52
|
# @yield [Hash] {id, title, doi, handle, url, published_date}
|
50
|
-
def search(
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
def search( search_for:,
|
54
|
+
institute: false,
|
55
|
+
group_id: nil,
|
56
|
+
published_since: nil,
|
57
|
+
modified_since: nil,
|
58
|
+
item_type: nil,
|
59
|
+
resource_doi: nil,
|
60
|
+
doi: nil,
|
61
|
+
handle: nil,
|
62
|
+
order: 'published_date',
|
63
|
+
order_direction: 'desc',
|
64
|
+
&block
|
65
|
+
)
|
57
66
|
args = { 'search_for' => search_for }
|
58
|
-
args['institution'] = @institute_id
|
59
|
-
args['group_id'] = group_id
|
60
|
-
args['item_type'] = item_type
|
61
|
-
args['resource_doi'] = resource_doi
|
62
|
-
args['doi'] = doi
|
63
|
-
args['handle'] = handle
|
64
|
-
args['published_since'] = published_since
|
65
|
-
args['modified_since'] = modified_since
|
66
|
-
args['order'] = order
|
67
|
-
args['order_direction'] = order_direction
|
67
|
+
args['institution'] = @institute_id unless institute.nil?
|
68
|
+
args['group_id'] = group_id unless group_id.nil?
|
69
|
+
args['item_type'] = item_type unless item_type.nil?
|
70
|
+
args['resource_doi'] = resource_doi unless resource_doi.nil?
|
71
|
+
args['doi'] = doi unless doi.nil?
|
72
|
+
args['handle'] = handle unless handle.nil?
|
73
|
+
args['published_since'] = published_since unless published_since.nil?
|
74
|
+
args['modified_since'] = modified_since unless modified_since.nil?
|
75
|
+
args['order'] = order unless order.nil?
|
76
|
+
args['order_direction'] = order_direction unless order_direction.nil?
|
68
77
|
post(api_query: 'account/articles/search', args: args, &block)
|
69
78
|
end
|
70
79
|
|
@@ -73,7 +82,7 @@ module Figshare
|
|
73
82
|
# @param collection_id [Integer] Figshare id of the collection
|
74
83
|
# @yield [Hash] See figshare api docs
|
75
84
|
def detail(collection_id:, &block)
|
76
|
-
get(api_query: "collections/#{collection_id}",
|
85
|
+
get(api_query: "collections/#{collection_id}", &block)
|
77
86
|
end
|
78
87
|
|
79
88
|
# Return details of a list of public collection Versions
|
@@ -81,7 +90,7 @@ module Figshare
|
|
81
90
|
# @param collection_id [Integer] Figshare id of the collection
|
82
91
|
# @yield [Hash] See figshare api docs
|
83
92
|
def versions(collection_id:, &block)
|
84
|
-
get(api_query: "collections/#{collection_id}/versions",
|
93
|
+
get(api_query: "collections/#{collection_id}/versions", &block)
|
85
94
|
end
|
86
95
|
|
87
96
|
# Get details of specific collection version
|
@@ -90,9 +99,9 @@ module Figshare
|
|
90
99
|
# @param version_id [Integer] Figshare id of the collection's version
|
91
100
|
# @yield [Hash] See figshare api docs
|
92
101
|
def version_detail(collection_id:, version_id:, &block)
|
93
|
-
get(api_query: "collections/#{collection_id}/versions/#{version_id}",
|
102
|
+
get(api_query: "collections/#{collection_id}/versions/#{version_id}", &block)
|
94
103
|
end
|
95
|
-
|
104
|
+
|
96
105
|
# Get details of list of articles for a specific collection
|
97
106
|
#
|
98
107
|
# @param collection_id [Integer] Figshare id of the collection
|
@@ -100,6 +109,5 @@ module Figshare
|
|
100
109
|
def articles(collection_id:, &block)
|
101
110
|
get_paginate(api_query: "collections/#{collection_id}/articles", &block)
|
102
111
|
end
|
103
|
-
end
|
104
|
-
|
105
|
-
end #of module
|
112
|
+
end
|
113
|
+
end
|
data/lib/public_projects.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module Figshare
|
2
|
-
|
3
2
|
# Figshare public projects api
|
4
3
|
#
|
5
4
|
class PublicProjects < Base
|
@@ -11,13 +10,13 @@ module Figshare
|
|
11
10
|
# @param order [String] "published_date" Default, "modified_date", "views", "cites", "shares"
|
12
11
|
# @param order_direction [String] "desc" Default, "asc"
|
13
12
|
# @yield [Hash] {url, published_date, id, title}
|
14
|
-
def list(institute: false,group_id: nil, published_since: nil, order: 'published_date', order_direction: 'desc', &block)
|
13
|
+
def list(institute: false, group_id: nil, published_since: nil, order: 'published_date', order_direction: 'desc', &block)
|
15
14
|
args = {}
|
16
|
-
args['institution'] = @institute_id
|
17
|
-
args['group'] = group_id
|
18
|
-
args['published_since'] = published_since
|
19
|
-
args['order'] = order
|
20
|
-
args['order_direction'] = order_direction
|
15
|
+
args['institution'] = @institute_id unless institute.nil?
|
16
|
+
args['group'] = group_id unless group_id.nil?
|
17
|
+
args['published_since'] = published_since unless published_since.nil?
|
18
|
+
args['order'] = order unless order.nil?
|
19
|
+
args['order_direction'] = order_direction unless order_direction.nil?
|
21
20
|
get_paginate(api_query: 'projects', args: args, &block)
|
22
21
|
end
|
23
22
|
|
@@ -30,19 +29,22 @@ module Figshare
|
|
30
29
|
# @param order [String] "published_date" Default, "modified_date", "views", "cites", "shares"
|
31
30
|
# @param order_direction [String] "desc" Default, "asc"
|
32
31
|
# @yield [Hash] {id, title, doi, handle, url, published_date}
|
33
|
-
def search(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
def search( search_for:,
|
33
|
+
institute: false,
|
34
|
+
group_id: nil,
|
35
|
+
published_since: nil,
|
36
|
+
modified_since: nil,
|
37
|
+
order: 'published_date',
|
38
|
+
order_direction: 'desc',
|
39
|
+
&block
|
40
|
+
)
|
39
41
|
args = { 'search_for' => search_for }
|
40
|
-
args['institution'] = @institute_id
|
41
|
-
args['group'] = group_id
|
42
|
-
args['published_since'] = published_since
|
43
|
-
args['modified_since'] = modified_since
|
44
|
-
args['order'] = order
|
45
|
-
args['order_direction'] = order_direction
|
42
|
+
args['institution'] = @institute_id unless institute.nil?
|
43
|
+
args['group'] = group_id unless group_id.nil?
|
44
|
+
args['published_since'] = published_since unless published_since.nil?
|
45
|
+
args['modified_since'] = modified_since unless modified_since.nil?
|
46
|
+
args['order'] = order unless order.nil?
|
47
|
+
args['order_direction'] = order_direction unless order_direction.nil?
|
46
48
|
post(api_query: 'account/projects/search', args: args, &block)
|
47
49
|
end
|
48
50
|
|
@@ -51,7 +53,7 @@ module Figshare
|
|
51
53
|
# @param project_id [Integer] Figshare id of the project_id
|
52
54
|
# @yield [Hash] See figshare api docs
|
53
55
|
def detail(project_id:, &block)
|
54
|
-
get(api_query: "projects/#{project_id}",
|
56
|
+
get(api_query: "projects/#{project_id}", &block)
|
55
57
|
end
|
56
58
|
|
57
59
|
# Get list of articles for a specific project
|
@@ -61,6 +63,5 @@ module Figshare
|
|
61
63
|
def articles(project_id:, &block)
|
62
64
|
get_paginate(api_query: "projects/#{project_id}/articles", &block)
|
63
65
|
end
|
64
|
-
|
65
|
-
|
66
|
-
end # of module
|
66
|
+
end
|
67
|
+
end
|
data/lib/upload.rb
CHANGED
@@ -1,27 +1,27 @@
|
|
1
|
-
module Figshare
|
1
|
+
module Figshare # :nodoc:
|
2
2
|
require 'digest'
|
3
3
|
require 'dir_r'
|
4
4
|
|
5
5
|
# Upload files to figshare
|
6
6
|
# Nb. This can sometimes fail, so you need to check the md5 to ensure the file got there
|
7
7
|
# It can take a short while for the md5 to be calculated, so upload, wait, then check for a computed_md5.
|
8
|
-
# The status will show as "ic_checking", "moving_to_final" then to "available",
|
8
|
+
# The status will show as "ic_checking", "moving_to_final" then to "available",
|
9
9
|
# I have seen it stuck at "moving_to_final", but with the right computed_md5.
|
10
10
|
#
|
11
11
|
class Upload < PrivateArticles
|
12
12
|
CHUNK_SIZE = 1048576
|
13
|
-
attr_accessor :file_info, :upload_query, :upload_host, :upload_parts_detail
|
13
|
+
attr_accessor :file_info, :upload_query, :upload_host, :upload_parts_detail, :file_id, :article_id, :file_name
|
14
14
|
attr_accessor :new_count, :bad_count
|
15
|
-
|
15
|
+
|
16
16
|
# Calculate a local files MD5.
|
17
17
|
#
|
18
18
|
# @param filename [String] Path/name of local file to MD5
|
19
19
|
# @return [String,Integer] MD5 as a Hex String, Size of the file in bytes.
|
20
20
|
def self.get_file_check_data(filename)
|
21
|
-
stat_record =
|
21
|
+
stat_record = File.stat(filename)
|
22
22
|
md5 = Digest::MD5.new
|
23
23
|
File.open(filename, 'rb') do |fd|
|
24
|
-
while(buffer = fd.read(CHUNK_SIZE))
|
24
|
+
while (buffer = fd.read(CHUNK_SIZE))
|
25
25
|
md5.update(buffer)
|
26
26
|
end
|
27
27
|
end
|
@@ -37,31 +37,31 @@ module Figshare
|
|
37
37
|
@article_id = article_id
|
38
38
|
@file_name = file_name
|
39
39
|
@trace = trace
|
40
|
-
|
40
|
+
|
41
41
|
@file_id = nil
|
42
42
|
@file_info = nil
|
43
43
|
@upload_query = nil
|
44
44
|
@upload_host = nil
|
45
|
-
@upload_parts_detail
|
46
|
-
|
47
|
-
initiate_new_upload
|
45
|
+
@upload_parts_detail = nil
|
46
|
+
|
47
|
+
initiate_new_upload
|
48
48
|
puts "New File_id: #{@file_id}\n\n" if @trace > 1
|
49
|
-
|
50
|
-
get_file_info
|
49
|
+
|
50
|
+
get_file_info
|
51
51
|
puts "@file_info: #{@file_info.to_j}\n\n" if @trace > 1
|
52
|
-
|
53
|
-
get_upload_parts_details
|
52
|
+
|
53
|
+
get_upload_parts_details
|
54
54
|
puts "@upload_parts_detail: #{@upload_parts_detail.to_j}\n\n" if @trace > 1
|
55
|
-
|
56
|
-
upload_the_parts
|
57
|
-
|
58
|
-
complete_upload
|
55
|
+
|
56
|
+
upload_the_parts
|
57
|
+
|
58
|
+
complete_upload
|
59
59
|
if @trace > 1
|
60
60
|
status
|
61
61
|
puts "Final Status: #{@file_info.to_j}\n\n"
|
62
62
|
end
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
# Upload all files in a directory, into one article.
|
66
66
|
# Check checksums, and only upload changed or new files
|
67
67
|
# Does not recurse through sub-directories, as figshare has a flat file structure.
|
@@ -73,16 +73,17 @@ module Figshare
|
|
73
73
|
def upload_dir(article_id:, directory:, delete_extras: false, exclude_dot_files: true, trace: 0)
|
74
74
|
@new_count = 0
|
75
75
|
@bad_count = 0
|
76
|
-
|
76
|
+
|
77
77
|
files = {}
|
78
78
|
cache_article_file_md5(article_id: article_id)
|
79
|
-
|
80
|
-
DirR.walk_dir(directory: directory, walk_sub_directories: false) do |d,f|
|
79
|
+
|
80
|
+
DirR.walk_dir(directory: directory, walk_sub_directories: false) do |d, f|
|
81
81
|
next if exclude_dot_files && f =~ /^\..*/
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
|
83
|
+
files[f] = true # NOTE: that we have seen this filename
|
84
|
+
if @md5_cache[f] # check to see if it has already been uploaded
|
85
|
+
md5, _size = Upload.get_file_check_data("#{d}/#{f}")
|
86
|
+
if @md5_cache[f][:md5] != md5 # file is there, but has changed, or previously failed to upload.
|
86
87
|
puts "Deleting: #{article_id} << #{d}/#{f} #{@md5_cache[f][:id]} MISMATCH '#{@md5_cache[f]}' != '#{md5}'" if trace > 0
|
87
88
|
file_delete(article_id: article_id, file_id: @md5_cache[f][:id])
|
88
89
|
@bad_count += 1
|
@@ -98,16 +99,16 @@ module Figshare
|
|
98
99
|
@new_count += 1
|
99
100
|
end
|
100
101
|
end
|
101
|
-
|
102
|
+
|
102
103
|
# Print out filename of files in the Figshare article, that weren't in the directory.
|
103
|
-
@md5_cache.each do |fn,v|
|
104
|
-
if ! files[fn]
|
105
|
-
#File exists on Figshare, but not on the local disk
|
104
|
+
@md5_cache.each do |fn, v|
|
105
|
+
if ! files[fn]
|
106
|
+
# File exists on Figshare, but not on the local disk
|
106
107
|
if delete_extras
|
107
108
|
puts "Deleteing EXTRA: #{article_id} << #{fn} #{v[:id]}" if trace > 0
|
108
|
-
file_delete(article_id: article_id, file_id: @md5_cache[f][:id])
|
109
|
+
file_delete(article_id: article_id, file_id: @md5_cache[f][:id])
|
109
110
|
elsif trace > 0
|
110
|
-
puts "EXTRA: #{article_id} << #{fn} #{v[:id]}"
|
111
|
+
puts "EXTRA: #{article_id} << #{fn} #{v[:id]}"
|
111
112
|
end
|
112
113
|
end
|
113
114
|
end
|
@@ -115,16 +116,16 @@ module Figshare
|
|
115
116
|
|
116
117
|
# Retrieve md5 sums of the existing files in the figshare article
|
117
118
|
# Sets @md5_cache[filename] => figshare.computed_md5
|
118
|
-
#
|
119
|
+
#
|
119
120
|
# @param article_id [Integer] Figshare article ID
|
120
121
|
private def cache_article_file_md5(article_id:)
|
121
122
|
@md5_cache = {}
|
122
123
|
files(article_id: article_id) do |f|
|
123
|
-
@md5_cache[f['name']] = {:
|
124
|
+
@md5_cache[f['name']] = { article_id: article_id, id: f['id'], md5: f['computed_md5'] }
|
124
125
|
end
|
125
126
|
end
|
126
|
-
|
127
|
-
# Get status of the current upload.
|
127
|
+
|
128
|
+
# Get status of the current upload.
|
128
129
|
# Just fetches the file record from figshare.
|
129
130
|
# Of interest is the status field, and the computed_md5 field
|
130
131
|
#
|
@@ -134,33 +135,35 @@ module Figshare
|
|
134
135
|
file_detail(article_id: @article_id, file_id: @file_id) do |f|
|
135
136
|
@file_info = f
|
136
137
|
end
|
137
|
-
raise
|
138
|
+
raise 'Upload::status(): Failed to get figshare file record' if @file_info.nil?
|
138
139
|
end
|
139
|
-
|
140
|
+
|
140
141
|
# Creates a new Figshare file record, in the figshare article, and we get the file_id from the upload URL
|
141
142
|
# file status == 'created'
|
142
143
|
#
|
143
144
|
private def initiate_new_upload
|
144
145
|
md5, size = Upload.get_file_check_data(@file_name)
|
145
|
-
args = {'name' => File.basename(@file_name),
|
146
|
-
|
147
|
-
|
146
|
+
args = { 'name' => File.basename(@file_name),
|
147
|
+
'md5' => md5,
|
148
|
+
'size' => size
|
148
149
|
}
|
149
150
|
post( api_query: "account/articles/#{@article_id}/files", args: args ) do |f|
|
150
151
|
@file_id = f['location'].gsub(/^.*\/([0-9]+)$/, '\1')
|
151
152
|
end
|
152
|
-
raise
|
153
|
+
raise 'Upload::initiate_new_upload(): failed to create Figshare file record' if @file_id.nil?
|
153
154
|
end
|
154
|
-
|
155
|
+
|
155
156
|
# Gets the Figshare file info
|
156
157
|
# We need the upload URLs to continue
|
157
158
|
#
|
159
|
+
# rubocop:disable Naming/AccessorMethodName This isn't an accessor method.
|
158
160
|
private def get_file_info
|
159
161
|
status
|
160
162
|
@upload_host = @file_info['upload_url'].gsub(/^http.*\/\/(.*)\/upload.*$/, '\1')
|
161
163
|
@upload_query = @file_info['upload_url'].gsub(/^http.*\/\/(.*)\/(upload.*)$/, '\2')
|
162
164
|
puts "Upload_host: #{@upload_host} URL: #{@upload_query}" if @trace > 1
|
163
165
|
end
|
166
|
+
# rubocop:enable Naming/AccessorMethodName This isn't an accessor method.
|
164
167
|
|
165
168
|
# Completes the upload.
|
166
169
|
# Figshare then calculates the md5 in the background, which may take a while to complete
|
@@ -168,24 +171,27 @@ module Figshare
|
|
168
171
|
#
|
169
172
|
private def complete_upload
|
170
173
|
post( api_query: "account/articles/#{@article_id}/files/#{@file_id}" )
|
171
|
-
puts
|
174
|
+
puts 'complete_upload' if @trace > 1
|
172
175
|
end
|
173
176
|
|
174
177
|
# Get the upload settings
|
175
178
|
#
|
179
|
+
# rubocop:disable Naming/AccessorMethodName This isn't an accessor method.
|
176
180
|
private def get_upload_parts_details
|
177
181
|
@upload_parts_detail = nil
|
178
182
|
result = nil
|
179
183
|
WIKK::WebBrowser.https_session( host: @upload_host, verify_cert: false ) do |ws|
|
180
184
|
result = ws.get_page( query: @upload_query,
|
181
|
-
authorization: "token #{@auth_token}"
|
185
|
+
authorization: "token #{@auth_token}"
|
182
186
|
)
|
183
187
|
end
|
184
188
|
raise "get_upload_parts_detail(#{@article_id}) failed to get upload URL" if result.nil?
|
189
|
+
|
185
190
|
@upload_parts_detail = JSON.parse(result)
|
186
|
-
|
191
|
+
|
187
192
|
puts "Part URL #{@upload_parts_detail['parts']}" if @trace > 1
|
188
193
|
end
|
194
|
+
# rubocop:enable Naming/AccessorMethodName This isn't an accessor method.
|
189
195
|
|
190
196
|
# Upload the file in parts
|
191
197
|
#
|
@@ -210,6 +216,5 @@ module Figshare
|
|
210
216
|
)
|
211
217
|
end
|
212
218
|
end
|
213
|
-
|
214
219
|
end
|
215
|
-
end
|
220
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: figshare_api_v2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Burrowes
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: wikk_json
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '3.
|
75
|
+
version: '3.23'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '3.
|
82
|
+
version: '3.23'
|
83
83
|
description: Figshare version 2 API.
|
84
84
|
email:
|
85
85
|
- r.burrowes@auckland.ac.nz
|