attache-rails 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/attache-rails.svg)](https://badge.fury.io/rb/attache-rails)
|
4
|
+
[![Build Status](https://travis-ci.org/choonkeat/attache-rails.svg?branch=master)](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: []
|