fieldview 0.0.0 → 0.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c291877633d7f4126668164ee7cc60c7c5c67bac
4
- data.tar.gz: df16497405345a46fab2c65066eec659ffc074b7
3
+ metadata.gz: 05858ad578d5fc9f1a42d4f15c8d9ee0b7299f01
4
+ data.tar.gz: be48bc245a2c2a38c6b850b51f735ca4d8db193a
5
5
  SHA512:
6
- metadata.gz: 883b9f63fcaf3af60958bc8407660a76608afca697fa043afae906240d6c7f2f36ed3116dea2de6c76630c3e4ba82effe08d6d11ab3e349c4cb03b962eae052d
7
- data.tar.gz: 8d027e67cdabf34fd66a7284196c34533d82b2f002df8826b2d91b8005ae4246fb8632225f5b67041dd518c465a4826244f5e5eabc9e7a2d6927acb91d63808d
6
+ metadata.gz: d1893ef1af5bdcc6a9ca2790e9806d58828d24a859ad944109b611a1af556688a45761290f0694821a5690c44c3b3e86d59a69135178eec56bcad0598890997c
7
+ data.tar.gz: cad12cb23d33a5569cdd3583429bab951ede0d58c5fcc4ca6ea80e27266fff1554717d49edc4b28c76a4c5dfbaf2c7fc72507c5585e380d18e832f3f43866341
data/Gemfile.lock CHANGED
@@ -2,7 +2,6 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  fieldview (0.0.0)
5
- faraday (~> 0.9)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
@@ -12,11 +11,8 @@ GEM
12
11
  byebug (9.0.6)
13
12
  crack (0.4.3)
14
13
  safe_yaml (~> 1.0.0)
15
- faraday (0.12.0.1)
16
- multipart-post (>= 1.2, < 3)
17
14
  hashdiff (0.3.2)
18
15
  minitest (5.8.4)
19
- multipart-post (2.0.0)
20
16
  public_suffix (2.0.5)
21
17
  rake (11.3.0)
22
18
  safe_yaml (1.0.4)
data/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  The FieldView Ruby library provides convenient access to the FieldView API from applications written in the Ruby language. It includes a pre-defined set of classes for API resources that are available currently from the API. You will need to get access from a Climate Corporation representative and the interface utilize OAUTH 2.0.
4
4
 
5
+ OAuth token refreshing is handled for your if for some reason the token expires in the middle of requests (this requires that you provide the `refresh_token` with the creation of the `AuthToken`).
6
+
7
+ ## Listable Objects
8
+
9
+ FieldView returns pages of objects that are tracked via the headers, you cannot go back in pages easily (perhaps we could be tracking the next-token?). Use `more_pages?` on the listable object to see if there are more pages to be acquired by using `next_page!`. The `next_token` on listable objects can also be preserved when reaching the end of a list to see if there are any additional changes since the `next_token` was saved.
10
+
11
+ The raw data can be acquired by using `.data` on a listable object.
12
+
13
+ When finished you will want to store information of the `AuthToken` somewhere as it may have changed with requests.
14
+
5
15
  ## Usage
6
16
 
7
17
  The library needs to be configured with your FieldView account's client secret,
@@ -25,9 +35,10 @@ auth_token = FieldView::AuthToken.new_auth_token_with_code_from_redirect_code(<C
25
35
  # Or initialize with previous information
26
36
  auth_token = FieldView::AuthToken.new(
27
37
  access_token: <ATOKEN>,
28
- expiration_at: <DATETIME>,
38
+ expiration_at: <DATETIME>, # not required, but should be known
29
39
  refresh_token: <RTOKEN>,
30
- refresh_token_expiration_at: <DATETIME>)
40
+ refresh_token_expiration_at: <DATETIME> # Not required, but should be known
41
+ )
31
42
 
32
43
  # Or with just auth_token (assuming it hasn't expired)
33
44
  auth_token = FieldView::AuthToken.new(access_token: <ATOKEN>)
@@ -35,6 +46,15 @@ auth_token = FieldView::AuthToken.new(access_token: <ATOKEN>)
35
46
  # refresh token and a new access/refresh token will be associated with the object
36
47
  auth_token = FieldView::AuthToken.new(refresh_token: <RTOKEN>)
37
48
 
49
+ # Then you can do something like this:
50
+
51
+ fields = FieldView::Fields.list(auth_token)
52
+
53
+ fields.each do |field|
54
+ puts field.boundary
55
+ end
56
+
57
+ fields.has_more?
38
58
 
39
59
  ```
40
60
 
@@ -52,6 +72,13 @@ Run a single test:
52
72
 
53
73
  bundle exec ruby -Ilib/ test/field_view_test.rb -n /client.id/
54
74
 
75
+ ## TODOs
76
+
77
+ - [ ] Thread safety of the auth token object since it's passed to all created objects
78
+ - [ ] Probably add configurable behavior for auto-refresh
79
+ - [ ] Change the configuration to non-static variables (at least the items that are required)
80
+ - [ ] Use faraday so that we are middleware agnostic
81
+
55
82
  ## Disclaimer
56
83
 
57
84
  This Gem is in no way associated with The Climate Corporation, and they are in no way associated with it's support, maintenance, or updates.
data/fieldview.gemspec CHANGED
@@ -1,18 +1,18 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
2
 
3
+ require 'fieldview/version'
4
+
3
5
  spec = Gem::Specification.new do |s|
4
6
  s.name = 'fieldview'
5
- s.version = "0.0.0"
7
+ s.version = FieldView::VERSION
6
8
  s.required_ruby_version = '>= 1.9.3'
7
9
  s.summary = 'Ruby bindings for the FieldView API'
8
- s.description = ' FieldView is used make data-driven decisions to maximize your return on every acre.'
10
+ s.description = ' FieldView is used to make data-driven decisions to maximize your return on every acre. This Ruby Gem is provided as a convenient way to access their API.'
9
11
  s.author = 'Paul Susmarski'
10
12
  s.email = 'paul@susmarski.com'
11
13
  s.homepage = 'http://rubygems.org/gems/fielview'
12
14
  s.license = 'MIT'
13
15
 
14
- s.add_dependency('faraday', '~> 0.9')
15
-
16
16
  s.files = Dir['lib/**/*.rb']
17
17
  s.files = `git ls-files`.split("\n")
18
18
  s.test_files = `git ls-files -- test/*`.split("\n")
@@ -12,14 +12,13 @@ module FieldView
12
12
  # When the access token expires we'll need to refresh,
13
13
  # can be specified as part of the object, 14399 is what is being
14
14
  # returned at the time of writing this (2017-04-11)
15
- self.access_token_expiration_at = params[:access_token_expiration_at] || (now + (params[:expires_in]||14399) - 10)
15
+ self.access_token_expiration_at = params[:access_token_expiration_at] || (now + (params[:expires_in]||14399))
16
16
 
17
17
  # Refresh token isn't required, but can be initialized with this
18
18
  self.refresh_token = params[:refresh_token]
19
19
 
20
- # Refresh token technically expires in 30 days, but
21
- # we'll subtract one day for safety
22
- self.refresh_token_expiration_at = params[:access_token_expiration_at] || (now + (30-1)*24*60*60)
20
+ # Refresh token technically expires in 30 days
21
+ self.refresh_token_expiration_at = params[:access_token_expiration_at] || (now + (30)*24*60*60)
23
22
  end
24
23
 
25
24
  def access_token_expired?
@@ -61,15 +60,27 @@ module FieldView
61
60
  http = Net::HTTP.new(uri.host, uri.port)
62
61
  http.use_ssl = FieldView.api_base.start_with?("https")
63
62
  request = Net::HTTP.class_eval(method.to_s.capitalize).new(uri.request_uri)
63
+ if method == :get then
64
+ new_query_ar = URI.decode_www_form(uri.query || '')
65
+ params.each do |key,value|
66
+ new_query_ar << [key.to_s, value.to_s]
67
+ end
68
+ uri.query = URI.encode_www_form(new_query_ar)
69
+ request = Net::HTTP.class_eval(method.to_s.capitalize).new(uri.request_uri)
70
+ else
71
+ if params.is_a?(Hash)
72
+ request.body = params.to_json()
73
+ else
74
+ request.body = params
75
+ end
76
+ end
64
77
  request["Accept"] = "*/*"
65
78
  request["Authorization"] = "Bearer #{self.access_token}"
66
79
  request["X-Api-Key"] = FieldView.x_api_key
67
80
  request["Content-Type"] = "application/json"
68
-
69
81
  headers.each do |header,value|
70
82
  request[header] = value.to_s
71
83
  end
72
-
73
84
  response = http.request(request)
74
85
  FieldView.handle_response_error_codes(response)
75
86
  return FieldViewResponse.new(response)
@@ -92,10 +103,10 @@ module FieldView
92
103
  end
93
104
 
94
105
  # Code will be collected from the redirect to your server
95
- def self.new_auth_token_with_code_from_redirect_code(code)
106
+ def self.new_auth_token_with_code_from_redirect_code(code, redirect_uri: nil)
96
107
  http, request = build_token_request([
97
108
  ["grant_type", "authorization_code"],
98
- ["redirect_uri", FieldView.redirect_uri],
109
+ ["redirect_uri", FieldView.redirect_uri || redirect_uri],
99
110
  ["code", code]
100
111
  ])
101
112
  response = http.request(request)
@@ -107,5 +118,12 @@ module FieldView
107
118
  return AuthToken.new(json)
108
119
  end
109
120
  end
121
+
122
+ def to_s()
123
+ [
124
+ "AccessToken: #{self.access_token}, Expiration: #{self.access_token_expiration_at}",
125
+ "RefreshToken: #{self.refresh_token}, Expiration: #{self.refresh_token_expiration_at}"
126
+ ].join('\n')
127
+ end
110
128
  end
111
129
  end
@@ -14,8 +14,14 @@ module FieldView
14
14
  attr_accessor :response
15
15
  # Initializes a FieldViewError.
16
16
  def initialize(message=nil, http_status: nil, http_body: nil,
17
- http_headers: nil)
17
+ http_headers: nil, fieldview_response: nil)
18
18
  @message = message
19
+ if fieldview_response then
20
+ self.response = fieldview_response
21
+ http_status = fieldview_response.http_status
22
+ http_body = fieldview_response.http_body
23
+ http_headers = fieldview_response.http_headers
24
+ end
19
25
  @http_status = http_status
20
26
  @http_body = http_body
21
27
  @http_headers = http_headers || {}
@@ -61,4 +67,10 @@ module FieldView
61
67
  # Raised when the server is busy
62
68
  class ServerBusyError < FieldViewError
63
69
  end
70
+
71
+ # Raised when a response code that is non-breaking is outside
72
+ # API specifications, ex: We expect a 201 when creating an upload, but it
73
+ # returns a 200
74
+ class UnexpectedResponseError < FieldViewError
75
+ end
64
76
  end
@@ -1,14 +1,13 @@
1
1
  module FieldView
2
- class Field
2
+ class Field < Requestable
3
3
  attr_accessor :id
4
4
  attr_accessor :name
5
5
  attr_accessor :boundary_id
6
- attr_accessor :auth_token
7
6
  def initialize(json_object, auth_token = nil)
8
7
  self.id = json_object[:id]
9
8
  self.name = json_object[:name]
10
9
  self.boundary_id = json_object[:boundaryId]
11
- self.auth_token = auth_token
10
+ super(auth_token)
12
11
  end
13
12
 
14
13
  def boundary
@@ -12,9 +12,13 @@ module FieldView
12
12
  end
13
13
 
14
14
  self.http_body = response.body
15
- begin
16
- self.data = JSON.parse(response.body, symbolize_names: true)
17
- rescue JSON::ParserError
15
+ if response.body then
16
+ begin
17
+ self.data = JSON.parse(response.body, symbolize_names: true)
18
+ rescue JSON::ParserError
19
+ self.data = nil
20
+ end
21
+ else
18
22
  self.data = nil
19
23
  end
20
24
  self.http_status = response.code.to_i
@@ -1,16 +1,16 @@
1
1
  module FieldView
2
- class ListObject
2
+ class ListObject < Requestable
3
3
  attr_accessor :limit
4
- attr_reader :auth_token, :data, :last_http_status, :next_token, :listable
4
+ attr_reader :data, :last_http_status, :next_token, :listable
5
5
  include Enumerable
6
6
 
7
7
  def initialize(listable, auth_token, data, http_status, next_token: nil, limit: 100)
8
8
  @listable = listable
9
- @auth_token = auth_token
10
9
  @data = data
11
10
  @last_http_status = http_status
12
11
  @next_token = next_token
13
12
  @limit = 100
13
+ super(auth_token)
14
14
  end
15
15
 
16
16
  def each(&blk)
@@ -0,0 +1,9 @@
1
+ module FieldView
2
+ class Requestable
3
+ attr_accessor :auth_token
4
+
5
+ def initialize(auth_token = nil)
6
+ self.auth_token = auth_token
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,62 @@
1
+ module FieldView
2
+ class Upload < Requestable
3
+ # 5 Megabytes is the current chunk size
4
+ REQUIRED_CHUNK_SIZE = 5*1024*1024
5
+ CHUNK_CONTENT_TYPE = "application/octet-stream"
6
+
7
+ PATH = "uploads"
8
+ attr_accessor :id, :content_length, :content_type
9
+
10
+ # Creates an upload with the specified items, all required.
11
+ # will return an a new upload object that has it's ID which can be
12
+ # used to upload. The following are known content types:
13
+ # image/vnd.climate.thermal.geotiff, image/vnd.climate.ndvi.geotiff,
14
+ # image/vnd.climate.chlorophyll.geotiff, image/vnd.climate.cci.geotiff,
15
+ # image/vnd.climate.waterstress.geotiff, image/vnd.climate.infrared.geotiff
16
+ def self.create(auth_token, md5, content_length, content_type)
17
+ response = auth_token.execute_request!(:post, PATH,
18
+ params: {
19
+ "md5" => md5,
20
+ "length" => content_length,
21
+ "content-type" => content_type
22
+ })
23
+
24
+ Util.verify_response_with_code("Upload creation", response, 201)
25
+
26
+ return new(response.http_body.gsub('"', ''), content_length, auth_token)
27
+ end
28
+
29
+ def initialize(id, content_length, auth_token = nil)
30
+ self.id = id
31
+ self.content_length = content_length.to_i
32
+ if self.content_length <= 0 then
33
+ raise ArgumentError.new("You must set content_length to a non-zero value")
34
+ end
35
+ super(auth_token)
36
+ end
37
+
38
+ # Upload a chunk of a file, specify the range of the bytes, 0 based, for the bytes
39
+ # you're passing, for example, the first 256 bytes would be start_bytes = 0 and
40
+ # end_bytes = 255, with bytes = 256 byte size object
41
+ def upload_chunk(start_bytes, end_bytes, bytes)
42
+ if (end_bytes.to_i - start_bytes.to_i + 1) != bytes.bytesize ||
43
+ bytes.bytesize > REQUIRED_CHUNK_SIZE then
44
+ raise ArgumentError.new("End bytes (#{end_bytes}) - Start bytes (#{start_bytes})" \
45
+ " must be equal to bytes (#{bytes}) and no greater than #{REQUIRED_CHUNK_SIZE}")
46
+ end
47
+ response = auth_token.execute_request!(:put, "#{PATH}/#{self.id}",
48
+ headers: {
49
+ "Content-Range" => "bytes #{start_bytes}-#{end_bytes}/#{self.content_length}",
50
+ "Content-Length" => bytes.bytesize,
51
+ "Content-Type" => CHUNK_CONTENT_TYPE,
52
+ "Transfer-Encoding" => "chunked"
53
+ },
54
+ params: bytes)
55
+
56
+ # We expect a 204
57
+ Util.verify_response_with_code("Chunk upload", response, 204)
58
+
59
+ return response
60
+ end
61
+ end
62
+ end
@@ -3,5 +3,12 @@ module FieldView
3
3
  def self.http_status_is_more_in_list?(http_status)
4
4
  return http_status.to_i == 206
5
5
  end
6
+
7
+ def self.verify_response_with_code(message, fieldview_response, *acceptable_response_codes)
8
+ if not acceptable_response_codes.include?(fieldview_response.http_status) then
9
+ raise UnexpectedResponseError.new("#{message} expects #{acceptable_response_codes}",
10
+ fieldview_response: fieldview_response)
11
+ end
12
+ end
6
13
  end
7
14
  end
@@ -0,0 +1,3 @@
1
+ module FieldView
2
+ VERSION = '0.0.1'
3
+ end
data/lib/fieldview.rb CHANGED
@@ -3,13 +3,18 @@ require 'json'
3
3
  require 'rbconfig'
4
4
  require 'base64'
5
5
 
6
+ # Version
7
+ require 'fieldview/version'
8
+
6
9
  # API Support Classes
7
10
  require 'fieldview/errors'
11
+ require 'fieldview/requestable'
8
12
  require 'fieldview/auth_token'
9
13
  require 'fieldview/fields'
10
14
  require 'fieldview/util'
11
15
  require 'fieldview/fieldview_response'
12
16
  require 'fieldview/field'
17
+ require 'fieldview/upload'
13
18
  require 'fieldview/list_object'
14
19
  require 'fieldview/boundary'
15
20
 
@@ -9,8 +9,8 @@ class TestAuthToken < Minitest::Test
9
9
  def check_token_matches_fixture(token)
10
10
  assert_equal FIXTURE[:access_token], token.access_token
11
11
  assert_equal FIXTURE[:refresh_token], token.refresh_token
12
- assert_equal FieldView.now + FIXTURE[:expires_in] - 15, token.access_token_expiration_at
13
- assert_equal FieldView.now - 5 + 29*24*60*60, token.refresh_token_expiration_at, "Should be 29 days in the future"
12
+ assert_equal FieldView.now + FIXTURE[:expires_in] - 5, token.access_token_expiration_at
13
+ assert_equal FieldView.now + 30*24*60*60 - 5, token.refresh_token_expiration_at, "Should be 30 days in the future"
14
14
  end
15
15
 
16
16
  def setup
data/test/test_helper.rb CHANGED
@@ -13,6 +13,9 @@ class Minitest::Test
13
13
  #
14
14
  API_FIXTURES = APIFixtures.new
15
15
 
16
+ def teardown
17
+ WebMock.reset!
18
+ end
16
19
 
17
20
  def next_token_headers(next_token = "AZXJKLA123")
18
21
  return {FieldView::NEXT_TOKEN_HEADER_KEY => next_token}
@@ -0,0 +1,89 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestUpload < Minitest::Test
4
+
5
+ def setup
6
+ setup_for_api_requests
7
+ end
8
+
9
+ def teardown
10
+ teardown_for_api_request
11
+ end
12
+
13
+ def test_expects_certain_status_code
14
+ stub_request(:post, /uploads/).
15
+ to_return(status: 200)
16
+
17
+ assert_raises(FieldView::UnexpectedResponseError) do
18
+ FieldView::Upload.create(new_auth_token, "dontcare", 5, "dontcare")
19
+ end
20
+ end
21
+
22
+ def test_create()
23
+ md5 = "eb782356d6011dab2b112bdcb098ec7d"
24
+ uuid = "1a2623b3-d2a7-4e52-ba21-a3eb521a3208"
25
+ content_length = 275
26
+ content_type = "image/vnd.climate.thermal.geotiff"
27
+ stub_request(:post, /uploads/).
28
+ with(body: {
29
+ "md5" => md5,
30
+ "length" => content_length,
31
+ "content-type" => content_type
32
+ }).
33
+ to_return(status: 201, body: %Q!"#{uuid}"!)
34
+
35
+ auth_token = new_auth_token
36
+
37
+ upload = FieldView::Upload.create(auth_token, md5, content_length, content_type)
38
+
39
+ assert_equal uuid, upload.id
40
+ assert_equal content_length, upload.content_length
41
+ assert_equal auth_token, upload.auth_token
42
+ end
43
+
44
+ def test_checks_content_length
45
+ assert_raises(ArgumentError) do
46
+ # nil is going to result in 0 when converting to integer
47
+ FieldView::Upload.new(1,nil,new_auth_token)
48
+ end
49
+ end
50
+
51
+ def test_uploading_wrong_chunk_size
52
+ upload = FieldView::Upload.new("dontcare",
53
+ 50000000, new_auth_token)
54
+
55
+ bytes = "12345678"
56
+ start_bytes = 0
57
+ end_bytes = 5
58
+ assert_raises(ArgumentError, "Wrong byte start/end") do
59
+ upload.upload_chunk(start_bytes, end_bytes, bytes)
60
+ end
61
+
62
+ bytes = "A" * (FieldView::Upload::REQUIRED_CHUNK_SIZE + 1)
63
+ end_bytes = bytes.bytesize - 1
64
+ assert_raises(ArgumentError, "Larger than the accepted chunk size") do
65
+ upload.upload_chunk(start_bytes, end_bytes, bytes)
66
+ end
67
+ end
68
+
69
+ def test_can_upload_chunk
70
+ uuid = "1a2623b3-d2a7-4e52-ba21-a3eb521a3208"
71
+ total_content_length = FieldView::Upload::REQUIRED_CHUNK_SIZE * 2
72
+ upload = FieldView::Upload.new(uuid,
73
+ total_content_length, new_auth_token)
74
+
75
+ chunk = "A"
76
+ start_bytes = 0
77
+ end_bytes = chunk.bytesize - 1
78
+
79
+ stub_request(:put, /uploads\/#{uuid}/).
80
+ with(headers: {
81
+ content_range: "bytes #{start_bytes}-#{end_bytes}/#{total_content_length}",
82
+ content_length: chunk.bytesize.to_s,
83
+ content_type: "application/octet-stream",
84
+ transfer_encoding: "chunked"
85
+ }).
86
+ to_return(status: 204, body: nil)
87
+ assert upload.upload_chunk(start_bytes, end_bytes, chunk)
88
+ end
89
+ end
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fieldview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Susmarski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-15 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: faraday
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ~>
18
- - !ruby/object:Gem::Version
19
- version: '0.9'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ~>
25
- - !ruby/object:Gem::Version
26
- version: '0.9'
27
- description: ' FieldView is used make data-driven decisions to maximize your return
28
- on every acre.'
11
+ date: 2017-04-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: ' FieldView is used to make data-driven decisions to maximize your return
14
+ on every acre. This Ruby Gem is provided as a convenient way to access their API.'
29
15
  email: paul@susmarski.com
30
16
  executables: []
31
17
  extensions: []
@@ -46,7 +32,10 @@ files:
46
32
  - lib/fieldview/fields.rb
47
33
  - lib/fieldview/fieldview_response.rb
48
34
  - lib/fieldview/list_object.rb
35
+ - lib/fieldview/requestable.rb
36
+ - lib/fieldview/upload.rb
49
37
  - lib/fieldview/util.rb
38
+ - lib/fieldview/version.rb
50
39
  - spec/fixtures.json
51
40
  - test/api_fixtures.rb
52
41
  - test/test_auth_token.rb
@@ -56,6 +45,7 @@ files:
56
45
  - test/test_fieldview.rb
57
46
  - test/test_helper.rb
58
47
  - test/test_list_object.rb
48
+ - test/test_upload.rb
59
49
  homepage: http://rubygems.org/gems/fielview
60
50
  licenses:
61
51
  - MIT
@@ -89,3 +79,4 @@ test_files:
89
79
  - test/test_fieldview.rb
90
80
  - test/test_helper.rb
91
81
  - test/test_list_object.rb
82
+ - test/test_upload.rb