sheets_db 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/documentation/example_models/organization.rb +3 -0
- data/documentation/example_models/pet.rb +7 -0
- data/documentation/example_models/project.rb +6 -0
- data/documentation/example_models/task.rb +10 -0
- data/documentation/example_models/team.rb +8 -0
- data/documentation/example_models/user.rb +10 -0
- data/documentation/images/example_erd.png +0 -0
- data/documentation/images/example_spreadsheet_data.png +0 -0
- data/documentation/usage.md +184 -0
- data/lib/sheets_db/collection.rb +18 -0
- data/lib/sheets_db/resource.rb +68 -0
- data/lib/sheets_db/session.rb +34 -0
- data/lib/sheets_db/spreadsheet.rb +46 -0
- data/lib/sheets_db/support.rb +22 -0
- data/lib/sheets_db/version.rb +3 -0
- data/lib/sheets_db/worksheet/column.rb +18 -0
- data/lib/sheets_db/worksheet/row.rb +221 -0
- data/lib/sheets_db/worksheet.rb +124 -0
- data/lib/sheets_db.rb +11 -0
- data/sheets_db.gemspec +27 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0522857a175930f3d8da223feca5a4c6c2776416
|
4
|
+
data.tar.gz: 8675133f8dc45b598ec153bd15a2e0929ab518ec
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 829c7a9ba78e488acb5b7a53f1e73dfb8c81f0852269e20b318490dc239fe448fbf35df7cf9081f3c86cbe66f255c2ac21f7db7f8bfc8123b3bfeb3d86440fa0
|
7
|
+
data.tar.gz: d92a4221ac985d3075af370a7137d4ed305437d4b854b234068864a6268e61f2b37b8a81517beaf887cf5d76715d97feb1a28c0eef4430c0bf781f50074d1329
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at ravi@ablschools.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Ravi Gadad
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# SheetsDB
|
2
|
+
|
3
|
+
SheetsDB is essentially a Ruby ORM for tabular and relational data stored in Google Sheets. While storing relational data in a spreadsheet is kind of like using a pint glass to hammer a nail, there are situations in which this can be useful, especially for prototyping or collaborating with people who aren't as comfortable seeding data into an RDBMS.
|
4
|
+
|
5
|
+
There are some conventions you must follow in your Google drive folders and the Sheets themselves, but most setup of attributes, primitive type mapping, etc. is done within your object classes themselves.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'sheets_db'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install sheets_db
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
View usage documentation [here](documentation/usage.md).
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sheets_db. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
|
+
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
41
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sheets_db"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
class Project < SheetsDB::Spreadsheet
|
2
|
+
has_many :tasks, worksheet_name: "Tasks", class_name: "Task"
|
3
|
+
has_many :users, worksheet_name: "Users", class_name: "User"
|
4
|
+
has_many :pets, worksheet_name: "Pets", class_name: "Pet"
|
5
|
+
belongs_to_many :teams, class_name: "Team"
|
6
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Task < SheetsDB::Worksheet::Row
|
2
|
+
attribute :id, type: Integer
|
3
|
+
attribute :name
|
4
|
+
attribute :creator_id, type: Integer
|
5
|
+
attribute :assignee_id, type: Integer
|
6
|
+
attribute :finished_at, type: DateTime
|
7
|
+
|
8
|
+
has_one :creator, from_collection: :users, key: :creator_id
|
9
|
+
has_one :assignee, from_collection: :users, key: :assignee_id
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class User < SheetsDB::Worksheet::Row
|
2
|
+
attribute :id, type: Integer
|
3
|
+
attribute :first_name
|
4
|
+
attribute :last_name
|
5
|
+
attribute :pet_ids, multiple: true
|
6
|
+
|
7
|
+
has_many :pets, from_collection: :pets, key: :pet_ids
|
8
|
+
|
9
|
+
belongs_to_many :created_tasks, from_collection: :tasks, foreign_key: :creator_id
|
10
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# SheetsDB
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
**NOTE**: Currently, SheetsDB only supports reading and updating entities within Google Drive and Sheets; adding new entities or deleting existing ones is not available yet. Also, updating is restricted to worksheet rows, so folders and spreadsheets are not yet renamable using SheetsDB.
|
6
|
+
|
7
|
+
### The Data Model
|
8
|
+
|
9
|
+
For the sake of documentation, we'll set up an example data model that looks like the following:
|
10
|
+
|
11
|
+
![](images/example_erd.png)
|
12
|
+
|
13
|
+
SheetsDB works with several layers of the hierarchy in Google Sheets and Drive (within a worksheet, across worksheets in a Sheet, and across folders in Drive). We'll briefly go over each of these, and then later we'll dive more deeply.
|
14
|
+
|
15
|
+
#### Worksheets
|
16
|
+
|
17
|
+
Worksheets are the best analog we have to database tables - they contain rows, and we can use one of the rows as a header to establish columns. In SheetsDB, a worksheet is the only way we can represent arbitrary tabular data (outside of relationships that map directly to the folder and file structure in Google Drive). Therefore, for the example above, we'll need to use worksheets to describe the `Task`, `User`, and `Pet` models.
|
18
|
+
|
19
|
+
#### Spreadsheets
|
20
|
+
|
21
|
+
All worksheets live in a spreadsheet, and there is no way to access a worksheet or its rows outside of the spreadsheet context. Therefore, there will always be a single parent relationship from the worksheet-based models to a spreadsheet-based model, in this case `Project`. Note that we can't persist any arbitrary metadata at the project level in this example - if we needed to, `Project` would need to become a worksheet-based model. Within the spreadsheet, worksheets can contain rows having relationships to rows in other worksheets.
|
22
|
+
|
23
|
+
#### Folders
|
24
|
+
|
25
|
+
Folders can contain spreadsheets, other folders, or both (obviously Drive folders can also contain other types of files, but SheetsDB ignores these). They inherently have a zero-to-many relationship with subfolders or any number of spreadsheets, and also (unless they are root folders) can have a one-to-many relationship to parent folders. Therefore, `Organization` and `Team` will both be folder-based models.
|
26
|
+
|
27
|
+
Note that subfolders within a folder-based model must all be the same type - there is no support for different kinds of subfolder relationships from a single folder type. For example, in the data model we're working with, we couldn't have some subfolders of an `Organization` folder be `Team` folders while others were "`Building`" folders. The same goes for spreadsheets within a folder-based model - only one spreadsheet type relationship is possible.
|
28
|
+
|
29
|
+
You *can*, however, have a single type of subfolder relationship and also a single type of spreadsheet relationship. In the example above, that would mean a `Team` folder could have many spreadsheets, which would all have to be set up as `Project` spreadsheets; but also many subfolders, which would all have to be set up as the same kind of folder-based model.
|
30
|
+
|
31
|
+
### Setting Up the Structure in Google Drive
|
32
|
+
|
33
|
+
To create the structures in Google Drive to represent our example data model, we'll start at the top and work our way down the hierarchy.
|
34
|
+
|
35
|
+
#### Folders
|
36
|
+
|
37
|
+
First, go into your Google Drive account and create a new folder that will represent the top of our hierarchy. It doesn't matter where you create this folder, since we'll reference it directly by its id within Google Drive; it can be at the root, or deep within nested folders. Let's call this folder "Organization A" (though the folder name is meaningless); we'll map this to an `Organization` instance in SheetsDB.
|
38
|
+
|
39
|
+
Since our data model specifies that an `Organization` has zero-to-many `Team`s, and `Team` is a folder-based model, all subfolders in our new folder will represent teams. Navigate into the "Organization A" folder and create two new folders: "Team Alpha" and "Team Bravo". We're creating two just to demonstrate the subfolder relationship, but we'll work only in "Team Alpha" for now.
|
40
|
+
|
41
|
+
#### Spreadsheets
|
42
|
+
|
43
|
+
Navigate into the "Team Alpha" folder. This folder represents a `Team` in our data model, and `Team`s have many `Project`s. A `Project` is a spreadsheet-based model, so let's create a new Google Sheets spreadsheet in this folder. Name your new empty spreadsheet "Test Project 1". Later, when you want to create a new `Project`, you'll go back into one of your `Team` folders and create a new spreadsheet there; for now, we'll just use a single `Project` spreadsheet.
|
44
|
+
|
45
|
+
#### Worksheets
|
46
|
+
|
47
|
+
Now we get to the fun part - representing tabular data and relationships between worksheets. In our data model, a `Project` has three child relationships - `Task`s, `User`s, and `Pet`s. Therefore, let's create three worksheets. What you name these worksheets is flexible and doesn't have to be the same as the relationship name, but obviously it'll be less confusing to map them semantically. So let's call the three worksheets "Tasks", "Users", and "Pets".
|
48
|
+
|
49
|
+
Within the "Tasks" worksheet, set up a header row with each cell being one of the `Task` column names from our data model (`id`, `name`, `creator_id`, `assignee_id`, and `finished_at`). Repeat this for the "Users" worksheet with `User` columns (`id`, `first_name`, `last_name`, and `pet_ids`), and the "Pets" worksheet with `Pet` columns (`id`, `first_name`, and `favorite_colors`).
|
50
|
+
|
51
|
+
Now let's fill in some dummy data. Make sure any entries in association columns (like `creator_id` and `pet_ids`) reference the `id` column in their respective child worksheets. For multiple value columns (`pet_ids` and `favorite_colors`), use a comma-separated list of values (__currently, values that contain commas themselves aren't supported - we'll add escaping in a later version__).
|
52
|
+
|
53
|
+
When you're done, your spreadsheet should look similar to the following:
|
54
|
+
|
55
|
+
![](images/example_spreadsheet_data.png)
|
56
|
+
|
57
|
+
**A few things to note:**
|
58
|
+
|
59
|
+
1. Empty cells will result in null values. Blank string values are currently not supported in any way.
|
60
|
+
2. The `finished_at` column actually represents a date & time in Google Sheets and not an arbitrary string field; you can confirm this by highlighting one of those cells and seeing the value in the formula bar. If you want to use a column as a Ruby `DateTime` in SheetsDB, make sure the actual data is formatted as `%m/%d/%Y %H:%M:%S` (single digit months and days are acceptable).
|
61
|
+
3. In comma-separated lists of values, spaces after commas are ignored, so "1, 2, 3" works just as well as "1,2,3".
|
62
|
+
|
63
|
+
### Accessing the Data
|
64
|
+
|
65
|
+
Now that we're done entering all our data, let's set up our models in Ruby so we can start reading and updating this data. All the example model classes below are also in the [examples](example_models) directory.
|
66
|
+
|
67
|
+
#### Creating a Session
|
68
|
+
|
69
|
+
To access your files in Google Drive, you need to start by creating a SheetsDB::Session. This is done by wrapping a GoogleDrive::Session. There are several ways to do this, all described in the `google_drive` gem's documentation [here](https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md). Note that the GoogleDrive gem is already available to you if you're using SheetsDB, as it is a loaded dependency.
|
70
|
+
|
71
|
+
Once you have a GoogleDrive::Session, create a SheetsDB::Session using it:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
google_drive_session = GoogleDrive::Session.from_config("config.json")
|
75
|
+
sheets_db_session = SheetsDB::Session.new(google_drive_session)
|
76
|
+
```
|
77
|
+
|
78
|
+
You'll then use this session when fetching Sheets or folders from Google Drive, by id:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# in this example, Organization is a class inheriting from SheetsDB::Collection
|
82
|
+
Organization.find_by_id("b3FeFCg3p-IyjdL27crOR", session: sheets_db_session)
|
83
|
+
```
|
84
|
+
|
85
|
+
However, in cases where you're using a single session for all your requests to Drive (for example, if you're using a service account rather than OAuth), you can set an instance of SheetsDB::Session as your default, and that default will be used for your requests until it's changed or the class is reloaded.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
SheetsDB::Session.default = sheets_db_session
|
89
|
+
Organization.find_by_id("b3FeFCg3p-IyjdL27crOR")
|
90
|
+
```
|
91
|
+
|
92
|
+
Make sure the user or service account you've authenticated as, in your session, has access to the folder hierarchy we created earlier.
|
93
|
+
|
94
|
+
Great, now we have a session and we can fetch resources from Google Drive!
|
95
|
+
|
96
|
+
#### Folder Models
|
97
|
+
|
98
|
+
Model classes representing folders should inherit from `SheetsDB::Collection`, and can have relationships to subfolders using the `has_many` class method. Here is our `Organization` class:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class Organization < SheetsDB::Collection
|
102
|
+
has_many :teams, class_name: "Team"
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
Our `Team` class is similar, but specifies that `Project` children are spreadsheets (instead of subfolders), and also uses the `belongs_to_many` method to define the parent relationship to `Organization`. Note that there is no way to restrict a folder or file's parent structure to allow only one parent; therefore, there is no `belongs_to_one` method. If you know your structure will only ever have a single parent for a given resource, you could set up a method to access the first element of this collection, as we've done in the `Team` class:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class Team < SheetsDB::Collection
|
110
|
+
has_many :projects, class_name: "Project", resource_type: :spreadsheets
|
111
|
+
belongs_to_many :organizations, class_name: "Organization"
|
112
|
+
|
113
|
+
def organization
|
114
|
+
organizations.first
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
#### Spreadsheet Models
|
120
|
+
|
121
|
+
The `Project` model uses a different base class (`SheetsDB::Spreadsheet`). From this type of model class, the `has_many` method references worksheets instead of subfolders or spreadsheets, so it has a different signature:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class Project < SheetsDB::Spreadsheet
|
125
|
+
has_many :tasks, worksheet_name: "Tasks", class_name: "Task"
|
126
|
+
has_many :users, worksheet_name: "Users", class_name: "User"
|
127
|
+
has_many :pets, worksheet_name: "Pets", class_name: "Pet"
|
128
|
+
belongs_to_many :teams, class_name: "Team"
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
#### Worksheet Row Models
|
133
|
+
|
134
|
+
Finally, we get to the worksheets. We're not trying to model the worksheets themselves - those are analogous to the schema of a table. What we're modeling is rows within the worksheets, so each of these models will inherit from `SheetsDB::Worksheet::Row`. We'll need to specify what the attributes are, to set up accessor methods and to specify their types; we'll also be setting up relationships to other worksheet models. Here are the three models:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
class Task < SheetsDB::Worksheet::Row
|
138
|
+
attribute :id, type: Integer
|
139
|
+
attribute :name
|
140
|
+
attribute :creator_id, type: Integer
|
141
|
+
attribute :assignee_id, type: Integer
|
142
|
+
attribute :finished_at, type: DateTime
|
143
|
+
|
144
|
+
has_one :creator, from_collection: :users, key: :creator_id
|
145
|
+
has_one :assignee, from_collection: :users, key: :assignee_id
|
146
|
+
end
|
147
|
+
|
148
|
+
class User < SheetsDB::Worksheet::Row
|
149
|
+
attribute :id, type: Integer
|
150
|
+
attribute :first_name
|
151
|
+
attribute :last_name
|
152
|
+
attribute :pet_ids, multiple: true
|
153
|
+
|
154
|
+
has_many :pets, from_collection: :pets, key: :pet_ids
|
155
|
+
|
156
|
+
belongs_to_many :created_tasks, from_collection: :tasks, foreign_key: :creator_id
|
157
|
+
end
|
158
|
+
|
159
|
+
class Pet < SheetsDB::Worksheet::Row
|
160
|
+
attribute :id
|
161
|
+
attribute :first_name, transform: ->(first_name) { first_name.upcase }
|
162
|
+
attribute :favorite_colors, multiple: true
|
163
|
+
|
164
|
+
belongs_to_one :parent, from_collection: :users, foreign_key: :pet_ids
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
##### The Attribute Method
|
169
|
+
|
170
|
+
The only required argument to the `attribute` class method is the name of the column itself. All other options are optional:
|
171
|
+
|
172
|
+
`type`: The `attribute` method currently supports three types: `Integer`, `DateTime`, and the default `String`. Any other value given to the `type` option will be ignored and the default will be used.
|
173
|
+
|
174
|
+
`multiple`: This option defaults to false. If true, it tells SheetsDB to parse the value as a comma-separated list of values, and return an array by splitting on the comma (if no comma is found, it will return a single element array).
|
175
|
+
|
176
|
+
`transform`: If you supply a Proc (or lambda) to the `transform` option, reading this column from the spreadsheet will cause it to first be sent through this transformation Proc. For `multiple` columns, each element will be transformed independently.
|
177
|
+
|
178
|
+
##### `has_many` and `has_one` associations
|
179
|
+
|
180
|
+
For associations in which the key exists in the specifying row, use `has_many` or `has_one`. The `from_collection` parameter should reference the `has_many` entry on the Spreadsheet model that maps to the target worksheet, and the `key` parameter is the name of the local column that maps to the foreign worksheet's `id` column.
|
181
|
+
|
182
|
+
##### `belongs_to_many` and `belongs_to_one` associations
|
183
|
+
|
184
|
+
For associations in which the key exists in the foreign row, use `belongs_to_many` or `belongs_to_one`. The `from_collection` parameter should reference the `has_many` entry on the Spreadsheet model that maps to the target worksheet, and the `foreign_key` parameter is the name of the foreign worksheet's column that maps to the local worksheet's `id` column.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SheetsDB
|
2
|
+
class Collection < Resource
|
3
|
+
set_resource_type GoogleDrive::Collection
|
4
|
+
|
5
|
+
def self.has_many(resource, class_name:, resource_type: :subcollections)
|
6
|
+
unless [:subcollections, :spreadsheets].include?(resource_type)
|
7
|
+
raise ArgumentError, "resource_type must be :subcollections or :spreadsheets"
|
8
|
+
end
|
9
|
+
register_association(resource, class_name: class_name, resource_type: resource_type)
|
10
|
+
define_method(resource) do
|
11
|
+
result = instance_variable_get(:"@#{resource}")
|
12
|
+
result || instance_variable_set(:"@#{resource}",
|
13
|
+
google_drive_resource.send(resource_type).map { |raw| Support.constantize(class_name).new(raw) }
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module SheetsDB
|
2
|
+
class Resource
|
3
|
+
class ResourceTypeMismatchError < StandardError; end
|
4
|
+
class CollectionTypeAlreadyRegisteredError < StandardError; end
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :resource_type
|
8
|
+
|
9
|
+
def set_resource_type(resource_type)
|
10
|
+
@resource_type = resource_type
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_by_id(id, session: SheetsDB::Session.default)
|
14
|
+
google_drive_resource = session.raw_file_by_id(id)
|
15
|
+
if @resource_type && !google_drive_resource.is_a?(@resource_type)
|
16
|
+
fail(ResourceTypeMismatchError, "The file with id #{id} is not a #{@resource_type}")
|
17
|
+
end
|
18
|
+
new(google_drive_resource)
|
19
|
+
end
|
20
|
+
|
21
|
+
def belongs_to_many(resource, class_name:)
|
22
|
+
register_association(resource, class_name: class_name, resource_type: :parents)
|
23
|
+
define_method(resource) do
|
24
|
+
result = instance_variable_get(:"@#{resource}")
|
25
|
+
result || instance_variable_set(:"@#{resource}",
|
26
|
+
google_drive_resource.parents.map { |id| Support.constantize(class_name).find_by_id(id) }
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def register_association(resource, class_name:, resource_type:)
|
32
|
+
@associations ||= {}
|
33
|
+
if @associations.values.any? { |value| value[:resource_type] == resource_type }
|
34
|
+
raise CollectionTypeAlreadyRegisteredError
|
35
|
+
end
|
36
|
+
@associations[resource] = {
|
37
|
+
resource_type: resource_type,
|
38
|
+
class_name: class_name
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
extend Forwardable
|
44
|
+
def_delegators :google_drive_resource, :id, :name
|
45
|
+
def_delegator :google_drive_resource, :created_time, :created_at
|
46
|
+
def_delegator :google_drive_resource, :modified_time, :updated_at
|
47
|
+
|
48
|
+
attr_reader :google_drive_resource
|
49
|
+
|
50
|
+
def initialize(google_drive_resource)
|
51
|
+
@google_drive_resource = google_drive_resource
|
52
|
+
end
|
53
|
+
|
54
|
+
def ==(other)
|
55
|
+
other.is_a?(self.class) &&
|
56
|
+
other.google_drive_resource == google_drive_resource
|
57
|
+
end
|
58
|
+
|
59
|
+
def base_attributes
|
60
|
+
{
|
61
|
+
id: id,
|
62
|
+
name: name,
|
63
|
+
created_at: created_at,
|
64
|
+
updated_at: updated_at
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "google_drive"
|
2
|
+
|
3
|
+
module SheetsDB
|
4
|
+
class Session
|
5
|
+
class IllegalDefaultError < StandardError; end
|
6
|
+
class NoDefaultSetError < StandardError; end
|
7
|
+
|
8
|
+
def self.default=(default)
|
9
|
+
unless default.is_a?(self)
|
10
|
+
raise IllegalDefaultError.new("Default must be a SheetsDB::Session")
|
11
|
+
end
|
12
|
+
@default = default
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default
|
16
|
+
unless @default
|
17
|
+
raise NoDefaultSetError.new("No default Session defined yet")
|
18
|
+
end
|
19
|
+
@default
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_service_account_key(*args)
|
23
|
+
new(GoogleDrive::Session.from_service_account_key(*args))
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(google_drive_session)
|
27
|
+
@google_drive_session = google_drive_session
|
28
|
+
end
|
29
|
+
|
30
|
+
def raw_file_by_id(id)
|
31
|
+
@google_drive_session.file_by_id(id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module SheetsDB
|
2
|
+
class Spreadsheet < Resource
|
3
|
+
class WorksheetAssociationAlreadyRegisteredError < StandardError; end
|
4
|
+
|
5
|
+
set_resource_type GoogleDrive::Spreadsheet
|
6
|
+
|
7
|
+
def self.has_many(resource, worksheet_name:, class_name:)
|
8
|
+
register_worksheet_association(resource, worksheet_name: worksheet_name, class_name: class_name)
|
9
|
+
define_method(resource) do
|
10
|
+
@worksheets ||= {}
|
11
|
+
@worksheets[resource] ||= Worksheet.new(
|
12
|
+
spreadsheet: self,
|
13
|
+
google_drive_resource: google_drive_resource.worksheet_by_title(worksheet_name),
|
14
|
+
type: Support.constantize(class_name)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.register_worksheet_association(resource, worksheet_name:, class_name:)
|
20
|
+
@associations ||= {}
|
21
|
+
if @associations.fetch(resource, nil)
|
22
|
+
raise WorksheetAssociationAlreadyRegisteredError
|
23
|
+
end
|
24
|
+
@associations[resource] = {
|
25
|
+
worksheet_name: worksheet_name,
|
26
|
+
class_name: class_name
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_association_by_id(association_name, id)
|
31
|
+
find_associations_by_ids(association_name, [id]).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_associations_by_ids(association_name, ids)
|
35
|
+
send(association_name).find_by_ids(ids)
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_associations_by_attribute(association_name, attribute_name, value)
|
39
|
+
send(association_name).find_by_attribute(attribute_name, value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def select_from_association(association_name, &block)
|
43
|
+
send(association_name).select(&block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SheetsDB
|
2
|
+
module Support
|
3
|
+
class << self
|
4
|
+
def camelize(string)
|
5
|
+
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
|
6
|
+
string = string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
|
7
|
+
end
|
8
|
+
|
9
|
+
def constantize(string)
|
10
|
+
name_parts = camelize(string).split('::')
|
11
|
+
name_parts.shift if name_parts.first.empty?
|
12
|
+
constant = Object
|
13
|
+
|
14
|
+
name_parts.each do |name_part|
|
15
|
+
constant_defined = constant.const_defined?(name_part, false)
|
16
|
+
constant = constant_defined ? constant.const_get(name_part) : constant.const_missing(name_part)
|
17
|
+
end
|
18
|
+
constant
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SheetsDB
|
2
|
+
class Worksheet
|
3
|
+
class Column
|
4
|
+
attr_reader :name, :column_position
|
5
|
+
|
6
|
+
def initialize(name:, column_position:)
|
7
|
+
@name = name
|
8
|
+
@column_position = column_position
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
other.is_a?(self.class) &&
|
13
|
+
other.name == name &&
|
14
|
+
other.column_position == column_position
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module SheetsDB
|
2
|
+
class Worksheet
|
3
|
+
class Row
|
4
|
+
class AttributeAlreadyRegisteredError < StandardError; end
|
5
|
+
class AssociationAlreadyRegisteredError < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :association_definitions
|
9
|
+
|
10
|
+
def inherited(subclass)
|
11
|
+
super
|
12
|
+
subclass.instance_variable_set(:@attribute_definitions, @attribute_definitions)
|
13
|
+
subclass.instance_variable_set(:@association_definitions, @association_definitions)
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute(name, type: String, multiple: false, transform: nil)
|
17
|
+
register_attribute(name, type: type, multiple: multiple, transform: transform, association: false)
|
18
|
+
|
19
|
+
define_method(name) do
|
20
|
+
begin
|
21
|
+
get_modified_attribute(name)
|
22
|
+
rescue KeyError
|
23
|
+
get_persisted_attribute(name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method("#{name}=") do |value|
|
28
|
+
stage_attribute_modification(name, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_association(name, from_collection:, key:, multiple: false)
|
33
|
+
register_attribute(name, from_collection: from_collection, key: key, multiple: multiple, association: true)
|
34
|
+
|
35
|
+
define_method(name) do
|
36
|
+
@loaded_associations[name] ||= begin
|
37
|
+
response = spreadsheet.find_associations_by_ids(from_collection, Array(send(key)))
|
38
|
+
multiple ? response : response.first
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
define_method("#{name}=") do |value|
|
43
|
+
assignment_value = multiple ? value.map(&:id) : value.id
|
44
|
+
send("#{key}=", assignment_value)
|
45
|
+
@loaded_associations[name] = value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_one(name, from_collection:, key:)
|
50
|
+
has_association(name, from_collection: from_collection, key: key, multiple: false)
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_many(name, from_collection:, key:)
|
54
|
+
has_association(name, from_collection: from_collection, key: key, multiple: true)
|
55
|
+
end
|
56
|
+
|
57
|
+
def belongs_to_association(name, from_collection:, foreign_key:, multiple: false)
|
58
|
+
register_attribute(name, from_collection: from_collection, foreign_key: foreign_key, multiple: multiple, association: true)
|
59
|
+
|
60
|
+
define_method(name) do
|
61
|
+
@loaded_associations[name] ||= begin
|
62
|
+
response = spreadsheet.find_associations_by_attribute(from_collection, foreign_key, id)
|
63
|
+
multiple ? response : response.first
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
define_method("#{name}=") do |value|
|
68
|
+
existing_values = Array(send(name))
|
69
|
+
Array(value).each do |foreign_item|
|
70
|
+
next if existing_values.delete(foreign_item)
|
71
|
+
foreign_item.add_element_to_attribute(foreign_key, id)
|
72
|
+
@changed_foreign_items << foreign_item
|
73
|
+
end
|
74
|
+
existing_values.each do |existing_foreign_item|
|
75
|
+
existing_foreign_item.remove_element_from_attribute(foreign_key, id)
|
76
|
+
@changed_foreign_items << existing_foreign_item
|
77
|
+
end
|
78
|
+
@loaded_associations[name] = value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def belongs_to_one(name, from_collection:, foreign_key:)
|
83
|
+
belongs_to_association(name, from_collection: from_collection, foreign_key: foreign_key, multiple: false)
|
84
|
+
end
|
85
|
+
|
86
|
+
def belongs_to_many(name, from_collection:, foreign_key:)
|
87
|
+
belongs_to_association(name, from_collection: from_collection, foreign_key: foreign_key, multiple: true)
|
88
|
+
end
|
89
|
+
|
90
|
+
def register_attribute(name, **options)
|
91
|
+
if attribute_definitions.fetch(name, nil)
|
92
|
+
raise AttributeAlreadyRegisteredError, name
|
93
|
+
end
|
94
|
+
attribute_definitions[name] = options
|
95
|
+
end
|
96
|
+
|
97
|
+
def attribute_definitions
|
98
|
+
@attribute_definitions ||= {}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :worksheet, :row_position, :loaded_attributes, :loaded_associations, :changed_foreign_items
|
103
|
+
|
104
|
+
def initialize(worksheet:, row_position:)
|
105
|
+
@worksheet = worksheet
|
106
|
+
@row_position = row_position
|
107
|
+
@loaded_attributes = {}
|
108
|
+
@loaded_associations = {}
|
109
|
+
@changed_foreign_items = []
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_modified_attribute(name)
|
113
|
+
loaded_attributes.fetch(name, {}).
|
114
|
+
fetch(:changed)
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_persisted_attribute(name)
|
118
|
+
loaded_attributes[name] ||= {}
|
119
|
+
loaded_attributes[name][:original] ||=
|
120
|
+
worksheet.attribute_at_row_position(name, row_position)
|
121
|
+
end
|
122
|
+
|
123
|
+
def stage_attribute_modification(name, value)
|
124
|
+
loaded_attributes[name] ||= {}
|
125
|
+
loaded_attributes[name][:changed] = value
|
126
|
+
end
|
127
|
+
|
128
|
+
def modify_collection_attribute(name, value, remove:)
|
129
|
+
attribute_definition = self.class.attribute_definitions.fetch(name, {})
|
130
|
+
existing_value = Array(send(name))
|
131
|
+
return if remove && !existing_value.include?(value)
|
132
|
+
return if !remove && existing_value.include?(value)
|
133
|
+
assignment_value = if attribute_definition[:multiple]
|
134
|
+
remove ? existing_value - [value] : existing_value.concat([value])
|
135
|
+
else
|
136
|
+
remove ? nil : value
|
137
|
+
end
|
138
|
+
send("#{name}=", assignment_value)
|
139
|
+
end
|
140
|
+
|
141
|
+
def add_element_to_attribute(name, value)
|
142
|
+
modify_collection_attribute(name, value, remove: false)
|
143
|
+
end
|
144
|
+
|
145
|
+
def remove_element_from_attribute(name, value)
|
146
|
+
modify_collection_attribute(name, value, remove: true)
|
147
|
+
end
|
148
|
+
|
149
|
+
def reload!
|
150
|
+
worksheet.reload!
|
151
|
+
reset_attributes_and_associations_cache
|
152
|
+
end
|
153
|
+
|
154
|
+
def save!
|
155
|
+
worksheet.update_attributes_at_row_position(staged_attributes, row_position: row_position)
|
156
|
+
save_changed_foreign_items!
|
157
|
+
reset_attributes_and_associations_cache
|
158
|
+
end
|
159
|
+
|
160
|
+
def save_changed_foreign_items!
|
161
|
+
changed_foreign_items.each do |foreign_item|
|
162
|
+
foreign_item.save!
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def reset_attributes_and_associations_cache
|
167
|
+
@loaded_attributes = {}
|
168
|
+
@loaded_associations = {}
|
169
|
+
end
|
170
|
+
|
171
|
+
def staged_attributes
|
172
|
+
loaded_attributes.each_with_object({}) { |(key, value), hsh|
|
173
|
+
next unless value
|
174
|
+
hsh[key] = value[:changed] if value[:changed]
|
175
|
+
hsh
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
def spreadsheet
|
180
|
+
worksheet.spreadsheet
|
181
|
+
end
|
182
|
+
|
183
|
+
def attributes
|
184
|
+
get_all_attributes do |options|
|
185
|
+
!options.fetch(:association, false)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def associations
|
190
|
+
get_all_attributes do |options|
|
191
|
+
options.fetch(:association, false)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def get_all_attributes(&block)
|
196
|
+
self.class.attribute_definitions.each_with_object({}) { |(name, options), memo|
|
197
|
+
memo[name] = send(name) if block.call(options)
|
198
|
+
memo
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
def to_hash(depth: 0)
|
203
|
+
hashed_associations = if depth > 0
|
204
|
+
Hash[
|
205
|
+
associations.map { |name, association|
|
206
|
+
association_hash = if association.is_a?(Array)
|
207
|
+
association.map { |item| item.to_hash(depth: depth - 1) }
|
208
|
+
else
|
209
|
+
association.to_hash(depth: depth - 1)
|
210
|
+
end
|
211
|
+
[name, association_hash]
|
212
|
+
}
|
213
|
+
]
|
214
|
+
else
|
215
|
+
{}
|
216
|
+
end
|
217
|
+
attributes.merge(hashed_associations)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require_relative "worksheet/column"
|
2
|
+
require_relative "worksheet/row"
|
3
|
+
|
4
|
+
module SheetsDB
|
5
|
+
class Worksheet
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :spreadsheet, :google_drive_resource, :type
|
9
|
+
|
10
|
+
def initialize(spreadsheet:, google_drive_resource:, type:)
|
11
|
+
@spreadsheet = spreadsheet
|
12
|
+
@google_drive_resource = google_drive_resource
|
13
|
+
@type = type
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
other.is_a?(self.class) &&
|
18
|
+
other.google_drive_resource == google_drive_resource &&
|
19
|
+
other.type == type
|
20
|
+
end
|
21
|
+
|
22
|
+
def columns
|
23
|
+
@columns ||= begin
|
24
|
+
{}.tap { |directory|
|
25
|
+
google_drive_resource.rows.first.each_with_index do |name, i|
|
26
|
+
unless name == ""
|
27
|
+
directory[name.to_sym] = Column.new(name: name.to_sym, column_position: i + 1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def attribute_definitions
|
35
|
+
type.attribute_definitions
|
36
|
+
end
|
37
|
+
|
38
|
+
def attribute_at_row_position(column_name, row_position)
|
39
|
+
attribute_definition = attribute_definitions.fetch(column_name, {})
|
40
|
+
column = columns[column_name]
|
41
|
+
raw_value = read_value_from_google_drive_resource(
|
42
|
+
dimensions: [row_position, column.column_position],
|
43
|
+
attribute_definition: attribute_definition
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_value_from_google_drive_resource(dimensions:, attribute_definition:)
|
48
|
+
raw_value = case attribute_definition[:type].to_s
|
49
|
+
when "DateTime"
|
50
|
+
google_drive_resource.input_value(*dimensions)
|
51
|
+
else
|
52
|
+
google_drive_resource[*dimensions]
|
53
|
+
end
|
54
|
+
if attribute_definition[:multiple]
|
55
|
+
raw_value.split(/,\s*/).map { |value| convert_value(value, attribute_definition) }
|
56
|
+
else
|
57
|
+
convert_value(raw_value, attribute_definition)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_attributes_at_row_position(staged_attributes, row_position:)
|
62
|
+
staged_attributes.each do |name, value|
|
63
|
+
column = columns[name]
|
64
|
+
definition = attribute_definitions[name]
|
65
|
+
assignment_value = definition[:multiple] ? value.join(",") : value
|
66
|
+
google_drive_resource[row_position, column.column_position] = assignment_value
|
67
|
+
end
|
68
|
+
google_drive_resource.synchronize
|
69
|
+
end
|
70
|
+
|
71
|
+
def each
|
72
|
+
return to_enum(:each) unless block_given?
|
73
|
+
(google_drive_resource.num_rows - 1).times do |i|
|
74
|
+
yield type.new(worksheet: self, row_position: i + 2)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def all
|
79
|
+
to_a
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_by_id(id)
|
83
|
+
find_by_ids([id]).first
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_by_ids(ids)
|
87
|
+
result = []
|
88
|
+
each do |model|
|
89
|
+
break if result.count == ids.count
|
90
|
+
if ids.include?(model.id)
|
91
|
+
result << model
|
92
|
+
end
|
93
|
+
end
|
94
|
+
result
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_by_attribute(attribute_name, value)
|
98
|
+
definition = attribute_definitions[attribute_name]
|
99
|
+
select { |item|
|
100
|
+
attribute = item.send(attribute_name)
|
101
|
+
definition[:multiple] ? attribute.include?(value) : attribute == value
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
def reload!
|
106
|
+
google_drive_resource.reload
|
107
|
+
end
|
108
|
+
|
109
|
+
def convert_value(raw_value, attribute_definition)
|
110
|
+
return nil if raw_value == ""
|
111
|
+
converted_value = case attribute_definition[:type].to_s
|
112
|
+
when "Integer"
|
113
|
+
raw_value.to_i
|
114
|
+
when "DateTime"
|
115
|
+
DateTime.strptime(raw_value, "%m/%d/%Y %H:%M:%S")
|
116
|
+
else
|
117
|
+
raw_value
|
118
|
+
end
|
119
|
+
attribute_definition[:transform] ?
|
120
|
+
attribute_definition[:transform].call(converted_value) :
|
121
|
+
converted_value
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/sheets_db.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "sheets_db/version"
|
2
|
+
require "sheets_db/support"
|
3
|
+
require "sheets_db/session"
|
4
|
+
require "sheets_db/resource"
|
5
|
+
require "sheets_db/collection"
|
6
|
+
require "sheets_db/spreadsheet"
|
7
|
+
require "sheets_db/worksheet"
|
8
|
+
|
9
|
+
module SheetsDB
|
10
|
+
# Your code goes here...
|
11
|
+
end
|
data/sheets_db.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sheets_db/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sheets_db"
|
8
|
+
spec.version = SheetsDB::VERSION
|
9
|
+
spec.authors = ["Ravi Gadad"]
|
10
|
+
spec.email = ["ravi@ablschools.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Adapter for pseudo-relational data stored in Google Sheets}
|
13
|
+
spec.homepage = "https://github.com/ablschools/sheets_db"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "google_drive", "~> 2.1"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "simplecov"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sheets_db
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ravi Gadad
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: google_drive
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.12'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.12'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- ravi@ablschools.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".ruby-version"
|
93
|
+
- ".travis.yml"
|
94
|
+
- CODE_OF_CONDUCT.md
|
95
|
+
- Gemfile
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- bin/console
|
100
|
+
- bin/setup
|
101
|
+
- documentation/example_models/organization.rb
|
102
|
+
- documentation/example_models/pet.rb
|
103
|
+
- documentation/example_models/project.rb
|
104
|
+
- documentation/example_models/task.rb
|
105
|
+
- documentation/example_models/team.rb
|
106
|
+
- documentation/example_models/user.rb
|
107
|
+
- documentation/images/example_erd.png
|
108
|
+
- documentation/images/example_spreadsheet_data.png
|
109
|
+
- documentation/usage.md
|
110
|
+
- lib/sheets_db.rb
|
111
|
+
- lib/sheets_db/collection.rb
|
112
|
+
- lib/sheets_db/resource.rb
|
113
|
+
- lib/sheets_db/session.rb
|
114
|
+
- lib/sheets_db/spreadsheet.rb
|
115
|
+
- lib/sheets_db/support.rb
|
116
|
+
- lib/sheets_db/version.rb
|
117
|
+
- lib/sheets_db/worksheet.rb
|
118
|
+
- lib/sheets_db/worksheet/column.rb
|
119
|
+
- lib/sheets_db/worksheet/row.rb
|
120
|
+
- sheets_db.gemspec
|
121
|
+
homepage: https://github.com/ablschools/sheets_db
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata: {}
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 2.5.1
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Adapter for pseudo-relational data stored in Google Sheets
|
145
|
+
test_files: []
|