flexi_model 0.2.0

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