Je suis nouveau chez Rails et je suis un peu coincé avec ce problème de conception, qui pourrait être facile à résoudre, mais je n’arrive nulle part: J'ai deux types de publicités différentes: les points forts et les bonnes affaires. Les deux ont les mêmes attributs: titre, description et une image (avec un trombone). Ils ont également le même type d’actions à appliquer: indexer, créer, éditer, créer, mettre à jour et détruire.
Je pose une ITS comme ceci:
Modèle d'annonce: ad.rb
class Ad < ActiveRecord::Base
end
Modèle d'affaire: bargain.rb
class Bargain < Ad
end
Modèle en surbrillance: highlight.rb
class Highlight < Ad
end
Le problème est que je voudrais avoir un seul contrôleur (AdsController
) qui exécute les actions que j'ai dites sur les bonnes affaires ou met en surbrillance en fonction de l'URL, disons www.foo.com/bargains [/ ...] ou www.foo. com/met en évidence [/ ...].
Par exemple:
Comment puis je faire ça?
Merci!
Premier. Ajoutez de nouvelles routes:
resources :highlights, :controller => "ads", :type => "Highlight"
resources :bargains, :controller => "ads", :type => "Bargain"
Et corrigez certaines actions dans AdsController
. Par exemple:
def new
@ad = Ad.new()
@ad.type = params[:type]
end
Pour une meilleure approche pour tout ce travail de contrôleur, regardez ce commentaire
C'est tout. Vous pouvez maintenant aller à localhost:3000/highlights/new
et la nouvelle Highlight
sera initialisée.
L'action d'index peut ressembler à ceci:
def index
@ads = Ad.where(:type => params[:type])
end
Allez à localhost:3000/highlights
et la liste des points forts apparaîtra.
Même méthode pour les bonnes affaires: localhost:3000/bargains
etc
URLS
<%= link_to 'index', :highlights %>
<%= link_to 'new', [:new, :highlight] %>
<%= link_to 'edit', [:edit, @ad] %>
<%= link_to 'destroy', @ad, :method => :delete %>
pour être polymorphe :)
<%= link_to 'index', @ad.class %>
fl00r a une bonne solution, mais je ferais un ajustement.
Cela peut ou peut ne pas être requis dans votre cas. Cela dépend du comportement de vos modèles STI, en particulier des validations et des points d'ancrage du cycle de vie.
Ajoutez une méthode privée à votre contrôleur pour convertir votre type param en la constante de classe que vous voulez utiliser:
def ad_type
params[:type].constantize
end
Ce qui précède n'est toutefois pas sûr. Ajouter une liste blanche de types:
def ad_types
[MyType, MyType2]
end
def ad_type
params[:type].constantize if params[:type].in? ad_types
end
Pour plus d’informations sur la méthode de constantisation de Rails ici: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize
Ensuite, dans les actions du contrôleur, vous pouvez faire:
def new
ad_type.new
end
def create
ad_type.new(params)
# ...
end
def index
ad_type.all
end
Et maintenant, vous utilisez la classe réelle avec le comportement correct au lieu de la classe parente avec le type d'attribut défini.
Je voulais juste inclure ce lien car il existe de nombreuses astuces intéressantes liées à ce sujet.
Je sais que c’est une vieille question; voici un schéma qui me plait et qui inclut les réponses de @flOOr et @Alan_Peabody. (Testé dans Rails 4.2, fonctionne probablement dans Rails 5)
Dans votre modèle, créez votre liste blanche au démarrage. En dev, cela doit être chargé.
class Ad < ActiveRecord::Base
Rails.application.eager_load! if Rails.env.development?
TYPE_NAMES = self.subclasses.map(&:name)
#You can add validation like the answer by @dankohn
end
Nous pouvons maintenant référencer cette liste blanche dans n’importe quel contrôleur pour construire la bonne portée, ainsi que dans une collection pour un: type select sur un formulaire, etc.
class AdsController < ApplicationController
before_action :set_ad, :only => [:show, :compare, :edit, :update, :destroy]
def new
@ad = ad_scope.new
end
def create
@ad = ad_scope.new(ad_params)
#the usual stuff comes next...
end
private
def set_ad
#works as normal but we use our scope to ensure subclass
@ad = ad_scope.find(params[:id])
end
#return the scope of a Ad STI subclass based on params[:type] or default to Ad
def ad_scope
#This could also be done in some kind of syntax that makes it more like a const.
@ad_scope ||= params[:type].try(:in?, Ad::TYPE_NAMES) ? params[:type].constantize : Ad
end
#strong params check works as expected
def ad_params
params.require(:ad).permit({:foo})
end
end
Nous devons gérer nos formulaires car le routage doit être envoyé au contrôleur de classe de base, malgré le type réel de l'objet. Pour ce faire, nous utilisons "devient" pour amener le générateur de formulaire à un routage correct, et la directive: as pour forcer les noms d'entrée à être également la classe de base. Cette combinaison nous permet d'utiliser des itinéraires non modifiés (ressources: annonces) ainsi que le contrôle puissant des paramètres sur les paramètres [: ad] qui reviennent du formulaire.
#/views/ads/_form.html.erb
<%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>
[Réécrit avec une solution plus simple qui fonctionne pleinement:]
En parcourant les autres réponses, j'ai mis au point la solution suivante pour un seul contrôleur avec l'héritage de table unique qui fonctionne bien avec les paramètres forts dans Rails 4.1. Le simple fait d'inclure: type en tant que paramètre autorisé provoque une erreur ActiveRecord::SubclassNotFound
si un type non valide est entré. De plus, le type n'est pas mis à jour car la requête SQL recherche explicitement l'ancien type. Au lieu de cela, :type
doit être mis à jour séparément avec update_column s'il est différent de ce qui est défini et est de type valide. Notez également que j'ai réussi à assécher toutes les listes de types.
# app/models/company.rb
class Company < ActiveRecord::Base
COMPANY_TYPES = %w[Publisher Buyer Printer Agent]
validates :type, inclusion: { in: COMPANY_TYPES,
:message => "must be one of: #{COMPANY_TYPES.join(', ')}" }
end
Company::COMPANY_TYPES.each do |company_type|
string_to_eval = <<-heredoc
class #{company_type} < Company
def self.model_name # http://stackoverflow.com/a/12762230/1935918
Company.model_name
end
end
heredoc
eval(string_to_eval, TOPLEVEL_BINDING)
end
Et dans le contrôleur:
# app/controllers/companies_controller.rb
def update
@company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type)
@company.update_column :type, new_type
end
@company.update(company_params)
respond_with(@company)
end
Et des itinéraires:
# config/routes.rb
Rails.application.routes.draw do
resources :companies
Company::COMPANY_TYPES.each do |company_type|
resources company_type.underscore.to_sym, type: company_type, controller: 'companies', path: 'companies'
end
root 'companies#index'
Enfin, je recommande d'utiliser la gemme responders et de configurer l'échafaudage pour utiliser un responders_controller, compatible avec STI. La configuration pour échafaudage est:
# config/application.rb
config.generators do |g|
g.scaffold_controller "responders_controller"
end