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