Validation to prevent parent referencing self as children - ruby-on-rails

I'm looking for a solution to prevent "parents" to add his self as "children".
My Model looks like this:
class Category < ActiveRecord::Base
belongs_to :parent, :class_name => 'Category'
has_many :children, :class_name => 'Category', :foreign_key => 'parent_id'
end
Now I look for a solution to prevent things like this
parent = Category.create(name: "Parent")
Category.new(name: "Children", parent_id: parent.id).valid? # should be => false

You can add a custom validation for that.
Something like
class ParentValidator < ActiveModel::Validator
def validate(record)
if record.parent_id == record.id
record.errors[:parent_id] << 'A record\'s parent cannot be the record itself'
end
end
end
class Category
include ActiveModel::Validations
validates_with ParentValidator
end
or even simpler (if it is a one off thing)
class Category < ActiveRecord::Base
validate :parent_not_self, on: :save
def parent_not_self
if parent_id == id
errors.add(:parent_id, 'A record\'s parent cannot be the record itself')
end
end
end
Both cases will generate a validation error when you try to assign the record itself as the parent's record

Related

Rails 4 Override belongs_to setter

I am having an issue overriding a setter on a belongs_to attribute. I have the following:
class Task < ActiveRecord::Base
belongs_to :parent_task, :class_name => 'Task', :foreign_key => 'parent_task_id'
def parent_task=(value)
write_attribute(:parent_task, value)
unless value == nil
#remove all groups_belonging_to if this has been made into a child task -i.e. if it has a parent
self.groups_belonging_to = []
end
self.save
end
The user model has many tasks:
class User < ActiveRecord::Base
has_many :tasks_created_by, class_name: 'Task', foreign_key: 'created_by_id', dependent: :destroy
In my testing I am creating a task like so:
#child_task = #user.tasks_created_by.create!(name: "Task to Delete", parent_task: #top_parent)
Which gives the error:
ActiveModel::MissingAttributeError: can't write unknown attribute `parent_task`
When I remove the override there is no problem, so I am definitely doing the override wrong somehow. I have used very similar override logic elsewhere but not through a relation before.
This would be better written as a callback. You can use a before_save callback to check for a parent_task and if it is set, clear groups_belonging_to:
class Task < ActiveRecord::Base
belongs_to :parent_task, class_name: 'Task', foreign_key: 'parent_task_id'
before_save :clear_groups if: :parent_task
def clear_groups
self.groups_belonging_to = []
end
end
I solved this problem using alias_method before I override the setter:
class Task < ActiveRecord::Base
belongs_to :parent_task, :class_name => 'Task', :foreign_key => 'parent_task_id'
alias_method :set_parent_task, :parent_task=
def parent_task=(value)
set_parent_task(value)
unless value == nil
#remove all groups_belonging_to if this has been made into a child task -i.e. if it has a parent
self.groups_belonging_to = []
end
self.save
end
end

Using ActiveRelation with :joins on polymorphic has_one to implicitly create relation

We can use ActiveRelation like this:
MyModel.where(:field => "test").create => #<Message ... field:"test">
But it doesnt work for joins with polymorphic has_one associations:
class RelatedModel < AR::Base
# has :some_field
belongs_to :subject, :polymorphic => true
end
class MyModel < AR::Base
# need some dirty magic here
# to build default related_model with params from active_relation
has_one :related_model, :as => :subject, :dependent => :destroy
end
describe MyModel do
it "should auto-create has_one association with joins" do
test = MyModel.joins(:related_model).where("related_models.subject_type" => "MyModel", "related_models.some_field" => "chachacha").create
test.related_model.should_not be_nil
test.related_model.some_field.should == "chachacha"
test.related_model.subject_type.should == "MyModel"
test.related_model.subject_id.should == test.id
# fails =)
end
end
Is it possible to extract active_relation params, pass them to MyModel for use in before_create and build RelatedModel with them?
Diving into ActiveRecord sources i found that
ActiveRecord::Relation covers 'create' with 'scoping' method.
ActiveRecord::Persistance 'create' calls 'initialize' from ActiveRecord::Core.
ActiveRecord::Core 'initialize' calls 'populate_with_current_scope_attributes'
This method declared in ActiveRecord::Scoping uses 'scope_attributes' declared in ActiveRecord::Scoping::Named.
scope_attributes creating relation 'all' and calls 'scope_for_create' on it.
'ActiveRecord::Relation's 'scope_for_create' uses only 'where_values_hash' from current_scope that does not contain rules like 'related_models.subject_type' (this values are contained in where_clauses). So we need to have simple key-value wheres to be used with 'create' on ActiveRecord::Relation. But ActiveRecord not clever enough to know that 'some_field' in where clause should be used with join table.
I found it can be implemented only by accessing where options with self.class.current_scope.where_clauses in 'before_create' on MyModel, parsing them and setting up attributes.
class MyModel < AR::Base
before_create :create_default_node
def create_default_node
clause = self.class.current_scope.where_clauses.detect{|clause| clause =~ /\`related_models\`.\`some_field\`/}
value = clause.scan(/\=.+\`([[:word:]]+)\`/).flatten.first
self.create_node(:some_field => value)
end
end
But it is so dirty, then i decided to find simpler solution and inverted dependency as described in Railscast Pro #394, moved RelatedModel functionality to MyModel with STI. Actually i needed such complicated relation creation because RelatedModel had some functionality common for all models (acts as tree). I decided to delegate 'ancestors' and 'children' to RelatedModel. Inverting dependency solved this problem.
class MyModel < AR::Base
acts_as_tree
belongs_to :subject, :polymorphic => true
end
class MyModel2 < MyModel
end
class RelatedModel < AR::Base
# has :some_field
has_one :my_model, :as => :subject, :dependent => :destroy
end
MyModel.create{|m| m.subject = RelatedModel.create(:some_field => "chachacha")}
MyModel.ancestors # no need to proxy relations

how to emulate has_many :through with polymorphic classes

I understand why ActiveRecord can't support has_many :through on polymorphic classes. But I would like to emulate some of its functionality. Consider the following, where a join table associates two polymorphic classes:
class HostPest < ActiveRecord::Base
belongs_to :host, :polymorphic => true
belongs_to :pest, :polymorphic => true
end
class Host < ActiveRecord::Base
self.abstract_class = true
has_many :host_pests, :as => :host
end
class Pest < ActiveRecord::Base
self.abstract_class = true
has_one :host_pest, :as => :pest
end
class Dog < Host ; end
class Cat < Host ; end
class Flea < Pest ; end
class Tick < Pest ; end
The goal
Since I can't do has_many :pests, :through=>:host_pests, :as=>:host (etc), I'd like to emulate these four methods:
dog.pests (returns a list of pests associated with this dog)
flea.host (return the host associated with this flea)
cat.pests << Tick.create (creates a HostPest record)
tick.host = Cat.create (creates a HostPest record)
Question 1
I've got a working implementation for the first two methods (pests and host), but want to know if this is the best way (specifically, am I overlooking something in ActiveRecord associations that would help):
class Host < ActiveRecord::Base
def pests
HostPest.where(:host_id => self.id, :host_type => self.class).map {|hp| hp.pest}
end
end
class Pest < ActiveRecord::Base
def host
HostPest.where(:pest_id => self.id, :pest_type => self.class).first.host
end
end
Question 2
I'm stumped on how to implement the << and = methods implied here:
cat.pests << Tick.create # => HostPest(:host=>cat, :pest=>tick).create
tick.host = Cat.create # => HostPest(:host=>cat, :pest=>tick).create
Any suggestions? (And again, can ActiveRecord associations provide any help?)
Implementing the host= method on the Pest class is straight forward. We need to make sure we clear the old host while setting a new host (as AR doesn't clear the old value from the intermediary table.).
class Pest < ActiveRecord::Base
self.abstract_class = true
has_one :host_pest, :as => :pest
def host=(host)
Pest.transaction do
host_pest.try(:destroy) # destroy the current setting if any
create_host_pest(:host => host)
end
end
end
Implementing pests<< method on Host class is bit more involved. Add the pests method on the Host class to return the aggregated list of pests. Add the << method on the object returned by pests method.
class Host < ActiveRecord::Base
self.abstract_class = true
has_many :host_pests, :as => :host
# pest list accessor
def pests
#pests ||= begin
host = self # variable to hold the current self.
# We need it later in the block
list = pest_list
# declare << method on the pests list
list.singleton_class.send(:define_method, "<<") do |pest|
# host variable accessible in the block
host.host_pests.create(:pest => pest)
end
list
end
end
private
def pest_list
# put your pest concatenation code here
end
end
Now
cat.pests # returns a list
cat.pests << flea # appends the flea to the pest list
You can address your problem by using STI and regular association:
class HostPest < ActiveRecord::Base
belongs_to :host
belongs_to :pest
end
Store all the hosts in a table called hosts. Add a string column called type to the table.
class Host < ActiveRecord::Base
has_many :host_pests
has_many :pests, :through => :host_pests
end
Inherit the Host class to create new hosts.
class Dog < Host ; end
class Cat < Host ; end
Store all the pests in a table called pests. Add a string column called type to the table.
class Pest < ActiveRecord::Base
has_one :host_pest
has_one :host, :through => :host_pest
end
Inherit the Pest class to create new pests.
class Flea < Pest ; end
class Tick < Pest ; end
Now when you can run following commands:
dog.pests (returns a list of pests associated with this dog)
flea.host (return the host associated with this flea)
cat.pests << Tick.create (creates a HostPest record)
tick.host = Cat.create (creates a HostPest record)
Note
Rails supports has_many :through on polymorphic classes. You need to specify the source_type for this to work.
Consider the models for tagging:
class Tag
has_many :tag_links
end
class TagLink
belongs_to :tag
belongs_to :tagger, :polymorphic => true
end
Let's say products and companies can be tagged.
class Product
has_many :tag_links, :as => :tagger
has_many :tags, :through => :tag_links
end
class Company
has_many :tag_links, :as => :tagger
has_many :tags, :through => :tag_links
end
We can add an association on Tag model to get all the tagged products as follows:
class Tag
has_many :tag_links
has_many :products, :through => :tag_links,
:source => :tagger, :source_type => 'Product'
end

Creating a model that has a tree structure

I have categories that are in a tree structure. I am trying to link them together by defining a parent for each one. (I couldn't figure out how to call the property parent so it's just category for now, but it means the parent).
class Category < ActiveRecord::Base
has_one :category # the parent category
end
But the relationship ends up the wrong way around.
The getter function is on the child category (correctly) but the category_id is stored on the parent:
parent = Category.create(:name => "parent")
child = Category.create(:name => "child", :category => parent)
parent.id # 1
child.id # 2
child.category_id # nil
parent.category_id # 2
child.category.name # "parent" (!!)
The parent needs to be able to have multiple children so this isn't going to work.
What you're looking for is self joins. Check this section of the Rails guide out: http://guides.rubyonrails.org/association_basics.html#self-joins
class Category < ActiveRecord::Base
  has_many :children, class_name: "Category", foreign_key: "parent_id"
  belongs_to :parent, class_name: "Category"
end
Every Category will belong_to a parent, even your parent categories. You can create a single category parent that your highest level categories all belong to, then you can disregard that information in your application.
You can use acts_as_tree gem to achieve this, find below example and link.
https://github.com/amerine/acts_as_tree/tree/master
class Category < ActiveRecord::Base
include ActsAsTree
acts_as_tree order: "name"
end
root = Category.create("name" => "root")
child1 = root.children.create("name" => "child1")
subchild1 = child1.children.create("name" => "subchild1")
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1
You should take a look at the ancestry gem: https://github.com/stefankroes/ancestry
It provides all the functionality you need and is able to get all descendants, siblings, parents, etc with a single SQL query by using a variant of materialized paths so it'll have better performance than the self-joins and acts_as_tree answers above.
Category should have many categories, and the foreign key of each category should be the parent_id. So, when you do parent.children it lists all the categories which have parent_id=parent.id.
Have you read on Single Table Inheritance?
Full Article - https://blog.francium.tech/best-practices-for-handling-hierarchical-data-structure-in-ruby-on-rails-b5830c5ea64d
A Simple table
Table Emp
id: Integer
name: String
parent_id: Integer
Associations
app/models/emp.rb
class Emp < ApplicationRecord
has_many :subs, class_name: 'Emp', foreign_key: :parent_id
belongs_to :superior, class_name: 'Emp', foreign_key: :parent_id
end
Scope Definition
class Emp < ApplicationRecord
----
----
scope :roots, -> { where(parent_id: nil) }
end
Fetching data
def tree_data
output = []
Emp.roots.each do |emp|
output << data(emp)
end
output.to_json
end
def data(employee)
subordinates = []
unless employee.subs.blank?
employee.subs.each do |emp|
subordinates << data(emp)
end
end
{name: employee.name, subordinates: subordinates}
end
Eager Loading
def tree_data
output = []
Emp.roots.includes(subs: {subs: {subs: subs}}}.each do |emp|
output << data(emp)
end
output.to_json
end

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Resources