nested form, embedded docs, mongoid 2.2.0 problem - mongoid

I cant seem to find answer for this here or with the google, any help would be awesome.
The Building saves correctly, but the embedded doc PriorityArea doesnt get updated...
I want to eventually have it ajax a new form for new priority areas evenutally, but need it to update first.
Mongoid::Errors::InvalidFind in BuildingsController#update
Calling Document#find with nil is invalid
class Building
include Mongoid::Document
embeds_many :priority_areas
accepts_nested_attributes_for :priority_areas, :allow_destroy => true, :autosave => true
end
class PriorityArea
include Mongoid::Document
embedded_in :building, :inverse_of => :priority_areas
end
#view
= form_for [#customer, #building] do |f|
...
...
= f.fields_for :priority_areas do |pa|
= f.name
...
...
#controller
#building.update_attributes(params[:building])
It correctly yeilds the correct data from the db, but fails to error above on building#update. Any help is greatly appreciated.
update
in the building#update im
puts params[:building][:priority_areas_attributes].to_yaml
which yeilds
--- !map:ActiveSupport::HashWithIndifferentAccess
"0": !map:ActiveSupport::HashWithIndifferentAccess
name: area 51
location: near front door
notes: ""
priority: "1"
id: ""
"1": !map:ActiveSupport::HashWithIndifferentAccess
name: area 52
location: near rear door
notes: ""
priority: "2"
id: ""
im guessing the problem is the null id:""

The problem was the null id
it needed to have an ObjectId to work correctly. stupid error on my part.

I encountered the exact problem. simple_form was automatically passing an id parameter to my controller, but it was blank.
Why was the id for my embedded document blank? I'm guessing it's because I imported the parent document via mongoimport. If I manually generate a parent document via web forms, then the embedded documents have IDs as expected.
Here was my workaround:
class Foo
include Mongoid::Document
embeds_many :bars
accepts_nested_attributes_for :bars
####
# simple_form_for / embedded document workaround
#
# Because simple_form wants to provide the ID for an existing object,
# it will output a blank ID because imported embedded documents
# have an ID of nil.
#
# Intercept it to avoid
# Mongoid::Errors::InvalidFind in FoosController#update
def bars_attributes=(attribs)
attribs.each do |key, value|
index = key.to_i
fixed_attrib = value.delete_if { |k,v| k=="id" and v=="" }
self.bars[index].update_attributes(fixed_attrib)
end
end
end
class Bar
include Mongoid::Document
embedded_in :foo
end

Related

Can't get my strong parameters nested correct in Ruby on Rails

I'm trying to add an array of objects (tasks in this case) as a return type in my controller for the root object (project). One project has many tasks and I'd like to save it all at once, but I keep getting the following error indicating that the return type isn't correct.
ActiveRecord::AssociationTypeMismatch (Task(#47457277775360) expected, got {"name"=>"Some task", "start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z"} which is an instance of ActiveSupport::HashWithIndifferentAccess(#47457266882220))
The whitelisted parameters I have are like this
# whitelist params
params.permit(:name, :description, tasks: [:name, :start, :end])
The data being returned for the above example:
{"name"=>"asdf", "description"=>"zxvccxvzzxcvxcvcxvzxcvz", "tasks"=>[{"start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z", "name"=>"Some task"}]}
[Edit] - Here are the models we're working with
# app/models/task.rb
class Task < ApplicationRecord
# model association
belongs_to :project
# validation
validates_presence_of :name
end
# app/models/project.rb
class Project < ApplicationRecord
# model association
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks
# validations
validates_presence_of :name
end
As per rails documentation Nested attributes allow you to save attributes on associated records through the parent. So in your strong params you need to pass attributes like this.
params.permit(:name, :description, task_attributes: [:name, :start, :end])
I suggest you to bind all params under one attribute like this
params.require(:project).permit(:name, :description, task_attributes: [:name, :start, :end])
so you must send params from frontend like
{"project": {"name"=>"asdf", "description"=>"zxvccxvzzxcvxcvcxvzxcvz", "task_attributes"=>[{"start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z", "name"=>"Some task"}]}}
You can read the documentation from https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

How to find all document by association Mongoid 4

I have a model Tag which potentially belongs to several other models, but at the moment only one model Todo which in turn belongs to User like so:
class User
include Mongoid::Document
field: name, type: String
has_many :todos
end
class Todo
include Mongoid::Document
field: name, type: String
belongs_to :user
end
class Tag
include Mongoid::Document
field: name, type: String
belongs_to :todos
end
How can I query all Tags that belongs to a particular user? I've written the following:
todo_ids = Todo.where(user_id: '86876876787')
and then:
tags = Tag.where('todo_id.in': todo_ids)
But those didn't work. What am I missing?
You're missing two things:
Mongoid isn't ActiveRecord so it won't know what to do with todo_ids in the Tag query.
'todo_id.in' is a field path that is trying to look at the in field inside a todo_id hash, this isn't a use of MongoDB's $in operator.
You can only work with one collection at a time so to fix the first one, you need to pull an array of IDs out of MongoDB:
todo_ids = Todo.where(user_id: '86876876787').pluck(:id)
# -------------------------------------------^^^^^^^^^^^
To fix the second one, use the $in operator:
tags = Tag.where(todo_id: { '$in': todo_ids })
tags = Tag.where(:todo_id.in => todo_ids)
tags = Tag.in(todo_id: todo_ids)
#...

Ruby Script unable to gather data

#!/usr/bin/ruby
# Fetches all Virginia Tech classes from the timetable and spits them out into a nice JSON object
# Can be run with option of which file to save output to or will save to classes.json by default
require 'rubygems'
require 'mechanize'
require 'nokogiri'
require 'json'
#Create Mechanize Browser and Class Data hash to load data into
agent = Mechanize.new
classData = Hash.new
#Get Subjects from Timetable page
page = agent.get("https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest")
subjects = page.forms.first.field_with(:name => 'subj_code').options
#Loop subjects
subjects.each do |subject|
#Get the Timetable Request page & Form
timetableSearch = agent.get("https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest")
searchDetails = page.forms.first
#Submit with specific subject
searchDetails.set_fields({
:SUBJ_CODE => subject,
:TERMYEAR => '201401',
:CAMPUS => 0
})
#Submit the form and store results into courseListings
courseListings = Nokogiri::HTML(
searchDetails.submit(searchDetails.buttons[0]).body
)
#Create Array in Hash to store all classes for subjects
classData[subject] = []
#For every Class
courseListings.css('table.dataentrytable/tr').collect do |course|
subjectClassesDetails = Hash.new
#Map Table Cells for each course to appropriate values
[
[ :crn, 'td[1]/p/a/b/text()'],
[ :course, 'td[2]/font/text()'],
[ :title, 'td[3]/text()'],
[ :type, 'td[4]/p/text()'],
[ :hrs, 'td[5]/p/text()'],
[ :seats, 'td[6]/text()'],
[ :instructor, 'td[7]/text()'],
[ :days, 'td[8]/text()'],
[ :begin, 'td[9]/text()'],
[ :end, 'td[10]/text()'],
[ :location, 'td[11]/text()'],
# [ :exam, 'td[12]/text()']
].collect do |name, xpath|
#Not an additional time session (2nd row)
if (course.at_xpath('td[1]/p/a/b/text()').to_s.strip.length > 2)
subjectClassesDetails[name] = course.at_xpath(xpath).to_s.strip
end
end
#Add class to Array for Subject!
classData[subject].push(subjectClassesDetails)
end
end
#Write Data to JSON file
open(ARGV[0] || "classes.json", 'w') do |file|
file.print JSON.pretty_generate(classData)
end
The above code is supposed to retrieve data from https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest
but if I print subjects.length is prints 0 so it clearly isn't getting the correct data. The given term code "201401" is definitely the right one.
I've noticed that when I manually enter in the link to my browser the subject field doesn't allow you to select an option until a term is selected, however when I view the page source the data is clearly already there. What can I do to retrieve this data?
I'm looking at that vtech page and I can see that you need to select a TERMYEAR first before the subj_code dropdown fills allowing you to get the options. Unfortunately this happens with javascript in function dropdownlist(listindex). Mechanize doesn't handle javascript so this script is doomed to fail.
Your options are to run a browser automator like Watir or Selenium: discussed here: How do I use Mechanize to process JavaScript?
Or to read the source of that page and parse out the values of these lines:
document.ttform.subj_code.options[0]=new Option("All Subjects","%",false, false);
document.ttform.subj_code.options[1]=new Option("AAEC - Agricultural and Applied Economics","AAEC",false, false);
document.ttform.subj_code.options[2]=new Option("ACIS - Accounting and Information Systems","ACIS",false, false);
To get the options. You could do that by simply using open-uri:
require 'open-uri'
page = open("https://banweb.banner.vt.edu/ssb/prod/HZSKVTSC.P_ProcRequest")
page_source = page.read
Now you can use a regex to scan for all the options:
page_source.scan /document\.ttform.+;/
That'll give you an array with all the lines that have the javascript codes that contain the options. Craft your regex a little better and you can extract the option text from those. I'll see if I can come up with something for that and I'll post back. Hopefully this will get you headed in the right direction.
I'm back. I was able to parse out all the subj_code options with this regex:
subjects = page_source.scan(/Option\("(.*?)"/).uniq # remove duplicates
subjects.shift # get rid of the first option because it's just "All Subjects"
subjects.size == 137
Hope that helps.

Mongoid 3.1 eager loading, json, and field names

Recently updated to Mongoid 3.1 from 3.0.3 and this resulted in some broken code and confusion on my side.
Say you have a pair of classes with a belongs_to/has_many relationship, like so:
class Band
include Mongoid::Document
field :name, type: String
has_many :members, :autosave => true
end
class Member
include Mongoid::Document
field :name, type: String
belongs_to :band
end
Saving all this to the database like so:
b = Band.new
b.name = "Sonny and Cher"
b.members << Member.new(name: "Sonny")
b.members << Member.new(name: "Cher")
b.save
I would in my API, be able to return a 'member' object like so:
m = Member.where(name: "Sonny").first
m.to_json
which yields the following, as expected:
{"_id":"<removed>","band_id":"5151d89f5dd99dd9ec000002","name":"Sonny"}
My client can request the full band object with a subsequent call if it wants to. However, in some cases I DO want to include the referenced item directly. With 3.0.3, I would just do the following:
m = Member.where(name: "Sonny").first
m[:band] = m.band
m.to_json
and this would add a new field with the full band information to it. With 3.1, however (it may have started in earlier versions, but I didn't test), I now get this:
{"_id":"<removed>","band_id":{"_id":"5151dc025dd99d579e000002","name":"Sonny and Cher"},"name":"Sonny"}
So, it looks like the band info has been eager-loaded into the field? Why is it stored under the key ':band_id' and not ':band'? I guess ':band' is protected, but I still don't think the data should be stored under the ':band_id' key. I suspect I am missing something here. Any ideas?
You can specify an :include option for to_json like so:
m.to_json(include: :band)
The JSON will then have a key band with the Band object converted to JSON and band_id will still be present.

Dirty Tracking of embedded document on the parent doc in Mongoid

I had to track the dirty objects. And it works fine with the parent
doc. But when I change the embedded or referenced in doc, the dirty
has to be accessed via the embedded/referenced in doc itself.
How can I track the dirty on the parent doc itself??
I have put together mongoid extension to solve this problem https://github.com/versative/mongoid_relations_dirty_tracking. It simply tracks changes on document relations in the similar way as mongoid do it for attributes so the timestamps and versions are updated correctly.
I don't think Mongoid's own dirty attributes feature tracks changes to embedded documents. But it's not hard to implement basic dirty tracking of embedded documents yourself with something like this:
class Bar
include Mongoid::Document
embeds_many :foos
attr_accessor :foos_changed
alias_method :foos_changed?, :foos_changed
# ...
end
class Foo
include Mongoid::Document
embedded_in :bar, :inverse_of => :foos
after_save :notify_parent
def notify_parent
bar.foos_changed = true
end
# ...
end
b = Bar.create
b.foos_changed? # => nil
b.foos.create
b.foos_changed? # => true
If you need more options, like including embedded documents changes in Bar.changes or tracking the specific changes, you'll have to override the changes and changed? methods, which I wouldn't do unless you absolutely need those features.
Here's a way to track what changed in the embedded documents:
class Bar
include Mongoid::Document
embeds_many :foos
attr_writer :embedded_changes
def embedded_changes
#embedded_changes ||= begin
self._children.inject({}) do |memo, child|
memo.merge(
{child.collection_name => {child.id => child.changes}}
) if child.changed?
end
end
end
def changes
original_value = super
if original_value.blank?
embedded_changes
else
original_value.merge(embedded_changes)
end
end
def changed?
super || self._children.any?(&:changed?)
end
# ...
end
class Foo
include Mongoid::Document
embedded_in :bar, :inverse_of => :foos
# ...
field :hat
end
b = Bar.create
f = b.foos.create
b.changes # => nil
b.changed? # => false
f.hat = 1
f.changes # => {"hat"=>[nil, "1"]}
b.changes # => {"foos"=>{BSON::ObjectId('4cf...')=>{"hat"=>[nil, "1"]}}}
b.changed? # => true

Resources