Rails Model doesn't save data with polymorphic - ruby-on-rails

I have a model job and a model user, the both can choose contracts types (that's why i use polymorphic).
I created a model contract for each contracts and i create an attached_contract model too.
Job model
class Job < ApplicationRecord
has_many :contracts, through: :attached_contracts
has_many :attached_contracts, as: :contractable, dependent: :destroy
accepts_nested_attributes_for :attached_contracts, reject_if: :all_blank, allow_destroy: true
end
AttachedContract model
class AttachedContract < ApplicationRecord
belongs_to :contract
belongs_to :contractable, polymorphic: true
validates :contract, uniqueness: { scope: [:contractable_type,:contractable_id] }
end
Contract model
class Contract < ApplicationRecord
validates :name, presence: true, allow_blank: false
has_many :attached_contracts
end
Jobs_controller
def new
#job = Job.new
#job.attached_contracts.build
end
def create
#job = current_company.jobs.build(set_params)
if #job.save
redirect_to job_path(#job)
end
else
render :new
end
end
def set_params
params.require(:job).permit(:title, :description, :address, attached_contracts_attributes: [:id, :contract_id, :_destroy]
end
In my view :
<%= simple_form_for([:company, #job]) do |f| %>
<div class="nested-fields">
<%= f.association :contracts, as: :check_boxes %>
</div>
<% end %>
When I submit my form my model AttachedContract still empty, and the data are lost.
I try tu put a "raise" in my controller after #job = current_company.jobs.build(set_params)
and I have a empty array if I call #job.attached_contracts
I don't understand beause in the "Request parameters" (rails debug console) I have the values : "contract_ids"=>["", "1", "3"]
Any idea ? May be the problem is in the polymorphic implantation ?

Finally, I changed the requested parameters by "contract_ids: [ ]" and that's work perfectly !

Related

Edit with a belongs_to relationship

Issue
I'm encountering a problem when editing a form with a belongs_to relationship (extra_guest belongs_to age_table).
I am able to create a new extra_guest and assign it to an age_table, but I cannot get the edit/update to work as my update function returns a falseClass.--> #extra_guest.update(extra_guest_params).errors.full_messages returns undefined method `errors' for false:FalseClass
Code
models
class ExtraGuest < ApplicationRecord
belongs_to :age_table
validates :age_table, presence: true
end
class AgeTable < ApplicationRecord
belongs_to :park
has_many :extra_guests, dependent: :destroy
validates :name, :age_from, :age_to, presence: true
validates_associated :extra_guests
end
class Attraction < ApplicationRecord
belongs_to :park
has_many :extra_guests, dependent: :destroy
accepts_nested_attributes_for :extra_guests, allow_destroy: true
validates :name, presence: true
end
class Park < ApplicationRecord
has_many :attractions, dependent: :destroy
has_many :age_tables, dependent: :destroy
validates :name, :currency, presence: true
end
extra_guests_controller
def edit
#extra_guest = ExtraGuest.find(params[:id])
#age_table = #extra_guest.age_table
#age_table_list = AgeTable.where(park: #attraction.park)
end
def update
#extra_guest = #attraction.extra_guests.find(params[:id])
#age_table = AgeTable.find(params[:age_table])
authorize #extra_guest
if #extra_guest = #extra_guest.update(extra_guest_params)
redirect_to root_path
else
#attraction = Attraction.find(params[:attraction_id])
#extra_guest = ExtraGuest.find(params[:id])
#age_table_list = #attraction.park.age_tables
render 'edit'
end
end
private
def extra_guest_params
params.require(:extra_guest).permit(:name, :age_table_id,
extra_guest_prices_attributes: [:id, :name, :price_type, :start_date, :end_date, :price, :duration, :duration_min, :duration_max, :backend_only, :weekend_extra, :_destroy])
end
views/extra_guests/form
<%= simple_form_for [#attraction, #extra_guest] do |f|%>
<%= f.input :age_table, :as => :select, :selected => #age_table.id, :collection => #age_table_list.map {|u| [u.name, u.id]}, :include_blank => false %>
<% f.button :submit %>
Error message + params
Couldn't find AgeTable without an ID
{"utf8"=>"✓",
"_method"=>"patch",
"authenticity_token"=>"l8HMnVIRybZg==",
"extra_guest"=>
{"age_table"=>"104",
"extra_guest_prices_attributes"=>
{"0"=>{"price"=>"1.0", "weekend_extra"=>"", "start_date"=>"2019-10-15", "end_date"=>"20-09-2019", "duration"=>"", "duration_min"=>"", "duration_max"=>"", "_destroy"=>"false", "id"=>"42"},
"1"=>{"price"=>"1.0", "weekend_extra"=>"", "start_date"=>"2019-10-15", "end_date"=>"2019-10-16", "duration"=>"", "duration_min"=>"", "duration_max"=>"", "_destroy"=>"false", "id"=>"43"}}},
"commit"=>"Save new option",
"attraction_id"=>"185",
"id"=>"55"}
First of all, you say you have an error with this code #extra_guest.update(extra_guest_params).errors.full_messages but the code you show does not have that line.
Now, the update method returns false if it fails https://apidock.com/rails/ActiveRecord/Persistence/update
This line:
#extra_guest = #extra_guest.update(extra_guest_params)
will set #extra_guest to false if it fails, you don't need to set #extra_guest, just use if #extra_guest.update(extra_guest_params)
Using the line of code you name but it's not on the code you showed,#extra_guest.update(extra_guest_params).errors.full_messages, if there are errors then #extra_guest.update(extra_guest_params) will be false, so no .errors method is found.
you have to split it in two lines:
#extra_guest.update(extra_guest_params) # after this, #extra_guest will have the errors hash set
#extra_guest.errors.full_messages # call it on the object and not on the result value from the update method
EDIT: you are permitting age_table_id but the parameter is age_table, fix the name of the parameter to be age_table_id too
It looks to me like you tried to use #attraction before defining it. You could fix this by moving your definition of #attraction further up in the method, but I would move it into its own method like so:
private
def attraction
#attraction ||= Attraction.find(params[:attraction_id])
end
Then you use the method name, which is now defined for the whole controller and invoked when you use it (as opposed to an instance variable which will just be 'nil' if you invoke it without defining it). The ||= allows the method to return the existing value of the instance variable if it is defined, rather than running the query every time the method is called. So first line of your update action would be
#extra_guest = attraction.extra_guests.find(params[:id])
I would do something similar for the other instance variables you have there (#extra_guest, #age_table, and #age_table_list should be defined in private methods separately). Incidentally, using a lot of instance variables for a single controller (you have 4 in this controller, which is a lot) is considered a bit of a code smell, but you should make something that works first and then refactor. Reference for later: https://thoughtbot.com/blog/sandi-metz-rules-for-developers

Rails 5: Nested attributes aren't updated for model

I can't get rails to update my nested attributes, though regular attributes work fine. This is my structure:
unit.rb:
class Unit < ApplicationRecord
has_many :unit_skill_lists
has_many :skill_lists, through: :unit_skill_lists, inverse_of: :units, autosave: true
accepts_nested_attributes_for :skill_lists, reject_if: :all_blank, allow_destroy: true
end
unit_skill_list.rb:
class UnitSkillList < ApplicationRecord
belongs_to :unit
belongs_to :skill_list
end
skill_list.rb:
class SkillList < ApplicationRecord
has_many :unit_skill_lists
has_many :units, through: :unit_skill_lists, inverse_of: :skill_lists
end
And this is (part of) the controller:
class UnitsController < ApplicationController
def update
#unit = Unit.find(params[:id])
if #unit.update(unit_params)
redirect_to edit_unit_path(#unit), notice: "Unit updated"
else
redirect_to edit_unit_path(#unit), alert: "Unit update failed"
end
end
private
def unit_params
unit_params = params.require(:unit).permit(
...
skill_list_attributes: [:id, :name, :_destroy]
)
unit_params
end
end
The relevant rows in the form (using formtastic and cocoon):
<%= label_tag :skill_lists %>
<%= f.input :skill_lists, :as => :check_boxes, collection: SkillList.where(skill_list_type: :base), class: "inline" %>
Any idea where I'm going wrong? I have tried following all guides I could find but updating does nothing for the nested attributes.
Edit after help from Vasilisa:
This is the error when I try to update a Unit:
ActiveRecord::RecordInvalid (Validation failed: Database must exist):
This is the full unit_skill_list.rb:
class UnitSkillList < ApplicationRecord
belongs_to :unit
belongs_to :skill_list
belongs_to :database
end
There is no input field for "database". It is supposed to be set from a session variable when the unit is updated.
If you look at the server log you'll see something like skill_list_ids: [] in params hash. You don't need accepts_nested_attributes_for :skill_lists, since you don't create new SkillList on Unit create/update. Change permitted params to:
def unit_params
params.require(:unit).permit(
...
skill_list_ids: []
)
end
UPDATE
I think the best options here is to set optional parameter - belongs_to :database, optional: true. And update it in the controller manually.
def update
#unit = Unit.find(params[:id])
if #unit.update(unit_params)
#unit.skill_lists.update_all(database: session[:database])
redirect_to edit_unit_path(#unit), notice: "Unit updated"
else
redirect_to edit_unit_path(#unit), alert: "Unit update failed"
end
end

Rails use an instance variable in controller or generate within a helper?

So I'm trying to build out on an Invoice page the past_due_amount where I'm trying to find only the invoices for the current account, that are not paid off, and should be in the past.
So roughly I have:
past_due_amount = Invoice.where(account: invoice.account, status: :unpaid).where('date < ? ', invoice.date).map(&:due).sum
For additional context here are the models involved:
Invoice:
class Invoice < ApplicationRecord
belongs_to :account
has_many :line_items, dependent: :destroy
has_many :payment_destinations, dependent: :destroy
has_many :prorated_fees, dependent: :nullify
enum status: [:unpaid, :paid]
validates :date, presence: true
validates :period_start, :period_end,
uniqueness: { scope: :account, allow_blank: true }, on: :create
validate :start_is_before_end
DAYS_DUE_AFTER_DATE = 14.days
scope :descending, -> { order(date: :desc) }
scope :ascending, -> { order(date: :asc) }
scope :due, -> { unpaid.where(arel_table[:date].lteq(Time.zone.today - DAYS_DUE_AFTER_DATE)) }
def total
if persisted?
line_items.sum(:amount)
else
line_items.map(&:amount).sum
end
end
end
Account:
class Account < ApplicationRecord
belongs_to :customer
belongs_to :property_address,
class_name: Address.to_s,
dependent: :destroy,
required: false
[:products, :account_changes, :equipments,
:payments, :invoices].each do |assoc|
has_many assoc, dependent: :destroy
end
accepts_nested_attributes_for :property_address
delegate :street, :city, :state, :zip,
to: :property_address, allow_nil: true
delegate :email, :full_name, to: :customer
enum status: [:staged, :active, :inactive]
scope :active_or_staged, -> { where(status: [:staged, :active]) }
scope :past_due, lambda {
joins(:invoices)
.where(
Invoice.arel_table[:status].eq(:unpaid)
.and(Invoice.arel_table[:date].lt(Time.zone.today - 14.days))
).distinct
}
scope :search, lambda { |term|
joins(:customer)
.where(
arel_table[:account_num].matches("%#{term}%")
.or(Customer.arel_search(term))
)
}
end
With the rough code in place I decided to build out a instance variable on the InvoicesController within the show method as below:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
No errors appear but that's not saying much since the examples I have are poor, at best. But my question is...should I actually be putting this in a helper instead of the show method on an InvoicesController or even in the model?
EDIT:
I've also tried putting in my Invoice model:
def self.past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
Then in my InvoicesController:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.past_due_amount
end
End up getting undefined method `date' for #invoice.date.
The best way is to create a method past_due_amount in the InvoicesHelper
module InvoicesHelper
def past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date <?', #invoice.date).map(&:due).sum
end
end
In you controller just initialize all the instance variables
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
end
In the view you should use: <%= past_due_amount > to show your data
Create an instance method in Account model
def past_due_amount
invoices.map(&:due).sum
end
and then from view you can all it #account.past_due_amount. no need to create extra instance variable in controller action
So I sort of used Patrick's answer but it was actually failing so I switched to passing invoice as params.
Helper
module InvoicesHelper
def past_due_amount(invoice)
Invoice.where(account: invoice.account, status: :unpaid).where('date < ?', invoice.date).map(&:due).sum
end
end
Then in my view:
<% if past_due_amount(invoice).positive? %>
<p><%= number_to_currency past_due_amount(invoice) %></p>
<% end %>

How to create nested models from API request?

I've a Rails API and I've two models:
class Event < ActiveRecord::Base
belongs_to :category
has_many :event_categories
has_many :events, through: :event_categories
attr_accessible :title, :description, :event_categories_attributes
accepts_nested_attributes_for :event_categories
end
and
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
attr_accessible :category_id, :event_id, :principal
validates :event, :presence => true
validates :category, :presence => true
validates_uniqueness_of :event_id, :scope => :category_id
end
In a first moment, EventCategory didn't exist so I created Event resources sending params like event[title]='event1', event[description] = 'blablbla' thought POST REST request.
My API EventsController was like this (I haven't a new method because I don't need views):
def create
#event = Event.create(params[:event])
if #event
respond_with #event
else
respond_with nil, location: nil, status: 404
end
end
This way worked correctly for me. Now, with the new EventCategory model I don't know how I could create EventCategories models at the same time.
I've trying this... but it doesn't work:
def create
#event = Event.new(params[:event])
#event.event_categories.build
if #event.save
respond_with #event
else
respond_with nil, location: nil, status: 404
end
end
Rails told me:
{
"event_categories.event": [
"can't be blank"
],
"event_categories.category": [
"can't be blank"
]
}
I send the category_id like this:
event[event_categories_attributes][0][category_id] = 2
Any ideas?
In your create action, instead of this:
#event.event_categories.build
Try this:
#event.event_categories = EventCategory.new do |ec|
ec.event = #event
ec.category = the_cattegory_you_want_to_specify
# You need both of these as you are validating the presence of event AND category
end

Application design of controller (Rails 3)

This is hard to explain, but I will do my best:
I am building a system where user's can take courses. Courses are made up of steps, that must be taken in order. In the system there are 6 step types (Download, Presentation, Video, Text, Quiz, and Survey)
The way a user accesses a STEP currently is:
http://example.com/courses/2/course_steps/1
As you can tell course_steps are nested under courses.
Below is the show method in course steps:
def show
render "show_#{#course_step.step.step_type.name.downcase}"
end
As you can tell it basically picks a view to show_[TYPE] (quiz, survey, text..etc)
This works fine for simple steps such as a text, video, or download, but for complicated steps such as a quiz, this model does not work well for the following reasons:
How do I validate a form for a quiz or survey as I would be using a different controller (QuizAttemptsController).
It seems to break the REST principal as a quiz, survey..etc should be treated separately. (I know they are step types, but they can have their own actions and validations)
Step Model
class Step < ActiveRecord::Base
belongs_to :step_type
belongs_to :client
has_one :step_quiz, :dependent => :destroy
has_one :step_survey, :dependent => :destroy
has_one :step_text, :dependent => :destroy
has_one :step_download, :dependent => :destroy
has_one :step_video, :dependent => :destroy
has_one :step_presentation, :dependent => :destroy
has_many :course_steps, :dependent => :destroy
has_many :courses, :through => :course_steps
has_many :patient_course_steps, :dependent => :destroy
attr_accessible :step_type_id, :client_id, :title, :subtitle, :summary
validates :title, :presence=>true
validates :summary, :presence=>true
def getSpecificStepObject()
case self.step_type.name.downcase
when "text"
return StepText.find_by_step_id(self.id)
when "quiz"
return StepQuiz.find_by_step_id(self.id)
when "survey"
return StepSurvey.find_by_step_id(self.id)
when "download"
return StepDownload.find_by_step_id(self.id)
when "video"
return StepVideo.find_by_step_id(self.id)
when "presentation"
return StepPresentation.find_by_step_id(self.id)
end
end
end
Step Quiz Model:
class StepQuiz < ActiveRecord::Base
belongs_to :step, :dependent => :destroy
has_many :step_quiz_questions, :dependent => :destroy
has_many :quiz_attempts, :dependent => :destroy
accepts_nested_attributes_for :step
accepts_nested_attributes_for :step_quiz_questions, :allow_destroy => true
attr_accessible :step_id, :instructions, :step_attributes, :step_quiz_questions_attributes
validates :instructions, :presence=>true
end
CourseStep Model
class CourseStep < ActiveRecord::Base
belongs_to :step
belongs_to :course
validates_uniqueness_of :step_id, :scope => :course_id
def next_step()
Course.find(self.course.id).course_steps.order(:position).where("position >= ?", self.position).limit(1).offset(1).first
end
def previous_step()
Course.find(self.course.id).course_steps.order("position DESC").where("position <= ?", self.position).limit(1).offset(1).first
end
end
How would you suggest fixing this?
What you want to do is implement your Model as a Finite State Machine and continually reload the new or edit action until the desired state is reached, then your controller can display different views depending on state to allow multiple steps to happen.
One way I have solved the problem is by adding a member action of "submit_quiz"to the course_steps controller. I am not sure if I like this, as the code looks kind of ugly. I would appreciate feedback.(Note: I Am using CanCan so #course_step is created automagically in the course_steps_controller)
The things I don't like are:
show_quiz view has a lot of code in it
submit_quiz is in the course_steps_controller
quiz_attempt model has virtual attribute of quiz_questions (for validation purposes)
show_quiz.html.erb
<%= form_for (#quiz_attempt.blank? ? QuizAttempt.new(:started => Time.now.utc, :step_quiz_id => #course_step.step.step_quiz.id) : #quiz_attempt, :url => submit_quiz_course_course_step_path(#course_step.course, #course_step)) do |f| %>
<%= render :partial => 'shared/error_messages', :object => f.object %>
<% #course_step.step.step_quiz.step_quiz_questions.each do |quiz_question| %>
<h3><%= quiz_question.value %></h3>
<% quiz_question.step_quiz_question_choices.each do |quiz_question_choice| %>
<%= radio_button_tag("quiz_attempt[quiz_questions][#{quiz_question.id}]", quiz_question_choice.value, f.object.get_quiz_question_choice(quiz_question.id) == quiz_question_choice.value)%>
<%= quiz_question_choice.value %><br />
<% end %>
<% end %>
<%= f.hidden_field(:step_quiz_id)%>
<%= f.hidden_field(:started)%>
<%= submit_tag("Submit Quiz")%>
<% end %>
course_steps_controller.rb
def show
PatientCourseStep.viewed(current_user.id, params[:course_id], #course_step.step.id )
render "show_#{#course_step.step.step_type.name.downcase}"
end
def submit_quiz
#quiz_attempt = QuizAttempt.new(params[:quiz_attempt])
if !#quiz_attempt.save()
render 'show_quiz'
end
end
quiz_attempt.rb
class QuizAttempt < ActiveRecord::Base
belongs_to :step_quiz
belongs_to :patient
attr_accessor :quiz_questions
attr_accessible :step_quiz_id, :patient_id, :started, :ended, :correct, :incorrect, :quiz_questions
validate :answered_all_questions?
def get_quiz_question_choice(quiz_question_id)
unless self.quiz_questions.blank?
quiz_questions[quiz_question_id.to_s]
end
end
private
def answered_all_questions?
#Making sure they answered all the questions
if self.quiz_questions.blank? or self.quiz_questions.try(:keys).try(:count) != self.step_quiz.step_quiz_questions.count
errors.add_to_base "Not all questions were answered"
end
end
end
def submit_quiz
#quiz_attempt = QuizAttempt.new(params[:quiz_attempt])
if !#quiz_attempt.save()
render 'show_quiz'
end
end

Resources