populate-me 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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
+