web-dev-qa-db-fra.com

STI, un contrôleur

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:

  • OBTENIR www.foo.com/highlights => une liste de toutes les annonces en surbrillance.
  • OBTENIR www.foo.com/highlights/new => créer un nouveau surlignage Etc ...

Comment puis je faire ça?

Merci!

56
Pizzicato

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 %>
99
fl00r

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.

63
Alan Peabody

Je voulais juste inclure ce lien car il existe de nombreuses astuces intéressantes liées à ce sujet.

Alex Reisner - Héritage à table unique en rails

12
Andrew Lank

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| %>
0
genkilabs

[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
0
dankohn