Pregunta

Tengo insignias (algo así como StackOverflow).

Algunos de ellos se pueden adjuntar a elementos identificables (p. ej.se adjunta a la publicación una insignia para >X comentarios en una publicación).Casi todos vienen en múltiples niveles (p. ej.>20, >100, >200), y solo puedes tener un nivel por cada tipo de insignia con distintivo (= badgeset_id).

Para que sea más fácil aplicar la restricción de un nivel por insignia, quiero que las insignias especifiquen su insignia mediante una clave externa de dos columnas: badgeset_id y level - en lugar de por clave principal (badge_id), aunque las insignias también tienen una clave principal estándar.

En codigo:

class Badge < ActiveRecord::Base
  has_many :badgings, :dependent => :destroy
  # integer: badgeset_id, level

  validates_uniqueness_of :badgeset_id, :scope => :level
end

class Badging < ActiveRecord::Base
  belongs_to :user
  # integer: badgset_id, level instead of badge_id
  #belongs_to :badge # <-- how to specify? 
  belongs_to :badgeable, :polymorphic => true

  validates_uniqueness_of :badgeset_id, :scope => [:user_id, :badgeable_id]
  validates_presence_of :badgeset_id, :level, :user_id  

  # instead of this:
  def badge
    Badge.first(:conditions => {:badgeset_id => self.badgeset_id, :level => self.level})
  end
end

class User < ActiveRecord::Base
  has_many :badgings, :dependent => :destroy do
    def grant badgeset, level, badgeable = nil
      b = Badging.first(:conditions => {:user_id => proxy_owner.id, :badgeset_id => badgeset,
        :badgeable_id => badgeable.try(:id), :badgeable_type => badgeable.try(:class)}) ||
        Badging.new(:user => proxy_owner, :badgeset_id => badgeset, :badgeable => badgeable)
      b.level = level
      b.save
    end
  end
  has_many :badges, :through => :badgings
  # ....
end

¿Cómo puedo especificar un belongs_to asociación que hace eso (y no intenta utilizar un badge_id), para poder utilizar el has_many :through?

Hora estimada de llegada:Esto funciona parcialmente (es decir,@badging.badge funciona), pero se siente sucio:

belongs_to :badge, :foreign_key => :badgeset_id, :primary_key => :badgeset_id, :conditions => 'badges.level = #{level}'

Tenga en cuenta que las condiciones están en soltero comillas, no dobles, lo que hace que se interprete en tiempo de ejecución en lugar de en tiempo de carga.

Sin embargo, cuando intento usar esto con la asociación :through, aparece el error undefined local variable or method 'level' for #<User:0x3ab35a8>.Y nada obvio (por ej. 'badges.level = #{badgings.level}') parece funcionar...

ETA 2:Tomar el código de EmFi y limpiarlo un poco funciona.requiere agregar badge_set_id a Badge, que es redundante, pero bueno.

El código:

class Badge < ActiveRecord::Base
  has_many :badgings
  belongs_to :badge_set
  has_friendly_id :name

  validates_uniqueness_of :badge_set_id, :scope => :level

  default_scope :order => 'badge_set_id, level DESC'
  named_scope :with_level, lambda {|level| { :conditions => {:level => level}, :limit => 1 } }

  def self.by_ids badge_set_id, level
    first :conditions => {:badge_set_id => badge_set_id, :level => level} 
  end

  def next_level
    Badge.first :conditions => {:badge_set_id => badge_set_id, :level => level + 1}
  end
end

class Badging < ActiveRecord::Base
  belongs_to :user
  belongs_to :badge 
  belongs_to :badge_set
  belongs_to :badgeable, :polymorphic => true

  validates_uniqueness_of :badge_set_id, :scope => [:user_id, :badgeable_id]
  validates_presence_of :badge_set_id, :badge_id, :user_id  

  named_scope :with_badge_set, lambda {|badge_set|
    {:conditions => {:badge_set_id => badge_set} }
  }

  def level_up level = nil
    self.badge = level ? badge_set.badges.with_level(level).first : badge.next_level
  end

  def level_up! level = nil
    level_up level
    save
  end
end

class User < ActiveRecord::Base
  has_many :badgings, :dependent => :destroy do
    def grant! badgeset_id, level, badgeable = nil
      b = self.with_badge_set(badgeset_id).first || 
         Badging.new(
            :badge_set_id => badgeset_id,
            :badge => Badge.by_ids(badgeset_id, level), 
            :badgeable => badgeable,
            :user => proxy_owner
         )
      b.level_up(level) unless b.new_record?
      b.save
    end
    def ungrant! badgeset_id, badgeable = nil
      Badging.destroy_all({:user_id => proxy_owner.id, :badge_set_id => badgeset_id,
        :badgeable_id => badgeable.try(:id), :badgeable_type => badgeable.try(:class)})
    end
  end
  has_many :badges, :through => :badgings
end

Si bien esto funciona, y probablemente sea una mejor solución, no considero que esto sea una respuesta real a la pregunta de cómo hacer a) claves externas de múltiples claves, ob) asociaciones de condiciones dinámicas que funcionan con asociaciones :through.Entonces, si alguien tiene una solución para eso, que lo diga.

¿Fue útil?

Solución

Parece que podría entrenar mejor si separa la Insignia en dos modelos. Así es como lo desglosaría para lograr la funcionalidad que desea. Agregué algunos ámbitos con nombre para mantener el código que realmente hace las cosas limpias.

class BadgeSet
  has_many :badges
end

class Badge
  belongs_to :badge_set
  validates_uniqueness_of :badge_set_id, :scope => :level

  named_scope :with_level, labmda {|level
    { :conditions => {:level => level} }
  }

  named_scope :next_levels, labmda {|level
    { :conditions => ["level > ?", level], :order => :level }
  }

  def next_level 
    Badge.next_levels(level).first
  end
end

class Badging < ActiveRecord::Base
  belongs_to :user
  belongs_to :badge 
  belongs_to :badge_set
  belongs_to :badgeable, :polymorphic => true

  validates_uniqueness_of :badge_set_id, :scope => [:user_id, :badgeable_id]
  validates_presence_of :badge_set_id, :badge_id, :user_id  

  named_scope :with_badge_set, lambda {|badge_set|
    {:conditions => {:badge_set_id => badge_set} }
  }

  def level_up(level = nil)
    self.badge = level ? badge_set.badges.with_level(level).first 
      : badge.next_level
    save
  end
end

class User < ActiveRecord::Base
  has_many :badgings, :dependent => :destroy do
    def grant badgeset, level, badgeable = nil
      b = badgings.with_badgeset(badgeset).first() || 
         badgings.build(
            :badge_set => :badgeset,
            :badge => badgeset.badges.level(level), 
            :badgeable => badgeable
         )

      b.level_up(level) unless b.new_record?

      b.save
    end
  end
  has_many :badges, :through => :badgings
  # ....
end
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top