attachinary 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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.
@@ -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,4 @@
1
+ module Attachinary
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module Attachinary
2
+ class CorsController < Attachinary::ApplicationController
3
+ respond_to :json
4
+
5
+ def show
6
+ respond_with request.query_parameters
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ Attachinary::Engine.routes.draw do
2
+ get '/cors' => 'cors#show', format: 'json', as: 'cors'
3
+ get '/callback' => 'files#callback', format: 'json', as: 'callback'
4
+ end
@@ -0,0 +1,4 @@
1
+ require "attachinary/engine"
2
+
3
+ module Attachinary
4
+ end
@@ -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