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,83 @@
1
+ module PopulateMe
2
+ module DocumentMixins
3
+ module Outcasting
4
+
5
+ # This module prepares the field for being send to the Admin API
6
+ # and build the form.
7
+ # It compiles the value and all other info in a hash.
8
+ # Therefore, it is a complement to the AdminAdapter module.
9
+
10
+ def outcast field, item, o={}
11
+ item = item.dup
12
+ item[:input_name] = "#{o[:input_name_prefix]}[#{item[:field_name]}]"
13
+ unless item[:type]==:list
14
+ WebUtils.ensure_key! item, :input_value, self.__send__(field)
15
+ end
16
+ meth = "outcast_#{item[:type]}".to_sym
17
+ if respond_to?(meth)
18
+ __send__(meth, field, item, o)
19
+ else
20
+ item
21
+ end
22
+ end
23
+
24
+ def outcast_list field, item, o={}
25
+ item = item.dup
26
+ item[:items] = self.__send__(field).map do |nested|
27
+ nested.to_admin_form(o.merge(input_name_prefix: item[:input_name]+'[]'))
28
+ end
29
+ item
30
+ end
31
+
32
+ def outcast_select field, item, o={}
33
+ item = item.dup
34
+ unless item[:select_options].nil?
35
+ if item[:multiple]==true
36
+ item[:input_name] = item[:input_name]+'[]'
37
+ end
38
+ opts = WebUtils.deep_copy(WebUtils.get_value(item[:select_options],self))
39
+ opts.map! do |opt|
40
+ if opt.is_a?(String)||opt.is_a?(Symbol)
41
+ opt = [opt.to_s.capitalize,opt]
42
+ end
43
+ if opt.is_a?(Array)
44
+ opt = {description: opt[0].to_s, value: opt[1].to_s}
45
+ end
46
+ if item[:input_value].respond_to?(:include?)
47
+ opt[:selected] = true if item[:input_value].include?(opt[:value])
48
+ else
49
+ opt[:selected] = true if item[:input_value]==opt[:value]
50
+ end
51
+ opt
52
+ end
53
+ if item[:multiple]
54
+ (item[:input_value]||[]).reverse.each do |iv|
55
+ opt = opts.find{|opt| opt[:value]==iv }
56
+ opts.unshift(opts.delete(opt)) unless opt.nil?
57
+ end
58
+ end
59
+ item[:select_options] = opts
60
+ item
61
+ else
62
+ item
63
+ end
64
+ end
65
+
66
+ def outcast_attachment field, item, o={}
67
+ item = item.dup
68
+ item[:url] = self.attachment(field).url
69
+ item
70
+ end
71
+
72
+ def outcast_price field, item, o={}
73
+ item = item.dup
74
+ if item[:input_value].is_a?(Integer)
75
+ item[:input_value] = WebUtils.display_price item[:input_value]
76
+ end
77
+ item
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,95 @@
1
+ module PopulateMe
2
+ module DocumentMixins
3
+ module Persistence
4
+
5
+ def persistent_instance_variables
6
+ instance_variables.select do |k|
7
+ if self.class.fields.empty?
8
+ k !~ /^@_/
9
+ else
10
+ self.class.fields.key? k[1..-1].to_sym
11
+ end
12
+ end
13
+ end
14
+
15
+ def attachment f
16
+ attacher = WebUtils.resolve_class_name self.class.fields[f][:class_name]
17
+ attacher.new self, f
18
+ end
19
+
20
+ # Saving
21
+ def save
22
+ return unless valid?
23
+ exec_callback :before_save
24
+ if new?
25
+ exec_callback :before_create
26
+ id = perform_create
27
+ exec_callback :after_create
28
+ else
29
+ exec_callback :before_update
30
+ id = perform_update
31
+ exec_callback :after_update
32
+ end
33
+ exec_callback :after_save
34
+ id
35
+ end
36
+ def perform_create
37
+ self.class.documents << self.to_h
38
+ self.id
39
+ end
40
+ def perform_update
41
+ index = self.class.documents.index{|d| d['id']==self.id }
42
+ raise MissingDocumentError, "No document can be found with this ID: #{self.id}" if self.id.nil?||index.nil?
43
+ self.class.documents[index] = self.to_h
44
+ end
45
+
46
+ # Deletion
47
+ def delete o={}
48
+ exec_callback :before_delete
49
+ perform_delete
50
+ exec_callback :after_delete
51
+ end
52
+ def perform_delete
53
+ index = self.class.documents.index{|d| d['id']==self.id }
54
+ raise MissingDocumentError, "No document can be found with this ID: #{self.id}" if self.id.nil?||index.nil?
55
+ self.class.documents.delete_at(index)
56
+ end
57
+
58
+ def self.included(base)
59
+ base.extend(ClassMethods)
60
+ end
61
+
62
+ module ClassMethods
63
+
64
+ attr_writer :documents
65
+
66
+ def documents; @documents ||= []; end
67
+
68
+ def id_string_key
69
+ (self.fields.keys[0]||'id').to_s
70
+ end
71
+
72
+ def set_indexes f, ids=[]
73
+ if self.fields and self.fields[f.to_sym] and self.fields[f.to_sym][:direction]==:desc
74
+ ids = ids.dup.reverse
75
+ end
76
+ ids.each_with_index do |id,i|
77
+ self.documents.each do |d|
78
+ d[f.to_s] = i if d[self.id_string_key]==id
79
+ end
80
+ end
81
+ self
82
+ end
83
+
84
+ def is_unique unique_id='unique'
85
+ if self.admin_get(unique_id).nil?
86
+ self.new.set_from_hash({id:unique_id}).save
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,198 @@
1
+ module PopulateMe
2
+ module DocumentMixins
3
+ module Schema
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ attr_writer :fields
12
+ attr_accessor :label_field
13
+
14
+ def fields; @fields ||= {}; end
15
+
16
+ def field name, o={}
17
+ set_id_field if self.fields.empty? and o[:type]!=:id
18
+ complete_field_options name, o
19
+ if o[:type]==:list
20
+ define_method(name) do
21
+ var = "@#{name}"
22
+ instance_variable_set(var, instance_variable_get(var)||[])
23
+ end
24
+ else
25
+ attr_accessor name
26
+ end
27
+ self.fields[name] = o
28
+ end
29
+
30
+ def complete_field_options name, o={}
31
+ o[:field_name] = name
32
+ WebUtils.ensure_key! o, :type, :string
33
+ WebUtils.ensure_key! o, :form_field, ![:id,:position].include?(o[:type])
34
+ o[:wrap] = false unless o[:form_field]
35
+ WebUtils.ensure_key! o, :wrap, ![:hidden,:list].include?(o[:type])
36
+ WebUtils.ensure_key! o, :label, WebUtils.label_for_field(name)
37
+ unless [:id, :polymorphic_type].include?(o[:type])
38
+ complete_only_for_field_option o
39
+ end
40
+ if o[:type]==:attachment
41
+ WebUtils.ensure_key! o, :class_name, settings.default_attachment_class
42
+ raise MissingAttachmentClassError, "No attachment class was provided for the #{self.name} field: #{name}" if o[:class_name].nil?
43
+ o[:class_name] = o[:class_name].name unless o[:class_name].is_a?(String)
44
+ end
45
+ if o[:type]==:list
46
+ o[:class_name] = WebUtils.guess_related_class_name(self.name,o[:class_name]||name)
47
+ o[:dasherized_class_name] = WebUtils.dasherize_class_name o[:class_name]
48
+ else
49
+ WebUtils.ensure_key! o, :input_attributes, {}
50
+ o[:input_attributes][:type] = :hidden if o[:type]==:hidden
51
+ unless o[:type]==:text
52
+ WebUtils.ensure_key! o[:input_attributes], :type, :text
53
+ end
54
+ end
55
+ end
56
+
57
+ def complete_only_for_field_option o={}
58
+ if @currently_only_for
59
+ o[:only_for] = @currently_only_for
60
+ end
61
+ if o[:only_for].is_a?(String)
62
+ o[:only_for] = [o[:only_for]]
63
+ end
64
+ if o.key?(:only_for)
65
+ self.polymorphic unless self.polymorphic?
66
+ self.fields[:polymorphic_type][:values] += o[:only_for]
67
+ self.fields[:polymorphic_type][:values].uniq!
68
+ end
69
+ end
70
+
71
+ def set_id_field
72
+ field :id, {type: :id}
73
+ end
74
+
75
+ def position_field o={}
76
+ name = o[:name]||:position
77
+ o[:type] = :position
78
+ field name, o
79
+ sort_by name, (o[:direction]||:asc)
80
+ end
81
+
82
+ def polymorphic o={}
83
+ WebUtils.ensure_key! o, :type, :polymorphic_type
84
+ WebUtils.ensure_key! o, :values, []
85
+ WebUtils.ensure_key! o, :wrap, false
86
+ WebUtils.ensure_key! o, :input_attributes, {}
87
+ WebUtils.ensure_key! o[:input_attributes], :type, :hidden
88
+ field(:polymorphic_type, o) unless self.polymorphic?
89
+ end
90
+
91
+ def only_for polymorphic_type_values, &bloc
92
+ @currently_only_for = polymorphic_type_values
93
+ yield if block_given?
94
+ remove_instance_variable(:@currently_only_for)
95
+ end
96
+
97
+ def polymorphic?
98
+ self.fields.key? :polymorphic_type
99
+ end
100
+
101
+ def field_applicable? f, p_type=nil
102
+ applicable? :fields, f, p_type
103
+ end
104
+
105
+ def relationship_applicable? f, p_type=nil
106
+ applicable? :relationships, f, p_type
107
+ end
108
+
109
+ def applicable? target, f, p_type=nil
110
+ return false unless self.__send__(target).key?(f)
111
+ return true unless self.polymorphic?
112
+ return true unless self.__send__(target)[f].key?(:only_for)
113
+ return true if p_type.nil?
114
+ self.__send__(target)[f][:only_for].include? p_type
115
+ end
116
+ private :applicable?
117
+
118
+ def label sym # sets the label_field
119
+ @label_field = sym.to_sym
120
+ end
121
+
122
+ def label_field
123
+ return @label_field if self.fields.empty?
124
+ @label_field || self.fields.find do |k,v|
125
+ not [:id,:polymorphic_type].include?(v[:type])
126
+ end[0]
127
+ end
128
+
129
+ def sort_by f, direction=:asc
130
+ raise(ArgumentError) unless [:asc,:desc].include? direction
131
+ raise(ArgumentError) unless self.new.respond_to? f
132
+ @sort_proc = Proc.new do |a,b|
133
+ a,b = b,a if direction==:desc
134
+ a.__send__(f)<=>b.__send__(f)
135
+ end
136
+ self
137
+ end
138
+
139
+ def relationships; @relationships ||= {}; end
140
+
141
+ def relationship name, o={}
142
+ o[:class_name] = WebUtils.guess_related_class_name(self.name,o[:class_name]||name)
143
+ WebUtils.ensure_key! o, :label, name.to_s.gsub('_',' ').capitalize
144
+ WebUtils.ensure_key! o, :foreign_key, "#{WebUtils.dasherize_class_name(self.name).gsub('-','_')}_id"
145
+ o[:foreign_key] = o[:foreign_key].to_sym
146
+ WebUtils.ensure_key! o, :dependent, true
147
+ complete_only_for_field_option o
148
+ self.relationships[name] = o
149
+
150
+ define_method(name) do
151
+ var = "@cached_#{name}"
152
+ instance_variable_set(var, instance_variable_get(var)||WebUtils.resolve_class_name(o[:class_name]).admin_find(query: {o[:foreign_key]=>self.id}))
153
+ end
154
+
155
+ define_method("#{name}_first".to_sym) do
156
+ var = "@cached_#{name}_first"
157
+ instance_variable_set(var, instance_variable_get(var)||WebUtils.resolve_class_name(o[:class_name]).admin_find_first(query: {o[:foreign_key]=>self.id}))
158
+ end
159
+ end
160
+
161
+ def to_select_options o={}
162
+ proc do
163
+ items = self.admin_find({
164
+ query: (o[:query]||{}),
165
+ fields: [self.id_string_key, self.label_field, self.admin_image_field].compact.uniq
166
+ })
167
+ output = items.sort_by do |i|
168
+ i.to_s.downcase
169
+ end.map do |i|
170
+ item = { description: i.to_s, value: i.id }
171
+ unless self.admin_image_field.nil?
172
+ item.merge! preview_uri: i.admin_image_url
173
+ end
174
+ item
175
+ end
176
+ output.unshift(description: '?', value: '') if o[:allow_empty]
177
+ output
178
+ end
179
+ end
180
+
181
+ end
182
+
183
+ # Instance methods
184
+
185
+ def field_applicable? f
186
+ p_type = self.class.polymorphic? ? self.polymorphic_type : nil
187
+ self.class.field_applicable? f, p_type
188
+ end
189
+
190
+ def relationship_applicable? f
191
+ p_type = self.class.polymorphic? ? self.polymorphic_type : nil
192
+ self.class.relationship_applicable? f, p_type
193
+ end
194
+
195
+ end
196
+ end
197
+ end
198
+
@@ -0,0 +1,70 @@
1
+ module PopulateMe
2
+ module DocumentMixins
3
+ module Typecasting
4
+
5
+ # This module deals with typecasting the fields
6
+ # when they are received as strings,
7
+ # generally from a form or a csv file
8
+
9
+ def typecast k, v
10
+ unless self.class.fields.key?(k)
11
+ return WebUtils.automatic_typecast(v)
12
+ end
13
+ f = self.class.fields[k].dup
14
+ meth = "typecast_#{f[:type]}".to_sym
15
+ unless respond_to? meth
16
+ return WebUtils.automatic_typecast(v, [f[:type],:nil])
17
+ end
18
+ __send__ meth, k, v
19
+ end
20
+
21
+ def typecast_integer k, v
22
+ v.to_i
23
+ end
24
+
25
+ def typecast_price k, v
26
+ return nil if WebUtils.blank?(v)
27
+ WebUtils.parse_price(v)
28
+ end
29
+
30
+ def typecast_select k, v
31
+ if v.is_a?(Array)
32
+ v.reject{|str| str=='nil' }
33
+ else
34
+ v
35
+ end
36
+ end
37
+
38
+ def typecast_date k, v
39
+ if v[/\d\d(\/|-)\d\d(\/|-)\d\d\d\d/]
40
+ Date.parse v
41
+ elsif v[/\d\d\d\d(\/|-)\d\d(\/|-)\d\d/]
42
+ Date.parse v
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ def typecast_datetime k, v
49
+ if v[/\d\d(\/|-)\d\d(\/|-)\d\d\d\d \d\d?:\d\d?:\d\d?/]
50
+ d,m,y,h,min,s = v.split(/[-:\s\/]/)
51
+ Time.utc(y,m,d,h,min,s)
52
+ else
53
+ nil
54
+ end
55
+ end
56
+
57
+ def typecast_attachment k, v
58
+ attached = self.attachment k
59
+ if WebUtils.blank? v
60
+ attached.delete_all
61
+ return nil
62
+ elsif v.is_a?(Hash)&&v.key?(:tempfile)
63
+ return attached.create v
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+ end
70
+
@@ -0,0 +1,44 @@
1
+ module PopulateMe
2
+ module DocumentMixins
3
+ module Validation
4
+
5
+ attr_accessor :_errors
6
+
7
+ def errors; self._errors; end
8
+
9
+ def error_on k,v
10
+ self._errors[k] = (self._errors[k]||[]) << v
11
+ self
12
+ end
13
+
14
+ def valid?
15
+ self._errors = {}
16
+ exec_callback :before_validate
17
+ validate
18
+ exec_callback :after_validate
19
+ nested_docs.reduce self._errors.empty? do |result,d|
20
+ result &= d.valid?
21
+ end
22
+ end
23
+
24
+ def validate; end
25
+
26
+ def error_report
27
+ report = self._errors.dup || {}
28
+ persistent_instance_variables.each do |var|
29
+ value = instance_variable_get var
30
+ if is_nested_docs?(value)
31
+ k = var[1..-1].to_sym
32
+ report[k] = []
33
+ value.each do |d|
34
+ report[k] << d.error_report
35
+ end
36
+ end
37
+ end
38
+ report
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+