1
votes

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:

  1. Users who can simultaneously buy and sell products through the site (just like craigslist).

  2. Users become vendors in that they are associated as the owners of the products that they list for sale.

  3. 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.

  4. 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
1

1 Answers

0
votes

You should try the Spree Admin Roles and Acess Gem, it can make most of the things you want to in an easy way.

Add it to your Gemfile -> gem 'spree_admin_roles_and_access' Then install it and run the installation generator:

$ bundle install
$ rails generate spree_admin_roles_and_access:install
$ rake spree_roles:permissions:populate # To populate user and admin roles with their permissions
$ rake spree_roles:permissions:populate_permission_sets # To set up some convenient permission sets.

After that there will be a permissions settings in each Spree::User