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,192 @@
|
|
1
|
+
require 'web_utils'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
require 'populate_me/document_mixins/typecasting'
|
5
|
+
require 'populate_me/document_mixins/outcasting'
|
6
|
+
require 'populate_me/document_mixins/schema'
|
7
|
+
require 'populate_me/document_mixins/admin_adapter'
|
8
|
+
require 'populate_me/document_mixins/callbacks'
|
9
|
+
require 'populate_me/document_mixins/validation'
|
10
|
+
require 'populate_me/document_mixins/persistence'
|
11
|
+
|
12
|
+
module PopulateMe
|
13
|
+
|
14
|
+
class MissingDocumentError < StandardError; end
|
15
|
+
class MissingAttachmentClassError < StandardError; end
|
16
|
+
|
17
|
+
class Document
|
18
|
+
|
19
|
+
# PopulateMe::Document is the base for any document
|
20
|
+
# the Backend is supposed to deal with.
|
21
|
+
#
|
22
|
+
# Any module for a specific ORM or ODM should
|
23
|
+
# subclass it.
|
24
|
+
# It contains what is not specific to a particular kind
|
25
|
+
# of database and it provides defaults.
|
26
|
+
#
|
27
|
+
# It can be used on its own but it keeps everything
|
28
|
+
# in memory. Which means it is only for tests and conceptual
|
29
|
+
# understanding.
|
30
|
+
|
31
|
+
include DocumentMixins::Typecasting
|
32
|
+
include DocumentMixins::Outcasting
|
33
|
+
include DocumentMixins::Schema
|
34
|
+
include DocumentMixins::AdminAdapter
|
35
|
+
include DocumentMixins::Callbacks
|
36
|
+
include DocumentMixins::Validation
|
37
|
+
include DocumentMixins::Persistence
|
38
|
+
|
39
|
+
class << self
|
40
|
+
|
41
|
+
def inherited sub
|
42
|
+
super
|
43
|
+
sub.callbacks = WebUtils.deep_copy callbacks
|
44
|
+
sub.settings = settings.dup # no deep copy because of Mongo.settings.db
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
super.gsub(/[A-Z]/, ' \&')[1..-1].gsub('::','')
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s_short
|
52
|
+
self.name[/[^:]+$/].gsub(/[A-Z]/, ' \&')[1..-1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s_plural; WebUtils.pluralize(self.to_s); end
|
56
|
+
def to_s_short_plural; WebUtils.pluralize(self.to_s_short); end
|
57
|
+
|
58
|
+
def from_hash hash, o={}
|
59
|
+
self.new(_is_new: false).set_from_hash(hash, o).snapshot
|
60
|
+
end
|
61
|
+
|
62
|
+
def cast o={}, &block
|
63
|
+
target = block.arity==0 ? instance_eval(&block) : block.call(self)
|
64
|
+
return nil if target.nil?
|
65
|
+
return from_hash(target, o) if target.is_a?(Hash)
|
66
|
+
return target.map{|t| from_hash(t,o)} if target.respond_to?(:map)
|
67
|
+
raise(TypeError, "The block passed to #{self.name}::cast is meant to return a Hash or a list of Hash which respond to `map`")
|
68
|
+
end
|
69
|
+
|
70
|
+
# inheritable settings
|
71
|
+
attr_accessor :settings
|
72
|
+
def set name, value
|
73
|
+
self.settings[name] = value
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_accessor :id, :_is_new, :_old
|
79
|
+
|
80
|
+
def initialize attributes=nil
|
81
|
+
self._is_new = true
|
82
|
+
set attributes if attributes
|
83
|
+
self._errors = {}
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
"#<#{self.class.name}:#{to_h.inspect}>"
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_s
|
91
|
+
default = "#{self.class}#{' ' unless WebUtils.blank?(self.id)}#{self.id}"
|
92
|
+
return default if self.class.label_field.nil?
|
93
|
+
me = self.__send__(self.class.label_field).dup
|
94
|
+
WebUtils.blank?(me) ? default : me
|
95
|
+
end
|
96
|
+
|
97
|
+
def new?; self._is_new; end
|
98
|
+
|
99
|
+
def to_h
|
100
|
+
persistent_instance_variables.inject({'_class'=>self.class.name}) do |h,var|
|
101
|
+
k = var.to_s[1..-1]
|
102
|
+
v = instance_variable_get var
|
103
|
+
if is_nested_docs?(v)
|
104
|
+
h[k] = v.map(&:to_h)
|
105
|
+
else
|
106
|
+
h[k] = v
|
107
|
+
end
|
108
|
+
h
|
109
|
+
end
|
110
|
+
end
|
111
|
+
alias_method :to_hash, :to_h
|
112
|
+
|
113
|
+
def snapshot
|
114
|
+
self._old = self.to_h
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
def nested_docs
|
119
|
+
persistent_instance_variables.map do |var|
|
120
|
+
instance_variable_get var
|
121
|
+
end.find_all do |val|
|
122
|
+
is_nested_docs?(val)
|
123
|
+
end.flatten
|
124
|
+
end
|
125
|
+
|
126
|
+
def == other
|
127
|
+
return false unless other.respond_to?(:to_h)
|
128
|
+
other.to_h==to_h
|
129
|
+
end
|
130
|
+
|
131
|
+
def set attributes
|
132
|
+
attributes.dup.each do |k,v|
|
133
|
+
setter = "#{k}="
|
134
|
+
if respond_to? setter
|
135
|
+
__send__ setter, v
|
136
|
+
end
|
137
|
+
end
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def set_defaults o={}
|
142
|
+
self.class.fields.each do |k,v|
|
143
|
+
if v.key?(:default)&&(__send__(k).nil?||o[:force])
|
144
|
+
set k.to_sym => WebUtils.get_value(v[:default],self)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_from_hash hash, o={}
|
151
|
+
raise(TypeError, "#{hash} is not a Hash") unless hash.is_a? Hash
|
152
|
+
hash = hash.dup # Leave original untouched
|
153
|
+
hash.delete('_class')
|
154
|
+
hash.each do |k,v|
|
155
|
+
getter = k.to_sym
|
156
|
+
if is_nested_hash_docs?(v)
|
157
|
+
break unless respond_to?(getter)
|
158
|
+
__send__(getter).clear
|
159
|
+
v.each do |d|
|
160
|
+
obj = WebUtils.resolve_class_name(d['_class']).new.set_from_hash(d,o)
|
161
|
+
__send__(getter) << obj
|
162
|
+
end
|
163
|
+
else
|
164
|
+
v = typecast(getter,v) if o[:typecast]
|
165
|
+
set getter => v
|
166
|
+
end
|
167
|
+
end
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
# class settings
|
172
|
+
def settings
|
173
|
+
self.class.settings
|
174
|
+
end
|
175
|
+
self.settings = OpenStruct.new
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def is_nested_docs? val
|
180
|
+
# Differenciate nested docs array from other king of array
|
181
|
+
val.is_a?(Array) and !val.empty? and val[0].is_a?(PopulateMe::Document)
|
182
|
+
end
|
183
|
+
|
184
|
+
def is_nested_hash_docs? val
|
185
|
+
# Differenciate nested docs array from other king of array
|
186
|
+
# when from a hash
|
187
|
+
val.is_a?(Array) and !val.empty? and val[0].is_a?(Hash) and val[0].has_key?('_class')
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module PopulateMe
|
2
|
+
module DocumentMixins
|
3
|
+
module AdminAdapter
|
4
|
+
|
5
|
+
def to_admin_url
|
6
|
+
"#{WebUtils.dasherize_class_name(self.class.name)}/#{id}".sub(/\/$/,'')
|
7
|
+
end
|
8
|
+
|
9
|
+
def admin_image_url
|
10
|
+
thefield = self.class.admin_image_field
|
11
|
+
return nil if thefield.nil?
|
12
|
+
self.attachment(thefield).url(:populate_me_thumb)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_admin_list_item o={}
|
16
|
+
{
|
17
|
+
class_name: self.class.name,
|
18
|
+
id: self.id.to_s,
|
19
|
+
admin_url: to_admin_url,
|
20
|
+
title: WebUtils.truncate(to_s, 60),
|
21
|
+
image_url: admin_image_url,
|
22
|
+
local_menu: self.class.relationships.inject([]) do |out,(k,v)|
|
23
|
+
if not v[:hidden] and self.relationship_applicable?(k)
|
24
|
+
out << {
|
25
|
+
title: "#{v[:label]}",
|
26
|
+
href: "#{o[:request].script_name}/list/#{WebUtils.dasherize_class_name(v[:class_name])}?filter[#{v[:foreign_key]}]=#{self.id}",
|
27
|
+
new_page: false
|
28
|
+
}
|
29
|
+
end
|
30
|
+
out
|
31
|
+
end
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_admin_form o={}
|
36
|
+
o[:input_name_prefix] ||= 'data'
|
37
|
+
class_item = {
|
38
|
+
type: :hidden,
|
39
|
+
input_name: "#{o[:input_name_prefix]}[_class]",
|
40
|
+
input_value: self.class.name,
|
41
|
+
}
|
42
|
+
self.class.complete_field_options :_class, class_item
|
43
|
+
items = self.class.fields.inject([class_item]) do |out,(k,item)|
|
44
|
+
if item[:form_field] and self.field_applicable?(k)
|
45
|
+
out << outcast(k, item, o)
|
46
|
+
end
|
47
|
+
out
|
48
|
+
end
|
49
|
+
page_title = self.new? ? "New #{self.class.to_s_short}" : self.to_s
|
50
|
+
# page_title << " (#{self.polymorphic_type})" if self.class.polymorphic?
|
51
|
+
{
|
52
|
+
template: "template#{'_nested' if o[:nested]}_form",
|
53
|
+
page_title: page_title,
|
54
|
+
admin_url: self.to_admin_url,
|
55
|
+
is_new: self.new?,
|
56
|
+
polymorphic_type: self.class.polymorphic? ? self.polymorphic_type : nil,
|
57
|
+
fields: items
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.included(base)
|
62
|
+
base.extend(ClassMethods)
|
63
|
+
end
|
64
|
+
|
65
|
+
module ClassMethods
|
66
|
+
|
67
|
+
def admin_image_field
|
68
|
+
res = self.fields.find do |k,v|
|
69
|
+
if v[:type]==:attachment and !v[:variations].nil?
|
70
|
+
v[:variations].any?{|var|var.name==:populate_me_thumb}
|
71
|
+
else
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
res.nil? ? nil : res[0]
|
76
|
+
end
|
77
|
+
|
78
|
+
def admin_get id
|
79
|
+
return self.admin_get_multiple(id) if id.is_a?(Array)
|
80
|
+
self.cast do
|
81
|
+
documents.find{|doc| doc[id_string_key] == id }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def admin_get_multiple ids, o={sort: nil}
|
86
|
+
self.admin_find(o.merge(query: {id_string_key => {'$in' => ids.uniq.compact}}))
|
87
|
+
end
|
88
|
+
|
89
|
+
def admin_find o={}
|
90
|
+
o[:query] ||= {}
|
91
|
+
docs = self.cast{documents}.find_all do |d|
|
92
|
+
o[:query].inject(true) do |out,(k,v)|
|
93
|
+
out && (d.__send__(k)==v)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
docs.sort!(&@sort_proc) if @sort_proc.is_a?(Proc)
|
97
|
+
docs
|
98
|
+
end
|
99
|
+
|
100
|
+
def admin_find_first o={}
|
101
|
+
self.admin_find(o)[0]
|
102
|
+
end
|
103
|
+
|
104
|
+
def admin_distinct field, o={}
|
105
|
+
self.admin_find(o).map{|d| d.__send__ field}.compact.uniq
|
106
|
+
end
|
107
|
+
|
108
|
+
def sort_field_for o={}
|
109
|
+
filter = o[:params][:filter]
|
110
|
+
return nil if !filter.nil?&&filter.size>1
|
111
|
+
expected_scope = filter.nil? ? nil : filter.keys[0].to_sym
|
112
|
+
f = self.fields.find do |k,v|
|
113
|
+
v[:type]==:position&&v[:scope]==expected_scope
|
114
|
+
end
|
115
|
+
f.nil? ? nil : f[0]
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_admin_list o={}
|
119
|
+
o[:params] ||= {}
|
120
|
+
unless o[:params][:filter].nil?
|
121
|
+
query = o[:params][:filter].inject({}) do |q, (k,v)|
|
122
|
+
q[k.to_sym] = self.new.typecast(k,v)
|
123
|
+
q
|
124
|
+
end
|
125
|
+
new_data = Rack::Utils.build_nested_query(data: o[:params][:filter])
|
126
|
+
end
|
127
|
+
{
|
128
|
+
template: 'template_list',
|
129
|
+
grid_view: self.settings[:grid_view]==true,
|
130
|
+
page_title: self.to_s_short_plural,
|
131
|
+
dasherized_class_name: WebUtils.dasherize_class_name(self.name),
|
132
|
+
new_data: new_data,
|
133
|
+
is_polymorphic: self.polymorphic?,
|
134
|
+
polymorphic_type_values: self.polymorphic? ? self.fields[:polymorphic_type][:values] : nil,
|
135
|
+
sort_field: self.sort_field_for(o),
|
136
|
+
# 'command_plus'=> !self.populate_config[:no_plus],
|
137
|
+
# 'command_search'=> !self.populate_config[:no_search],
|
138
|
+
items: self.admin_find(query: query).map do |d|
|
139
|
+
d.to_admin_list_item(o)
|
140
|
+
end
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module PopulateMe
|
4
|
+
module DocumentMixins
|
5
|
+
module Callbacks
|
6
|
+
|
7
|
+
def exec_callback name
|
8
|
+
name = name.to_sym
|
9
|
+
return self if self.class.callbacks[name].nil?
|
10
|
+
self.class.callbacks[name].each do |job|
|
11
|
+
if job.respond_to?(:call)
|
12
|
+
self.instance_exec name, &job
|
13
|
+
else
|
14
|
+
meth = self.method(job)
|
15
|
+
meth.arity==1 ? meth.call(name) : meth.call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def recurse_callback name
|
22
|
+
nested_docs.each do |d|
|
23
|
+
d.exec_callback name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def ensure_id
|
28
|
+
if self.id.nil?
|
29
|
+
self.id = SecureRandom.hex
|
30
|
+
end
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def ensure_not_new; self._is_new = false; end
|
35
|
+
|
36
|
+
def ensure_position
|
37
|
+
self.class.fields.each do |k,v|
|
38
|
+
if v[:type]==:position
|
39
|
+
return unless self.__send__(k).nil?
|
40
|
+
values = if v[:scope].nil?
|
41
|
+
self.class.admin_distinct k
|
42
|
+
else
|
43
|
+
self.class.admin_distinct(k, query: {
|
44
|
+
v[:scope] => self.__send__(v[:scope])
|
45
|
+
})
|
46
|
+
end
|
47
|
+
val = values.empty? ? 0 : (values.max + 1)
|
48
|
+
self.set k => val
|
49
|
+
end
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def ensure_delete_related
|
55
|
+
self.class.relationships.each do |k,v|
|
56
|
+
if v[:dependent]
|
57
|
+
klass = WebUtils.resolve_class_name v[:class_name]
|
58
|
+
next if klass.nil?
|
59
|
+
klass.admin_find(query: {v[:foreign_key]=>self.id}).each do |d|
|
60
|
+
d.delete
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ensure_delete_attachments
|
67
|
+
self.class.fields.each do |k,v|
|
68
|
+
if v[:type]==:attachment
|
69
|
+
self.attachment(k).delete_all
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_new; self._is_new = true; end
|
75
|
+
|
76
|
+
def self.included(base)
|
77
|
+
base.extend(ClassMethods)
|
78
|
+
base.class_eval do
|
79
|
+
[:save,:create,:update,:delete].each do |cb|
|
80
|
+
before cb, :recurse_callback
|
81
|
+
after cb, :recurse_callback
|
82
|
+
end
|
83
|
+
before :create, :ensure_id
|
84
|
+
before :create, :ensure_position
|
85
|
+
after :create, :ensure_not_new
|
86
|
+
after :save, :snapshot
|
87
|
+
before :delete, :ensure_delete_related
|
88
|
+
before :delete, :ensure_delete_attachments
|
89
|
+
after :delete, :ensure_new
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module ClassMethods
|
94
|
+
|
95
|
+
attr_accessor :callbacks
|
96
|
+
|
97
|
+
def register_callback name, item=nil, options={}, &block
|
98
|
+
name = name.to_sym
|
99
|
+
if block_given?
|
100
|
+
options = item || {}
|
101
|
+
item = block
|
102
|
+
end
|
103
|
+
self.callbacks ||= {}
|
104
|
+
self.callbacks[name] ||= []
|
105
|
+
if options[:prepend]
|
106
|
+
self.callbacks[name].unshift item
|
107
|
+
else
|
108
|
+
self.callbacks[name] << item
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def before name, item=nil, options={}, &block
|
113
|
+
register_callback "before_#{name}", item, options, &block
|
114
|
+
end
|
115
|
+
|
116
|
+
def after name, item=nil, options={}, &block
|
117
|
+
register_callback "after_#{name}", item, options, &block
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|