attache_rails 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23145e14f7f10f15b0ee5817d168619e2b6d0e5c
4
- data.tar.gz: cbe0855bc37ec5ab2137c3af923a53077cb4150e
3
+ metadata.gz: 0c7546fc76fade2bf70f9075fe87be7b71fffa6f
4
+ data.tar.gz: ee7f0526cbebe7292f685cb86ed501721d46f7b2
5
5
  SHA512:
6
- metadata.gz: a6e380a7dbb6fed113a7ad7e71f5af307dbcffaa39480e95054083053358d4bd76b04a4066e51d564387c5ebb949b05449173b1d2dd8f33a758916a92aaec599
7
- data.tar.gz: 2ac7cd485271384c199c2376b4ee163b62387a16e7cb5a9db584d0cf8a894c9450a926746e63c7daddd30917d5d614d55de07e7ff993384b22342b3b6edcf660
6
+ metadata.gz: 76d9c2a0aeec9e6de2980cd752bcf4e7fc83720426e073d60e85073e5d6a97e2ef4211733a0e415fd9af9280c49da0e7af52a8678bc72ce27548b6b442983290
7
+ data.tar.gz: 1b955d139c9fda1b3828e6f91a7ace1d8b213a92085cce02af4e0936c16b273558ee040082815dc93c391c1bc03530a5eed3a94ec9346d5b28e77999c488bf7d
data/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  Ruby on Rails integration for [attache server](https://github.com/choonkeat/attache)
4
4
 
5
- NOTE: You can learn how to use this gem by looking at the [changes made to our example app](https://github.com/choonkeat/attache-railsapp/compare/fc47c17...master) or follow the step by step instructions in this `README`
6
-
7
5
  ## Dependencies
8
6
 
9
7
  [React](https://github.com/reactjs/react-rails), jQuery, Bootstrap 3
@@ -40,47 +38,38 @@ If you want to customize the file upload look and feel, define your own react `<
40
38
 
41
39
  ### Database
42
40
 
43
- To use `attache`, you only need to store the `path`, given to you after you've uploaded a file. So if you have an existing model, you only need to add a string, varchar or text field
41
+ 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
44
42
 
45
43
  ``` bash
46
- rails generate migration AddPhotoPathToUsers photo_path:string
44
+ rails generate migration AddPhotoPathToUsers photo:text
47
45
  ```
48
46
 
49
- To assign **multiple** images to **one** model, you'd only need one text field
47
+ To assign **multiple** images to **one** model, the same column can be used, although pluralized column name reads better
50
48
 
51
49
  ``` bash
52
- rails generate migration AddPhotoPathToUsers photo_path:text
50
+ rails generate migration AddPhotoPathToUsers photos:text
53
51
  ```
54
52
 
55
53
  ### Model
56
54
 
57
- In your model, `serialize` the column
55
+ In your model, define whether it `has_one_attache` or `has_many_attaches`
58
56
 
59
57
  ``` ruby
60
58
  class User < ActiveRecord::Base
61
- serialize :photo_path, JSON
59
+ has_many_attaches :photos
62
60
  end
63
61
  ```
64
62
 
65
63
  ### New or Edit form
66
64
 
67
- In your form, you would add some options to `file_field` using the `attache_options` helper method. For example, a regular file field may look like this:
65
+ In your form, you would add some options to `file_field` using the `attache_options` helper method:
68
66
 
69
67
  ``` slim
70
- = f.file_field :photo_path
68
+ = f.file_field :photos, f.object.avatar_options('64x64#')
71
69
  ```
72
70
 
73
- Change it to
74
-
75
- ``` slim
76
- = f.file_field :photo_path, **attache_options('64x64#', f.object.photo_path)
77
- ```
71
+ If you were using `has_many_attaches` the file input will automatically allow multiple files, otherwise the file input will only accept 1 file.
78
72
 
79
- Or if you're expecting multiple files uploaded, simply add `multiple: true`
80
-
81
- ``` slim
82
- = f.file_field :photo_path, multiple: true, **attache_options('64x64#', f.object.photo_path)
83
- ```
84
73
 
85
74
  NOTE: `64x64#` is just an example, you should define a suitable [geometry](http://www.imagemagick.org/Usage/resize/) for your form
86
75
 
@@ -98,45 +87,38 @@ If you're only accepting a single file upload, change it to
98
87
 
99
88
  ``` ruby
100
89
  def user_params
101
- params.require(:user).permit(:name, :photo_path)
90
+ params.require(:user).permit(:name, :photo, attaches_discarded: [])
102
91
  end
103
92
  ```
104
93
 
105
- If you're accepting multiple file uploads via `multiple: true`, change it to
94
+ If you're accepting multiple file uploads, change it to
106
95
 
107
96
  ``` ruby
108
97
  def user_params
109
- params.require(:user).permit(:name, photo_path: [])
98
+ params.require(:user).permit(:name, photos: [], attaches_discarded: [])
110
99
  end
111
100
  ```
112
101
 
102
+ 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.
103
+
113
104
  ### Show
114
105
 
115
- Use the `attache_urls` helper to obtain full urls for the values you've captured in your database.
106
+ Use the `*_url` or `*_urls` methods (depending on whether you are accepting multiple files) to obtain full urls.
116
107
 
117
108
  ``` slim
118
- - attache_urls(@user.photo_path, '128x128#') do |url|
119
- = image_tag(url)
109
+ = image_tag @user.photo_url('100x100#')
120
110
  ```
121
111
 
122
- You can also include `AttacheRails::Helper` directly in your model
123
-
124
- ``` ruby
125
- class User < ActiveRecord::Base
126
- include AttacheRails::Helper
127
- serialize :photo_path, JSON
112
+ or
128
113
 
129
- def photo_url
130
- attache_urls(photo_path, '128x128#').sample
131
- end
132
- end
114
+ ``` slim
115
+ - @user.photos_urls('200x200#').each do |url|
116
+ = image_tag url
133
117
  ```
134
118
 
135
119
  ### Environment configs
136
120
 
137
- `ATTACHE_UPLOAD_URL` points to the attache server upload url. e.g. `http://localhost:9292/upload`
138
-
139
- `ATTACHE_DOWNLOAD_URL` points to url prefix for downloading the resized images, e.g. `http://cdn.lvh.me:9292/view`
121
+ `ATTACHE_URL` points to the attache server. e.g. `http://localhost:9292`
140
122
 
141
123
  `ATTACHE_UPLOAD_DURATION` refers to the number of seconds before a signed upload request is considered expired, e.g. `600`
142
124
 
@@ -1,4 +1,4 @@
1
1
  //= require attache/cors_upload
2
- //= require attache/file_input
3
2
  //= require attache/bootstrap3
3
+ //= require attache/file_input
4
4
  //= require attache/ujs
@@ -46,3 +46,29 @@ if (typeof AttacheFilePreview === 'undefined') {
46
46
  });
47
47
 
48
48
  }
49
+
50
+ if (typeof AttachePlaceholder === 'undefined') {
51
+
52
+ var AttachePlaceholder = React.createClass({displayName: "AttachePlaceholder",
53
+ render: function() {
54
+ return (
55
+ React.createElement("div", {className: "attache-file-preview"},
56
+ React.createElement("img", {src: this.props.src})
57
+ )
58
+ );
59
+ }
60
+ });
61
+
62
+ }
63
+
64
+ if (typeof AttacheHeader === 'undefined') {
65
+
66
+ var AttacheHeader = React.createClass({displayName: "AttacheHeader",
67
+ render: function() {
68
+ return (
69
+ React.createElement("noscript", null)
70
+ );
71
+ }
72
+ });
73
+
74
+ }
@@ -46,3 +46,29 @@ if (typeof AttacheFilePreview === 'undefined') {
46
46
  });
47
47
 
48
48
  }
49
+
50
+ if (typeof AttachePlaceholder === 'undefined') {
51
+
52
+ var AttachePlaceholder = React.createClass({
53
+ render: function() {
54
+ return (
55
+ <div className="attache-file-preview">
56
+ <img src={this.props.src} />
57
+ </div>
58
+ );
59
+ }
60
+ });
61
+
62
+ }
63
+
64
+ if (typeof AttacheHeader === 'undefined') {
65
+
66
+ var AttacheHeader = React.createClass({
67
+ render: function() {
68
+ return (
69
+ <noscript />
70
+ );
71
+ }
72
+ });
73
+
74
+ }
@@ -14,7 +14,7 @@ var AttacheCORSUpload = (function() {
14
14
  }
15
15
 
16
16
  AttacheCORSUpload.prototype.handleFileSelect = function(file_element) {
17
- var f, files, output, _i, _len, _results, url, $ele;
17
+ var f, files, output, _i, _len, _results, url, $ele, prefix;
18
18
  $ele = $(file_element);
19
19
  url = $ele.data('uploadurl');
20
20
  if ($ele.data('hmac')) {
@@ -25,12 +25,13 @@ var AttacheCORSUpload = (function() {
25
25
  ""
26
26
  }
27
27
 
28
+ prefix = Date.now() + "_";
28
29
  files = file_element.files;
29
30
  output = [];
30
31
  _results = [];
31
32
  for (_i = 0, _len = files.length; _i < _len; _i++) {
32
33
  f = files[_i];
33
- f.uid = counter++;
34
+ f.uid = prefix + (counter++);
34
35
  this.onProgress(f.uid, { filename: f.name, percentLoaded: 0, bytesLoaded: 0, bytesTotal: f.size });
35
36
  _results.push(this.performUpload(f, url));
36
37
  }
@@ -2,25 +2,31 @@ var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
2
2
 
3
3
  getInitialState: function() {
4
4
  var files = {};
5
- var array = ([].concat(JSON.parse(this.props['data-value'])));
6
- $.each(array, function(uid, json) {
7
- if (json) files[uid] = { path: json };
5
+ if (this.props['data-value']) $.each(JSON.parse(this.props['data-value']), function(uid, json) {
6
+ if (json) files[uid] = json;
8
7
  });
9
- return {files: files};
8
+ return {files: files, attaches_discarded: []};
10
9
  },
11
10
 
12
11
  onRemove: function(uid, e) {
13
- delete this.state.files[uid];
14
- this.setState(this.state);
15
12
  e.preventDefault();
16
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);
17
22
  },
18
23
 
19
24
  onChange: function() {
20
25
  var file_element = this.getDOMNode().firstChild;
21
26
  // user cancelled file chooser dialog. ignore
22
27
  if (file_element.files.length == 0) return;
23
- this.state.files = {};
28
+ if (! this.props.multiple) this.state.files = {};
29
+
24
30
  this.setState(this.state);
25
31
  // upload the file via CORS
26
32
  var that = this;
@@ -45,26 +51,52 @@ var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
45
51
 
46
52
  render: function() {
47
53
  var that = this;
54
+
55
+ var Header = eval(this.props['data-header-component'] || 'AttacheHeader');
56
+ var Preview = eval(this.props['data-preview-component'] || 'AttacheFilePreview');
57
+ var Placeholder = eval(this.props['data-placeholder-component'] || 'AttachePlaceholder');
58
+
48
59
  var previews = [];
49
60
  $.each(that.state.files, function(key, result) {
50
- var json = JSON.stringify(result);
51
61
  if (result.path) {
52
62
  var parts = result.path.split('/');
53
63
  parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
54
64
  result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
55
65
  result.filename = result.src.split('/').pop().split(/[#?]/).shift();
66
+ result.multiple = that.props.multiple;
56
67
  }
68
+ var json = JSON.stringify(result);
57
69
  previews.push(
58
70
  React.createElement("div", {className: "attache-file-input"},
59
- React.createElement("input", {type: "hidden", name: that.props.name, value: result.path, readOnly: "true"}),
60
- React.createElement(AttacheFilePreview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
71
+ React.createElement("input", {type: "hidden", name: that.props.name, value: json, readOnly: "true"}),
72
+ React.createElement(Preview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
61
73
  )
62
74
  );
63
75
  });
76
+
77
+ var placeholders = [];
78
+ if (previews.length == 0 && that.props['data-placeholder']) $.each(JSON.parse(that.props['data-placeholder']), function(uid, src) {
79
+ placeholders.push(
80
+ React.createElement(Placeholder, React.__spread({}, that.props, {src: src}))
81
+ );
82
+ });
83
+
84
+ var discards = [];
85
+ $.each(that.state.attaches_discarded, function(index, discard) {
86
+ discards.push(
87
+ React.createElement("input", {type: "hidden", name: discard.fieldname, value: discard.path})
88
+ );
89
+ });
90
+
91
+ var className = ["attache-file-selector", "attache-placeholders-count-" + placeholders.length, "attache-previews-count-" + previews.length, this.props['data-classname']].join(' ').trim();
64
92
  return (
65
- React.createElement("label", {htmlFor: this.props.id, className: "attache-file-selector"},
66
- React.createElement("input", React.__spread({type: "file"}, this.props, {onChange: this.onChange})),
67
- previews
93
+ React.createElement("label", {htmlFor: that.props.id, className: className},
94
+ React.createElement("input", React.__spread({type: "file"}, that.props, {onChange: this.onChange})),
95
+ React.createElement("input", {type: "hidden", name: that.props.name, value: ""}),
96
+ React.createElement(Header, React.__spread({}, that.props)),
97
+ previews,
98
+ placeholders,
99
+ discards
68
100
  )
69
101
  );
70
102
  }
@@ -2,25 +2,31 @@ var AttacheFileInput = React.createClass({
2
2
 
3
3
  getInitialState: function() {
4
4
  var files = {};
5
- var array = ([].concat(JSON.parse(this.props['data-value'])));
6
- $.each(array, function(uid, json) {
7
- if (json) files[uid] = { path: json };
5
+ if (this.props['data-value']) $.each(JSON.parse(this.props['data-value']), function(uid, json) {
6
+ if (json) files[uid] = json;
8
7
  });
9
- return {files: files};
8
+ return {files: files, attaches_discarded: []};
10
9
  },
11
10
 
12
11
  onRemove: function(uid, e) {
13
- delete this.state.files[uid];
14
- this.setState(this.state);
15
12
  e.preventDefault();
16
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);
17
22
  },
18
23
 
19
24
  onChange: function() {
20
25
  var file_element = this.getDOMNode().firstChild;
21
26
  // user cancelled file chooser dialog. ignore
22
27
  if (file_element.files.length == 0) return;
23
- this.state.files = {};
28
+ if (! this.props.multiple) this.state.files = {};
29
+
24
30
  this.setState(this.state);
25
31
  // upload the file via CORS
26
32
  var that = this;
@@ -45,26 +51,52 @@ var AttacheFileInput = React.createClass({
45
51
 
46
52
  render: function() {
47
53
  var that = this;
54
+
55
+ var Header = eval(this.props['data-header-component'] || 'AttacheHeader');
56
+ var Preview = eval(this.props['data-preview-component'] || 'AttacheFilePreview');
57
+ var Placeholder = eval(this.props['data-placeholder-component'] || 'AttachePlaceholder');
58
+
48
59
  var previews = [];
49
60
  $.each(that.state.files, function(key, result) {
50
- var json = JSON.stringify(result);
51
61
  if (result.path) {
52
62
  var parts = result.path.split('/');
53
63
  parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
54
64
  result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
55
65
  result.filename = result.src.split('/').pop().split(/[#?]/).shift();
66
+ result.multiple = that.props.multiple;
56
67
  }
68
+ var json = JSON.stringify(result);
57
69
  previews.push(
58
70
  <div className="attache-file-input">
59
- <input type="hidden" name={that.props.name} value={result.path} readOnly="true" />
60
- <AttacheFilePreview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
71
+ <input type="hidden" name={that.props.name} value={json} readOnly="true" />
72
+ <Preview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
61
73
  </div>
62
74
  );
63
75
  });
76
+
77
+ var placeholders = [];
78
+ if (previews.length == 0 && that.props['data-placeholder']) $.each(JSON.parse(that.props['data-placeholder']), function(uid, src) {
79
+ placeholders.push(
80
+ <Placeholder {...that.props} src={src} />
81
+ );
82
+ });
83
+
84
+ var discards = [];
85
+ $.each(that.state.attaches_discarded, function(index, discard) {
86
+ discards.push(
87
+ <input type="hidden" name={discard.fieldname} value={discard.path} />
88
+ );
89
+ });
90
+
91
+ var className = ["attache-file-selector", "attache-placeholders-count-" + placeholders.length, "attache-previews-count-" + previews.length, this.props['data-classname']].join(' ').trim();
64
92
  return (
65
- <label htmlFor={this.props.id} className="attache-file-selector">
66
- <input type="file" {...this.props} onChange={this.onChange}/>
93
+ <label htmlFor={that.props.id} className={className}>
94
+ <input type="file" {...that.props} onChange={this.onChange}/>
95
+ <input type="hidden" name={that.props.name} value="" />
96
+ <Header {...that.props} />
67
97
  {previews}
98
+ {placeholders}
99
+ {discards}
68
100
  </label>
69
101
  );
70
102
  }
data/lib/attache_rails.rb CHANGED
@@ -1,43 +1,10 @@
1
- require "attache_rails/engine"
2
-
3
1
  module AttacheRails
4
- module Helper
5
- ATTACHE_UPLOAD_URL = ENV.fetch('ATTACHE_UPLOAD_URL') { 'http://localhost:9292/upload' }
6
- ATTACHE_DOWNLOAD_URL = ENV.fetch('ATTACHE_DOWNLOAD_URL') { 'http://localhost:9292/view' }
7
- ATTACHE_UPLOAD_DURATION = ENV.fetch('ATTACHE_UPLOAD_DURATION') { '600' }.to_i # 10 minutes
8
-
9
- def attache_urls(json, geometry)
10
- array = json.kind_of?(Array) ? json : [*json]
11
- array.collect do |path|
12
- download_url = ATTACHE_DOWNLOAD_URL
13
- prefix, basename = File.split(path)
14
- [download_url, prefix, CGI.escape(geometry), basename].join('/').tap do |url|
15
- yield url if block_given?
16
- end
17
- end
18
- end
19
-
20
- def attache_options(geometry, current_value)
21
- auth = if ENV['ATTACHE_SECRET_KEY']
22
- uuid = SecureRandom.uuid
23
- expiration = (Time.now + ATTACHE_UPLOAD_DURATION).to_i
24
- hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), ENV['ATTACHE_SECRET_KEY'], "#{uuid}#{expiration}")
25
- { uuid: uuid, expiration: expiration, hmac: hmac }
26
- else
27
- {}
28
- end
29
-
30
- {
31
- class: 'enable-attache',
32
- data: {
33
- geometry: geometry,
34
- value: [*current_value],
35
- uploadurl: ATTACHE_UPLOAD_URL,
36
- downloadurl: ATTACHE_DOWNLOAD_URL,
37
- }.merge(auth),
38
- }
39
- end
40
- end
2
+ ATTACHE_URL = ENV.fetch('ATTACHE_URL') { "http://localhost:9292" }
3
+ ATTACHE_UPLOAD_URL = ENV.fetch('ATTACHE_UPLOAD_URL') { "#{ATTACHE_URL}/upload" }
4
+ ATTACHE_DOWNLOAD_URL = ENV.fetch('ATTACHE_DOWNLOAD_URL') { "#{ATTACHE_URL}/view" }
5
+ ATTACHE_DELETE_URL = ENV.fetch('ATTACHE_DELETE_URL') { "#{ATTACHE_URL}/delete" }
6
+ ATTACHE_UPLOAD_DURATION = ENV.fetch('ATTACHE_UPLOAD_DURATION') { "600" }.to_i # signed upload form expires in 10 minutes
41
7
  end
42
8
 
43
- ActionView::Base.send(:include, AttacheRails::Helper)
9
+ require "attache_rails/engine"
10
+ require "attache_rails/model"
@@ -0,0 +1,91 @@
1
+ require "net/http"
2
+ require "uri"
3
+
4
+ module AttacheRails
5
+ module Utils
6
+ class << self
7
+ def attache_url_for(json_string, geometry)
8
+ JSON.parse(json_string).tap do |attrs|
9
+ prefix, basename = File.split(attrs['path'])
10
+ attrs['url'] = [ATTACHE_DOWNLOAD_URL, prefix, CGI.escape(geometry), basename].join('/')
11
+ end
12
+ end
13
+
14
+ def attache_auth_options
15
+ if ENV['ATTACHE_SECRET_KEY']
16
+ uuid = SecureRandom.uuid
17
+ expiration = (Time.now + ATTACHE_UPLOAD_DURATION).to_i
18
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), ENV['ATTACHE_SECRET_KEY'], "#{uuid}#{expiration}")
19
+ { uuid: uuid, expiration: expiration, hmac: hmac }
20
+ else
21
+ {}
22
+ end
23
+ end
24
+
25
+ def attache_options(geometry, current_value, options)
26
+ {
27
+ multiple: options[:multiple],
28
+ class: 'enable-attache',
29
+ data: {
30
+ geometry: geometry,
31
+ value: [*current_value],
32
+ placeholder: [*options[:placeholder]],
33
+ uploadurl: ATTACHE_UPLOAD_URL,
34
+ downloadurl: ATTACHE_DOWNLOAD_URL,
35
+ }.merge(options[:data_attrs] || {}).merge(attache_auth_options),
36
+ }
37
+ end
38
+ end
39
+ end
40
+ module Model
41
+ def self.included(base)
42
+ base.extend ClassMethods
43
+ base.class_eval do
44
+ attr_accessor :attaches_discarded
45
+ after_commit :attaches_discard!, if: :attaches_discarded
46
+ end
47
+ end
48
+
49
+ def attaches_discard!(files = attaches_discarded)
50
+ if files.present?
51
+ logger.info "DELETE #{files.inspect}"
52
+ Net::HTTP.post_form(
53
+ URI.parse(ATTACHE_DELETE_URL),
54
+ Utils.attache_auth_options.merge(paths: files.join("\n"))
55
+ )
56
+ end
57
+ end
58
+
59
+ module ClassMethods
60
+ def has_one_attache(name)
61
+ serialize name, JSON
62
+ define_method "#{name}_options", -> (geometry, options = {}) { Utils.attache_options(geometry, [self.send("#{name}_attributes", geometry)], multiple: false, **options) }
63
+ define_method "#{name}_url", -> (geometry) { self.send("#{name}_attributes", geometry).try(:[], 'url') }
64
+ define_method "#{name}_attributes", -> (geometry) { str = self.send(name); Utils.attache_url_for(str, geometry) if str; }
65
+ define_method "#{name}_discard", -> do
66
+ self.attaches_discarded ||= []
67
+ self.attaches_discarded.push(self.send("#{name}_attributes", 'original')['path'])
68
+ end
69
+ after_destroy "#{name}_discard"
70
+ end
71
+
72
+ def has_many_attaches(name)
73
+ serialize name, JSON
74
+ define_method "#{name}_options", -> (geometry, options = {}) { Utils.attache_options(geometry, self.send("#{name}_attributes", geometry), multiple: true, **options) }
75
+ define_method "#{name}_urls", -> (geometry) { self.send("#{name}_attributes", geometry).collect {|attrs| attrs['url'] } }
76
+ define_method "#{name}_attributes", -> (geometry) {
77
+ (self.send(name) || []).inject([]) do |sum, str|
78
+ sum + (str.blank? ? [] : [Utils.attache_url_for(str, geometry)])
79
+ end
80
+ }
81
+ define_method "#{name}_discard", -> do
82
+ self.attaches_discarded ||= []
83
+ self.send("#{name}_attributes", 'original').each {|attrs| self.attaches_discarded.push(attrs['path']) }
84
+ end
85
+ after_destroy "#{name}_discard"
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ ActiveRecord::Base.send(:include, AttacheRails::Model)
@@ -1,3 +1,3 @@
1
1
  module AttacheRails
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attache_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - choonkeat
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-01 00:00:00.000000000 Z
11
+ date: 2015-03-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -29,6 +29,7 @@ files:
29
29
  - app/assets/javascripts/attache/ujs.js
30
30
  - lib/attache_rails.rb
31
31
  - lib/attache_rails/engine.rb
32
+ - lib/attache_rails/model.rb
32
33
  - lib/attache_rails/version.rb
33
34
  homepage: https://github.com/choonkeat/attache_rails
34
35
  licenses:
@@ -50,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
51
  version: '0'
51
52
  requirements: []
52
53
  rubyforge_project:
53
- rubygems_version: 2.4.5
54
+ rubygems_version: 2.4.6
54
55
  signing_key:
55
56
  specification_version: 4
56
57
  summary: Client lib to use attache server