I have a very specific customization question for Spree. I wish to add multi-tenancy to its core functionality. My app needs to have the following setup:
Users who can simultaneously buy and sell products through the site (just like craigslist).
Users become vendors in that they are associated as the owners of the products that they list for sale.
There will be a products index page that can reference every Spree::Product. When you click on a specific product it will tell you who the seller is and you can quickly navigate to that sellers profile page and see that seller's products.
Each user can manage the products they are selling: add product images, change the product description, see who has ordered their products, and of course get money from those transactions.
Trying to implement this has been a challenge, to put it lightly. I'm a relative newcomer to rails. I have investigated and implemented (somewhat successfully) the spree multi-tenant gem. It seemed to overcomplicate things, and it only works with Spree 2.2.
What I think would be the best way for me to accomplish this is to use the class eval method on the product/create action. Here's some Spree documentation that led me to believe this: link
I would like all of your help in giving me some insight into how to do this. Or, if this doesn't make sense, what would be a better way to go about doing this?
I have done some work already: I added the necessary associations in a product model decorator and my spree user class. I have created and ran a migration to add a user_id column to the spree::products as well.
What I need to do now is write the code to assign the current_user's id to a column on the product that they create. I am unsure of how to do this. Basically, I don't know exactly what code I need to override in order to just make this change and not break anything else.
Thanks for your responses in advance. I am happy to clarify anything that I've left out.
Below is the code from the spree_backend gem for the products controller:
module Spree
module Admin
class ProductsController < ResourceController
helper 'spree/products'
before_filter :load_data, :except => :index
create.before :create_before
update.before :update_before
helper_method :clone_object_url
def show
session[:return_to] ||= request.referer
redirect_to( :action => :edit )
end
def index
session[:return_to] = request.url
respond_with(@collection)
end
def update
if params[:product][:taxon_ids].present?
params[:product][:taxon_ids] = params[:product][:taxon_ids].split(',')
end
if params[:product][:option_type_ids].present?
params[:product][:option_type_ids] = params[:product][:option_type_ids].split(',')
end
invoke_callbacks(:update, :before)
if @object.update_attributes(permitted_resource_params)
invoke_callbacks(:update, :after)
flash[:success] = flash_message_for(@object, :successfully_updated)
respond_with(@object) do |format|
format.html { redirect_to location_after_save }
format.js { render :layout => false }
end
else
# Stops people submitting blank slugs, causing errors when they try to update the product again
@product.slug = @product.slug_was if @product.slug.blank?
invoke_callbacks(:update, :fails)
respond_with(@object)
end
end
def destroy
@product = Product.friendly.find(params[:id])
@product.destroy
flash[:success] = Spree.t('notice_messages.product_deleted')
respond_with(@product) do |format|
format.html { redirect_to collection_url }
format.js { render_js_for_destroy }
end
end
def clone
@new = @product.duplicate
if @new.save
flash[:success] = Spree.t('notice_messages.product_cloned')
else
flash[:error] = Spree.t('notice_messages.product_not_cloned')
end
redirect_to edit_admin_product_url(@new)
end
def stock
@variants = @product.variants
@variants = [@product.master] if @variants.empty?
@stock_locations = StockLocation.accessible_by(current_ability, :read)
if @stock_locations.empty?
flash[:error] = Spree.t(:stock_management_requires_a_stock_location)
redirect_to admin_stock_locations_path
end
end
protected
def find_resource
Product.with_deleted.friendly.find(params[:id])
end
def location_after_save
spree.edit_admin_product_url(@product)
end
def load_data
@taxons = Taxon.order(:name)
@option_types = OptionType.order(:name)
@tax_categories = TaxCategory.order(:name)
@shipping_categories = ShippingCategory.order(:name)
end
def collection
return @collection if @collection.present?
params[:q] ||= {}
params[:q][:deleted_at_null] ||= "1"
params[:q][:s] ||= "name asc"
@collection = super
@collection = @collection.with_deleted if params[:q].delete(:deleted_at_null) == '0'
# @search needs to be defined as this is passed to search_form_for
@search = @collection.ransack(params[:q])
@collection = @search.result.
distinct_by_product_ids(params[:q][:s]).
includes(product_includes).
page(params[:page]).
per(Spree::Config[:admin_products_per_page])
@collection
end
def create_before
return if params[:product][:prototype_id].blank?
@prototype = Spree::Prototype.find(params[:product][:prototype_id])
end
def update_before
# note: we only reset the product properties if we're receiving a post from the form on that tab
return unless params[:clear_product_properties]
params[:product] ||= {}
end
def product_includes
[{ :variants => [:images], :master => [:images, :default_price]}]
end
def clone_object_url resource
clone_admin_product_url resource
end
def permit_attributes
params.require(:product).permit!
end
end
end end
You'll notice that there is no Create action in this products controller. It is inheriting it from the resource controller here:
class Spree::Admin::ResourceController < Spree::Admin::BaseController
include Spree::Backend::Callbacks
helper_method :new_object_url, :edit_object_url, :object_url, :collection_url
before_filter :load_resource, :except => [:update_positions]
rescue_from ActiveRecord::RecordNotFound, :with => :resource_not_found
respond_to :html
def new
invoke_callbacks(:new_action, :before)
respond_with(@object) do |format|
format.html { render :layout => !request.xhr? }
if request.xhr?
format.js { render :layout => false }
end
end
end
def edit
respond_with(@object) do |format|
format.html { render :layout => !request.xhr? }
if request.xhr?
format.js { render :layout => false }
end
end
end
def update
invoke_callbacks(:update, :before)
if @object.update_attributes(permitted_resource_params)
invoke_callbacks(:update, :after)
flash[:success] = flash_message_for(@object, :successfully_updated)
respond_with(@object) do |format|
format.html { redirect_to location_after_save }
format.js { render :layout => false }
end
else
invoke_callbacks(:update, :fails)
respond_with(@object) do |format|
format.html do
flash.now[:error] = @object.errors.full_messages.join(", ")
render action: 'edit'
end
format.js { render layout: false }
end
end
end
def create
invoke_callbacks(:create, :before)
@object.attributes = permitted_resource_params
if @object.save
invoke_callbacks(:create, :after)
flash[:success] = flash_message_for(@object, :successfully_created)
respond_with(@object) do |format|
format.html { redirect_to location_after_save }
format.js { render :layout => false }
end
else
invoke_callbacks(:create, :fails)
respond_with(@object) do |format|
format.html do
flash.now[:error] = @object.errors.full_messages.join(", ")
render action: 'new'
end
format.js { render layout: false }
end
end
end
def update_positions
params[:positions].each do |id, index|
model_class.find(id).update_attributes(:position => index)
end
respond_to do |format|
format.js { render :text => 'Ok' }
end
end
def destroy
invoke_callbacks(:destroy, :before)
if @object.destroy
invoke_callbacks(:destroy, :after)
flash[:success] = flash_message_for(@object, :successfully_removed)
respond_with(@object) do |format|
format.html { redirect_to location_after_destroy }
format.js { render :partial => "spree/admin/shared/destroy" }
end
else
invoke_callbacks(:destroy, :fails)
respond_with(@object) do |format|
format.html { redirect_to location_after_destroy }
end
end
end
protected
class << self
attr_accessor :parent_data
def belongs_to(model_name, options = {})
@parent_data ||= {}
@parent_data[:model_name] = model_name
@parent_data[:model_class] = model_name.to_s.classify.constantize
@parent_data[:find_by] = options[:find_by] || :id
end
end
def resource_not_found
flash[:error] = flash_message_for(model_class.new, :not_found)
redirect_to collection_url
end
def model_class
"Spree::#{controller_name.classify}".constantize
end
def model_name
parent_data[:model_name].gsub('spree/', '')
end
def object_name
controller_name.singularize
end
def load_resource
if member_action?
@object ||= load_resource_instance
# call authorize! a third time (called twice already in Admin::BaseController)
# this time we pass the actual instance so fine-grained abilities can control
# access to individual records, not just entire models.
authorize! action, @object
instance_variable_set("@#{object_name}", @object)
else
@collection ||= collection
# note: we don't call authorize here as the collection method should use
# CanCan's accessible_by method to restrict the actual records returned
instance_variable_set("@#{controller_name}", @collection)
end
end
def load_resource_instance
if new_actions.include?(action)
build_resource
elsif params[:id]
find_resource
end
end
def parent_data
self.class.parent_data
end
def parent
if parent_data.present?
@parent ||= parent_data[:model_class].send("find_by_#{parent_data[:find_by]}", params["#{model_name}_id"])
instance_variable_set("@#{model_name}", @parent)
else
nil
end
end
def find_resource
if parent_data.present?
parent.send(controller_name).find(params[:id])
else
model_class.find(params[:id])
end
end
def build_resource
if parent_data.present?
parent.send(controller_name).build
else
model_class.new
end
end
def collection
return parent.send(controller_name) if parent_data.present?
if model_class.respond_to?(:accessible_by) && !current_ability.has_block?(params[:action], model_class)
model_class.accessible_by(current_ability, action)
else
model_class.where(nil)
end
end
def location_after_destroy
collection_url
end
def location_after_save
collection_url
end
# URL helpers
def new_object_url(options = {})
if parent_data.present?
spree.new_polymorphic_url([:admin, parent, model_class], options)
else
spree.new_polymorphic_url([:admin, model_class], options)
end
end
def edit_object_url(object, options = {})
if parent_data.present?
spree.send "edit_admin_#{model_name}_#{object_name}_url", parent, object, options
else
spree.send "edit_admin_#{object_name}_url", object, options
end
end
def object_url(object = nil, options = {})
target = object ? object : @object
if parent_data.present?
spree.send "admin_#{model_name}_#{object_name}_url", parent, target, options
else
spree.send "admin_#{object_name}_url", target, options
end
end
def collection_url(options = {})
if parent_data.present?
spree.polymorphic_url([:admin, parent, model_class], options)
else
spree.polymorphic_url([:admin, model_class], options)
end
end
# Allow all attributes to be updatable.
#
# Other controllers can, should, override it to set custom logic
def permitted_resource_params
params.require(object_name).permit!
end
def collection_actions
[:index]
end
def member_action?
!collection_actions.include? action
end
def new_actions
[:new, :create]
end
end