attachinary 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +96 -0
- data/Rakefile +43 -0
- data/app/controllers/attachinary/application_controller.rb +4 -0
- data/app/controllers/attachinary/cors_controller.rb +9 -0
- data/app/controllers/attachinary/files_controller.rb +21 -0
- data/app/models/attachinary/attachment.rb +10 -0
- data/app/models/attachinary/file.rb +39 -0
- data/config/routes.rb +4 -0
- data/lib/attachinary.rb +4 -0
- data/lib/attachinary/active_record_extension.rb +187 -0
- data/lib/attachinary/engine.rb +25 -0
- data/lib/attachinary/simple_form.rb +18 -0
- data/lib/attachinary/version.rb +3 -0
- data/lib/attachinary/view_helpers.rb +48 -0
- data/vendor/assets/javascripts/attachinary.js +5 -0
- data/vendor/assets/javascripts/attachinary/jquery.attachinary.js.coffee +179 -0
- data/vendor/assets/javascripts/attachinary/jquery.cloudinary.js +311 -0
- data/vendor/assets/javascripts/attachinary/jquery.fileupload.js +904 -0
- data/vendor/assets/javascripts/attachinary/jquery.iframe-transport.js +171 -0
- data/vendor/assets/javascripts/attachinary/jquery.ui.widget.js +282 -0
- metadata +182 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Milovan Zogovic
|
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,96 @@
|
|
1
|
+
# Attachinary
|
2
|
+
|
3
|
+
Handling image and raw file attachments with ease.
|
4
|
+
It uses [Cloudinary](http://cloudinary.com) as storage.
|
5
|
+
|
6
|
+
It is structured as mountable rails engine.
|
7
|
+
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add following line to your `Gemfile`:
|
12
|
+
|
13
|
+
gem 'attachinary'
|
14
|
+
|
15
|
+
Then, run following rake command in terminal to create necessary tables:
|
16
|
+
|
17
|
+
rake attachinary:install:migrations
|
18
|
+
rake db:migrate
|
19
|
+
|
20
|
+
Add following line in your `routes.rb` file to mount the engine:
|
21
|
+
|
22
|
+
mount Attachinary::Engine => "/attachinary"
|
23
|
+
|
24
|
+
That's it. Oh, and make sure that you have [cloudinary gem](https://github.com/cloudinary/cloudinary_gem) installed and properly configured.
|
25
|
+
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Lets say that we want all of our **users** to have single **avatar** and many **photos** in their gallery. We also want *avatar* to be required. We also want to limit the number of photos user can upload to 10. We can declare it like this:
|
30
|
+
|
31
|
+
class User < ActiveRecord::Base
|
32
|
+
...
|
33
|
+
has_attachment :avatar, accept: ['jpg', 'png', 'gif']
|
34
|
+
has_attachments :photos, maximum: 10
|
35
|
+
|
36
|
+
validates :avatar_id, presence: true
|
37
|
+
...
|
38
|
+
end
|
39
|
+
|
40
|
+
In our `_form.html.erb` template, we need to add only this:
|
41
|
+
|
42
|
+
<%= attachinary_file_field_tag 'user[avatar_id]', user.avatar_id, attachinary: user.avatar_options %>
|
43
|
+
<%= attachinary_file_field_tag 'user[photo_ids]', user.photo_ids, attachinary: user.photo_options %>
|
44
|
+
|
45
|
+
If you're using [SimpleForm](https://github.com/plataformatec/simple_form), you can even shorten this to:
|
46
|
+
|
47
|
+
<%= f.input :avatar, as: :attachinary %>
|
48
|
+
<%= f.input :photos, as: :attachinary %>
|
49
|
+
|
50
|
+
Finally, you have to include `attachinary` into your asset pipeline. In your `application.js`, add following line:
|
51
|
+
|
52
|
+
//= require attachinary
|
53
|
+
|
54
|
+
And, add this code on document ready:
|
55
|
+
|
56
|
+
$('.attachinary-input').attachinary()
|
57
|
+
|
58
|
+
Attachinary jquery plugin is based upon [jQuery File Upload plugin](https://github.com/blueimp/jQuery-File-Upload) but without any fancy UI (it leaves it up to you to decorate it).
|
59
|
+
|
60
|
+
Plugin is fully customizable. It uses John Resig's micro templating in the background, but you can override it with whatever you like. Check out the source code for more configuration options you can set.
|
61
|
+
|
62
|
+
### Displaying avatar and photos
|
63
|
+
|
64
|
+
Here comes the good part. There is no need to transform images on your server. Instead, you can request image transformations directly from Cloudinary. First time you request image, it is created and cached on the Cloudinary server for later use. Here is sample code that you can use in your `_user.html.erb` partial:
|
65
|
+
|
66
|
+
<% if @user.avatar? %>
|
67
|
+
<%= cl_image_tag(@user.avatar.path, { size: '50x50', crop: :face }) %>
|
68
|
+
<% end %>
|
69
|
+
|
70
|
+
<% @user.photos.each do |photo| %>
|
71
|
+
<%= cl_image_tag(photo.path, { size: '125x125', crop: :fit }) %>
|
72
|
+
<% end %>
|
73
|
+
|
74
|
+
Avatar will be automatically cropped to 50x50px to show only user face. You read it right: **face detection** :) All other user photos are just cropped to fit within 125x125.
|
75
|
+
|
76
|
+
Whenever you feel like changing image sizes, you don't need to set rake task that will take forever to re-process thousands of photos. You just change the dimension in your partial and thats it.
|
77
|
+
|
78
|
+
|
79
|
+
## Conventions
|
80
|
+
|
81
|
+
* always use singular name for `has_attachment`
|
82
|
+
* always use plural name for `has_attachments`
|
83
|
+
|
84
|
+
|
85
|
+
## Requirements and Compatibility
|
86
|
+
|
87
|
+
* Cloudinary
|
88
|
+
* Ruby 1.9
|
89
|
+
* Rails 3.2+
|
90
|
+
|
91
|
+
|
92
|
+
## Credits and License
|
93
|
+
|
94
|
+
Developed by Milovan Zogovic.
|
95
|
+
|
96
|
+
This software is released under the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Attachinary'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
Bundler::GemHelper.install_tasks
|
29
|
+
|
30
|
+
#require 'rake/spectask'
|
31
|
+
|
32
|
+
# Spec::Rake::SpecTask.new(:spec) do |t|
|
33
|
+
# t.libs << 'lib'
|
34
|
+
# t.libs << 'spec'
|
35
|
+
# t.pattern = 'spec/**/*_spec.rb'
|
36
|
+
# t.verbose = false
|
37
|
+
# end
|
38
|
+
|
39
|
+
require 'rspec/core/rake_task'
|
40
|
+
RSpec::Core::RakeTask.new(:spec)
|
41
|
+
task :default => :spec
|
42
|
+
|
43
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Attachinary
|
2
|
+
class FilesController < Attachinary::ApplicationController
|
3
|
+
respond_to :json
|
4
|
+
|
5
|
+
def callback
|
6
|
+
success = valid_cloudinary_response?
|
7
|
+
if success && !params[:error].present?
|
8
|
+
@file = File.create(file_params)
|
9
|
+
respond_with @file
|
10
|
+
else
|
11
|
+
render nothing: true, status: 400
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def file_params
|
17
|
+
request.query_parameters.slice(:public_id, :version, :width, :height, :format, :resource_type)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Attachinary
|
2
|
+
class Attachment < ::ActiveRecord::Base
|
3
|
+
belongs_to :parent, polymorphic: true, touch: true
|
4
|
+
belongs_to :file, class_name: 'Attachinary::File', foreign_key: 'file_id'
|
5
|
+
|
6
|
+
validates :parent_id, :parent_type, :scope, presence: true
|
7
|
+
|
8
|
+
attr_accessible :parent_id, :parent_type, :file_id
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Attachinary
|
2
|
+
class File < ::ActiveRecord::Base
|
3
|
+
validates :public_id, :version, presence: true
|
4
|
+
validates :resource_type, presence: true
|
5
|
+
|
6
|
+
attr_accessible :public_id, :version, :width, :height, :format, :resource_type
|
7
|
+
after_destroy :destroy_file
|
8
|
+
|
9
|
+
def as_json(options)
|
10
|
+
super(only: [:id, :public_id, :format, :version, :resource_type], methods: [:path])
|
11
|
+
end
|
12
|
+
|
13
|
+
def path(custom_format=nil)
|
14
|
+
p = "v#{version}/#{public_id}"
|
15
|
+
if resource_type == 'image' && custom_format != false
|
16
|
+
custom_format ||= format
|
17
|
+
p<< ".#{custom_format}"
|
18
|
+
end
|
19
|
+
p
|
20
|
+
end
|
21
|
+
|
22
|
+
def fullpath(options={})
|
23
|
+
format = options.delete(:format)
|
24
|
+
Cloudinary::Utils.cloudinary_url(path(format), options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.upload!(file)
|
28
|
+
if file.respond_to?(:read)
|
29
|
+
response = Cloudinary::Uploader.upload(file, tags: "env_#{Rails.env}")
|
30
|
+
create! response.slice('public_id', 'version', 'width', 'height', 'format', 'resource_type')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def destroy_file
|
36
|
+
Cloudinary::Uploader.destroy(public_id) if public_id
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/config/routes.rb
ADDED
data/lib/attachinary.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
module Attachinary
|
2
|
+
module ActiveRecordExtension
|
3
|
+
|
4
|
+
def has_attachment(scope, options={})
|
5
|
+
apply_defaults!(options)
|
6
|
+
|
7
|
+
# has_one :photo_attachment, ...
|
8
|
+
has_one :"#{scope}_attachment",
|
9
|
+
as: :parent,
|
10
|
+
class_name: '::Attachinary::Attachment',
|
11
|
+
conditions: { scope: scope.to_s },
|
12
|
+
dependent: :destroy
|
13
|
+
|
14
|
+
# has_one :photo_attachment_file, through: :photo_attachment, ...
|
15
|
+
has_one :"#{scope}_attachment_file",
|
16
|
+
through: :"#{scope}_attachment",
|
17
|
+
source: :file
|
18
|
+
|
19
|
+
# attr_accessible :photo_id, :photo_file
|
20
|
+
attr_accessible :"#{scope}_id", :"#{scope}_file" if options[:accessible]
|
21
|
+
|
22
|
+
# attr_accessor :photo
|
23
|
+
attr_accessor :"#{scope}"
|
24
|
+
|
25
|
+
# def photo_id=(id)
|
26
|
+
# photo = ::Attachinary::File.find_by_id(id)
|
27
|
+
# end
|
28
|
+
define_method :"#{scope}_id=" do |id|
|
29
|
+
send(:"#{scope}=", ::Attachinary::File.find_by_id(id))
|
30
|
+
end
|
31
|
+
|
32
|
+
# def photo_file=(f)
|
33
|
+
# photo = ::Attachinary::File.upload!(f)
|
34
|
+
# end
|
35
|
+
define_method :"#{scope}_file=" do |f|
|
36
|
+
send(:"#{scope}=", ::Attachinary::File.upload!(f))
|
37
|
+
end
|
38
|
+
|
39
|
+
# def photo_id
|
40
|
+
# photo.try(:id)
|
41
|
+
# end
|
42
|
+
define_method :"#{scope}_id" do
|
43
|
+
send(:"#{scope}").try(:id)
|
44
|
+
end
|
45
|
+
|
46
|
+
# def photo?
|
47
|
+
# photo.present?
|
48
|
+
# end
|
49
|
+
define_method :"#{scope}?" do
|
50
|
+
send(:"#{scope}").present?
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# after_initialize do
|
55
|
+
# unless photo?
|
56
|
+
# photo = photo_attachment_file
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
after_initialize do
|
60
|
+
unless send(:"#{scope}?")
|
61
|
+
send(:"#{scope}=", send(:"#{scope}_attachment_file"))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# before_save do
|
66
|
+
# photo_attachment_file = photo
|
67
|
+
# end
|
68
|
+
before_save do
|
69
|
+
send(:"#{scope}_attachment_file=", send(:"#{scope}"))
|
70
|
+
end
|
71
|
+
|
72
|
+
# def photo_options
|
73
|
+
# options.merge({
|
74
|
+
# field_name: "photo_id",
|
75
|
+
# maximum: 1
|
76
|
+
# })
|
77
|
+
# end
|
78
|
+
define_method :"#{scope}_options" do
|
79
|
+
options.merge({
|
80
|
+
field_name: "#{scope}_id",
|
81
|
+
file_field_name: "#{scope}_file",
|
82
|
+
single: true,
|
83
|
+
maximum: 1
|
84
|
+
})
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def has_attachments(scope, options={})
|
89
|
+
apply_defaults!(options)
|
90
|
+
singular = scope.to_s.singularize
|
91
|
+
|
92
|
+
# has_many :image_attachments
|
93
|
+
has_many :"#{singular}_attachments",
|
94
|
+
as: :parent,
|
95
|
+
class_name: '::Attachinary::Attachment',
|
96
|
+
conditions: { scope: scope.to_s },
|
97
|
+
dependent: :destroy
|
98
|
+
|
99
|
+
# has_many :image_attachment_files, through: :image_attachments
|
100
|
+
has_many :"#{singular}_attachment_files",
|
101
|
+
through: :"#{singular}_attachments",
|
102
|
+
source: :file
|
103
|
+
|
104
|
+
# attr_accessible :image_ids, :image_files
|
105
|
+
attr_accessible :"#{singular}_ids", :"#{singular}_files" if options[:accessible]
|
106
|
+
|
107
|
+
# attr_accessor :images
|
108
|
+
attr_accessor :"#{scope}"
|
109
|
+
|
110
|
+
# def image_ids=(ids)
|
111
|
+
# files = [ids].flatten.compact.uniq.reject(&:blank?) do |id|
|
112
|
+
# ::Attachinary::File.find_by_id(id)
|
113
|
+
# end.compact
|
114
|
+
# images = files
|
115
|
+
# end
|
116
|
+
define_method :"#{singular}_ids=" do |ids|
|
117
|
+
files = [ids].flatten.compact.uniq.reject(&:blank?).map do |id|
|
118
|
+
::Attachinary::File.find_by_id(id)
|
119
|
+
end.compact
|
120
|
+
send(:"#{scope}=", files)
|
121
|
+
end
|
122
|
+
|
123
|
+
# def image_files=(fs)
|
124
|
+
# files = fs.map { |f| ::Attachinary::File.upload!(f) }
|
125
|
+
# images = files
|
126
|
+
# end
|
127
|
+
define_method :"#{singular}_files=" do |fs|
|
128
|
+
files = fs.map{ |f| ::Attachinary::File.upload!(f) }.compact
|
129
|
+
send(:"#{scope}=", files)
|
130
|
+
end
|
131
|
+
|
132
|
+
# def image_ids
|
133
|
+
# images.map(&:id)
|
134
|
+
# end
|
135
|
+
define_method :"#{singular}_ids" do
|
136
|
+
send(:"#{scope}").map(&:id)
|
137
|
+
end
|
138
|
+
|
139
|
+
# def images?
|
140
|
+
# images.present?
|
141
|
+
# end
|
142
|
+
define_method :"#{scope}?" do
|
143
|
+
send(:"#{scope}").present?
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
# after_initialize do
|
148
|
+
# unless images?
|
149
|
+
# images = image_attachment_files
|
150
|
+
# end
|
151
|
+
# end
|
152
|
+
after_initialize do
|
153
|
+
unless send(:"#{scope}?")
|
154
|
+
send(:"#{scope}=", send(:"#{singular}_attachment_files"))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# before_save do
|
159
|
+
# image_attachment_files = images
|
160
|
+
# end
|
161
|
+
before_save do
|
162
|
+
send(:"#{singular}_attachment_files=", send(:"#{scope}"))
|
163
|
+
end
|
164
|
+
|
165
|
+
# def image_options
|
166
|
+
# options.merge({
|
167
|
+
# field_name: "image_ids"
|
168
|
+
# })
|
169
|
+
# end
|
170
|
+
define_method :"#{singular}_options" do
|
171
|
+
options.merge({
|
172
|
+
field_name: "#{singular}_ids",
|
173
|
+
file_field_name: "#{singular}_files",
|
174
|
+
single: false
|
175
|
+
})
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
def apply_defaults!(options)
|
181
|
+
options.reverse_merge!({
|
182
|
+
accessible: true
|
183
|
+
})
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|