flexi_model 0.2.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.
@@ -0,0 +1,239 @@
1
+ module FlexiModel
2
+ module ArQueryable
3
+ extend ActiveSupport::Concern
4
+
5
+ RECORD = FlexiModel::ArPersistence::RECORD
6
+
7
+ module ClassMethods
8
+ # Return count of items from the model
9
+ def count
10
+ _build_criteria.count
11
+ end
12
+
13
+ # Alias to :count
14
+ def length;
15
+ self.count
16
+ end
17
+
18
+ # Find a record instance by id
19
+ # Returns model instance
20
+ def find(id_or_ids)
21
+ records = RECORD.includes(:values).where(id: id_or_ids)
22
+
23
+ if records.present?
24
+ if id_or_ids.is_a?(Array)
25
+ records.map { |_record| initialize_with_record(_record) }
26
+ else
27
+ initialize_with_record(records.first)
28
+ end
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
34
+ # Load all records from the model
35
+ def all
36
+ _build_criteria
37
+ end
38
+
39
+ # Load first record from the model
40
+ def first
41
+ _build_criteria.first
42
+ end
43
+
44
+ # Load last record from the model
45
+ def last
46
+ _build_criteria.last
47
+ end
48
+
49
+ # Apply conditions using hash map
50
+ def where(conditions = {})
51
+ _build_criteria.where(conditions)
52
+ end
53
+
54
+ private
55
+ def _build_criteria
56
+ Criteria.new(RECORD, self, self.get_flexi_namespace)
57
+ end
58
+
59
+ end
60
+
61
+ def count;
62
+ Criteria.new(RECORD, self, self.get_flexi_namespace).count
63
+ end
64
+
65
+ def length;
66
+ self.class.count
67
+ end
68
+
69
+ class Criteria
70
+ include Enumerable
71
+
72
+ attr_accessor :offset, :limit, :conditions, :host, :orders, :joins,
73
+ :groups, :select_field
74
+ attr_reader :target_model
75
+
76
+ protected :"joins=", :joins, :"orders=", :orders, :"groups=", :groups
77
+
78
+ def initialize(host, target_model, namespace)
79
+ @conditions = {namespace: namespace}
80
+ @joins = []
81
+ @orders = {}
82
+ @groups = []
83
+ @target_model = target_model
84
+ @select_object = nil
85
+
86
+ # By default Max 100 items will be retrieved
87
+ @limit = 100
88
+ @offset = 0
89
+ @host = host
90
+ @members = nil
91
+ end
92
+
93
+ def where(hash)
94
+ self._convert_query(hash)
95
+ self
96
+ end
97
+
98
+ def offset(num)
99
+ self.offset = num
100
+ self
101
+ end
102
+
103
+ def get_offset
104
+ @offset
105
+ end
106
+
107
+ def limit(num)
108
+ self.limit = num
109
+ self
110
+ end
111
+
112
+ def get_limit
113
+ @limit
114
+ end
115
+
116
+ def select_field(field)
117
+ self.select_field = field
118
+ self
119
+ end
120
+
121
+ def destroy_all
122
+ _perform_query.destroy_all
123
+ end
124
+
125
+ DEFAULT_SORTABLE_FIELDS = [:created_at, :updated_at]
126
+
127
+ # Order query by the given field and order type
128
+ #
129
+ # key - field name in symbol or string
130
+ # order_type - order type :asc or :desc
131
+ # :asc - stands for ascending order
132
+ # :desc - stands for descending order
133
+ # Returns self instance
134
+ def order(key, order_type)
135
+ if DEFAULT_SORTABLE_FIELDS.include?(key.to_sym)
136
+ self.orders[key.to_sym] = order_type
137
+ else
138
+ field = _find_field(key)
139
+ _join_table :values
140
+ column = "`#{FlexiModel::ArModels::Value.table_name}`.`#{field.value_column}`"
141
+ self.orders[column] = order_type
142
+ self.groups << "`#{FlexiModel::ArModels::Record.table_name}`.`id`"
143
+ end
144
+
145
+ self
146
+ end
147
+
148
+ def each(&block)
149
+ _members.each {|m| block.call _convert_to_model(m) }
150
+ end
151
+
152
+ def last
153
+ @target_model.initialize_with_record(_members.last)
154
+ end
155
+
156
+ def to_sql
157
+ self._perform_query.to_sql
158
+ end
159
+
160
+ protected
161
+ def _convert_query(hash = {})
162
+ hash.each do |k, v|
163
+ case k.to_sym
164
+
165
+ when :namespace
166
+ self.conditions[:namespace] = v
167
+
168
+ when :id
169
+ self.conditions[:id] = v
170
+
171
+ else
172
+ _join_table :values
173
+
174
+ # Find associated field
175
+ field = _find_field(k)
176
+ self.conditions["#{FlexiModel::ArModels::Value.table_name}.field_id"] = field.id
177
+ self.conditions["#{FlexiModel::ArModels::Value.table_name}.#{field.value_column.to_s}"] = v
178
+ end
179
+ end
180
+ end
181
+
182
+ def _join_table(model)
183
+ unless @joins.include?(model)
184
+ @joins.push(model)
185
+ end
186
+ end
187
+
188
+ def _find_field(k)
189
+ field = (@empty_inst ||= @target_model.new)
190
+ .get_flexi_fields_map[k.to_sym]
191
+ raise "Invalid field - #{k}" unless field
192
+
193
+ field
194
+ end
195
+
196
+ def _convert_to_model(r)
197
+ _inst = @target_model.initialize_with_record(r)
198
+ if @select_field.present?
199
+ _inst.send(@select_field.to_sym)
200
+ else
201
+ _inst
202
+ end
203
+ end
204
+
205
+ def _singular_field(class_name)
206
+ class_name.name.split('::').last.underscore
207
+ end
208
+
209
+ def _members
210
+ @members ||= _perform_query
211
+ end
212
+
213
+ def _perform_query
214
+ # Prepare conditions
215
+ _host = self.host
216
+ .where(self.conditions)
217
+ .limit(self.get_limit)
218
+ .offset(self.get_offset)
219
+
220
+ # Join tables
221
+ if self.joins.present?
222
+ _host = _host.joins(@joins)
223
+ end
224
+
225
+ # Add orders
226
+ self.orders.each do |k, v|
227
+ _host = _host.order("#{k} #{v.to_sym == :desc ? 'DESC' : 'ASC'}")
228
+ end
229
+
230
+ # Add groups
231
+ if self.groups.present?
232
+ _host = _host.group(self.groups)
233
+ end
234
+
235
+ _host
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,303 @@
1
+ module FlexiModel
2
+ module Association
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_eval <<-ATTRS, __FILE__, __LINE__ + 1
7
+ cattr_accessor :associations
8
+ @@associations = {}
9
+
10
+ cattr_accessor :associated_classes
11
+ @@associated_classes = {}
12
+ ATTRS
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ # Create relationship between two flexi model based on primary key
18
+ def belongs_to(model_name, options = { })
19
+ _id_field = options[:foreign_key] || "#{model_name.to_s}_id"
20
+ _target_model = (options[:class_name] || model_name).to_s.classify
21
+
22
+ self.flexi_field _id_field.to_sym, :integer
23
+
24
+ # Generate dynamic finder method
25
+ _build_belongs_to_accessors model_name, _target_model, _id_field
26
+
27
+ self.associated_classes["#{model_name.to_s.singularize}_id".to_sym] =
28
+ _target_model.to_sym
29
+
30
+ (self.associations[:belongs_to] ||= []) << model_name
31
+ end
32
+
33
+ # Create one to many relationship among the flexi models
34
+ #
35
+ # Options
36
+ # - :class_name - Target model class name
37
+ def has_many(model_name, options = { })
38
+ _class_name = (options[:class_name] ||
39
+ model_name.to_s.singularize.classify).to_sym
40
+
41
+ _build_has_many_accessors _class_name, model_name
42
+
43
+ self.associated_classes["#{model_name.to_s.singularize}_id".to_sym] =
44
+ _class_name.to_sym
45
+ (self.associations[:has_many] ||= []) << model_name
46
+ end
47
+
48
+ # Create many to many relationship between two flexi models based
49
+ # on foreign key
50
+ #
51
+ # Options
52
+ # - joining_class - You can define your joining class name or
53
+ # you can leave it to us to generate on the fly.
54
+ #
55
+ def has_and_belongs_to_many(method_name, options = { })
56
+ # Get joining class name (dev can specify over :joining_class option)
57
+ _self_ref, _class_name = _build_joining_class_name(method_name)
58
+ _joining_class_name = (options[:joining_class] || _class_name).to_sym
59
+
60
+ # Build field name
61
+ _fields = [_build_field_name(self.name), _build_field_name(method_name)]
62
+
63
+ # Eval dynamic joining class
64
+ _joining_class = _build_joining_class(
65
+ _joining_class_name, _fields, _self_ref)
66
+
67
+ # Create setter for setting all <target> records
68
+ _build_habtm_class_accessors(
69
+ _joining_class_name, method_name, _fields, _self_ref)
70
+
71
+ self.associated_classes["#{method_name.to_s.singularize}_id".to_sym] =
72
+ _joining_class.name.to_sym
73
+ (self.associations[:has_and_belongs_to_many] ||= []) << method_name
74
+ end
75
+
76
+ # Return list of fields excluding relationship's foreign key for
77
+ def flexi_fields_except_fk
78
+ _field_names = self.associations.values.flatten.map { |_v| :"#{_v.to_s.singularize}_id" }
79
+ if none_flexi_fields.present?
80
+ none_flexi_fields.each do |field|
81
+ ['file_name', 'content_type', 'file_size', 'updated_at'].each do |_suffix|
82
+ _field_names << :"#{field.name.to_s}_#{_suffix}"
83
+ end
84
+ end
85
+ end
86
+ self.flexi_fields.select { |_f| !_field_names.include?(_f.name.to_sym) }
87
+ end
88
+
89
+ private
90
+ def _build_belongs_to_accessors(model_name, target_model, id_field)
91
+ self.class_eval <<-ACCESSORS, __FILE__, __LINE__ + 1
92
+ # Return object instance
93
+ def #{model_name.to_s}
94
+ @#{model_name.to_s} ||= self.class.parent::#{target_model}.find(self.#{id_field})
95
+ end
96
+
97
+ def #{model_name.to_s}_type
98
+ :#{target_model.classify}
99
+ end
100
+
101
+ # Set object instance
102
+ def #{model_name.to_s}=(inst)
103
+ inst.save if inst.new_record?
104
+ self.#{id_field} = inst._id
105
+ @#{model_name.to_s} = inst
106
+ end
107
+ ACCESSORS
108
+ end
109
+
110
+ def _build_habtm_class_accessors(_class_name, method_name, _fields, self_ref)
111
+ _column_name = if self_ref
112
+ "first_id"
113
+ else
114
+ "#{_fields.first}_id"
115
+ end
116
+
117
+ self.class_eval <<-ACCESSORS, __FILE__, __LINE__ + 1
118
+ def #{method_name}_changed?; @#{method_name}_changed end
119
+
120
+ def #{method_name}
121
+ @#{method_name} ||= #{_class_name}.
122
+ #{self_ref ?
123
+ "where(:first_id => self._id).select_field(:second)" :
124
+ "where(#{_column_name}: self._id).select_field(:#{_fields.last})"}
125
+ end
126
+
127
+ def #{method_name.to_s.singularize}_ids=(ids)
128
+ @#{method_name.to_s.singularize}_ids = ids
129
+ self.#{method_name}= self.class.send(
130
+ :_find_class, :#{method_name.to_s.classify}).
131
+ where(id: ids).to_a
132
+ end
133
+
134
+ def #{method_name.to_s.singularize}_ids
135
+ @#{method_name.to_s.singularize}_ids ||= #{method_name}.to_a.compact.map(&:_id)
136
+ end
137
+
138
+ def #{method_name}=(an_array)
139
+ @#{method_name} = an_array
140
+ @#{method_name}_changed = true
141
+ end
142
+
143
+ after_save :_#{method_name}_after_save
144
+ after_update :_#{method_name}_after_update
145
+
146
+ def _#{method_name}_after_save
147
+ return if @#{method_name}.nil?
148
+ _create_#{method_name}_mappings!
149
+ end
150
+
151
+ def _#{method_name}_after_update
152
+ return if @#{method_name}.nil?
153
+ _update_#{method_name}_mappings!
154
+ end
155
+
156
+ def _update_#{method_name}_mappings!
157
+ _destroy_#{method_name}_mappings!
158
+ _create_#{method_name}_mappings!
159
+ end
160
+
161
+ def _destroy_#{method_name}_mappings!
162
+ #{_class_name}.where(#{_column_name}: self._id).destroy_all
163
+ end
164
+
165
+ def _create_#{method_name}_mappings!
166
+ @#{method_name}.each do |_target_inst|
167
+ _target_inst.save if _target_inst.new_record?
168
+ _map = #{_class_name}.create(
169
+ #{ self_ref ?
170
+ "first: self, second: _target_inst" :
171
+ "#{_fields.first}: self, #{_fields.last}: _target_inst"
172
+ }
173
+ )
174
+
175
+ raise _map.errors.inspect if _map.errors.present?
176
+ end
177
+ end
178
+ ACCESSORS
179
+ end
180
+
181
+ def _build_field_name(name)
182
+ name.to_s.split("::").last.underscore.singularize
183
+ end
184
+
185
+ def _build_joining_class(class_name, fields, self_ref)
186
+ _cls = self.send(:_find_class, class_name.to_s.split('::').last.to_sym)
187
+ _cls if _cls.present?
188
+
189
+ _relationships = ''
190
+
191
+ if self_ref
192
+ _relationships << <<-CODE
193
+ belongs_to :first, :class_name => :#{fields.first}
194
+ belongs_to :second, :class_name => :#{fields.last}
195
+ validates_presence_of :first_id, :second_id
196
+ CODE
197
+ else
198
+ _relationships << <<-CODE
199
+ belongs_to :#{fields.first}
200
+ belongs_to :#{fields.last}
201
+ validates_presence_of :#{fields.first}, :#{fields.last}
202
+ CODE
203
+ end
204
+
205
+ self.class_eval <<-CLASS, __FILE__, __LINE__ + 1
206
+ class #{class_name}
207
+ include FlexiModel
208
+ set_flexi_visible false
209
+
210
+ #{_relationships}
211
+ end
212
+
213
+ #{class_name}
214
+ CLASS
215
+ end
216
+
217
+ def _build_joining_class_name(model_name)
218
+ _parts = self.name.to_s.split("::")
219
+ _name = _parts.last
220
+ _self_ref = false
221
+
222
+ _name_parts = [model_name.to_s.pluralize.downcase,
223
+ _name.pluralize.downcase]
224
+
225
+ _class_name = if _name_parts.uniq.count == 2
226
+ _name_parts.sort.join('_').classify
227
+ else
228
+ _self_ref = true
229
+ _name_parts.join('_').classify
230
+ end
231
+
232
+ [_self_ref, if _parts.length > 1
233
+ "#{_parts[0..(_parts.length - 2)].join('::')}::#{_class_name}"
234
+ else
235
+ _class_name
236
+ end]
237
+ end
238
+
239
+ def _build_has_many_accessors(_class_name, _method)
240
+ _field_name = "#{self.name.underscore.split('/').last}_id"
241
+
242
+ self.class_eval <<-ACCESSORS, __FILE__, __LINE__ + 1
243
+ def #{_method}
244
+ @#{_method} ||= self.class.send(:_find_class, :#{_class_name}).where(#{_field_name}: self._id)
245
+ end
246
+
247
+ def #{_method}=(items)
248
+ @#{_method} = items
249
+ end
250
+
251
+ after_save :_#{_method}_after_save
252
+
253
+ def _#{_method}_after_save
254
+ return unless @#{_method}.present?
255
+ @#{_method}.each do |_item|
256
+ _item.update_attribute(#{_field_name}: self._id)
257
+ end
258
+ end
259
+
260
+ ACCESSORS
261
+ end
262
+
263
+ def _find_class(_constant)
264
+ if self.parent && self.parent.constants.include?(_constant)
265
+ self.parent.const_get(_constant)
266
+ elsif self.class.parent.constants.include?(_constant)
267
+ self.class.parent.const_get(_constant)
268
+ elsif self.class.constants.include?(_constant)
269
+ self.class.const_get(_constant)
270
+ else
271
+ nil
272
+ end
273
+ end
274
+
275
+ def _find_or_generate_joining_class(_class_name, _fields)
276
+ _class = _find_class _class_name
277
+ if _class.nil?
278
+ _generate_mapping_class _class_name, _fields.first, _fields.last
279
+ else
280
+ _class
281
+ end
282
+ end
283
+
284
+ def _generate_mapping_class(name, _field_1, _field_2)
285
+ self.class_eval <<-CLASS, __FILE__, __LINE__ + 1
286
+ class #{name.to_s}
287
+ include FlexiModel
288
+
289
+ set_flexi_visible false
290
+
291
+ flexi_field :#{_field_1}_id, :integer
292
+ flexi_field :#{_field_2}_id, :integer
293
+
294
+ belongs_to :#{_field_1}
295
+ belongs_to :#{_field_2}
296
+ end
297
+
298
+ #{name.to_s}
299
+ CLASS
300
+ end
301
+ end
302
+ end
303
+ end
@@ -0,0 +1,20 @@
1
+ module FlexiModel
2
+ module AttachmentField
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+
7
+ # Set filed definition which is used as attachment accessor.
8
+ #
9
+ # Ie. Paperclip exposes < attachment name > method over the host class.
10
+ # Using this method we can hint admin panel that we have a field which
11
+ # accepts file
12
+ def attachment_field(name, options = { })
13
+ options[:accessors] = false
14
+ flexi_field name, :attachment, options
15
+ end
16
+
17
+ alias_method :_attachment, :attachment_field
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ module FlexiModel
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ extend ActiveModel::Callbacks
7
+ include ActiveModel::Validations::Callbacks
8
+
9
+ # Set callbacks
10
+ define_model_callbacks :save, :create, :update, :destroy, :initialize
11
+ end
12
+ end
13
+
14
+ def initialize(*)
15
+ run_callbacks(:initialize) { super }
16
+ end
17
+
18
+ def save
19
+ run_callbacks(:save) { super }
20
+ end
21
+
22
+ def create
23
+ run_callbacks(:create) { super }
24
+ end
25
+
26
+ def update(*)
27
+ run_callbacks(:update) { super }
28
+ end
29
+
30
+ def update_attributes(*)
31
+ run_callbacks(:update) { super }
32
+ end
33
+
34
+ def update_attribute(*)
35
+ run_callbacks(:update) { super }
36
+ end
37
+
38
+ def destroy
39
+ run_callbacks(:destroy) { super }
40
+ end
41
+ end