Saving records recursively with Mongoid, Ancestry - ruby-on-rails

I have a model Line Items embedded in Line model. In Line create view, I have provided ability to define multiple nested levels of line items.
Here is a random snap of param[:line]:
=> {"title"=>"Hello", "type"=>"World", "line_items"=>{"1"=>{"name"=>"A",
"position"=>"1", "children"=>{"1"=>{"name"=>"A1", "position"=>"1",
"children"=>{"1"=>{"name"=> "A11", "position"=>"1"}, "2"=>{"name"=>"A12",
"position"=>"2"}}}, "2"=>{"name"=>"A2", "position"=>"2"}}}, "3"=>
{"name"=>"B", "position"=>"3"}}}
In Line#create, I have:
def create
#line = Line.new(params[:line])
if #line.save
save_lines(params[:line][:line_items])
flash[:success] = "Line was successfully created."
redirect_to line_path
else
render :action => "new"
end
end
In Line#save_lines, I have:
# Save children up to fairly infinite nested levels.. as much as it takes!
def save_lines(parent)
unless parent.blank?
parent.each do |i, values|
new_root = #line.line_items.create(values)
unless new_root[:children].blank?
new_root[:children].each do |child|
save_lines(new_root.children.create(child))
end
end
end
end
end
LineItem Model looks like:
class LineItem
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Ancestry
has_ancestry
# Fields
field :name, type: String,
field :type, type: String
field :position, type: Integer
field :parent_id, type: Moped::BSON::ObjectId
attr_accessible :name, :type, :url, :position, :parent_id
# Associations
embedded_in :line, :inverse_of => :line_items
end
in Line model, i have:
# Associations
embeds_many :line_items, cascade_callbacks: true
Which work as expected. But, is there a better way to save the line_items recursively with Ancestry?

I think your code looks fine. I just refactored it.
How about:
def save_lines(parent)
parent.each do |i, values|
#get children hash if any
children = values.delete("children")
# create the object with whatever remain in values hash
#line.line_items.create(values)
# recurse if children isn't empty
save_lines(children) if children
end
end

Related

Links between class rails

I'm new to rails and in my project I have 2 classes who have a relationship betwen them. The problem is they dont can list services with organs. The following is the code I have:
modelos
class Admin::Organ
include Mongoid::Document
include Mongoid::Search
include Mongoid::Pagination
field :name, type: String
field :address, type: String
field :acronym, type: String
field :phones, type: String
field :emails, type: String
field :image, type: String
field :permalink, type: String
field :schedules, type: Array
field :coordinates, type: Hash
has_many :services, class_name: "Service"
has_many :units, class_name: "Admin::Unit"
before_save :touch_permalink
search_in :name
paginates_per 10
def url
"/orgao/#{permalink}"
end
private
def touch_permalink
self.permalink = self.name.parameterize
end
end
class Service
include Mongoid::Document
include Mongoid::Search
include Mongoid::Pagination
field :name, type: String
field :acronym, type: String
field :popular_names, type: Array
field :description, type: String
field :free, type: Boolean
field :applicants, type: Array
field :estimated_time, type: Hash
field :steps, type: Array
field :permalink, type: String
field :other_informations, type: String
belongs_to :organ, class_name: "Admin::Organ"
has_and_belongs_to_many :units, class_name: "Admin::Unit"
has_and_belongs_to_many :audiences, class_name: "Admin::Audience"
has_and_belongs_to_many :categories, class_name: "Admin::Category"
before_save :touch_permalink
search_in :name, :popular_names
paginates_per 10
def organ_id
read_attribute(:organ_id).to_s
end
def url
"/servico/#{permalink}"
end
private
def touch_permalink
self.permalink = self.name.parameterize
end
end
#controlers
class ServicesController < ApplicationController
def index
#organs = Admin::Organ.all
#services = Service.page(params[:page].to_i).per(3)
end
def show
#service = Service.where(permalink: params[:permalink]).first
end
end
class OrgansController < ApplicationController
def index
#organs = Admin::Organ.page(params[:page]).per(2)
end
def show
#organ = Admin::Organ.where(permalink: params[:permalink]).first
#organs = Admin::Organ.page(params[:page]).per(1)
end
end
#call in index.html
<%= organs.services.name%>
This appears to be returning an error whenever I run it
Note: On mongo relationships when you call a relationship method (such as services in your case) all of the documents associated with it are returned. Also for the pagination plugin you are using (I'm assuming kaminari by the syntax), this will return a collection of documents as well.
If so, it could be expected that you have a view for organs that has something a little like this:
<% #organs.each do |organ| %>
<div>Name: <%= organ.name %></div>
<div>Address: <%= organ.address %></div>
<div>Services:</div>
<% organ.services.each do |service| %>
<div>Name: <%= service.name %></div>
<div>Acronym: <%= service.acronym %></div>
<% end %>
<% end %>
<%= paginate #organs %>
You'll notice that to print out the services you need to first have a handle on the current document within #organs, and then you need to iterate the services within it. If you were to call name (which is a method of a relationship) you will get the relationship name (I believe this is what you are seeing).
I hope this helps. I had to do some guessing between the lines, come back to me if you need any further help.

Rails "has_many" association: "collection=" doesn't work as expected

Invoice has many invoice entries:
class Invoice < ActiveRecord::Base
has_many :invoice_entries, :autosave => true, :dependent => :destroy
validates_presence_of :date
end
class InvoiceEntry < ActiveRecord::Base
belongs_to :invoice
validates_presence_of :description
end
Assume we have a single invoice in the database:
id: 1
date: '2013-06-16'
and it has two invoice entries:
id: 10 id: 11
invoice_id: 1 invoice_id: 1
description: 'do A' description: 'do C'
Now, I have the new invoice entries:
id: 10
description: 'do B' description: 'do D'
(Existing invoice entry (New invoice entry
with updated description) without id)
I would like the invoice to have only these new invoice entries (this means that invoice entry with id=11 should be deleted).
invoice.invoice_entries = new_invoice_entries seems to do half of the work. It removes the invoice entry with id=11, creates a new invoice entry with description 'Do D', but it doesn't update the description of invoice entry with id=10 from 'Do A' to 'Do B'. I guess that when Rails sees an existing id in new_invoice_entries, it totally ignores it. Is that true? If yes, what is the rationale behind this?
My full code is below. How would you fix this issue? (I use Rails 4, in case it simplifies the code.)
# PATCH/PUT /api/invoices/5
def update
#invoice = Invoice.find(params[:id])
errors = []
# Invoice entries
invoice_entries_params = params[:invoice_entries] || []
invoice_entries = []
for invoice_entry_params in invoice_entries_params
if invoice_entry_params[:id].nil?
invoice_entry = InvoiceEntry.new(invoice_entry_params)
errors << invoice_entry.errors.messages.values if not invoice_entry.valid?
else
invoice_entry = InvoiceEntry.find_by_id(invoice_entry_params[:id])
if invoice_entry.nil?
errors << "Couldn't find invoice entry with id = #{invoice_entry_params[:id]}"
else
invoice_entry.assign_attributes(invoice_entry_params)
errors << invoice_entry.errors.messages.values if not invoice_entry.valid?
end
end
invoice_entries << invoice_entry
end
# Invoice
#invoice.assign_attributes(date: params[:date])
errors << #invoice.errors.messages.values if not #invoice.valid?
if errors.empty?
# Save everything
#invoice.invoice_entries = invoice_entries
#invoice.save
head :no_content
else
render json: errors.flatten, status: :unprocessable_entity
end
end
To change not only the association but also the attributes of the associated objects, you have to use accepts_nested_attributes_for:
class Invoice < ActiveRecord::Base
has_many :invoice_entries, :autosave => true, :dependent => :destroy
validates_presence_of :date
accepts_nested_attributes_for :invoice_entries, allow_destroy: true
end
There's a railscast episode 196 on how to build dynamic nested forms using nested_attributes.
Addendum:
accepts_nested_attributes_for expects attributes for the nested models in a nested hash, i.e.:
invoice_params={"date" => '2013-06-16',
"invoice_entries_attributes" => [
{"description" => "do A"},
{"description" => "do B"}]
}
invoice= Invoice.new(invoice_params)
invoice.save
the save saves invoice and two invoice_items.
Now
invoice=Invoice.find(1)
invoice_params={
"invoice_entries_attributes" => [
{"description" => "do A"},
{"description" => "do C"}]
}
invoice.update_attributes(invoice_params)
deletes the item do B and adds the item do C.
form_fields can be used to create forms that result in exaclty that kind of nested hashes.
For details see the railscast.
Try using accepts_nested_attributes_for. This would clean up a lot of your code! Here is a example:
class Invoice < ActiveRecord::Base
has_many :invoice_entries, :dependent => :destroy
validates_presence_of :date
attr_accessible :invoice_entries_attributes
accepts_nested_attributes_for :invoice_entries, :allow_destroy => true
end
In the view can you then use fields_for (simple_fields_for with simple form, and semantic_fields_for with formtastic if you use one of these gems).
<%= form_for #invoice do |invoice_form| %>
<%= invoice_form.fields_for :invoice_entries do |invoice_entry_form| %>
<%= invoice_entry_form.text_field :description %>
<%= invoice_entry_form.check_box :_destroy %>
<% end %>
<% end %>
In you controller can you now refactor down to the basics:
# PATCH/PUT /api/invoices/5
def update
#invoice = Invoice.find(params[:id])
if #invoice.update_attributes(params[:invoice]) # This also saves all associated invoice entries, and destroy all that is marked for destruction.
head :no_content
else
render json: #invoice.errors.flatten, status: :unprocessable_entity
end
end
You can read more about accepts_nested_attributes_for here: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Or you can watch this railscast about nested models: http://railscasts.com/episodes/196-nested-model-form-revised

How to save embedded classes in mongoid?

I am using Rails 3 with mongoid 2. I have a mongoid class forum, which embeds_many topics.
Topics embeds_many forumposts
When I try to save a forumpost doing the following in my controller...
#forum = Forum.find(params[:forum_id])
#forum.topics.find(params[:topic_id]).forumposts.build(:topic_id => params[:forumpost][:topic_id], :content => params[:forumpost][:content], :user_id => current_user.id,:posted_at => Time.now, :created_at => Time.now, :updated_at => Time.now)
if #forum.save
On save I get...
undefined method `each' for 2012-11-14 23:15:39 UTC:Time
Why am I getting that error?
My forumpost class is as follows...
class Forumpost
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Paranoia
field :content, type: String
field :topic_id, type: String
field :user_id, type: String
field :posted_at, type: DateTime
attr_accessible :content, :topic_id, :user_id, :posted_at, :created_at, :updated_at
validates :content, presence: true
validates :topic_id, presence: true
validates :user_id, presence: true
belongs_to :topic
belongs_to :user
end
There is alot wrong/wierd with your example code, so lets see if we can start at the start:
You say forum embeds many topics, which embeds many posts. But your model is using a belongs_to association. Belongs_to is used for references which are different than embedded documents. If your Topic model has this:
class Topic
...
embeds_many :forumposts
...
end
Then your Forumpost model should have this:
class Forumpost
...
embedded_in :topic
...
end
Read up on references vs embedded documents here: http://mongoid.org/en/mongoid/docs/relations.html
Next point, You don't need to put :topic_id into the forumpost since you are building it off the topic.
Next point, don't save the forum, save the forumpost. And instead of doing a build followed by a save, try just doing it as a create in one go.
Next point, instead of setting user_id => current_user.id, try setting user => current_user. This is the magic that the belongs_to association provides... its cleaner and avoids messing around with IDs.
Next point, why do you need both created_at (supplied by Mongoid::Timestamps) and posted_at ?
Last point, you shouldn't need to set the timestamps, they should be set automatically when created/updated (unless for some reason you actually need posted_at).
Try something more like this:
#forum = Forum.find(params[:forum_id])
#topic = #forum.topics.find(params[:topic_id])
if #topic.forumposts.create(:content => params[:forumpost][:content], :user => current_user)
#handle the success case
else
#handle the error case
end

Remove attributes in Mongoid before_save loop

I figured that instead of leaving certain attributes in parent and embedded documents nil/null (e.g. total of an order if no price exists), I better not save them at all. How can I remove attributes that are nil before saving?
# embedded order position for each order
class Orderitem
include Mongoid::Document
field :quantity, :type => Integer
field :unit_price, :type => Integer
field :total, :type => Integer
field :economical_potential, :type => Integer
embedded_in :order
belongs_to :supplier
belongs_to :item
before_save :remove_empty_fields
private
def remove_empty_fields
attributes.each do |attr_name, value|
if value.nil?
# don't save attribute
end
end
end
end
Why do you want to remove attributes from your model? In that case, I would add another model called unit and add :price as an attribute. Then add a function to Orderitem called def total_of_unit which will return the total based on the number of units and their price.
In code it would look like this:
class Orderitem
...
field :quantity, :type => Integer
# drop :unit_price
# drop :total
field :economical_potential, :type => Integer
...
has_many :units
...
def total
#total = 0
self.units.each do |unit|
#total = #total + unit.price
end
return #total
end
end
Unit would look like this:
class Unit
field :unit_price, :type => Integer
belongs_to :Orderitem
end
Mongoid supports #unset, so you can use something like this:
order_item.unset(:total)

Have user add "tags" while adding article with Mongoid & Ruby on Rails

Im using Ruby on Rails to allow a user to add a project post much like Stackoverflow. I can do this with the regular MySQL database, but I am unsure as to how it works with Mongoid.
This is how the process works:
User writes some details about the project (client, date, description)
Add tags like Stackoverflow, where they just simply need to add a space between each one.
Submit the post
Now in my model I try to break the tags up into an array (splitting where there is a space) and then saving the tags one after the other. However, the row for the Project and the Tag do not reference one another. The Project tag_ids = [] and the Tag project_ids = []
project.rb model
class Project
include Mongoid::Document
include Mongoid::MultiParameterAttributes
field :client, :type => String
field :description, :type => String
field :url, :type => String
field :project_date, :type => Date
has_and_belongs_to_many :tags
attr_accessor :tag_names
after_save :assign_tags
def tag_names
#tag_names || tags.map(&:name).join(" ")
end
def assign_tags
#project = self
#project_id = self.id
if #tag_names
self.tag_names = #tag_names.split(/\s+/).map do |name|
Tag.find_or_create_by(:name => name)
end
end
end
end
tag.rb model
class Tag
include Mongoid::Document
field :name, :type=> String
has_and_belongs_to_many :projects
end
Any ideas as to how to add these reference ids? Thanks!
I think you need to do this:
t = Tag.find_or_create_by(:name => name)
self.tags << t unless (self.tags.include? t)

Resources