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,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
|
+
|