Class: Model

Inherits:
SiteRecord show all
Extended by:
Forwardable
Defined in:
lib/yodel/models/core/model/model.rb

Constant Summary

Constants inherited from AbstractRecord

AbstractRecord::CALLBACKS, AbstractRecord::FIELD_CALLBACKS, AbstractRecord::ORDERS

Instance Attribute Summary collapse

Attributes inherited from SiteRecord

#site

Attributes inherited from AbstractRecord

#changed, #errors, #stash, #typecast, #values

Instance Method Summary collapse

Methods inherited from SiteRecord

#default_values, #inspect_hash, #perform_reload, #prepare_reload_params, #site_id

Methods included from SiteModel

#scoped, #scoped_for

Methods included from MongoModel

#collection, #scoped

Methods included from AbstractModel

#embed_many, #embed_one, #field, #fields, #many, #one

Methods inherited from MongoRecord

#collection, #default_values, #fields, #id, #increment!, #inspect_hash, #load_from_mongo, #load_mongo_document, #perform_destroy, #perform_reload, #perform_save, #set_id

Methods inherited from AbstractRecord

#changed!, #changed?, #clear_key, #default_values, #destroyed?, #eql?, #errors?, #field, #field?, #field_was, #fields, #from_json, #get, #get_meta, #get_raw, #hash, #id, #increment!, inherited, #inspect, #inspect_hash, #inspect_value, #method_missing, #new?, #prepare_reload_params, #present?, #reload, #save, #save_without_validation, #search_terms, #set, #set_meta, #set_raw, #to_json, #trigger_field_callback, #update, #valid?

Constructor Details

#initialize(site, values = {}) ⇒ Model

Returns a new instance of Model.



52
53
54
55
56
57
58
# File 'lib/yodel/models/core/model/model.rb', line 52

def initialize(site, values={})
  @cached_records_by_name = {}
  super
  @unscoped     = Record.scoped(site, self)
  @scope        = Record.scoped(site, self, 'model' => get_raw('descendants'))
  @record_class = Object.module_eval(get_raw('record_class_name'))
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class AbstractRecord

Instance Attribute Details

#record_classObject (readonly)

Returns the value of attribute record_class.



7
8
9
# File 'lib/yodel/models/core/model/model.rb', line 7

def record_class
  @record_class
end

#unscopedObject (readonly)

Returns the value of attribute unscoped.



7
8
9
# File 'lib/yodel/models/core/model/model.rb', line 7

def unscoped
  @unscoped
end

Instance Method Details

#[](name) ⇒ Object

Simple lookup operator for models that have records with unique names. Used as if the model object was a hash: site.emails



147
148
149
150
151
152
153
154
# File 'lib/yodel/models/core/model/model.rb', line 147

def [](name)
  unless @cached_records_by_name.key?(name)
    record = self.where(name: name).first
    @cached_records_by_name[name] = record
    site.cached_records[record.id] = record unless record.nil?
  end
  @cached_records_by_name[name]
end

#add_embed_many(name, options = {}, &block) ⇒ Object

TODO: modify versions of the association methods



307
308
309
310
# File 'lib/yodel/models/core/model/model.rb', line 307

def add_embed_many(name, options={}, &block)
  embedded_field = add_field(name, 'many_embedded', options)
  embedded_field.instance_exec(embedded_field, &block) if block_given?
end

#add_embed_one(name, options = {}, &block) ⇒ Object



316
317
318
319
# File 'lib/yodel/models/core/model/model.rb', line 316

def add_embed_one(name, options={}, &block)
  embedded_field = add_field(name, 'one_embedded', options)
  embedded_field.instance_exec(embedded_field, &block) if block_given?
end

#add_field(name, type, options = {}) ⇒ Object

TODO: ensure field name != a public method name

Raises:



269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/yodel/models/core/model/model.rb', line 269

def add_field(name, type, options={})
  name = name.to_s
  
  # preconditions
  raise InvalidModelField.new("Duplicate field name") if record_fields.key?(name)
  raise InvalidModelField.new("Type must be a known yodel field type") unless valid_type?(type)
  raise InvalidModelField.new("Field name cannot start with an underscore") if name.start_with?('_')
  
  # add the field to the model and subclasses
  field_type = Field.field_from_type(type.to_s)
  field = field_type.new(name, deep_stringify_keys(options.merge(type: type.to_s)))
  RecordIndex.add_index_for_field(self, field) if field.index?
  record_fields[name] = field
end

#add_index(name, *fields) ⇒ Object

Raises:



347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/yodel/models/core/model/model.rb', line 347

def add_index(name, *fields)
  raise InvalidIndex, 'Indexes must be built on at least one field' if fields.empty?
  spec = fields.collect do |field|
    if field.is_a?(Array)
      [field.first.to_s, (field.last == :desc) ? Mongo::DESCENDING : Mongo::ASCENDING]
    else
      [field.to_s, Mongo::ASCENDING]
    end
  end
  RecordIndex.add_index_for_model(self, name, spec)
  indexes << name
end

#add_many(name, options = {}) ⇒ Object



325
326
327
328
# File 'lib/yodel/models/core/model/model.rb', line 325

def add_many(name, options={})
  type = query_association?(options) ? 'many_query' : 'many_store'
  add_field(name, type, options)
end

#add_mixin(model) ⇒ Object

Add a new mixin to this model

Raises:



397
398
399
400
401
402
403
404
405
# File 'lib/yodel/models/core/model/model.rb', line 397

def add_mixin(model)
  raise InvalidMixin.new("#{model.name} already mixed in to this model") if mixins.include?(model)
  raise InvalidMixin.new("Mixin cannot be a parent") if ancestors.include?(model)
  
  # for all intents and purposes, by mixing in a model, we are a subtype of that model
  model.add_descendant(self)
  mixins << model
  save
end

#add_one(name, options = {}) ⇒ Object



334
335
336
337
# File 'lib/yodel/models/core/model/model.rb', line 334

def add_one(name, options={})
  type = query_association?(options) ? 'one_query' : 'one_store'
  add_field(name, type, options)
end

#all_record_fieldsObject



181
182
183
184
185
# File 'lib/yodel/models/core/model/model.rb', line 181

def all_record_fields
  parents_and_mixins.each_with_object({}) do |ancestor, fields|
    fields.merge! ancestor.record_fields # FIXME: should this be record_fields or all_record_fields?
  end
end

#allowed_child?(other_model) ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/yodel/models/core/model/model.rb', line 195

def allowed_child?(other_model)
  allowed_children_and_descendants.include?(other_model)
end

#allowed_children_and_descendantsObject


Admin interface




191
192
193
# File 'lib/yodel/models/core/model/model.rb', line 191

def allowed_children_and_descendants
  allowed_children.collect(&:descendants).flatten.uniq
end

#allowed_parent?(other_model) ⇒ Boolean

Based on the list of allowed parents, returns true if the supplied model is a descendant of a valid parent of this model.

Returns:

  • (Boolean)


201
202
203
204
# File 'lib/yodel/models/core/model/model.rb', line 201

def allowed_parent?(other_model)
  other_model_ancestors = other_model.ancestors.to_a
  allowed_parents.any? {|parent| other_model_ancestors.include?(parent)}
end

#ancestorsObject


Hierarchy




160
161
162
163
164
165
166
167
168
# File 'lib/yodel/models/core/model/model.rb', line 160

def ancestors
  next_parent = self
  Enumerator.new do |models|
    while next_parent
      models.yield next_parent
      next_parent = next_parent.parent
    end
  end
end

#create_model(name, &block) ⇒ Object

Create a new model which inherits from the current model. If supplied, a block is run and passed a reference to the new model.



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/yodel/models/core/model/model.rb', line 367

def create_model(name, &block)
  name = name.to_s.tableize
  raise "Model name '#{name}' is not unique" if site.model_types.key?(name)
  
  # create a new instance of model
  child = self.class.new(site)
  child.name = name.camelcase.singularize
  child.parent = self
  
  # inherited fields
  fields.each do |name, field|
    child.set(name, get(name)) if field.inherited?
  end
  
  # insert the model in to the site models list
  class_name = name.classify
  site.model_types[name] = child.id
  site.model_plural_names[class_name] = name
  site.save
  
  # append the model to ancestor descendant lists (these are used in queries to
  # restrict the type of records returned, e.g pages.all => _model: ['Page', ...]
  child.tap do |child|
    child.add_descendant(child)
    child.instance_exec(child, &block) if block_given?
    child.save
  end
end

#deep_stringify_keys(hash) ⇒ Object

TODO: remove copy of this method when abstract_model is mixed in



298
299
300
301
302
# File 'lib/yodel/models/core/model/model.rb', line 298

def deep_stringify_keys(hash)
  hash.each_with_object({}) do |(key, value), new_hash|
    new_hash[key.to_s] = (value.respond_to?(:to_hash) ? deep_stringify_keys(value) : value)
  end
end

#destroyObject

Destroys all records which are instances of this model, removes a reference to the model from the parent site, and repeats for any child models of the model.



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/yodel/models/core/model/model.rb', line 416

def destroy
  # remove this model from the model tree
  parent.try(:remove_descendant, self)
  mixins.each {|mixin| mixin.remove_descendant(self)}
  
  # destroy model subclasses, and all record instances
  children.each(&:destroy)
  all.each(&:destroy)
  
  # remove the association between the site and this model
  site.model_types.delete(name.underscore.pluralize)
  site.model_plural_names.delete(name)
  site.save
  
  # remove any remaining indexes
  indexes.each do |name|
    RecordIndex.remove_index_for_model(self, name)
  end
  
  record_fields.each do |name, field|
    RecordIndex.remove_index_for_field(self, field) if field.index?
  end
  
  # destroy the model record
  super
end

#load(site, values) ⇒ Object

Load a record from a mongo document. If this model is not the model of the record, the appropriate model is found and used instead.



120
121
122
123
124
125
126
127
# File 'lib/yodel/models/core/model/model.rb', line 120

def load(site, values)
  return nil if values.nil?
  if values['model'] != id
    site.models.find(values['model']).load(site, values)
  else
    record_class.new(self, site, values, false)
  end
end

#modify(&block) ⇒ Object


Migrations


Convenience method for migrations, so modifications can be specified with site.model_name.modify { field … etc. }



262
263
264
265
# File 'lib/yodel/models/core/model/model.rb', line 262

def modify(&block)
  instance_eval &block
  save
end

#modify_field(name, options = {}, &block) ⇒ Object



290
291
292
293
294
295
# File 'lib/yodel/models/core/model/model.rb', line 290

def modify_field(name, options={}, &block)
  field = record_fields[name.to_s]
  field.options = field.options.dup.merge(deep_stringify_keys(options))
  field.instance_exec(field, &block) if block_given?
  changed!('record_fields')
end

#new(values = {}) ⇒ Object



129
130
131
# File 'lib/yodel/models/core/model/model.rb', line 129

def new(values={})
  record_class.new(self, site).tap {|record| record.update(values, false)}
end

#parents_and_mixinsObject

Combine the full set of parents and mixins in a way that doesn’t duplicate models if mixins would cause a duplicate, and maintains the correct position of mixins in the inheritance tree for this model, any parents, and any mixins (and their mixins)



173
174
175
176
177
178
179
# File 'lib/yodel/models/core/model/model.rb', line 173

def parents_and_mixins
  models = parent.try(:parents_and_mixins) || []
  mixins.each do |mixin_model|
    models |= mixin_model.parents_and_mixins
  end
  models << self
end

#query_association?(options) ⇒ Boolean

Returns:

  • (Boolean)


343
344
345
# File 'lib/yodel/models/core/model/model.rb', line 343

def query_association?(options)
  options[:store] == false || [:foreign_key, :extends, :through].any? {|opt| options[opt].present?}
end

#remove_embed_many(name) ⇒ Object



312
313
314
# File 'lib/yodel/models/core/model/model.rb', line 312

def remove_embed_many(name)
  remove_field(name)
end

#remove_embed_one(name) ⇒ Object



321
322
323
# File 'lib/yodel/models/core/model/model.rb', line 321

def remove_embed_one(name)
  remove_field(name)
end

#remove_field(name) ⇒ Object

Raises:



284
285
286
287
288
# File 'lib/yodel/models/core/model/model.rb', line 284

def remove_field(name)
  field = record_fields.delete(name.to_s)
  raise InvalidModelField.new("Unknown field name") if field.nil?
  RecordIndex.remove_index_for_field(self, field) if field.index?
end

#remove_index(name) ⇒ Object



360
361
362
363
# File 'lib/yodel/models/core/model/model.rb', line 360

def remove_index(name)
  RecordIndex.remove_index_for_model(self, name)
  indexes.delete(name)
end

#remove_many(name) ⇒ Object



330
331
332
# File 'lib/yodel/models/core/model/model.rb', line 330

def remove_many(name)
  remove_field(name)
end

#remove_mixin(model) ⇒ Object

Remove a mixin from this model



408
409
410
411
412
# File 'lib/yodel/models/core/model/model.rb', line 408

def remove_mixin(model)
  model.remove_descendant(self)
  mixins.delete(model)
  save
end

#remove_one(name) ⇒ Object



339
340
341
# File 'lib/yodel/models/core/model/model.rb', line 339

def remove_one(name)
  remove_field(name)
end

#rootObject

Scope to retrieve the first (or only) root record of a model under a site, e.g Page.root(site) will retrieve the root page of a site



141
142
143
# File 'lib/yodel/models/core/model/model.rb', line 141

def root
  self.where(parent: nil).order('index asc').first
end

#rootsObject

Scope to retrieve all root records of a model type under a site, e.g Groups.roots(site). Returns all records with a nil parent.



135
136
137
# File 'lib/yodel/models/core/model/model.rb', line 135

def roots
  self.where(parent: nil).order('index asc')
end

#run_record_after_create_callbacks(record) ⇒ Object



89
90
91
# File 'lib/yodel/models/core/model/model.rb', line 89

def run_record_after_create_callbacks(record)
  record_after_create_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_after_destroy_callbacks(record) ⇒ Object



105
106
107
# File 'lib/yodel/models/core/model/model.rb', line 105

def run_record_after_destroy_callbacks(record)
  record_after_destroy_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_after_save_callbacks(record) ⇒ Object



81
82
83
# File 'lib/yodel/models/core/model/model.rb', line 81

def run_record_after_save_callbacks(record)
  record_after_save_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_after_update_callbacks(record) ⇒ Object



97
98
99
# File 'lib/yodel/models/core/model/model.rb', line 97

def run_record_after_update_callbacks(record)
  record_after_update_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_after_validation_callbacks(record) ⇒ Object



73
74
75
# File 'lib/yodel/models/core/model/model.rb', line 73

def run_record_after_validation_callbacks(record)
  record_after_validation_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_before_create_callbacks(record) ⇒ Object



85
86
87
# File 'lib/yodel/models/core/model/model.rb', line 85

def run_record_before_create_callbacks(record)
  record_before_create_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_before_destroy_callbacks(record) ⇒ Object



101
102
103
# File 'lib/yodel/models/core/model/model.rb', line 101

def run_record_before_destroy_callbacks(record)
  record_before_destroy_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_before_save_callbacks(record) ⇒ Object



77
78
79
# File 'lib/yodel/models/core/model/model.rb', line 77

def run_record_before_save_callbacks(record)
  record_before_save_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_before_update_callbacks(record) ⇒ Object



93
94
95
# File 'lib/yodel/models/core/model/model.rb', line 93

def run_record_before_update_callbacks(record)
  record_before_update_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#run_record_before_validation_callbacks(record) ⇒ Object


Callbacks


TODO: use loops like in abstract record to write these functions



69
70
71
# File 'lib/yodel/models/core/model/model.rb', line 69

def run_record_before_validation_callbacks(record)
  record_before_validation_callbacks.each {|fn| Function.new(fn).execute(record)}
end

#to_strObject



60
61
62
# File 'lib/yodel/models/core/model/model.rb', line 60

def to_str
  "#<Model: #{name}>"
end

#user_allowed_to?(user, action, record) ⇒ Boolean


Permissions


Returns:

  • (Boolean)


224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/yodel/models/core/model/model.rb', line 224

def user_allowed_to?(user, action, record)
  case action
  when :view
    group = view_group
  when :update
    group = update_group
  when :delete
    group = delete_group
  when :create
    group = create_group
  end
  
  return true if group.nil?
  group.permitted?(user, record)
end

#user_allowed_to_create?(user, record) ⇒ Boolean

Returns:

  • (Boolean)


252
253
254
# File 'lib/yodel/models/core/model/model.rb', line 252

def user_allowed_to_create?(user, record)
  user_allowed_to?(user, :create, record)
end

#user_allowed_to_delete?(user, record) ⇒ Boolean

Returns:

  • (Boolean)


248
249
250
# File 'lib/yodel/models/core/model/model.rb', line 248

def user_allowed_to_delete?(user, record)
  user_allowed_to?(user, :delete, record)
end

#user_allowed_to_update?(user, record) ⇒ Boolean

Returns:

  • (Boolean)


244
245
246
# File 'lib/yodel/models/core/model/model.rb', line 244

def user_allowed_to_update?(user, record)
  user_allowed_to?(user, :update, record)
end

#user_allowed_to_view?(user, record) ⇒ Boolean

Returns:

  • (Boolean)


240
241
242
# File 'lib/yodel/models/core/model/model.rb', line 240

def user_allowed_to_view?(user, record)
  user_allowed_to?(user, :view, record)
end

#valid_child?(other_model) ⇒ Boolean

Returns:

  • (Boolean)


216
217
218
# File 'lib/yodel/models/core/model/model.rb', line 216

def valid_child?(other_model)
  valid_children.include?(other_model)
end

#valid_childrenObject

Returns an array of all allowed children and descendants of those children. This list respects both allowed_children and allowed_parents restrictions, so Page (which allows children that are descendants of Page) won’t include Article which can only exist under a Blog page, even though Article is a descendant of Page.



212
213
214
# File 'lib/yodel/models/core/model/model.rb', line 212

def valid_children
  allowed_children_and_descendants.select {|child| child.allowed_parent?(self)}
end