voltron-upload 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ module Voltron
2
+ module Upload
3
+ module Field
4
+
5
+ include ActionView::Helpers::TagHelper
6
+
7
+ def file_field(method, options={})
8
+ if Voltron.config.upload.enabled && !options[:default]
9
+ template = instance_variable_get('@template')
10
+ field = UploadField.new(@object, method, template, options)
11
+
12
+ # +merge+ is because options is a hash with_indifferent_access, and will therefore have an 'object' attribute when converted to html
13
+ #super method, {}.merge(field.options)
14
+ content_tag 'v-upload', nil, field.options
15
+ else
16
+ options.delete(:default)
17
+ super method, options
18
+ end
19
+ end
20
+
21
+ class UploadField
22
+
23
+ include ::ActionDispatch::Routing::PolymorphicRoutes
24
+
25
+ include ::Rails.application.routes.url_helpers
26
+
27
+ include ::ActionView::Helpers::TextHelper
28
+
29
+ attr_reader :options, :template
30
+
31
+ def initialize(model, method, template, options)
32
+ @model = model
33
+ @method = method.to_sym
34
+ @template = template
35
+ @options = options.with_indifferent_access
36
+ prepare
37
+ end
38
+
39
+ def prepare
40
+ #add_preview_class if has_preview_template?
41
+
42
+ options.merge!({
43
+ ':multiple' => multiple?,
44
+ ':files' => files.to_json,
45
+ ':cached' => caches.to_json,
46
+ ':removed' => removals.to_json,
47
+ ':options' => preview_options.to_json,
48
+ 'accept' => accept,
49
+ 'preview' => preview_name,
50
+ 'param' => input_name,
51
+ 'url' => polymorphic_path(@model.class, action: :upload)
52
+ })
53
+
54
+ #options[:data] ||= {}
55
+ #options[:data].merge!({
56
+ # upload_files: files,
57
+ # upload_cache: caches,
58
+ # upload_remove: removals,
59
+ # upload_options: preview_options
60
+ #})
61
+ end
62
+
63
+ def preview_options
64
+ previews = Voltron.config.upload.previews || {}
65
+ opts = previews.with_indifferent_access.try(:[], preview_name) || {}
66
+ opts.merge!({
67
+ preview_template: preview_markup,
68
+ })
69
+ opts.merge!(options.delete(:options) || {})
70
+ opts.map { |k,v| { k.to_s.camelize(:lower) => v } }.reduce(Hash.new, :merge).compact
71
+ end
72
+
73
+ def preview_markup
74
+ if has_preview_template?
75
+ # Fetch the html found in the partial provided
76
+ ActionController::Base.new.render_to_string(partial: "voltron/upload/preview/#{preview_name}").squish
77
+ elsif has_preview_markup?
78
+ # If not blank, value of +preview+ is likely (should be) raw html, in which case, just return that markup
79
+ preview.squish
80
+ end
81
+ end
82
+
83
+ # Strip tags, they cause problems in the lookup_context +exists?+ and +render_to_string+
84
+ def preview_name
85
+ strip_tags(preview)
86
+ end
87
+
88
+ def preview
89
+ @preview ||= options.delete(:preview).to_s
90
+ end
91
+
92
+ def accept
93
+ @accept ||= options.delete(:accept).to_s
94
+ end
95
+
96
+ def has_preview_template?
97
+ preview_name.present? && template.lookup_context.exists?(preview_name, 'voltron/upload/preview', true)
98
+ end
99
+
100
+ # Eventually, consider utilizing Nokogiri to detect whether content also is actually HTML markup
101
+ # Right now the overhead and frustration of that gem is not worth it
102
+ def has_preview_markup?
103
+ preview.present?
104
+ end
105
+
106
+ #def add_preview_class
107
+ # options[:class] ||= ''
108
+ # classes = options[:class].split(/\s+/)
109
+ # classes << "dz-layout-#{preview_name}"
110
+ # options[:class] = classes.join(' ')
111
+ #end
112
+
113
+ def input_name
114
+ ActionView::Helpers::Tags::Base.new(ActiveModel::Naming.param_key(@model), @method, nil).send(:tag_name) + (multiple? ? '[]' : '')
115
+ end
116
+
117
+ def multiple?
118
+ @model.respond_to?("#{@method}_urls")
119
+ end
120
+
121
+ def files
122
+ # If set to not preserve files, return an empty array so nothing is shown
123
+ return [] if options[:preserve] === false
124
+
125
+ # Return an array of files' json data
126
+ uploads = Array.wrap(@model.send(@method)).map(&:to_upload_json).reject(&:blank?)
127
+
128
+ # Remove all uploads from the array that have been flagged for removal
129
+ removals.each do |removal|
130
+ uploads.reject! { |upload| upload[:id] == removal }
131
+ end
132
+
133
+ uploads
134
+ end
135
+
136
+ def caches
137
+ if multiple?
138
+ cache = @model.send("cache_#{@method}") rescue []
139
+ Array.wrap(cache)
140
+ else
141
+ Array.wrap(@model.send("cache_#{@method}"))
142
+ end
143
+ end
144
+
145
+ def removals
146
+ Array.wrap(@model.send("remove_#{@method}")).compact.reject { |i| !i }
147
+ end
148
+
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,87 @@
1
+ module Voltron
2
+ module Upload
3
+ module Base
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def mount_uploader(*args)
10
+ super *args
11
+
12
+ column = args.first.to_sym
13
+
14
+ attr_accessor "cache_#{column}"
15
+
16
+ before_validation do
17
+ uploader = self.class.uploaders[column]
18
+
19
+ begin
20
+ cache_id = send("cache_#{column}")
21
+ send(column).retrieve_from_cache!(cache_id) if cache_id.present?
22
+ rescue ::CarrierWave::InvalidParameter => e
23
+ # Invalid cache id, we don't need to do anything but skip it
24
+ end
25
+ end
26
+ end
27
+
28
+ def mount_uploaders(*args)
29
+ super *args
30
+
31
+ column = args.first.to_sym
32
+
33
+ attr_accessor "cache_#{column}"
34
+
35
+ before_validation do
36
+ uploader = self.class.uploaders[column]
37
+ cache_ids = (send("cache_#{column}") rescue []) || []
38
+
39
+ # Store the existing files
40
+ files = send(column)
41
+
42
+ cache_ids.each do |cache_id|
43
+ begin
44
+ # Retrieve files from the cache and add them to the list of files
45
+ file = uploader.new(self, column)
46
+ file.retrieve_from_cache!(cache_id)
47
+ files << file
48
+ rescue ::CarrierWave::InvalidParameter => e
49
+ # Invalid cache id, we don't need to do anything but skip it
50
+ end
51
+ end
52
+
53
+ # Set the files
54
+ send("#{column}=", files)
55
+ end
56
+
57
+ # Only required for multiple uploads. Since Carrierwave does not have any way to remove individual files
58
+ # we must identify each file to be removed individually, remove it from the array of existing files,
59
+ # then reset the value of our mounted files
60
+ after_validation do
61
+ # Only attempt to remove files if there are no validation errors
62
+ if errors.empty?
63
+ # Merge any new uploads with the pre-existing uploads (so we can "add" new files, instead of overwriting)
64
+ uploads = Array.wrap(send(column))
65
+
66
+ # Get the ids of uploads we want to remove
67
+ removals = Array.wrap(send("remove_#{column}"))
68
+
69
+ removals.each do |removal|
70
+ uploads.reject! { |upload| upload.id == removal }
71
+ end
72
+
73
+ # Initially ensure carrierwave DOESN'T think we want to remove ALL files, we're just going to change the files (i.e. - remove some, not all)
74
+ send("remove_#{column}=", false)
75
+
76
+ # Ensure that nil is assigned as the value if we indeed have no more files
77
+ send("remove_#{column}!") if uploads.empty?
78
+
79
+ assign_attributes(column => uploads)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ module Voltron
2
+ module Upload
3
+ module CarrierWave
4
+ module Uploader
5
+ module Base
6
+
7
+ def initialize(*args)
8
+ self.class.send(:before, :store, :save_timestamp)
9
+ self.class.send(:after, :store, :apply_timestamp)
10
+ super(*args)
11
+ end
12
+
13
+ def to_upload_json
14
+ if present?
15
+ {
16
+ id: id,
17
+ url: url,
18
+ name: file.original_filename,
19
+ size: file.size,
20
+ type: file.content_type
21
+ }
22
+ else
23
+ {}
24
+ end
25
+ end
26
+
27
+ def id
28
+ if stored?
29
+ [File.mtime(full_store_path).to_i, file.original_filename].join('/')
30
+ elsif cached? && File.exists?(Rails.root.join('public', cache_path))
31
+ [cached?, file.original_filename].join('/')
32
+ else
33
+ file.original_filename
34
+ end
35
+ end
36
+
37
+ def stored?
38
+ File.exists?(full_store_path)
39
+ end
40
+
41
+ def full_store_path
42
+ Rails.root.join('public', store_path(file.filename))
43
+ end
44
+
45
+ private
46
+
47
+ # Before we store the file for good, grab the offset number
48
+ # so it can be used to create a unique timestamp after storing
49
+ def save_timestamp(*args)
50
+ id_components = File.basename(File.expand_path('..', file.path)).split('-')
51
+ @offset = id_components[2].to_i + 1000
52
+ end
53
+
54
+ # Update the modified time of the file to a unique timestamp
55
+ # This timestamp will later be used to help identify the file,
56
+ # as it will be part of the generated id
57
+ def apply_timestamp(*args)
58
+ @offset ||= rand(1..1000)
59
+ FileUtils.touch file.path, mtime: Time.now + @offset.seconds
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ module Voltron
2
+ module Upload
3
+ class Engine < Rails::Engine
4
+
5
+ isolate_namespace Voltron
6
+
7
+ initializer 'voltron.upload.initialize' do
8
+ ::ActionDispatch::Routing::Mapper.send :include, ::Voltron::Upload::Routes
9
+ ::ActionController::Base.send :extend, ::Voltron::Upload
10
+
11
+ ActiveSupport.on_load :active_record do
12
+ require 'voltron/upload/action_view/field'
13
+ ::ActionView::Helpers::FormBuilder.send :prepend, ::Voltron::Upload::Field
14
+ ::CarrierWave::Uploader::Base.send :prepend, ::Voltron::Upload::CarrierWave::Uploader::Base
15
+ ::ActiveRecord::Base.send :include, ::Voltron::Upload::Base
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Voltron
2
+ module Upload
3
+ class Error < StandardError
4
+
5
+ attr_accessor :messages
6
+
7
+ def initialize(*messages)
8
+ @messages = messages.flatten
9
+ end
10
+
11
+ def response
12
+ { success: false, error: @messages }
13
+ end
14
+
15
+ def status
16
+ :not_acceptable
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Voltron
2
+ module Upload
3
+ VERSION = '0.2.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,51 @@
1
+ require 'voltron'
2
+ require 'carrierwave'
3
+ require 'voltron/upload/version'
4
+ require 'voltron/config/upload'
5
+ require 'voltron/upload/error'
6
+ require 'voltron/uploader'
7
+ require 'voltron/upload/carrierwave/uploader/base'
8
+ require 'voltron/upload/active_record/base'
9
+ require 'voltron/upload/action_dispatch/routes'
10
+
11
+ module Voltron
12
+ module Upload
13
+
14
+ LOG_COLOR = :light_cyan
15
+
16
+ def uploadable(resource = nil)
17
+ include ControllerMethods
18
+
19
+ resource ||= controller_name
20
+ @uploader ||= Voltron::Uploader.new(resource)
21
+
22
+ rescue_from ActionController::InvalidAuthenticityToken do |e|
23
+ raise unless action_name == 'upload'
24
+ render json: { success: false, error: 'Invalid authenticity token provided' }, status: :unauthorized
25
+ end
26
+ end
27
+
28
+ module ControllerMethods
29
+
30
+ def upload
31
+ begin
32
+ render json: uploader.process!(upload_params), status: :created
33
+ rescue Voltron::Upload::Error => e
34
+ render json: e.response, status: e.status
35
+ end
36
+ end
37
+
38
+ def uploader
39
+ self.class.instance_variable_get('@uploader')
40
+ end
41
+
42
+ def upload_params
43
+ request.parameters[uploader.resource_name].slice(*uploader.permitted_params)
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
50
+
51
+ require "voltron/upload/engine" if defined?(Rails)
@@ -0,0 +1,58 @@
1
+ module Voltron
2
+ class Uploader
3
+
4
+ attr_accessor :resource
5
+
6
+ def initialize(resource)
7
+ @resource = resource.to_s.classify.safe_constantize
8
+ end
9
+
10
+ # Resource name as it would appear in the params hash
11
+ def resource_name
12
+ resource.name.singularize.underscore.to_sym
13
+ end
14
+
15
+ # List of permitted parameters needed for upload action
16
+ def permitted_params
17
+ columns.keys.map(&:to_sym)
18
+ #.map { |name, multiple| multiple ? { name => [] } : name }
19
+ end
20
+
21
+ def process!(params)
22
+ params = params.map { |column, value| { column => multiple?(column) && value.is_a?(Array) ? value.map(&:values).flatten : value } }.reduce(Hash.new, :merge)
23
+ model = resource.new(params)
24
+
25
+ # Test the validity, get the errors if any
26
+ model.valid?
27
+
28
+ # Remove all errors that were not related to an uploader, they're expected in this case
29
+ (model.errors.keys - resource.uploaders.keys).each { |k| model.errors.delete k }
30
+
31
+ if model.errors.any?
32
+ # If any errors, return the messages
33
+ raise ::Voltron::Upload::Error.new(model.errors.full_messages)
34
+ else
35
+ { uploads: files_from(model) }
36
+ end
37
+ end
38
+
39
+ def files_from(model)
40
+ model.slice(columns.keys).values.flatten.reject(&:blank?).map(&:to_upload_json)
41
+ end
42
+
43
+ # Get a hash of uploader columns and whether or not it accepts multiple uploads
44
+ # i.e. - { column => multiple_uploads? }
45
+ # i.e. - { avatar: false }
46
+ def columns
47
+ @instance ||= resource.new
48
+ uploaders = resource.uploaders.keys.map(&:to_s)
49
+ resource.uploaders.map { |k,v| { k.to_s => multiple?(k) } }.reduce(Hash.new, :merge)
50
+ end
51
+
52
+ def multiple?(column)
53
+ @instance ||= resource.new
54
+ @instance.respond_to?("#{column}_urls")
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'voltron/upload/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'voltron-upload'
8
+ spec.version = Voltron::Upload::VERSION
9
+ spec.authors = ['Eric Hainer']
10
+ spec.email = ['eric@commercekitchen.com']
11
+
12
+ spec.summary = %q{Adds logic to handle drag and drop file uploads with Dropzone.js}
13
+ spec.homepage = 'https://github.com/ehainer/voltron-upload'
14
+ spec.license = 'GNU GPL v3'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'rails', '>= 4.2'
22
+ spec.add_dependency 'voltron', '>= 0.2.5'
23
+ spec.add_dependency 'carrierwave', '~> 1.0'
24
+ spec.add_dependency 'sass-rails', '>= 5.0'
25
+
26
+ spec.add_development_dependency 'byebug'
27
+ spec.add_development_dependency 'bundler', '~> 1.12'
28
+ spec.add_development_dependency 'rake', '~> 11.3'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'rspec-rails', '~> 3.4'
31
+ spec.add_development_dependency 'factory_girl_rails', '~> 4.0'
32
+ spec.add_development_dependency 'jquery-rails', '~> 4'
33
+ spec.add_development_dependency 'sqlite3', '~> 1.2'
34
+ end