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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.md +655 -0
  6. data/Rakefile +14 -0
  7. data/example/config.ru +100 -0
  8. data/lib/populate_me.rb +2 -0
  9. data/lib/populate_me/admin.rb +157 -0
  10. data/lib/populate_me/admin/__assets__/css/asmselect.css +63 -0
  11. data/lib/populate_me/admin/__assets__/css/jquery-ui.min.css +6 -0
  12. data/lib/populate_me/admin/__assets__/css/main.css +244 -0
  13. data/lib/populate_me/admin/__assets__/img/help/children.png +0 -0
  14. data/lib/populate_me/admin/__assets__/img/help/create.png +0 -0
  15. data/lib/populate_me/admin/__assets__/img/help/delete.png +0 -0
  16. data/lib/populate_me/admin/__assets__/img/help/edit.png +0 -0
  17. data/lib/populate_me/admin/__assets__/img/help/form.png +0 -0
  18. data/lib/populate_me/admin/__assets__/img/help/list.png +0 -0
  19. data/lib/populate_me/admin/__assets__/img/help/login.png +0 -0
  20. data/lib/populate_me/admin/__assets__/img/help/logout.png +0 -0
  21. data/lib/populate_me/admin/__assets__/img/help/menu.png +0 -0
  22. data/lib/populate_me/admin/__assets__/img/help/overview.png +0 -0
  23. data/lib/populate_me/admin/__assets__/img/help/save.png +0 -0
  24. data/lib/populate_me/admin/__assets__/img/help/sort.png +0 -0
  25. data/lib/populate_me/admin/__assets__/img/help/sublist.png +0 -0
  26. data/lib/populate_me/admin/__assets__/js/asmselect.js +412 -0
  27. data/lib/populate_me/admin/__assets__/js/columnav.js +87 -0
  28. data/lib/populate_me/admin/__assets__/js/jquery-ui.min.js +7 -0
  29. data/lib/populate_me/admin/__assets__/js/main.js +388 -0
  30. data/lib/populate_me/admin/__assets__/js/mustache.js +578 -0
  31. data/lib/populate_me/admin/__assets__/js/sortable.js +2 -0
  32. data/lib/populate_me/admin/views/help.erb +94 -0
  33. data/lib/populate_me/admin/views/page.erb +189 -0
  34. data/lib/populate_me/api.rb +124 -0
  35. data/lib/populate_me/attachment.rb +186 -0
  36. data/lib/populate_me/document.rb +192 -0
  37. data/lib/populate_me/document_mixins/admin_adapter.rb +149 -0
  38. data/lib/populate_me/document_mixins/callbacks.rb +125 -0
  39. data/lib/populate_me/document_mixins/outcasting.rb +83 -0
  40. data/lib/populate_me/document_mixins/persistence.rb +95 -0
  41. data/lib/populate_me/document_mixins/schema.rb +198 -0
  42. data/lib/populate_me/document_mixins/typecasting.rb +70 -0
  43. data/lib/populate_me/document_mixins/validation.rb +44 -0
  44. data/lib/populate_me/file_system_attachment.rb +40 -0
  45. data/lib/populate_me/grid_fs_attachment.rb +103 -0
  46. data/lib/populate_me/mongo.rb +160 -0
  47. data/lib/populate_me/s3_attachment.rb +120 -0
  48. data/lib/populate_me/variation.rb +38 -0
  49. data/lib/populate_me/version.rb +4 -0
  50. data/populate-me.gemspec +34 -0
  51. data/test/helper.rb +37 -0
  52. data/test/test_admin.rb +183 -0
  53. data/test/test_api.rb +246 -0
  54. data/test/test_attachment.rb +167 -0
  55. data/test/test_document.rb +128 -0
  56. data/test/test_document_admin_adapter.rb +221 -0
  57. data/test/test_document_callbacks.rb +151 -0
  58. data/test/test_document_outcasting.rb +247 -0
  59. data/test/test_document_persistence.rb +83 -0
  60. data/test/test_document_schema.rb +280 -0
  61. data/test/test_document_typecasting.rb +128 -0
  62. data/test/test_grid_fs_attachment.rb +239 -0
  63. data/test/test_mongo.rb +324 -0
  64. data/test/test_s3_attachment.rb +281 -0
  65. data/test/test_variation.rb +91 -0
  66. data/test/test_version.rb +11 -0
  67. 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
+