attache-rails 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +172 -0
- data/Rakefile +5 -0
- data/app/assets/javascripts/attache.js +4 -0
- data/app/assets/javascripts/attache/bootstrap3.js +77 -0
- data/app/assets/javascripts/attache/bootstrap3.js.jsx +77 -0
- data/app/assets/javascripts/attache/cors_upload.js +87 -0
- data/app/assets/javascripts/attache/file_input.js +152 -0
- data/app/assets/javascripts/attache/file_input.js.jsx +152 -0
- data/app/assets/javascripts/attache/ujs.js +23 -0
- data/lib/attache/rails.rb +13 -0
- data/lib/attache/rails/engine.rb +7 -0
- data/lib/attache/rails/model.rb +57 -0
- data/lib/attache/rails/version.rb +5 -0
- data/lib/generators/attache/rails/templates/upgrade_v2_to_v3_migration.rb.erb +45 -0
- data/lib/generators/attache/rails/upgrade_v2_to_v3_generator.rb +57 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: de21791c6e1a726e7de66bd344db5355d5251114
|
4
|
+
data.tar.gz: 8c6d37a9ff4018386915af71e20b3b87fd0ae207
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9b78de46276eb9b77979c0e5782955b62921902491958c9b074747f4bccde6e5afe17e2d5851a7d1df4dfda3afdbd0425a9bcd884efcf5686a866b170990587e
|
7
|
+
data.tar.gz: c7ada47737eaa07cfeeb14e3078790b3e215b55fa4542f749cb61e16adbfd7ba0406b16596a2a87606fa00b4a45737ffc664ebb0b9cda984586dab1590b68c84
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 choonkeat
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# attache-rails
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/attache-rails)
|
4
|
+
[](https://travis-ci.org/choonkeat/attache-rails)
|
5
|
+
|
6
|
+
Ruby on Rails / ActiveRecord integration for [attache server](https://github.com/choonkeat/attache) using [attache/api](https://github.com/choonkeat/attache-api)
|
7
|
+
|
8
|
+
## Dependencies
|
9
|
+
|
10
|
+
[React](https://github.com/reactjs/react-rails), jQuery, Bootstrap 3
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
**WARNING: Please see upgrade notes below if you are upgrading from V2**
|
15
|
+
|
16
|
+
Install the attache-rails package from Rubygems:
|
17
|
+
|
18
|
+
``` bash
|
19
|
+
gem install attache-rails
|
20
|
+
```
|
21
|
+
|
22
|
+
Or add this to your `Gemfile`
|
23
|
+
|
24
|
+
``` ruby
|
25
|
+
gem "attache-rails"
|
26
|
+
```
|
27
|
+
|
28
|
+
Add the attache javascript to your `application.js`
|
29
|
+
|
30
|
+
``` javascript
|
31
|
+
//= require attache
|
32
|
+
```
|
33
|
+
|
34
|
+
If you want to customize the file upload look and feel, define your own react `<AttacheFilePreview/>` renderer *before* including the attache js. For example,
|
35
|
+
|
36
|
+
``` javascript
|
37
|
+
//= require ./my_attache_file_preview.js
|
38
|
+
//= require attache
|
39
|
+
```
|
40
|
+
|
41
|
+
The `AttacheRails.upgrade_fileinputs` idempotent function is setup to find all the elements with `enable-attache` css class and upgrade them to use the direct upload & preview javascript. If you wish to re-run this function any other time, e.g. hookup the `cocoon:after-insert` event, you may
|
42
|
+
|
43
|
+
``` javascript
|
44
|
+
$(document).on('cocoon:after-insert', AttacheRails.upgrade_fileinputs);
|
45
|
+
```
|
46
|
+
|
47
|
+
|
48
|
+
## Usage
|
49
|
+
|
50
|
+
### Database
|
51
|
+
|
52
|
+
To use `attache`, you only need to store the JSON attributes given to you after you've uploaded a file. So if you have an existing model, you only need to add a text column (PostgreSQL users see below)
|
53
|
+
|
54
|
+
``` bash
|
55
|
+
rails generate migration AddPhotoPathToUsers photo:text
|
56
|
+
```
|
57
|
+
|
58
|
+
To assign **multiple** images to **one** model, the same column can be used, although pluralized column name reads better
|
59
|
+
|
60
|
+
``` bash
|
61
|
+
rails generate migration AddPhotoPathToUsers photos:text
|
62
|
+
```
|
63
|
+
|
64
|
+
### Model
|
65
|
+
|
66
|
+
In your model, define whether it `has_one_attache` or `has_many_attaches`
|
67
|
+
|
68
|
+
``` ruby
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
has_many_attaches :photos
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
### New or Edit form
|
75
|
+
|
76
|
+
In your form, you would add some options to `file_field` using the `attache_options` helper method:
|
77
|
+
|
78
|
+
``` slim
|
79
|
+
= f.file_field :photos, f.object.avatar_options('64x64#')
|
80
|
+
```
|
81
|
+
|
82
|
+
If you were using `has_many_attaches` the file input will automatically allow multiple files, otherwise the file input will only accept 1 file.
|
83
|
+
|
84
|
+
|
85
|
+
NOTE: `64x64#` is just an example, you should define a suitable [geometry](http://www.imagemagick.org/Usage/resize/) for your form
|
86
|
+
|
87
|
+
### Strong Parameters
|
88
|
+
|
89
|
+
You'd need to permit the new field in your controller. For example, a strong parameters definition may look like this in your `Users` controller
|
90
|
+
|
91
|
+
``` ruby
|
92
|
+
def user_params
|
93
|
+
params.require(:user).permit(:name)
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
If you're only accepting a single file upload, change it to
|
98
|
+
|
99
|
+
``` ruby
|
100
|
+
def user_params
|
101
|
+
params.require(:user).permit(:name, :photo, attaches_discarded: [])
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
If you're accepting multiple file uploads, change it to
|
106
|
+
|
107
|
+
``` ruby
|
108
|
+
def user_params
|
109
|
+
params.require(:user).permit(:name, photos: [], attaches_discarded: [])
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
NOTE: You do not have to manage `params[:attaches_discarded]` yourself. It is automatically managed for you between the frontend javascript and the ActiveRecord integration: files that are discarded will be removed from the attache server when you update or destroy your model.
|
114
|
+
|
115
|
+
### Show
|
116
|
+
|
117
|
+
Use the `*_url` or `*_urls` methods (depending on whether you are accepting multiple files) to obtain full urls.
|
118
|
+
|
119
|
+
``` slim
|
120
|
+
= image_tag @user.photo_url('100x100#')
|
121
|
+
```
|
122
|
+
|
123
|
+
or
|
124
|
+
|
125
|
+
``` slim
|
126
|
+
- @user.photos_urls('200x200#').each do |url|
|
127
|
+
= image_tag url
|
128
|
+
```
|
129
|
+
|
130
|
+
### Environment configs
|
131
|
+
|
132
|
+
`ATTACHE_URL` points to the attache server. e.g. `http://localhost:9292`
|
133
|
+
|
134
|
+
`ATTACHE_UPLOAD_DURATION` refers to the number of seconds before a signed upload request is considered expired, e.g. `600`
|
135
|
+
|
136
|
+
`ATTACHE_SECRET_KEY` is the shared secret with the `attache` server. e.g. `t0psecr3t`
|
137
|
+
|
138
|
+
* If this variable is not set, then upload requests will not be signed & `ATTACHE_UPLOAD_DURATION` will be ignored
|
139
|
+
* If this variable is set, it must be the same value as `SECRET_KEY` is set on the `attache` server
|
140
|
+
|
141
|
+
### PostgreSQL
|
142
|
+
|
143
|
+
Take advantage of the [json support](http://guides.rubyonrails.org/active_record_postgresql.html#json) by using the [`json` or `jsonb` column types](http://www.postgresql.org/docs/9.4/static/functions-json.html) instead
|
144
|
+
|
145
|
+
``` bash
|
146
|
+
rails generate migration AddPhotoPathToUsers photo:json
|
147
|
+
```
|
148
|
+
|
149
|
+
This opens up the possibility to query inside the column, e.g.
|
150
|
+
|
151
|
+
``` ruby
|
152
|
+
User.where("photo ->> 'content_type' = ?", 'image/png')
|
153
|
+
```
|
154
|
+
|
155
|
+
## Upgrading from v2
|
156
|
+
|
157
|
+
`json` values in the database column has changed in v3. Previously, we are working with json *strings*, now we are working with json *objects*, aka `Hash`. i.e.
|
158
|
+
|
159
|
+
- in previous version, `@user.photos` value could be something like `["{\"path\":\"dirname/file.jpg\"}"]`. Notice that it is an array of 1 `String`.
|
160
|
+
- in v3, the value would be `[{"path"=>"dirname/file.jpg"}]`. Notice that it is an array of 1 `Hash`
|
161
|
+
|
162
|
+
If you're upgrading from V2, we have a generator that will create a migration file to fixup the data
|
163
|
+
|
164
|
+
```
|
165
|
+
rails g attache:rails:upgrade_v2_to_v3
|
166
|
+
```
|
167
|
+
|
168
|
+
NOTE: It is highly recommended that developers verify the migration with a dump of the production data in a staging environment. Please take a look at the generated migration file.
|
169
|
+
|
170
|
+
# License
|
171
|
+
|
172
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
if (typeof AttacheFilePreview === 'undefined') {
|
2
|
+
|
3
|
+
var AttacheFilePreview = React.createClass({displayName: "AttacheFilePreview",
|
4
|
+
|
5
|
+
getInitialState: function() {
|
6
|
+
return { srcWas: '' };
|
7
|
+
},
|
8
|
+
|
9
|
+
onSrcLoaded: function() {
|
10
|
+
this.setState({ srcWas: this.props.src });
|
11
|
+
},
|
12
|
+
|
13
|
+
render: function() {
|
14
|
+
var previewClassName = "attache-file-preview";
|
15
|
+
|
16
|
+
// progressbar
|
17
|
+
if (this.state.srcWas != this.props.src) {
|
18
|
+
previewClassName = previewClassName + " attache-loading";
|
19
|
+
var className = this.props.className || "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
|
20
|
+
var pctString = this.props.pctString || (this.props.src ? 100 : this.props.percentLoaded) + "%";
|
21
|
+
var pctDesc = this.props.pctDesc || (this.props.src ? 'Loading...' : pctString);
|
22
|
+
var pctStyle = { width: pctString, minWidth: '3em' };
|
23
|
+
var progress = (
|
24
|
+
React.createElement("div", {className: "progress"},
|
25
|
+
React.createElement("div", {className: className, role: "progressbar", "aria-valuenow": this.props.percentLoaded, "aria-valuemin": "0", "aria-valuemax": "100", style: pctStyle},
|
26
|
+
pctDesc
|
27
|
+
)
|
28
|
+
)
|
29
|
+
);
|
30
|
+
}
|
31
|
+
|
32
|
+
// img tag
|
33
|
+
if (this.props.src) {
|
34
|
+
var img = React.createElement("img", {src: this.props.src, onLoad: this.onSrcLoaded});
|
35
|
+
}
|
36
|
+
|
37
|
+
// combined
|
38
|
+
return (
|
39
|
+
React.createElement("div", {className: previewClassName},
|
40
|
+
progress,
|
41
|
+
img,
|
42
|
+
React.createElement("div", {className: "clearfix"},
|
43
|
+
React.createElement("div", {className: "pull-left"}, this.props.filename),
|
44
|
+
React.createElement("a", {href: "#remove", className: "pull-right", onClick: this.props.onRemove, title: "Click to remove"}, "×")
|
45
|
+
)
|
46
|
+
)
|
47
|
+
);
|
48
|
+
}
|
49
|
+
});
|
50
|
+
|
51
|
+
}
|
52
|
+
|
53
|
+
if (typeof AttachePlaceholder === 'undefined') {
|
54
|
+
|
55
|
+
var AttachePlaceholder = React.createClass({displayName: "AttachePlaceholder",
|
56
|
+
render: function() {
|
57
|
+
return (
|
58
|
+
React.createElement("div", {className: "attache-file-preview"},
|
59
|
+
React.createElement("img", {src: this.props.src})
|
60
|
+
)
|
61
|
+
);
|
62
|
+
}
|
63
|
+
});
|
64
|
+
|
65
|
+
}
|
66
|
+
|
67
|
+
if (typeof AttacheHeader === 'undefined') {
|
68
|
+
|
69
|
+
var AttacheHeader = React.createClass({displayName: "AttacheHeader",
|
70
|
+
render: function() {
|
71
|
+
return (
|
72
|
+
React.createElement("noscript", null)
|
73
|
+
);
|
74
|
+
}
|
75
|
+
});
|
76
|
+
|
77
|
+
}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
if (typeof AttacheFilePreview === 'undefined') {
|
2
|
+
|
3
|
+
var AttacheFilePreview = React.createClass({
|
4
|
+
|
5
|
+
getInitialState: function() {
|
6
|
+
return { srcWas: '' };
|
7
|
+
},
|
8
|
+
|
9
|
+
onSrcLoaded: function() {
|
10
|
+
this.setState({ srcWas: this.props.src });
|
11
|
+
},
|
12
|
+
|
13
|
+
render: function() {
|
14
|
+
var previewClassName = "attache-file-preview";
|
15
|
+
|
16
|
+
// progressbar
|
17
|
+
if (this.state.srcWas != this.props.src) {
|
18
|
+
previewClassName = previewClassName + " attache-loading";
|
19
|
+
var className = this.props.className || "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
|
20
|
+
var pctString = this.props.pctString || (this.props.src ? 100 : this.props.percentLoaded) + "%";
|
21
|
+
var pctDesc = this.props.pctDesc || (this.props.src ? 'Loading...' : pctString);
|
22
|
+
var pctStyle = { width: pctString, minWidth: '3em' };
|
23
|
+
var progress = (
|
24
|
+
<div className="progress">
|
25
|
+
<div className={className} role="progressbar" aria-valuenow={this.props.percentLoaded} aria-valuemin="0" aria-valuemax="100" style={pctStyle}>
|
26
|
+
{pctDesc}
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
);
|
30
|
+
}
|
31
|
+
|
32
|
+
// img tag
|
33
|
+
if (this.props.src) {
|
34
|
+
var img = <img src={this.props.src} onLoad={this.onSrcLoaded} />;
|
35
|
+
}
|
36
|
+
|
37
|
+
// combined
|
38
|
+
return (
|
39
|
+
<div className={previewClassName}>
|
40
|
+
{progress}
|
41
|
+
{img}
|
42
|
+
<div className="clearfix">
|
43
|
+
<div className="pull-left">{this.props.filename}</div>
|
44
|
+
<a href="#remove" className="pull-right" onClick={this.props.onRemove} title="Click to remove">×</a>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
);
|
48
|
+
}
|
49
|
+
});
|
50
|
+
|
51
|
+
}
|
52
|
+
|
53
|
+
if (typeof AttachePlaceholder === 'undefined') {
|
54
|
+
|
55
|
+
var AttachePlaceholder = React.createClass({
|
56
|
+
render: function() {
|
57
|
+
return (
|
58
|
+
<div className="attache-file-preview">
|
59
|
+
<img src={this.props.src} />
|
60
|
+
</div>
|
61
|
+
);
|
62
|
+
}
|
63
|
+
});
|
64
|
+
|
65
|
+
}
|
66
|
+
|
67
|
+
if (typeof AttacheHeader === 'undefined') {
|
68
|
+
|
69
|
+
var AttacheHeader = React.createClass({
|
70
|
+
render: function() {
|
71
|
+
return (
|
72
|
+
<noscript />
|
73
|
+
);
|
74
|
+
}
|
75
|
+
});
|
76
|
+
|
77
|
+
}
|
@@ -0,0 +1,87 @@
|
|
1
|
+
var AttacheCORSUpload = (function() {
|
2
|
+
var counter = 0;
|
3
|
+
|
4
|
+
AttacheCORSUpload.prototype.onComplete = function(uid, json) { };
|
5
|
+
AttacheCORSUpload.prototype.onProgress = function(uid, json) { };
|
6
|
+
AttacheCORSUpload.prototype.onError = function(uid, status) { alert(status); };
|
7
|
+
AttacheCORSUpload.prototype.createLocalThumbnail = function() { }; // for overwriting
|
8
|
+
|
9
|
+
function AttacheCORSUpload(options) {
|
10
|
+
if (options == null) options = {};
|
11
|
+
for (option in options) {
|
12
|
+
this[option] = options[option];
|
13
|
+
}
|
14
|
+
this.handleFileSelect(options.file_element, options.files);
|
15
|
+
}
|
16
|
+
|
17
|
+
AttacheCORSUpload.prototype.handleFileSelect = function(file_element, files) {
|
18
|
+
var f, output, _i, _len, _results, url, $ele, prefix;
|
19
|
+
$ele = $(file_element);
|
20
|
+
url = $ele.data('uploadurl');
|
21
|
+
if ($ele.data('hmac')) {
|
22
|
+
url = url +
|
23
|
+
"?hmac=" + encodeURIComponent($ele.data('hmac')) +
|
24
|
+
"&uuid=" + encodeURIComponent($ele.data('uuid')) +
|
25
|
+
"&expiration=" + encodeURIComponent($ele.data('expiration')) +
|
26
|
+
""
|
27
|
+
}
|
28
|
+
|
29
|
+
prefix = Date.now() + "_";
|
30
|
+
output = [];
|
31
|
+
_results = [];
|
32
|
+
for (_i = 0, _len = files.length; _i < _len; _i++) {
|
33
|
+
f = files[_i];
|
34
|
+
this.createLocalThumbnail(f); // if any
|
35
|
+
f.uid = prefix + (counter++);
|
36
|
+
this.onProgress(f.uid, { src: f.src, filename: f.name, percentLoaded: 0, bytesLoaded: 0, bytesTotal: f.size });
|
37
|
+
_results.push(this.performUpload(f, url));
|
38
|
+
}
|
39
|
+
return _results;
|
40
|
+
};
|
41
|
+
|
42
|
+
AttacheCORSUpload.prototype.createCORSRequest = function(method, url) {
|
43
|
+
var xhr;
|
44
|
+
xhr = new XMLHttpRequest();
|
45
|
+
if (xhr.withCredentials != null) {
|
46
|
+
xhr.open(method, url, true);
|
47
|
+
} else if (typeof XDomainRequest !== "undefined") {
|
48
|
+
xhr = new XDomainRequest();
|
49
|
+
xhr.open(method, url);
|
50
|
+
} else {
|
51
|
+
xhr = null;
|
52
|
+
}
|
53
|
+
return xhr;
|
54
|
+
};
|
55
|
+
|
56
|
+
AttacheCORSUpload.prototype.performUpload = function(file, url) {
|
57
|
+
var this_s3upload, xhr;
|
58
|
+
this_s3upload = this;
|
59
|
+
url = url + (url.indexOf('?') == -1 ? '?' : '&') + 'file=' + encodeURIComponent(file.name);
|
60
|
+
xhr = this.createCORSRequest('PUT', url);
|
61
|
+
if (!xhr) {
|
62
|
+
this.onError(file.uid, 'CORS not supported');
|
63
|
+
} else {
|
64
|
+
xhr.onload = function(e) {
|
65
|
+
if (xhr.status === 200) {
|
66
|
+
this_s3upload.onComplete(file.uid, JSON.parse(e.target.responseText));
|
67
|
+
} else {
|
68
|
+
return this_s3upload.onError(file.uid, xhr.status + ' ' + xhr.statusText);
|
69
|
+
}
|
70
|
+
};
|
71
|
+
xhr.onerror = function() {
|
72
|
+
return this_s3upload.onError(file.uid, 'Unable to reach server');
|
73
|
+
};
|
74
|
+
xhr.upload.onprogress = function(e) {
|
75
|
+
var percentLoaded;
|
76
|
+
if (e.lengthComputable) {
|
77
|
+
percentLoaded = Math.round((e.loaded / e.total) * 100);
|
78
|
+
return this_s3upload.onProgress(file.uid, { src: file.src, filename: file.name, percentLoaded: percentLoaded, bytesLoaded: e.loaded, bytesTotal: e.total });
|
79
|
+
}
|
80
|
+
};
|
81
|
+
}
|
82
|
+
return xhr.send(file);
|
83
|
+
};
|
84
|
+
|
85
|
+
return AttacheCORSUpload;
|
86
|
+
|
87
|
+
})();
|
@@ -0,0 +1,152 @@
|
|
1
|
+
var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
|
2
|
+
|
3
|
+
getInitialState: function() {
|
4
|
+
var files = {};
|
5
|
+
if (this.props['data-value']) $.each(JSON.parse(this.props['data-value']), function(uid, json) {
|
6
|
+
if (json) files[uid] = json;
|
7
|
+
});
|
8
|
+
return {files: files, attaches_discarded: [], uploading: 0 };
|
9
|
+
},
|
10
|
+
|
11
|
+
onRemove: function(uid, e) {
|
12
|
+
e.preventDefault();
|
13
|
+
e.stopPropagation();
|
14
|
+
|
15
|
+
var fieldname = this.getDOMNode().firstChild.name; // when 'user[avatar]'
|
16
|
+
var newfield = fieldname.replace(/\w+\](\[\]|)$/, 'attaches_discarded][]'); // become 'user[attaches_discarded][]'
|
17
|
+
|
18
|
+
this.state.attaches_discarded.push({ fieldname: newfield, path: this.state.files[uid].path });
|
19
|
+
delete this.state.files[uid];
|
20
|
+
|
21
|
+
this.setState(this.state);
|
22
|
+
},
|
23
|
+
|
24
|
+
performUpload: function(file_element, files) {
|
25
|
+
// user cancelled file chooser dialog. ignore
|
26
|
+
if (! files || files.length == 0) return;
|
27
|
+
if (! this.props.multiple) {
|
28
|
+
this.state.files = {};
|
29
|
+
files = [files[0]]; // array of 1 element
|
30
|
+
}
|
31
|
+
|
32
|
+
this.setState(this.state);
|
33
|
+
// upload the file via CORS
|
34
|
+
var that = this;
|
35
|
+
|
36
|
+
that.state.uploading = that.state.uploading + files.length;
|
37
|
+
if (! that.state.submit_buttons) that.state.submit_buttons = $("button,input[type='submit']", $(file_element).parents('form')[0]).filter(':not(:disabled)');
|
38
|
+
|
39
|
+
new AttacheCORSUpload({
|
40
|
+
file_element: file_element,
|
41
|
+
files: files,
|
42
|
+
onComplete: function() {
|
43
|
+
that.state.uploading--;
|
44
|
+
that.setFileValue.apply(this, arguments);
|
45
|
+
},
|
46
|
+
onProgress: this.setFileValue,
|
47
|
+
onError: function(uid, status) {
|
48
|
+
that.state.uploading--;
|
49
|
+
that.setFileValue(uid, { pctString: '90%', pctDesc: status, className: 'progress-bar progress-bar-danger' });
|
50
|
+
}
|
51
|
+
});
|
52
|
+
|
53
|
+
// we don't want the file binary to be uploaded in the main form
|
54
|
+
// so the actual file input is neutered
|
55
|
+
file_element.value = '';
|
56
|
+
},
|
57
|
+
|
58
|
+
onChange: function() {
|
59
|
+
var file_element = this.getDOMNode().firstChild;
|
60
|
+
this.performUpload(file_element, file_element && file_element.files);
|
61
|
+
},
|
62
|
+
|
63
|
+
onDragOver: function(e) {
|
64
|
+
e.stopPropagation();
|
65
|
+
e.preventDefault();
|
66
|
+
$(this.getDOMNode()).addClass('attache-dragover');
|
67
|
+
},
|
68
|
+
|
69
|
+
onDragLeave: function(e) {
|
70
|
+
e.stopPropagation();
|
71
|
+
e.preventDefault();
|
72
|
+
$(this.getDOMNode()).removeClass('attache-dragover');
|
73
|
+
},
|
74
|
+
|
75
|
+
onDrop: function(e) {
|
76
|
+
e.stopPropagation();
|
77
|
+
e.preventDefault();
|
78
|
+
var file_element = this.getDOMNode().firstChild;
|
79
|
+
this.performUpload(file_element, e.target.files || e.dataTransfer.files);
|
80
|
+
$(this.getDOMNode()).removeClass('attache-dragover');
|
81
|
+
},
|
82
|
+
|
83
|
+
setFileValue: function(key, value) {
|
84
|
+
this.state.files[key] = value;
|
85
|
+
this.setState(this.state);
|
86
|
+
},
|
87
|
+
|
88
|
+
render: function() {
|
89
|
+
var that = this;
|
90
|
+
|
91
|
+
if (that.state.uploading > 0) {
|
92
|
+
that.state.submit_buttons.attr('disabled', true);
|
93
|
+
} else if (that.state.submit_buttons) {
|
94
|
+
that.state.submit_buttons.attr('disabled', null);
|
95
|
+
}
|
96
|
+
|
97
|
+
var Header = eval(this.props['data-header-component'] || 'AttacheHeader');
|
98
|
+
var Preview = eval(this.props['data-preview-component'] || 'AttacheFilePreview');
|
99
|
+
var Placeholder = eval(this.props['data-placeholder-component'] || 'AttachePlaceholder');
|
100
|
+
|
101
|
+
var previews = [];
|
102
|
+
$.each(that.state.files, function(key, result) {
|
103
|
+
// json is input[value], drop non essential values
|
104
|
+
var copy = JSON.parse(JSON.stringify(result));
|
105
|
+
delete copy.src;
|
106
|
+
delete copy.filename;
|
107
|
+
var json = JSON.stringify(copy);
|
108
|
+
//
|
109
|
+
result.multiple = that.props.multiple;
|
110
|
+
if (result.path) {
|
111
|
+
var parts = result.path.split('/');
|
112
|
+
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
|
113
|
+
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
|
114
|
+
result.filename = result.src.split('/').pop().split(/[#?]/).shift();
|
115
|
+
}
|
116
|
+
var previewKey = "preview" + key;
|
117
|
+
previews.push(
|
118
|
+
React.createElement("div", {key: previewKey, className: "attache-file-input"},
|
119
|
+
React.createElement("input", {type: "hidden", name: that.props.name, value: json, readOnly: "true"}),
|
120
|
+
React.createElement(Preview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
|
121
|
+
)
|
122
|
+
);
|
123
|
+
});
|
124
|
+
|
125
|
+
var placeholders = [];
|
126
|
+
if (previews.length == 0 && that.props['data-placeholder']) $.each(JSON.parse(that.props['data-placeholder']), function(uid, src) {
|
127
|
+
placeholders.push(
|
128
|
+
React.createElement(Placeholder, React.__spread({key: "placeholder"}, that.props, {src: src}))
|
129
|
+
);
|
130
|
+
});
|
131
|
+
|
132
|
+
var discards = [];
|
133
|
+
$.each(that.state.attaches_discarded, function(index, discard) {
|
134
|
+
var discardKey = "discard" + discard.path;
|
135
|
+
discards.push(
|
136
|
+
React.createElement("input", {key: discardKey, type: "hidden", name: discard.fieldname, value: discard.path})
|
137
|
+
);
|
138
|
+
});
|
139
|
+
|
140
|
+
var className = ["attache-file-selector", "attache-placeholders-count-" + placeholders.length, "attache-previews-count-" + previews.length, this.props['data-classname']].join(' ').trim();
|
141
|
+
return (
|
142
|
+
React.createElement("label", {htmlFor: that.props.id, className: className, onDragOver: this.onDragOver, onDragLeave: this.onDragLeave, onDrop: this.onDrop},
|
143
|
+
React.createElement("input", React.__spread({type: "file"}, that.props, {onChange: this.onChange})),
|
144
|
+
React.createElement("input", {type: "hidden", name: that.props.name, value: ""}),
|
145
|
+
React.createElement(Header, React.__spread({}, that.props)),
|
146
|
+
previews,
|
147
|
+
placeholders,
|
148
|
+
discards
|
149
|
+
)
|
150
|
+
);
|
151
|
+
}
|
152
|
+
});
|
@@ -0,0 +1,152 @@
|
|
1
|
+
var AttacheFileInput = React.createClass({
|
2
|
+
|
3
|
+
getInitialState: function() {
|
4
|
+
var files = {};
|
5
|
+
if (this.props['data-value']) $.each(JSON.parse(this.props['data-value']), function(uid, json) {
|
6
|
+
if (json) files[uid] = json;
|
7
|
+
});
|
8
|
+
return {files: files, attaches_discarded: [], uploading: 0 };
|
9
|
+
},
|
10
|
+
|
11
|
+
onRemove: function(uid, e) {
|
12
|
+
e.preventDefault();
|
13
|
+
e.stopPropagation();
|
14
|
+
|
15
|
+
var fieldname = this.getDOMNode().firstChild.name; // when 'user[avatar]'
|
16
|
+
var newfield = fieldname.replace(/\w+\](\[\]|)$/, 'attaches_discarded][]'); // become 'user[attaches_discarded][]'
|
17
|
+
|
18
|
+
this.state.attaches_discarded.push({ fieldname: newfield, path: this.state.files[uid].path });
|
19
|
+
delete this.state.files[uid];
|
20
|
+
|
21
|
+
this.setState(this.state);
|
22
|
+
},
|
23
|
+
|
24
|
+
performUpload: function(file_element, files) {
|
25
|
+
// user cancelled file chooser dialog. ignore
|
26
|
+
if (! files || files.length == 0) return;
|
27
|
+
if (! this.props.multiple) {
|
28
|
+
this.state.files = {};
|
29
|
+
files = [files[0]]; // array of 1 element
|
30
|
+
}
|
31
|
+
|
32
|
+
this.setState(this.state);
|
33
|
+
// upload the file via CORS
|
34
|
+
var that = this;
|
35
|
+
|
36
|
+
that.state.uploading = that.state.uploading + files.length;
|
37
|
+
if (! that.state.submit_buttons) that.state.submit_buttons = $("button,input[type='submit']", $(file_element).parents('form')[0]).filter(':not(:disabled)');
|
38
|
+
|
39
|
+
new AttacheCORSUpload({
|
40
|
+
file_element: file_element,
|
41
|
+
files: files,
|
42
|
+
onComplete: function() {
|
43
|
+
that.state.uploading--;
|
44
|
+
that.setFileValue.apply(this, arguments);
|
45
|
+
},
|
46
|
+
onProgress: this.setFileValue,
|
47
|
+
onError: function(uid, status) {
|
48
|
+
that.state.uploading--;
|
49
|
+
that.setFileValue(uid, { pctString: '90%', pctDesc: status, className: 'progress-bar progress-bar-danger' });
|
50
|
+
}
|
51
|
+
});
|
52
|
+
|
53
|
+
// we don't want the file binary to be uploaded in the main form
|
54
|
+
// so the actual file input is neutered
|
55
|
+
file_element.value = '';
|
56
|
+
},
|
57
|
+
|
58
|
+
onChange: function() {
|
59
|
+
var file_element = this.getDOMNode().firstChild;
|
60
|
+
this.performUpload(file_element, file_element && file_element.files);
|
61
|
+
},
|
62
|
+
|
63
|
+
onDragOver: function(e) {
|
64
|
+
e.stopPropagation();
|
65
|
+
e.preventDefault();
|
66
|
+
$(this.getDOMNode()).addClass('attache-dragover');
|
67
|
+
},
|
68
|
+
|
69
|
+
onDragLeave: function(e) {
|
70
|
+
e.stopPropagation();
|
71
|
+
e.preventDefault();
|
72
|
+
$(this.getDOMNode()).removeClass('attache-dragover');
|
73
|
+
},
|
74
|
+
|
75
|
+
onDrop: function(e) {
|
76
|
+
e.stopPropagation();
|
77
|
+
e.preventDefault();
|
78
|
+
var file_element = this.getDOMNode().firstChild;
|
79
|
+
this.performUpload(file_element, e.target.files || e.dataTransfer.files);
|
80
|
+
$(this.getDOMNode()).removeClass('attache-dragover');
|
81
|
+
},
|
82
|
+
|
83
|
+
setFileValue: function(key, value) {
|
84
|
+
this.state.files[key] = value;
|
85
|
+
this.setState(this.state);
|
86
|
+
},
|
87
|
+
|
88
|
+
render: function() {
|
89
|
+
var that = this;
|
90
|
+
|
91
|
+
if (that.state.uploading > 0) {
|
92
|
+
that.state.submit_buttons.attr('disabled', true);
|
93
|
+
} else if (that.state.submit_buttons) {
|
94
|
+
that.state.submit_buttons.attr('disabled', null);
|
95
|
+
}
|
96
|
+
|
97
|
+
var Header = eval(this.props['data-header-component'] || 'AttacheHeader');
|
98
|
+
var Preview = eval(this.props['data-preview-component'] || 'AttacheFilePreview');
|
99
|
+
var Placeholder = eval(this.props['data-placeholder-component'] || 'AttachePlaceholder');
|
100
|
+
|
101
|
+
var previews = [];
|
102
|
+
$.each(that.state.files, function(key, result) {
|
103
|
+
// json is input[value], drop non essential values
|
104
|
+
var copy = JSON.parse(JSON.stringify(result));
|
105
|
+
delete copy.src;
|
106
|
+
delete copy.filename;
|
107
|
+
var json = JSON.stringify(copy);
|
108
|
+
//
|
109
|
+
result.multiple = that.props.multiple;
|
110
|
+
if (result.path) {
|
111
|
+
var parts = result.path.split('/');
|
112
|
+
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
|
113
|
+
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
|
114
|
+
result.filename = result.src.split('/').pop().split(/[#?]/).shift();
|
115
|
+
}
|
116
|
+
var previewKey = "preview" + key;
|
117
|
+
previews.push(
|
118
|
+
<div key={previewKey} className="attache-file-input">
|
119
|
+
<input type="hidden" name={that.props.name} value={json} readOnly="true" />
|
120
|
+
<Preview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
|
121
|
+
</div>
|
122
|
+
);
|
123
|
+
});
|
124
|
+
|
125
|
+
var placeholders = [];
|
126
|
+
if (previews.length == 0 && that.props['data-placeholder']) $.each(JSON.parse(that.props['data-placeholder']), function(uid, src) {
|
127
|
+
placeholders.push(
|
128
|
+
<Placeholder key="placeholder" {...that.props} src={src} />
|
129
|
+
);
|
130
|
+
});
|
131
|
+
|
132
|
+
var discards = [];
|
133
|
+
$.each(that.state.attaches_discarded, function(index, discard) {
|
134
|
+
var discardKey = "discard" + discard.path;
|
135
|
+
discards.push(
|
136
|
+
<input key={discardKey} type="hidden" name={discard.fieldname} value={discard.path} />
|
137
|
+
);
|
138
|
+
});
|
139
|
+
|
140
|
+
var className = ["attache-file-selector", "attache-placeholders-count-" + placeholders.length, "attache-previews-count-" + previews.length, this.props['data-classname']].join(' ').trim();
|
141
|
+
return (
|
142
|
+
<label htmlFor={that.props.id} className={className} onDragOver={this.onDragOver} onDragLeave={this.onDragLeave} onDrop={this.onDrop}>
|
143
|
+
<input type="file" {...that.props} onChange={this.onChange}/>
|
144
|
+
<input type="hidden" name={that.props.name} value="" />
|
145
|
+
<Header {...that.props} />
|
146
|
+
{previews}
|
147
|
+
{placeholders}
|
148
|
+
{discards}
|
149
|
+
</label>
|
150
|
+
);
|
151
|
+
}
|
152
|
+
});
|
@@ -0,0 +1,23 @@
|
|
1
|
+
window.AttacheRails = {
|
2
|
+
upgrade_fileinputs: function() {
|
3
|
+
var safeWords = { 'class': 'className', 'for': 'htmlFor' };
|
4
|
+
var sel = document.getElementsByClassName('enable-attache');
|
5
|
+
var ele, attrs, name, value;
|
6
|
+
for (var i = sel.length-1; i >= 0; i--) {
|
7
|
+
ele = sel[i];
|
8
|
+
attrs = {};
|
9
|
+
for (var j = 0; j < ele.attributes.length; j++) {
|
10
|
+
name = ele.attributes[j].name;
|
11
|
+
value = ele.attributes[j].value;
|
12
|
+
if (name === 'class') value = value.replace('enable-attache', 'attache-enabled');
|
13
|
+
name = safeWords[name] || name;
|
14
|
+
attrs[name] = value;
|
15
|
+
}
|
16
|
+
var wrap = document.createElement('div');
|
17
|
+
ele.parentNode.replaceChild(wrap, ele); // ele.parentNode.insertBefore(wrap, ele.nextSibling);
|
18
|
+
React.render(React.createElement(AttacheFileInput, React.__spread({}, attrs)), wrap);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
};
|
22
|
+
|
23
|
+
$(document).on('page:change', AttacheRails.upgrade_fileinputs);
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Attache
|
2
|
+
module Rails
|
3
|
+
ATTACHE_URL = ENV.fetch('ATTACHE_URL') { "http://localhost:9292" }
|
4
|
+
ATTACHE_UPLOAD_URL = ENV.fetch('ATTACHE_UPLOAD_URL') { "#{ATTACHE_URL}/upload" }
|
5
|
+
ATTACHE_DOWNLOAD_URL = ENV.fetch('ATTACHE_DOWNLOAD_URL') { "#{ATTACHE_URL}/view" }
|
6
|
+
ATTACHE_DELETE_URL = ENV.fetch('ATTACHE_DELETE_URL') { "#{ATTACHE_URL}/delete" }
|
7
|
+
ATTACHE_UPLOAD_DURATION = ENV.fetch('ATTACHE_UPLOAD_DURATION') { 3.hours }.to_i # expires signed upload form
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require "attache/rails/engine"
|
12
|
+
require "attache/rails/model"
|
13
|
+
require "attache/api/test" if defined?(Rails) && Rails.env.test?
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "uri"
|
3
|
+
require "httpclient"
|
4
|
+
require "attache/api"
|
5
|
+
|
6
|
+
module Attache
|
7
|
+
module Rails
|
8
|
+
module Model
|
9
|
+
include Attache::API::Model
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
# has_one_attache, has_many_attaches
|
13
|
+
base.extend ClassMethods
|
14
|
+
|
15
|
+
# `discard` management
|
16
|
+
base.class_eval do
|
17
|
+
attr_accessor :attaches_discarded
|
18
|
+
after_commit if: :attaches_discarded do |instance|
|
19
|
+
instance.attaches_discard!(instance.attaches_discarded)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def attache_setup_column(name)
|
26
|
+
case coltype = column_for_attribute(name).type
|
27
|
+
when :text, :string, :binary
|
28
|
+
serialize name, JSON
|
29
|
+
end
|
30
|
+
rescue Exception
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_one_attache(name)
|
34
|
+
attache_setup_column(name)
|
35
|
+
define_method "#{name}_options", -> (geometry, options = {}) { Hash(class: 'enable-attache', multiple: false).merge(attache_field_options(self.send(name), geometry, options)) }
|
36
|
+
define_method "#{name}_url", -> (geometry) { attache_field_urls(self.send(name), geometry).try(:first) }
|
37
|
+
define_method "#{name}_attributes", -> (geometry) { attache_field_attributes(self.send(name), geometry).try(:first) }
|
38
|
+
define_method "#{name}=", -> (value) { super(attache_field_set(Array.wrap(value)).try(:first)) }
|
39
|
+
after_update -> { self.attaches_discarded ||= []; attache_mark_for_discarding(self.send("#{name}_was"), self.send("#{name}"), self.attaches_discarded) }
|
40
|
+
after_destroy -> { self.attaches_discarded ||= []; attache_mark_for_discarding(self.send("#{name}_was"), [], self.attaches_discarded) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_many_attaches(name)
|
44
|
+
attache_setup_column(name)
|
45
|
+
define_method "#{name}_options", -> (geometry, options = {}) { Hash(class: 'enable-attache', multiple: true).merge(attache_field_options(self.send(name), geometry, options)) }
|
46
|
+
define_method "#{name}_urls", -> (geometry) { attache_field_urls(self.send(name), geometry) }
|
47
|
+
define_method "#{name}_attributes", -> (geometry) { attache_field_attributes(self.send(name), geometry) }
|
48
|
+
define_method "#{name}=", -> (value) { super(attache_field_set(Array.wrap(value))) }
|
49
|
+
after_update -> { self.attaches_discarded ||= []; attache_mark_for_discarding(self.send("#{name}_was"), self.send("#{name}"), self.attaches_discarded) }
|
50
|
+
after_destroy -> { self.attaches_discarded ||= []; attache_mark_for_discarding(self.send("#{name}_was"), [], self.attaches_discarded) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
ActiveRecord::Base.send(:include, Attache::Rails::Model)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
|
+
<%- ($has_one_attache + $has_many_attaches).each do |(klass, column)| -%>
|
3
|
+
class <%= klass %> < ActiveRecord::Base; serialize :<%= column %>, JSON; end
|
4
|
+
<%- end -%>
|
5
|
+
|
6
|
+
def self.up
|
7
|
+
# has_one_attache
|
8
|
+
<%- $has_one_attache.each do |(klass, column)| -%>
|
9
|
+
<%= klass %>.where.not(<%= column %>: [nil, ""]).find_each do |obj|
|
10
|
+
if obj[:<%= column %>].kind_of?(String)
|
11
|
+
obj.update(<%= column %>: JSON.parse(obj[:<%= column %>]))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
<%- end -%>
|
15
|
+
|
16
|
+
# has_many_attaches
|
17
|
+
<%- $has_many_attaches.each do |(klass, column)| -%>
|
18
|
+
<%= klass %>.where.not(<%= column %>: [nil, ""]).find_each do |obj|
|
19
|
+
if obj[:<%= column %>].first.kind_of?(String)
|
20
|
+
obj.update(<%= column %>: obj[:<%= column %>].collect {|v| JSON.parse(v) })
|
21
|
+
end
|
22
|
+
end
|
23
|
+
<%- end -%>
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.down
|
27
|
+
# has_one_attache
|
28
|
+
<%- $has_one_attache.each do |(klass, column)| -%>
|
29
|
+
<%= klass %>.where.not(<%= column %>: [nil, ""]).find_each do |obj|
|
30
|
+
if obj[:<%= column %>].kind_of?(Hash)
|
31
|
+
obj.update(<%= column %>: obj[:<%= column %>].to_json)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
<%- end -%>
|
35
|
+
|
36
|
+
# has_many_attaches
|
37
|
+
<%- $has_many_attaches.each do |(klass, column)| -%>
|
38
|
+
<%= klass %>.where.not(<%= column %>: [nil, ""]).find_each do |obj|
|
39
|
+
if obj[:<%= column %>].first.kind_of?(Hash)
|
40
|
+
obj.update(<%= column %>: obj[:<%= column %>].collect {|v| v.to_json })
|
41
|
+
end
|
42
|
+
end
|
43
|
+
<%- end -%>
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module Attache
|
4
|
+
module Rails
|
5
|
+
class UpgradeV2ToV3Generator < ::Rails::Generators::Base
|
6
|
+
include ::Rails::Generators::Migration
|
7
|
+
|
8
|
+
def self.next_migration_number(dir)
|
9
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.source_root
|
13
|
+
@source_root ||= File.expand_path('../templates', __FILE__)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_migration
|
17
|
+
migration_template "upgrade_v2_to_v3_migration.rb.erb", "db/migrate/#{migration_file_name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def migration_name
|
21
|
+
"UpgradeAttacheFieldsFromV2ToV3"
|
22
|
+
end
|
23
|
+
|
24
|
+
def migration_file_name
|
25
|
+
"#{migration_name.underscore}.rb"
|
26
|
+
end
|
27
|
+
|
28
|
+
def migration_class_name
|
29
|
+
migration_name.camelize
|
30
|
+
end
|
31
|
+
|
32
|
+
$has_one_attache = []
|
33
|
+
$has_many_attaches = []
|
34
|
+
|
35
|
+
ActiveRecord::Base.class_eval do
|
36
|
+
def self.has_one_attache(name)
|
37
|
+
$has_one_attache.push([self.name, name])
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.has_many_attaches(name)
|
41
|
+
$has_many_attaches.push([self.name, name])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Rails::ConsoleMethods#reload!
|
46
|
+
ActionDispatch::Reloader.cleanup!
|
47
|
+
ActionDispatch::Reloader.prepare!
|
48
|
+
|
49
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
50
|
+
if klass = table.classify.safe_constantize
|
51
|
+
else
|
52
|
+
puts "skipped #{table}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attache-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- choonkeat
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: attache-api
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.2.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.2.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httpclient
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-rspec
|
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
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: factory_girl_rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- choonkeat@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- MIT-LICENSE
|
119
|
+
- README.md
|
120
|
+
- Rakefile
|
121
|
+
- app/assets/javascripts/attache.js
|
122
|
+
- app/assets/javascripts/attache/bootstrap3.js
|
123
|
+
- app/assets/javascripts/attache/bootstrap3.js.jsx
|
124
|
+
- app/assets/javascripts/attache/cors_upload.js
|
125
|
+
- app/assets/javascripts/attache/file_input.js
|
126
|
+
- app/assets/javascripts/attache/file_input.js.jsx
|
127
|
+
- app/assets/javascripts/attache/ujs.js
|
128
|
+
- lib/attache/rails.rb
|
129
|
+
- lib/attache/rails/engine.rb
|
130
|
+
- lib/attache/rails/model.rb
|
131
|
+
- lib/attache/rails/version.rb
|
132
|
+
- lib/generators/attache/rails/templates/upgrade_v2_to_v3_migration.rb.erb
|
133
|
+
- lib/generators/attache/rails/upgrade_v2_to_v3_generator.rb
|
134
|
+
homepage: https://github.com/choonkeat/attache-rails
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.4.6
|
155
|
+
signing_key:
|
156
|
+
specification_version: 4
|
157
|
+
summary: Client lib to use attache server
|
158
|
+
test_files: []
|