it-swarm.com.ru

STI, один контроллер

Я новичок в Rails, и я застрял с этой проблемой дизайна, которая может быть легко решена, но я никуда не уйду: у меня есть два разных вида рекламы: основные моменты и сделки , Оба они имеют одинаковые атрибуты: заголовок, описание и одно изображение (с помощью скрепки). Они также могут выполнять такие же действия: индексировать, создавать новые, редактировать, создавать, обновлять и уничтожать.

Я установил ИППП так:

Модель объявления: ad.rb

class Ad < ActiveRecord::Base
end

Модель сделки: bargain.rb

class Bargain < Ad
end

Модель Highlight: highlight.rb

class Highlight < Ad
end

Проблема в том, что я хотел бы иметь только один контроллер (AdsController), который выполняет действия, которые я сказал в отношении сделок или выделений в зависимости от URL, например, www.foo.com/bargains [/ ...] или www.foo. ком/основные моменты [/ ...].

Например:

  • ПОЛУЧИТЕ www.foo.com/highlights => список всех объявлений, которые выделены.
  • ПОЛУЧИТЕ www.foo.com/highlights/new => форму для создания новой подсветки и т.д ...

Как я могу это сделать?

Спасибо!

58
Pizzicato

Первый. Добавьте несколько новых маршрутов:

resources :highlights, :controller => "ads", :type => "Highlight"
resources :bargains, :controller => "ads", :type => "Bargain"

И исправьте некоторые действия в AdsController. Например:

def new
  @ad = Ad.new()
  @ad.type = params[:type]
end

Для лучшего подхода для всей этой работы контроллера посмотрите этот комментарий

Это все. Теперь вы можете перейти к localhost:3000/highlights/new и новая Highlight будет инициализирована.

Индексное действие может выглядеть так:

def index
  @ads = Ad.where(:type => params[:type])
end

Перейдите к localhost:3000/highlights, и появится список основных моментов.
То же самое для сделок: localhost:3000/bargains

так далее

URLS

<%= link_to 'index', :highlights %>
<%= link_to 'new', [:new, :highlight] %>
<%= link_to 'edit', [:edit, @ad] %>
<%= link_to 'destroy', @ad, :method => :delete %>

за полиморфность :)

<%= link_to 'index', @ad.class %>
99
fl00r

у fl00r есть хорошее решение, но я бы сделал одну настройку.

Это может или не может потребоваться в вашем случае. Это зависит от того, какое поведение меняется в ваших моделях ИППП, особенно от проверок и хуков жизненного цикла.

Добавьте приватный метод в ваш контроллер, чтобы преобразовать ваш тип param в фактическую константу класса, которую вы хотите использовать:

def ad_type
  params[:type].constantize
end

Вышеуказанное небезопасно, однако. Добавить белый список типов:

def ad_types
  [MyType, MyType2]
end

def ad_type
  params[:type].constantize if params[:type].in? ad_types
end

Подробнее о методе Rails constantize здесь: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize

Тогда в действиях контроллера вы можете сделать:

def new
  ad_type.new
end

def create
  ad_type.new(params)
  # ...
end

def index
  ad_type.all
end

И теперь вы используете реальный класс с правильным поведением вместо родительского класса с установленным типом атрибута.

63
Alan Peabody

Я просто хотел включить эту ссылку, потому что есть ряд интересных трюков, связанных с этой темой.

Алекс Рейснер - Наследование в одной таблице в Rails

12
Andrew Lank

Я знаю, что это старый вопрос, вот шаблон, который мне нравится, который включает в себя ответы @flOOr и @Alan_Peabody. (Протестировано в Rails 4.2, возможно, работает в Rails 5)

В вашей модели создайте свой белый список при запуске. В Dev это должно быть загружено.

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

Теперь мы можем ссылаться на этот белый список в любом контроллере для построения правильной области видимости, а также в коллекции для выбора типа: на форме и т.д.

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

Нам нужно обрабатывать наши формы, потому что маршрутизацию следует отправлять контроллеру базового класса, несмотря на фактический тип объекта. Для этого мы используем «становление», чтобы обмануть построителя форм в правильной маршрутизации, и директиву: as, чтобы входные имена также были базовым классом. Эта комбинация позволяет нам использовать неизмененные маршруты (ресурсы: реклама), а также строгую проверку параметров на параметры [: ad], возвращаемые из формы.

#/views/ads/_form.html.erb
<%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>
0
genkilabs

[Переписано с более простым решением, которое работает полностью:]

Итерируя другие ответы, я придумал следующее решение для одного контроллера с наследованием одной таблицы, которое хорошо работает со строгими параметрами в Rails 4.1. Просто включение: type в качестве разрешенного параметра вызвало ошибку ActiveRecord::SubclassNotFound, если введен неверный тип. Более того, тип не обновляется, потому что SQL-запрос явно ищет старый тип. Вместо этого :type необходимо обновлять отдельно с помощью update_column, если он отличается от текущего набора и является допустимым типом. Обратите внимание, что мне удалось высушить все списки типов.

# 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

И в контроллере:

  # 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

И маршруты:

# 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'

Наконец, я рекомендую использовать responseders gem и установить scaffolding для использования responseders_controller, который совместим с STI. Конфиг для строительных лесов:

# config/application.rb
    config.generators do |g|
      g.scaffold_controller "responders_controller"
    end
0
dankohn