Подтвердить что ты не робот

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

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

Я установил STI следующим образом:

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

class Ad < ActiveRecord::Base
end

Сделка: bargain.rb

class Bargain < Ad
end

Выделите модель: highlight.rb

class Highlight < Ad
end

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

Например:

  • GET www.foo.com/highlights = > список всех отображаемых объявлений.
  • GET www.foo.com/highlights/new = > форма для создания новой подсветки и т.д...

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

Спасибо!

4b9b3361

Ответ 1

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

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

Ответ 2

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

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

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

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

Подробнее о методе константы рельсов здесь: 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

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

Ответ 4

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

Итерация по другим ответам, я придумал следующее решение для одного контроллера с одиночным наложением таблицы, который хорошо работает с сильными параметрами в Rails 4.1. Просто включите: введите как разрешенный параметр, вызвав ошибку 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'

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

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

Ответ 5

Я знаю, что это старый вопрос, вот шаблон, который мне нравится, который включает ответы от @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, чтобы заставить входные имена быть базовым классом. Эта комбинация позволяет нам использовать немодифицированные маршруты (ресурсы: объявления), а также сильную проверку параметров params [: ad], возвращающихся из формы.

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