(Apologies if this question is simple...I've been at this for a few days now)
So...I've recently changed my model, controller and partial _form.html.erb files to implement a Nested form...
And now my jbuilder file needs to be refactored but I'm a noob on pulling out nested values...
original index.json.jbuilder
json.array! @assignments do |assignment|
date_format = assignment.all_day_event? ? '%Y-%m-%d' : '%Y-%m-%dT%H:%M:%S'
#binding.pry
json.description assignment.description <---this used to work before I added my Nested Form
# REQUIRED
json.id assignment.id
json.title assignment.roster_title
json.start assignment.event_date.strftime('%Y-%m-%d') assignment.start_time.strftime('T%H:%M:%S')
json.end assignment.event_date.strftime('%Y-%m-%d') assignment.end_time.strftime('T%H:%M:%S')
# REQUIRED end
shift_id = assignment.volunteer_shift_id.nil? ? assignment.volunteer_task_type_id : assignment.volunteer_shift_id
json.volunteer_shift_id shift_id
# DATE
json.date assignment.event_date.strftime('%Y-%m-%d')
# TIME
json.start_time assignment.start_time.strftime('T%H:%M:%S')
json.end_time assignment.end_time.strftime('T%H:%M:%S')
json.color assignment.color unless assignment.color.blank?
# json.color assignment.skedj_style unless assignment.skedj_style.blank?
json.allDay assignment.all_day_event? ? true : false
json.update_url assignment_path(assignment, method: :patch)
json.edit_url edit_assignment_path(assignment)
end
This is the error I am getting...
Rendering assignments/index.json.jbuilder
Assignment Load (2.1ms) SELECT "assignments".* FROM "assignments" INNER JOIN "volunteer_shifts" ON "volunteer_shifts"."id" = "assignments"."volunteer_shift_id" INNER JOIN "volunteer_events" ON "volunteer_events"."id" = "volunteer_shifts"."volunteer_event_id" WHERE ("volunteer_events"."date" BETWEEN $1 AND $2) [["date", "2021-01-05"], ["date", "2021-01-06"]]
Rendered assignments/index.json.jbuilder (27.8ms)
Completed 500 Internal Server Error in 48ms (ActiveRecord: 6.3ms)
DEPRECATION WARNING: #original_exception is deprecated. Use #cause instead. (called from real_exception at /home/fonso/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/better_errors-0.3.2/lib/better_errors/error_page.rb:82)
NoMethodError - undefined method `volunteer_event' for nil:NilClass:
app/models/assignment.rb:46:in `description'
app/views/assignments/index.json.jbuilder:12:in `block in _app_views_assignments_index_json_jbuilder__1085023644391212852_37504860'
(gem) jbuilder-2.11.2/lib/jbuilder.rb:339:in `block (2 levels) in _map_collection'
(gem) jbuilder-2.11.2/lib/jbuilder.rb:346:in `_scope'
(gem) jbuilder-2.11.2/lib/jbuilder.rb:339:in `block in _map_collection'
(gem) activerecord-5.0.7.2/lib/active_record/relation/delegation.rb:38:in `map'
(gem) activerecord-5.0.7.2/lib/active_record/relation/delegation.rb:38:in `map'
(gem) jbuilder-2.11.2/lib/jbuilder.rb:338:in `_map_collection'
(gem) jbuilder-2.11.2/lib/jbuilder.rb:216:in `array!'
(gem) jbuilder-2.11.2/lib/jbuilder/jbuilder_template.rb:90:in `array!'
app/views/assignments/index.json.jbuilder:8:in `_app_views_assignments_index_json_jbuilder__1085023644391212852_37504860'
My new assignments_controller has been modified in the "new" action and the assignment_params
class AssignmentsController < ApplicationController
before_action :set_assignment, only: [:show, :edit, :update, :destroy]
skip_before_action :verify_authenticity_token #TODO refactor this line to be very specific
# GET /assignments or /assignments.json
def index
# @assignments = Assignment.limit(20)
# @assignments = Assignment.where(start: params[:start]..params[:end])
@assignments = Assignment.date_range(params[:start]..params[:end])
end
# GET /assignments/1 or /assignments/1.json
def show
end
# GET /assignments/new
def new
@assignment = Assignment.new
#fixme: build goes here
@assignment.volunteer_shift.build
@my_url = {:action => "create", :id => params[:id]}
end
# GET /assignments/1/edit
def edit
end
# POST /assignments or /assignments.json
def create
@assignment = Assignment.new(assignment_params)
# error wants contact.id not contact_id ???
respond_to do |format|
if @assignment.save
format.html { redirect_to @assignment, notice: "Assignment was successfully created." }
format.json { render :show, status: :created, location: @assignment }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @assignment.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /assignments/1 or /assignments/1.json
def update
@assignment.update(assignment_params)
end
# DELETE /assignments/1 or /assignments/1.json
def destroy
@assignment.destroy
# NOTE: comment original out 4 now
# respond_to do |format|
# format.html { redirect_to assignments_url, notice: "Assignment was successfully destroyed." }
# format.json { head :no_content }
# end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_assignment
@assignment = Assignment.find(params[:id])
end
# Only allow a list of trusted parameters through.
def assignment_params
#fixme: ,volunteer_shift_attributes: [:???, :???, :???] <--- insert this below?
params.require(:assignment).permit(:title,
:redirect_to, :set_date, :date_range, :contact_id,
:start_time, :end_time, :start, :end, :attendance_type_id, :notes,
:call_status_type_id, :closed, :lock_version, :color,
volunteer_shift_attributes: [:volunteer_task_type_id,:roster_id,:program_id,:set_description,:set_date,:id,:destroy])
end
end
Here are the three related model associations
class VolunteerEvent < ApplicationRecord
belongs_to :volunteer_default_event
validates_presence_of :date
has_many :volunteer_shifts, :dependent => :destroy
has_many :resources_volunteer_events, :dependent => :destroy
validates_associated :volunteer_shifts
...
end
class VolunteerShift < ApplicationRecord
validates_presence_of :roster_id #fixme: remove these validations?
validates_presence_of :end_time #fixme: remove these validations?
validates_presence_of :start_time #fixme: remove these validations?
belongs_to :volunteer_default_shift
belongs_to :volunteer_task_type
has_many :assignments
belongs_to :program
belongs_to :roster
belongs_to :volunteer_event
has_many :contact_volunteer_task_type_counts, :primary_key => 'volunteer_task_type_id', :foreign_key => 'volunteer_task_type_id' #:through => :volunteer_task_type
...
end
class Assignment < ApplicationRecord
attr_accessor :volunteer_shift #,:contact_id ???
belongs_to :volunteer_shift
has_one :volunteer_task_type, :through => :volunteer_shift, :source => :volunteer_task_type
belongs_to :contact ,optional: true
validates_presence_of :volunteer_shift
validates_associated :volunteer_shift
belongs_to :attendance_type
belongs_to :call_status_type
validates_presence_of :set_date, :if => :volshift_stuck
# validates_existence_of :contact, :allow_nil => true <----THIS IS BAD
accepts_nested_attributes_for :volunteer_shift, allow_destroy: true
#fixme: Nodule::DelegationError -
# This error appears when trying to show the "Add New Assignment" model on the /assignments view
delegate :set_date, :set_date=, :to => :volunteer_shift
delegate :set_description, :set_description=, :to => :volunteer_shift
scope :date_range, lambda { |range|
joins(volunteer_shift: :volunteer_event)
.where(volunteer_shifts: { volunteer_events: { date: range } })
}
scope :is_after_today, lambda {||
{ :conditions => ['(SELECT date FROM volunteer_events WHERE id = (SELECT volunteer_event_id FROM volunteer_shifts WHERE id = assignments.volunteer_shift_id)) > ?', Date.today] }
}
scope :on_or_after_today, lambda {||
{ :conditions => ['(SELECT date FROM volunteer_events WHERE id = (SELECT volunteer_event_id FROM volunteer_shifts WHERE id = assignments.volunteer_shift_id)) >= ?', Date.today] }
}
scope :not_cancelled, -> { where('(attendance_type_id IS NULL OR attendance_type_id NOT IN (SELECT id FROM attendance_types WHERE cancelled = \'t\'))')}
scope :roster_is_limited_by_program, -> {where("roster_id IN (SELECT id FROM rosters WHERE limit_shift_signup_by_program = 't')").joins(:volunteer_shift)}
attr_accessor :attendance_type_id
#fixme: fix all red dots on this page
def real_programs
return [] unless self.volunteer_shift&.roster
return [] unless self.volunteer_shift.roster.limit_shift_signup_by_program
self.volunteer_shift.roster.skeds.select{|x| x.category_type == "Program"}.map{|x| x.name}
end
# TODO: find all time_range_s methods and either pull out to DRY or give unique names
def time_range_s
return "" unless start_time and end_time
(start_time.strftime("%I:%M") ' - ' end_time.strftime("%I:%M")).gsub( ':00', '' ).gsub( ' 0', ' ').gsub( ' - ', '-' ).gsub(/^0/, "")
end
def description
self.volunteer_shift.volunteer_event.date.strftime("%D") " " self.time_range_s " " self.slot_type_desc
end
def roster_title
self.volunteer_shift.roster.name
end
#fixme: date / set_date is fracked since old code was ported over
def date
volunteer_shift.date
end
#full calendar uses this method name....see the assignment.json.jbuilder
def event_date
self.date
end
def slot_type_desc
b = (self.volunteer_shift.volunteer_task_type_id.nil? ? self.volunteer_shift.volunteer_event.description : self.volunteer_shift.volunteer_task_type.description)
b = b " (#{self.volunteer_shift.description})" if self.volunteer_shift.description and self.volunteer_shift.description.length > 0
b
end
def display_name
((!(self.volunteer_shift.description.nil? or self.volunteer_shift.description.blank?)) ? self.volunteer_shift.description ": " : "") self.contact_display
end
def cancelled?
(self.attendance_type&.cancelled)
end
def attended?
(self.attendance_type and !self.attendance_type.cancelled)
end
def contact_display
if self.closed
"(closed)"
elsif contact_id.nil?
return "(available)"
else
self.contact.display_name "(#{self.voltask_count})"
end
end
def <=>(other)
self.date <=> other.date
end
# arieljuod thinks this is suspect. "belongs_to :contact" should have taken care of setting contact object
# def contact_id=(new_val)
# self.write_attribute(:contact_id, new_val)
# self.contact = Contact.find_by_id(new_val.to_i)
# end
def contact_id_and_by_today
# Unless the contact id is empty, or the event date is after today.
!(contact_id.nil? || self.volunteer_shift.volunteer_event.date > Date.today)
end
def voltask_count
self.contact_volunteer_task_type_count ? self.contact_volunteer_task_type_count.attributes["count"] : 0
end
before_validation :set_values_if_stuck
def set_values_if_stuck
return unless volshift_stuck
volunteer_shift.set_values_if_stuck(self)
end
after_destroy { |record| if record.volunteer_shift&.stuck_to_assignment; record.volunteer_shift.destroy; else VolunteerShift.find_by_id(record.volunteer_shift_id).fill_in_available; end}
after_save {|record| if record.volunteer_shift&.stuck_to_assignment; record.volunteer_shift.save; end}
after_save { |record| VolunteerShift.find_by_id(record.volunteer_shift_id).fill_in_available }
# def volunteer_shift_attributes=(attrs) #fixme: why is this not getting called on volunteer_events/create_shift?
# self.volunteer_shift.attributes=(attrs) # just pass it up
# end
#fixme: where is stuck_to_assignment ??? WTF?
def volshift_stuck
self.volunteer_shift&.stuck_to_assignment
end
def first_time_in_area?
if self.contact and self.volunteer_shift and self.volunteer_shift.volunteer_task_type
!ContactVolunteerTaskTypeCount.has_volunteered?(self.contact_id, self.volunteer_shift.volunteer_task_type_id)
else
false
end # and self.contact_id_changed? moved outside because we use update_attributes
end
#for fullcalendar
def all_day_event?
self.start_time == self.start_time.midnight && self.end_time == self.end_time.midnight ? true : false
end
end
...
end
I don't know what's goin on..why is the jbuilder breaking now? is it because of the nested form? is it a relation in my models? Something with my assignment_params???
How do I re-write my index.json.jbuilder?
(Note: I suspect I have to do some nested array stuff in my json file, but I've not done that before...https://stackoverflow.com/questions/35749301/get-nested-arrays-in-json)
UPDATE:
So, I tested a theory out If I switch branches to an earlier version pre-nested form....all my json output gets displayed. If I switch back to the nested form branch I do not get any json output that my main page uses to display. (note: I have not been able to add a new record as the link to add a new record is on the main page...that now does not get displayed). Process of elimination points to the model associations recently added when I was trying to create a Nested Form. But if I comment those lines out... line 2 and 12... in my assignment model...
class Assignment < ApplicationRecord
# attr_accessor :volunteer_shift #,:contact_id ??? <----TURN THIS OFF AND JSON DISPLAYS AGAIN
belongs_to :volunteer_shift
has_one :volunteer_task_type, :through => :volunteer_shift, :source => :volunteer_task_type
belongs_to :contact ,optional: true
validates_presence_of :volunteer_shift #belongs_to takes care of this now
validates_associated :volunteer_shift
belongs_to :attendance_type
belongs_to :call_status_type
validates_presence_of :set_date, :if => :volshift_stuck #belongs_to takes care of this now??
#accepts_nested_attributes_for :volunteer_event, allow_destroy: true <----TURN THIS OFF
The json outputs to the main screen again no problems.
UPDATE 2:
if I just comment out line 2 of assignment.rb all the json data reapers
CodePudding user response:
The error message
NoMethodError - undefined method `volunteer_event` for nil:NilClass: app/models/assignment.rb:50:in `description'
tells that in the description
method in line 50
of the app/models/assignment.rb
file is called volunteer_event
on an instance of nil:NilClass
def description
self.volunteer_shift.volunteer_event.date.strftime("%D") " " self.time_range_s " " self.slot_type_desc
end
That means that self.volunteer_shift
must return nil
and that there are Assignments in your database that do not have a volunteer_shift
assigned.
The fix depends on your application, but a simple solution might be to have description
returning nil
when there is no volunteer_shift
– like this
def description
return unless volunteer_shift
self.volunteer_shift.volunteer_event.date.strftime("%D") " " self.time_range_s " " self.slot_type_desc
end
CodePudding user response:
UPDATE
attr_accessor :volunteer_shift #,:contact_id ???
this line will have been hiding the
belongs_to :volunteer_shift
declaration which is why there was no data to show, you should still check and validate though as discussed and as below.
@spickermann has pretty much nailed your problem in their answer but it is not a complete solution.
- Your data got messed up because you were creating assignment records with no volunteer_shift records associated.
This was allowed to happen because previous checks to stop this from happening are outdated and only work as expected in older versions of rails with the validates_presence_of
methods, so they need updating.
You need to have tests to ensure that this never happens again and you will be notified straight away if it does happen again. rspec, capybara and factorybot with guard are my tools of choice in place of standard rails testing framework however, the tools you use should be your preference, just make sure you have test coverage for the creation and updating of assignment records that ensure you have a valid volunteer_shift record, that way you will know when you have fixed the original problem that caused this in the first place and not just when you have dealt with this specific issue.
Lastly you either need to fix your data, or you need to add checks to any method that uses volunteer_shift in your assignment model as suggested by @spickermann by checking for volunteer_shift not being nil, and perhaps do both of these things.