populate-me 0.12.0
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.
- 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
|
+
|