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)
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 :)
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.
Related
I've got Rails 5 app with dry-monads on board. Monads are used to create the Appointment object inside create action in AppointmentsController. They return Success or Failure in the last step with below structure:
# services/appointments/create.rb
(...)
def call
Success(appointment_params: appointment_params)
(...)
.bind(method(:save_appointment))
end
private
def save_appointment(appointment)
if appointment.save
Success(appointment)
else
Failure(failure_appointments: appointment, appointments_errors: appointment.errors.full_messages)
end
end
After each action (success or failure) I want to send an email and display the corresponding json in AppointmentsController:
class Api::AppointmentsController < ApplicationController
def create
succeeded_appointments = []
failure_appointments = []
appointments_errors = []
batch_create_appointments_params[:_json].each do |appointment_params|
appointment = ::Appointments::Create.new(appointment_params).call
if appointment.success?
succeeded_appointments << appointment.value!
else
failure_appointments << appointment.failure[:failure_appointments] &&
appointments_errors << appointment.failure[:appointments_errors]
end
end
if failure_appointments.any?
AppointmentMailer.failed_mail(email, failure_appointments.size, appointments_errors).deliver_now
render json: {
error: appointments_errors.join(', '),
}, status: :bad_request
elsif succeeded_appointments.any?
AppointmentMailer.success_mail(email, succeeded_appointments.size).deliver_now
render json: {
success: succeeded_appointments.map do |appointment|
appointment.as_json(include: %i[car customer work_orders])
end,
}
end
end
I wonder if there is a better, faster way to record these errors than declaring 3 different empty arrays (succeeded_appointments, failure_appointments, appointments_errors) like at the beginning of create action? so far the create action looks heavy.
Create a separate service object for bulk creation:
# services/appointments/bulk_create.rb
class Appointments::BulkCreate
def initialize(bulk_params)
#bulk_params = bulk_params
end
def call
if failed_results.any?
AppointmentMailer.failed_mail(email, failed_results_errors.size, failed_results_errors).deliver_now
Failure(failed_results_errors.join(', '))
else
AppointmentMailer.success_mail(email, success_appointments.size).deliver_now
Success(success_appointments)
end
end
private
attr_reader :bulk_params
def failed_results
results.select(&:failure?)
end
def success_results
results.select(&:success?)
end
def success_appointments
#success_appointments ||= success_results.map do |appointment|
appointment.as_json(include: %i[car customer work_orders])
end
end
def failed_results_errors
#failed_results_errors ||= failed_results.map do |failed_result|
failed_result.failure[:appointments_errors]
end
end
def results
#results ||= bulk_params.map do |appointment_params|
::Appointments::Create.new(appointment_params).call
end
end
end
Then your controller will look like this:
class Api::AppointmentsController < ApplicationController
def create
result = ::Appointments::BulkCreate.new(batch_create_appointments_params[:_json]).call
if result.success?
render json: { success: result.value! }, status: :ok
else
render json: { error: result.failure }, status: :bad_request
end
end
end
I have three models: Employee, User, and Role. Relationship between these classes are employee --- one_to_one -- user and user -- one_to_many -- role.
My create action is working fine with following strong params methods
def employee_params
params.require(:employee).permit(:first_name, :middle_name, :last_name, :email, user_attributes: [:id, role_ids:[]])
end
For update if employee record has no user object, I am instantiating new user, mapping it to employee and calling update by passing string params. However, the update is failing with message:
Failed to save the new associated user
My update method code is
def update
#employee = Employee.find(params[:id])
if params[:employee][:user_attributes] != nil && params[:employee][:user_attributes][:role_ids] != nil && ! params[:employee][:user_attributes][:role_ids].empty?
if #employee.user == nil
#employee.user = User.new
temp_password = Devise.friendly_token.first(8)
#employee.user.is_temp_password = true
#employee.user.password = Devise.friendly_token.first(8)
#employee.user.email = #employee.email
#employee.user.email = params[:employee][:email] if params[:employee][:email]
end
end
respond_to do |format|
if #employee.update(employee_params)
format.json { render json: #employee.as_json}
else
format.json {render :json => #employee.errors.as_json, :status => 422}
end
end
end
I think as the above users suggested, you need to save the new User Object but also I think you should have the User creation code inside of the Employee create since you you would need to auto create it anyways in the update
Not sure if you also aware of helpers blank?, present? but I rewrote your code with that
def update
#employee = Employee.find(params[:id])
if params[:employee][:user_attributes].present? && params[:employee][:user_attributes][:role_ids].present? && params[:employee][:user_attributes][:role_ids].present?
unless #employee.user
user = User.new
#employee.user = user
temp_password = Devise.friendly_token.first(8)
user.is_temp_password = true
user.password = Devise.friendly_token.first(8)
user.email = #employee.email
user.email = params[:employee][:email] if params[:employee][:email]
user.save!
end
end
I have a table 'Likes' with columns business_id, user_id and liked(0,1) and a function 'change_like_status'.
Now on every function call, If the value is 1 then set it to 0 (or vice versa) and if record doesn't exists then create one with value 1.
The first_or_create method is working just fine but how can i toggle value of column 'liked' while using this method?
Here is my function:
def change_like_status
if current_user.present?
status = Like.where("business_id = ? AND user_id = ?",params['id'],current_user.id).first_or_create(:business_id => params['id'],:user_id => current_user.id,:liked => '1')
abort status.inspect
else
return render :json => {:status => false,:msg=>"You need to sign in before performing this action."}
end
end
In you controller, make the changes
def change_like_status
if current_user
status = Like.create_or_change_status(params[:id], current_user.id)
else
return render json: { status: false, msg: "You need to sign in before performing this action." }
end
end
In your model like.rb file, add a method
def self.create_or_change_status(business_id, user_id)
status = where(business_id: business_id, user_id: user_id).first
if status.nil?
status = create({business_id: business_id, user_id: user_id, liked: 1})
else
status.update_attributes(liked: !status.liked)
end
status
end
def change_like_status
if current_user
current_user.likes.find_by(business_id: params[:id]).switch_status!
else
return render json: { status: false, msg: "You need to sign in before performing this action." }
end
end
class Like
def switch_status!
self.update_column :liked, !liked
end
end
other approach should be something like that
class Like
def switch_status!
self.update_column :liked, !liked
end
end
class User
def likes id
likes_for_business id
end
def likes_for_business(id)
likes.find_by(business_id: id) || likes.create(:business_id: id, liked: true)
end
end
# controller
current_user.likes(params[:id]).switch_status!
I have a Study model which have many fields, but I'm having troubles with 1
profesion_name
so in my study model I have this
class Study < ActiveRecord::Base
attr_accessible :profesion_related, :profesion_name
attr_accessor :profesion_related
def profesion_related=(id)
if id.present?
if self.study_type_id == 4
if self.country_id == 170
#some code here
else
profesion_parent = Profesion.find(id)
new_profesion = Profesion.create({g_code: profesion_parent.g_code, mg_code: profesion_parent.mg_code, name: self.profesion_name})
self.profesion = new_profesion
end
end
end
end
end
but I'm getting an error on the line that create a Profesion, because self.profesion_name is nil
if in my controller I do this
def create
#study = Study.new(params[:study])
respond_to do |format|
#here
puts #study.to_yaml
if #study.save
.....
end
I will see in the console that profesion_name has a value
but if I do this
class Study < ActiveRecord::Base
...
def profesion_related=(id)
puts self.to_yaml
....
end
end
I can see that self.profesion_name is empty
Why could this be happening?
Here's the model file:
class ProfileTag < ActiveRecord::Base
def self.create_or_update(options = {})
id = options.delete(:id)
record = find_by_id(id) || new
record.id = id
record.attributes = options
puts "record.profile_id is"
puts record.profile_id
record.save!
record
end
end
This gives me the correct print out in my log. But it also says that there's a call to UPDATE that sets profile_id to NULL. Here's some of the output in the log file:
Processing ProfilesController#update (for 127.0.0.1 at 2010-05-28 18:20:54) [PUT]
Parameters: {"commit"=>"Save", "profile"=>{"id"=>"2", "password_confirmation"=>"", "username"=>"user2", "first_name"=>"user2_first", "password"=>"", "last_name"=>"user2_last"}, "authenticity_token"=>"...", "tag"=>"1", "id"=>"2"}
?[4;36;1mProfileTag Create (0.0ms)?[0m ?[0;1mINSERT INTO `profile_tags`
(`reputation_value`, `updated_at`, `tag_id`, `id`, `profile_id`, `created_at`) VALUES(0, '2010-05-29 01:20:54', 1, NULL, 4, '2010-05-29 01:20:54')?[0m
?[4;35;1mSQL (2.0ms)?[0m ?[0mCOMMIT?[0m
?[4;36;1mSQL (0.0ms)?[0m ?[0;1mBEGIN?[0m
?[4;35;1mSQL (0.0ms)?[0m ?[0mCOMMIT?[0m
?[4;36;1mProfileTag Load (0.0ms)?[0m ?[0;1mSELECT * FROM `profile_tags` WHERE (`profile_tags`.profile_id = 4) ?[0m
?[4;35;1mSQL (1.0ms)?[0m ?[0mBEGIN?[0m
?[4;36;1mProfileTag Update (0.0ms)?[0m ?[0;1mUPDATE `profile_tags` SET profile_id = NULL WHERE (profile_id = 4 AND id IN (35)) ?[0m
I'm not sure I understand why the INSERT puts the value into profile_id properly, but then it sets it to NULL on an UPDATE.
[Edit]
In ProfileController:
def update
#...stuff. Set tags array.
save_tags(tags) #These tags are correct. Verified by printouts before and after this call.
respond_to do |format|
if #profile.update_attributes(params[:profile])
flash[:notice] = 'Profile was successfully updated.'
#format.html { redirect_to(#profile) }
format.html { redirect_to :action=>'show' }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #profile.errors, :status => :unprocessable_entity }
end
end
end
def save_tags(tags)
profile = find_profile #finds the correct profile. And I confirm that it exists with a printout
tags.each do |t|
ProfileTags.create_or_update(:profile_id => profile.profile_id, :tag_id => t.id)
end
end
If you need more specifics, please let me know. I'm thinking that the save functionality does many things other than INSERTs into the database, but I don't know what I need to specify so that it will properly set profile_id.
Look at the line:
ProfileTags.create_or_update(:profile_id => profile.profile_id, :tag_id => t.id)
I believe you want to pass profile.id, and not profile.profile_id (which is probably null).
save! itself should't do this.
Maybe your problem is the name of the method. ActiveRecord::Base already have a method named create_or_update (see http://github.com/rails/rails/blob/2-3-stable/activerecord/lib/active_record/base.rb#L2913) which is called by save! - maybe replacing it causes this weird problem.
Try changing the name of your method to something else, it might help.
You aren't passing the id attribute to the create_or_update method in the first place, so you don't need to call it, just call create instead, like so:
def save_tags(tags)
profile = find_profile #finds the correct profile. And I confirm that it exists with a printout
tags.each do |t|
ProfileTag.create(:profile_id => profile.profile_id, :tag_id => t.id)
end
end