0

Use case: Upload and process a csv file and record the upload session in the database.

Approach: Create a model to hold data about the upload session and a controller with a new method that collects the csv file and a create method that creates, populates and saves an upload object.

Problem: The model includes an initialize method (see Model code) that seems to have the necessary information to initialize the object (see debug output), but, when the controller attempts to use the model's new method, the attempt does not result in a useful object (see controller code and debug output).

Question: What do I need to change to create the call_history_upload object?

Ruby 1.9.3 on Rails 3.2.13 on Windows 8.1

Model Code:

class CallHistoryUpload < ActiveRecord::Base
  attr_accessible :file_name, :user_id, :record_count, :international_call_count, :unknown_client_count

  has_many :call_histories, dependent: :destroy

  require 'csv'

  def initialize( file_name, user_id )
    logger.debug( "CallHistoryUpload.initialize start")
    logger.debug( "  call_history_file  = " + file_name )
    logger.debug( "  user_id            = " + user_id )
    @file_name = file_name
    @user_id = user_id
    @record_count = 0
    @international_call_count = 0
    @unknown_client_count = 0
    logger.debug( "CallHistoryUpload.initialize end")
  end
end

Controller code:

class CallHistoryUploadsController < ApplicationController

  def get_client_id
    -1
  end

  # GET /call_history_uploads/new
  # GET /call_history_uploads/new.json
  def new
    @call_history_upload = CallHistoryUpload.new( "Unknown", session[:user_id] ) # Needed to suppoprt json

    respond_to do |format|
     format.html # new.html.erb
      format.json { render json: @call_history_upload }
    end
  end

  # POST /call_history_uploads
  # POST /call_history_uploads.json
  def create
    logger.debug( "CallHistoryUploadsController start")
    @call_history_upload = CallHistoryUpload.new( params[:call_history_file].original_filename, session[:user_id] )
    logger.debug( "CallHistoryUploadsController after instantiation")
<<This is line 24>>logger.debug( "call_history_upload.file_name = " + @call_history_upload.file_name )

    respond_to do |format|
      if @call_history_upload.save
        format.html { redirect_to @call_history_upload, notice: 'Call history upload was successful.' }
        format.json { render json: @call_history_upload, status: :created, location: @call_history_upload }
      else
        format.html { render action: "new" }
        format.json { render json: @call_history_upload.errors, status: :unprocessable_entity }
      end
    end
  end 
end

log output:

Started POST "/call_history_uploads" for 127.0.0.1 at 2014-11-30 08:40:27 -0500
Processing by CallHistoryUploadsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"iDugtKa8b/U9q71Gk86sJMK6hnX1gvgQt496PX2q4Oo=", "call_history_file"=>#<ActionDispatch::Http::UploadedFile:0x4717788 @original_filename="Test_kiyvivzokpysxilfoftavyuftot.csv", @content_type="application/vnd.ms-excel", @headers="Content-Disposition: form-data; name=\"call_history_file\"; filename=\"Test_kiyvivzokpysxilfoftavyuftot.csv\"\r\nContent-Type: application/vnd.ms-excel\r\n", @tempfile=#<File:C:/Users/Gene/AppData/Local/Temp/RackMultipart20141130-5144-yn8i9>>, "commit"=>"Submit"}
CallHistoryUploadsController start
CallHistoryUpload.initialize start
  call_history_file  = Test_kiyvivzokpysxilfoftavyuftot.csv
  user_id            = KinteraAdmin
CallHistoryUpload.initialize end
CallHistoryUploadsController after instantiation
Completed 500 Internal Server Error in 0ms

NoMethodError (undefined method `has_key?' for nil:NilClass):
  app/controllers/call_history_uploads_controller.rb:24:in `create'


  Rendered C:/Ruby193/lib/ruby/gems/1.9.1/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/_trace.erb (0.0ms)
  Rendered C:/Ruby193/lib/ruby/gems/1.9.1/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (0.0ms)
  Rendered C:/Ruby193/lib/ruby/gems/1.9.1/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (0.0ms)

2 Answers 2

1

Oh god, what a mess... You should use composition instead of inheritance. A first refactoring could be:

class CallHistoryUpload < SimpleDelegator
  class Model < ActiveRecord::Base
    has_many :call_histories, dependent: :destroy
  end

  def initialize(file_name, user_id)
    logger.debug( 'CallHistoryUpload.initialize start')
    logger.debug( "  call_history_file  = #{file_name}" )
    logger.debug( "  user_id            = #{user_id}" )
    super( new_model(file_name, user_id) )
    logger.debug( "CallHistoryUpload.initialize end")
  end

  def model
    __getobj__
  end

  def self.create(file_name, user_id)
    logger.debug('CallHistoryUploadsController start')
    chu = self.new(file_name, user_id)
    logger.debug( 'CallHistoryUploadsController after instantiation')
    logger.debug( "call_history_upload.file_name = #{chu.file_name}")

    return chu
  end

  private
  def new_model(file_name, user_id)
    self.class::Model.new({
      file_name: file_name,
      user_id: user_id,
      record_count: 0,
      international_call_count: 0,
      unknown_client_count: 0,
    })
  end
end

In the controller we change only the creation:

  # POST /call_history_uploads
  # POST /call_history_uploads.json
  def create
    @call_history_upload = CallHistoryUpload.create( params[:call_history_file].original_filename, session[:user_id] )

    # rest of the code

If you use something like form_for you may want to pass the @call_history_upload.model to it.

Always try to put as little logic as possible in the controller and to encapsulate it in some object :)

Sign up to request clarification or add additional context in comments.

Comments

0

Part of the problem seems to be that you're overriding the normal behavior of ActiveRecord::Base on initialize. An active record object allows you to populate an object's properties like so MyObject.new(property: 'value')

Either remove the initialize method from your object and instantiate your object using

file_name = params[:call_history_file].original_filename 
user_id = session[:user_id]
CallHistoryUpload.new(file_name: file_name, user_id: user_id)

or

Call super from within your initialize method to allow ActiveRecord::Base to do its thing like so

def initialize(file_name, user_id)
  logger.debug( "CallHistoryUpload.initialize start")
  logger.debug( "  call_history_file  = " + file_name )
  logger.debug( "  user_id            = " + user_id )
  @record_count = 0
  @international_call_count = 0
  @unknown_client_count = 0
  super(file_name: file_name, user_id: user_id)
  logger.debug( "CallHistoryUpload.initialize end")
end

I'd recommend the first approach as it cleans up your model. If you need to assign default values on the instance of the model, I'd recommend creating a class method with a descriptive name which sets this values on the call to new doing the same assignment as on the first approach. Using an after_initialize call back would also be another option to set defaults but be aware of the consequences of it.

I'd really recommend reading the Getting Started guide to Rails if you're new and are trying to get used to Rails conventions.

3 Comments

Thanks for looking and your helpful response. I am new to Rails. However, I have read the Getting Started with Rails and other Rails Guides - apparently without the needed effect. The code I wrote followed the the response to stackoverflow question 13216976 found at stackoverflow.com/questions/13216976/….
I tried your suggestion with good effect. Thank you! Any thoughts you have on the reference I was using would be helpful to my learning.
The resource you saw looks to have a fine answer when speaking in Ruby only. However, when you added your class to inherit from ActiveRecord::Base, you have to be aware of what its doing under the covers. It does more than just assign instance variables to your object. If you want to find out how it does it, you'd have to dive into Rail's code of ActiveRecord and read up on the documentation. It's a good way to learn.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.