populate-me 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +655 -0
- data/Rakefile +14 -0
- data/example/config.ru +100 -0
- data/lib/populate_me.rb +2 -0
- data/lib/populate_me/admin.rb +157 -0
- data/lib/populate_me/admin/__assets__/css/asmselect.css +63 -0
- data/lib/populate_me/admin/__assets__/css/jquery-ui.min.css +6 -0
- data/lib/populate_me/admin/__assets__/css/main.css +244 -0
- data/lib/populate_me/admin/__assets__/img/help/children.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/create.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/delete.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/edit.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/form.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/list.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/login.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/logout.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/menu.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/overview.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/save.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/sort.png +0 -0
- data/lib/populate_me/admin/__assets__/img/help/sublist.png +0 -0
- data/lib/populate_me/admin/__assets__/js/asmselect.js +412 -0
- data/lib/populate_me/admin/__assets__/js/columnav.js +87 -0
- data/lib/populate_me/admin/__assets__/js/jquery-ui.min.js +7 -0
- data/lib/populate_me/admin/__assets__/js/main.js +388 -0
- data/lib/populate_me/admin/__assets__/js/mustache.js +578 -0
- data/lib/populate_me/admin/__assets__/js/sortable.js +2 -0
- data/lib/populate_me/admin/views/help.erb +94 -0
- data/lib/populate_me/admin/views/page.erb +189 -0
- data/lib/populate_me/api.rb +124 -0
- data/lib/populate_me/attachment.rb +186 -0
- data/lib/populate_me/document.rb +192 -0
- data/lib/populate_me/document_mixins/admin_adapter.rb +149 -0
- data/lib/populate_me/document_mixins/callbacks.rb +125 -0
- data/lib/populate_me/document_mixins/outcasting.rb +83 -0
- data/lib/populate_me/document_mixins/persistence.rb +95 -0
- data/lib/populate_me/document_mixins/schema.rb +198 -0
- data/lib/populate_me/document_mixins/typecasting.rb +70 -0
- data/lib/populate_me/document_mixins/validation.rb +44 -0
- data/lib/populate_me/file_system_attachment.rb +40 -0
- data/lib/populate_me/grid_fs_attachment.rb +103 -0
- data/lib/populate_me/mongo.rb +160 -0
- data/lib/populate_me/s3_attachment.rb +120 -0
- data/lib/populate_me/variation.rb +38 -0
- data/lib/populate_me/version.rb +4 -0
- data/populate-me.gemspec +34 -0
- data/test/helper.rb +37 -0
- data/test/test_admin.rb +183 -0
- data/test/test_api.rb +246 -0
- data/test/test_attachment.rb +167 -0
- data/test/test_document.rb +128 -0
- data/test/test_document_admin_adapter.rb +221 -0
- data/test/test_document_callbacks.rb +151 -0
- data/test/test_document_outcasting.rb +247 -0
- data/test/test_document_persistence.rb +83 -0
- data/test/test_document_schema.rb +280 -0
- data/test/test_document_typecasting.rb +128 -0
- data/test/test_grid_fs_attachment.rb +239 -0
- data/test/test_mongo.rb +324 -0
- data/test/test_s3_attachment.rb +281 -0
- data/test/test_variation.rb +91 -0
- data/test/test_version.rb +11 -0
- metadata +294 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'populate_me/attachment'
|
2
|
+
|
3
|
+
module PopulateMe
|
4
|
+
|
5
|
+
class FileSystemAttachment < Attachment
|
6
|
+
|
7
|
+
set :root, File.expand_path('public')
|
8
|
+
|
9
|
+
def next_available_filename filename
|
10
|
+
FileUtils.mkdir_p self.location_root
|
11
|
+
ext = File.extname(filename)
|
12
|
+
base = File.basename(filename,ext)
|
13
|
+
i = 0
|
14
|
+
loop do
|
15
|
+
suffix = i==0 ? '' : "-#{i}"
|
16
|
+
potential_filename = [base,suffix,ext].join
|
17
|
+
potential_location = self.location_for_filename(potential_filename)
|
18
|
+
if File.exist?(potential_location)
|
19
|
+
i += 1
|
20
|
+
else
|
21
|
+
filename = potential_filename
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
filename
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform_create hash
|
29
|
+
return File.basename(hash[:variation_path]) unless WebUtils.blank?(hash[:variation_path])
|
30
|
+
source = hash[:tempfile].path
|
31
|
+
filename = self.next_available_filename hash[:filename]
|
32
|
+
destination = self.location_for_filename filename
|
33
|
+
FileUtils.cp source, destination
|
34
|
+
filename
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'populate_me/attachment'
|
2
|
+
require 'mongo'
|
3
|
+
require 'rack/grid_serve'
|
4
|
+
|
5
|
+
module PopulateMe
|
6
|
+
|
7
|
+
class MissingMongoDBError < StandardError; end
|
8
|
+
|
9
|
+
class GridFSAttachment < Attachment
|
10
|
+
|
11
|
+
# set :url_prefix, '/attachment'
|
12
|
+
|
13
|
+
# Attachee_prefix is moved on field_value for gridfs
|
14
|
+
def url variation_name=:original
|
15
|
+
return nil if WebUtils.blank?(self.field_filename(variation_name))
|
16
|
+
"#{settings.url_prefix.sub(/\/$/,'')}/#{self.field_filename(variation_name)}"
|
17
|
+
end
|
18
|
+
# Attachee_prefix is moved on field_value for gridfs
|
19
|
+
def location_root
|
20
|
+
File.join(
|
21
|
+
settings.root,
|
22
|
+
settings.url_prefix
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def next_available_filename filename
|
27
|
+
ext = File.extname(filename)
|
28
|
+
base = "#{attachee_prefix}/#{File.basename(filename,ext)}"
|
29
|
+
i = 0
|
30
|
+
loop do
|
31
|
+
suffix = i==0 ? '' : "-#{i}"
|
32
|
+
potential_filename = [base,suffix,ext].join
|
33
|
+
if settings.db.fs.find(filename: potential_filename).count>0
|
34
|
+
i += 1
|
35
|
+
else
|
36
|
+
filename = potential_filename
|
37
|
+
break
|
38
|
+
end
|
39
|
+
end
|
40
|
+
filename
|
41
|
+
end
|
42
|
+
|
43
|
+
def perform_create hash
|
44
|
+
if hash[:variation].nil?
|
45
|
+
fn = next_available_filename(hash[:filename])
|
46
|
+
file = hash[:tempfile]
|
47
|
+
type = hash[:type]
|
48
|
+
else
|
49
|
+
fn = WebUtils.filename_variation hash[:future_field_value], hash[:variation].name, hash[:variation].ext
|
50
|
+
file = File.open(hash[:variation_path])
|
51
|
+
type = Rack::Mime.mime_type ".#{hash[:variation].ext}"
|
52
|
+
end
|
53
|
+
settings.db.fs.upload_from_stream(
|
54
|
+
fn,
|
55
|
+
file, {
|
56
|
+
content_type: type,
|
57
|
+
metadata: {
|
58
|
+
parent_collection: (self.document.class.respond_to?(:collection) ? self.document.class.collection.name : self.attachee_prefix),
|
59
|
+
}
|
60
|
+
}
|
61
|
+
)
|
62
|
+
file.close unless hash[:variation].nil?
|
63
|
+
fn
|
64
|
+
end
|
65
|
+
|
66
|
+
def deletable? variation_name=:original
|
67
|
+
!WebUtils.blank? self.field_filename(variation_name)
|
68
|
+
# Fine since deleting a non-existent file does not raise an error in mongo
|
69
|
+
end
|
70
|
+
|
71
|
+
def perform_delete variation_name=:original
|
72
|
+
gridfile = settings.db.fs.find(filename: self.field_filename(variation_name)).first
|
73
|
+
settings.db.fs.delete(gridfile['_id']) unless gridfile.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
class << self
|
77
|
+
|
78
|
+
def ensure_db
|
79
|
+
raise MissingMongoDBError, "Attachment class #{self.name} does not have a Mongo database." if settings.db.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
def middleware
|
83
|
+
Rack::GridServe
|
84
|
+
end
|
85
|
+
|
86
|
+
def middleware_options
|
87
|
+
[
|
88
|
+
{
|
89
|
+
prefix: settings.url_prefix.dup.gsub(/^\/|\/$/,''),
|
90
|
+
db: settings.db,
|
91
|
+
# lookup: :path
|
92
|
+
}
|
93
|
+
]
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
GridFS = GridFSAttachment
|
101
|
+
|
102
|
+
end
|
103
|
+
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'populate_me/document'
|
2
|
+
require 'mongo'
|
3
|
+
|
4
|
+
module PopulateMe
|
5
|
+
|
6
|
+
class MissingMongoDBError < StandardError; end
|
7
|
+
|
8
|
+
class Mongo < Document
|
9
|
+
|
10
|
+
self.settings.instance_eval do
|
11
|
+
def collection_name
|
12
|
+
puts 'yo'
|
13
|
+
if self[:collection_name].respond_to? :call
|
14
|
+
self[:collection_name].call
|
15
|
+
else
|
16
|
+
self[:collection_name]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
def inherited sub
|
24
|
+
super
|
25
|
+
# self.inherited is not useful anymore because we use ::collection_name
|
26
|
+
# so that the class name exist when this is run.
|
27
|
+
# Which is not the case with dynamically created classes.
|
28
|
+
#
|
29
|
+
# But we'll keep it for legacy code in the meantime.
|
30
|
+
# Decide if we want to keep it.
|
31
|
+
#
|
32
|
+
# If statment is here for dynamically created classes,
|
33
|
+
# because Class.new.name==nil and then it breaks (see tests).
|
34
|
+
unless sub.name.nil?
|
35
|
+
sub.set :collection_name, WebUtils.dasherize_class_name(sub.name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def collection_name
|
40
|
+
self.settings.collection_name || WebUtils.dasherize_class_name(self.name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def collection
|
44
|
+
raise MissingMongoDBError, "Document class #{self.name} does not have a Mongo database." if settings.db.nil?
|
45
|
+
settings.db[self.collection_name]
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_id_field
|
49
|
+
field :_id, {type: :id}
|
50
|
+
end
|
51
|
+
|
52
|
+
def sort_by f, direction=1
|
53
|
+
direction = 1 if direction==:asc
|
54
|
+
direction = -1 if direction==:desc
|
55
|
+
if f.is_a?(Hash)
|
56
|
+
@current_sort = f
|
57
|
+
elsif f.is_a?(Array)
|
58
|
+
@current_sort = f.inject({}) do |h,pair|
|
59
|
+
h.store(pair[0], pair[1])
|
60
|
+
h
|
61
|
+
end
|
62
|
+
else
|
63
|
+
raise(ArgumentError) unless [1,-1].include? direction
|
64
|
+
raise(ArgumentError) unless self.new.respond_to? f
|
65
|
+
@current_sort = {f => direction}
|
66
|
+
end
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def id_string_key
|
71
|
+
(self.fields.keys[0]||'_id').to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_indexes f, ids=[]
|
75
|
+
if self.fields[f.to_sym][:direction]==:desc
|
76
|
+
ids = ids.dup.reverse
|
77
|
+
end
|
78
|
+
requests = ids.each_with_index.inject([]) do |list, (theid, i)|
|
79
|
+
theid = string_or_object_id theid
|
80
|
+
list << {update_one:
|
81
|
+
{
|
82
|
+
filter: {self.id_string_key=>theid},
|
83
|
+
update: {'$set'=>{f=>i}}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
end
|
87
|
+
collection.bulk_write requests
|
88
|
+
end
|
89
|
+
|
90
|
+
def admin_get theid
|
91
|
+
return self.admin_get_multiple(theid) if theid.is_a?(Array)
|
92
|
+
theid = string_or_object_id theid
|
93
|
+
self.cast{ collection.find({id_string_key => theid}).first }
|
94
|
+
end
|
95
|
+
alias_method :[], :admin_get
|
96
|
+
|
97
|
+
def admin_get_multiple theids, o={sort: nil}
|
98
|
+
theids = theids.uniq.compact.map{|theid| string_or_object_id(theid) }
|
99
|
+
self.admin_find(o.merge({
|
100
|
+
query: {id_string_key => {'$in' => theids} }
|
101
|
+
}))
|
102
|
+
end
|
103
|
+
|
104
|
+
def admin_find o={}
|
105
|
+
query = o.delete(:query) || {}
|
106
|
+
o[:sort] ||= @current_sort
|
107
|
+
if o.key?(:fields)
|
108
|
+
o[:projection] = o[:fields].inject({}) do |h, f|
|
109
|
+
h[f.to_sym] = 1
|
110
|
+
h
|
111
|
+
end
|
112
|
+
o.delete(:fields)
|
113
|
+
end
|
114
|
+
self.cast{ collection.find(query, o) }
|
115
|
+
end
|
116
|
+
|
117
|
+
def admin_find_first o={}
|
118
|
+
self.admin_find(o.merge({limit: 1}))[0]
|
119
|
+
end
|
120
|
+
|
121
|
+
def admin_distinct field, o={}
|
122
|
+
query = o.delete(:query) || {}
|
123
|
+
self.collection.distinct field, query, o
|
124
|
+
end
|
125
|
+
|
126
|
+
def string_or_object_id theid
|
127
|
+
if BSON::ObjectId.legal?(theid)
|
128
|
+
BSON::ObjectId.from_string(theid)
|
129
|
+
else
|
130
|
+
theid
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
attr_accessor :_id
|
137
|
+
|
138
|
+
def id; @_id; end
|
139
|
+
def id= value; @_id = value; end
|
140
|
+
|
141
|
+
def perform_create
|
142
|
+
result = self.class.collection.insert_one(self.to_h)
|
143
|
+
if result.ok? and self.id.nil?
|
144
|
+
self.id = result.inserted_id
|
145
|
+
end
|
146
|
+
self.id
|
147
|
+
end
|
148
|
+
|
149
|
+
def perform_update
|
150
|
+
self.class.collection.update_one({'_id'=> self.id}, self.to_h)
|
151
|
+
self.id
|
152
|
+
end
|
153
|
+
|
154
|
+
def perform_delete
|
155
|
+
self.class.collection.delete_one({'_id'=> self.id})
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'populate_me/attachment'
|
2
|
+
require 'aws-sdk-s3'
|
3
|
+
|
4
|
+
module PopulateMe
|
5
|
+
|
6
|
+
class MissingBucketError < StandardError; end
|
7
|
+
|
8
|
+
class S3Attachment < Attachment
|
9
|
+
|
10
|
+
# For S3 this option behaves a bit differently.
|
11
|
+
# Because S3 file are served directly instead of using a middleware,
|
12
|
+
# the url_prefix is just used in the key name before the attachee_prefix.
|
13
|
+
# It helps saving keys under /public for example which is a common idiom
|
14
|
+
# to save file in a path public by default.
|
15
|
+
# This option can be overriden at the field level.
|
16
|
+
set :url_prefix, '/public'
|
17
|
+
|
18
|
+
# Attachee_prefix is moved on field_value for S3
|
19
|
+
# as well as url_prefix
|
20
|
+
def url variation_name=:original
|
21
|
+
return nil if WebUtils.blank?(self.field_filename(variation_name))
|
22
|
+
"#{settings.bucket.url}/#{self.field_filename(variation_name)}"
|
23
|
+
end
|
24
|
+
# Attachee_prefix is moved on field_value for S3
|
25
|
+
# as well as url_prefix
|
26
|
+
def location_root
|
27
|
+
File.join(
|
28
|
+
settings.root,
|
29
|
+
settings.url_prefix
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def local_url_prefix
|
34
|
+
(
|
35
|
+
self.field_options[:url_prefix] ||
|
36
|
+
self.document.class.settings.s3_url_prefix ||
|
37
|
+
settings.url_prefix
|
38
|
+
).gsub(/^\/|\/$/,'')
|
39
|
+
end
|
40
|
+
|
41
|
+
def next_available_filename filename
|
42
|
+
ext = File.extname(filename)
|
43
|
+
base = File.join(
|
44
|
+
local_url_prefix,
|
45
|
+
attachee_prefix,
|
46
|
+
File.basename(filename,ext)
|
47
|
+
).gsub(/^\//,'')
|
48
|
+
i = 0
|
49
|
+
loop do
|
50
|
+
suffix = i==0 ? '' : "-#{i}"
|
51
|
+
potential_filename = [base,suffix,ext].join
|
52
|
+
if settings.bucket.object(potential_filename).exists?
|
53
|
+
i += 1
|
54
|
+
else
|
55
|
+
filename = potential_filename
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
filename
|
60
|
+
end
|
61
|
+
|
62
|
+
def perform_create hash
|
63
|
+
if hash[:variation].nil?
|
64
|
+
fn = next_available_filename(hash[:filename])
|
65
|
+
file = hash[:tempfile]
|
66
|
+
type = hash[:type]
|
67
|
+
else
|
68
|
+
fn = WebUtils.filename_variation hash[:future_field_value], hash[:variation].name, hash[:variation].ext
|
69
|
+
file = File.open(hash[:variation_path])
|
70
|
+
type = Rack::Mime.mime_type ".#{hash[:variation].ext}"
|
71
|
+
end
|
72
|
+
settings.bucket.put_object({
|
73
|
+
acl: self.field_options[:acl] || 'public-read',
|
74
|
+
key: fn,
|
75
|
+
content_type: type,
|
76
|
+
body: file,
|
77
|
+
metadata: {
|
78
|
+
parent_collection: (self.document.class.respond_to?(:collection) ? self.document.class.collection.name : self.attachee_prefix),
|
79
|
+
}
|
80
|
+
})
|
81
|
+
file.close unless hash[:variation].nil?
|
82
|
+
fn
|
83
|
+
end
|
84
|
+
|
85
|
+
def deletable? variation_name=:original
|
86
|
+
!WebUtils.blank? self.field_filename(variation_name)
|
87
|
+
# Fine since deleting a non-existent file does not raise an error in S3
|
88
|
+
end
|
89
|
+
|
90
|
+
def perform_delete variation_name=:original
|
91
|
+
s3file = settings.bucket.object(self.field_filename(variation_name))
|
92
|
+
s3file.delete unless s3file.nil?
|
93
|
+
end
|
94
|
+
|
95
|
+
class << self
|
96
|
+
|
97
|
+
def ensure_bucket
|
98
|
+
raise MissingBucketError, "Attachment class #{self.name} does not have an S3 bucket." if settings.bucket.nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
def middleware
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# def middleware_options
|
106
|
+
# [
|
107
|
+
# {
|
108
|
+
# prefix: settings.url_prefix.dup.gsub(/^\/|\/$/,''),
|
109
|
+
# db: settings.db,
|
110
|
+
# # lookup: :path
|
111
|
+
# }
|
112
|
+
# ]
|
113
|
+
# end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module PopulateMe
|
2
|
+
|
3
|
+
class Variation < Struct.new :name, :ext, :job
|
4
|
+
|
5
|
+
# Simple class to deal with variations of an attachment
|
6
|
+
# Mainly variation of images using ImageMagick
|
7
|
+
# but it could be anything else like creating the pdf version
|
8
|
+
# of a text file
|
9
|
+
|
10
|
+
def initialize name, ext, job_as_proc=nil, &job_as_block
|
11
|
+
super name, ext, job_as_proc||job_as_block
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
def new_image_magick_job name, ext, convert_string, options={}
|
17
|
+
o = {
|
18
|
+
strip: true, progressive: true,
|
19
|
+
}.merge(options)
|
20
|
+
defaults = ""
|
21
|
+
defaults << "-strip " if o[:strip]
|
22
|
+
defaults << "-interlace Plane " if o[:progressive] and [:jpg,:jpeg].include?(ext.to_sym)
|
23
|
+
job = lambda{ |src,dst|
|
24
|
+
Kernel.system "convert \"#{src}\" #{defaults}#{convert_string} \"#{dst}\""
|
25
|
+
}
|
26
|
+
self.new name, ext, job
|
27
|
+
end
|
28
|
+
|
29
|
+
def default
|
30
|
+
self.new_image_magick_job(:populate_me_thumb, :jpg, "-flatten -resize '400x225^' -gravity center -extent 400x225")
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|