Rails nested attributes validation force save parent firstly - ruby

I have 2 models like:
class Father < ActiveRecord::Base
has_many :children
accepts_nested_attributes_for :children
end
class Child < ActiveRecord::Base
belongs_to :father
validate :validate_money
def validate_money
children = Child.where(id: self.father.id)
sum_of_children_pocket_money = my_func # function for getting sum of all pocket money of all children
if sum_of_children_pocket_money > self.father.money
errors.add(:pocket_money, "My error message!!!")
end
end
end
and initially I have:
and when pass passengers_attributes to update customer (id=1) like
{
id: 1,
name: "Father 1",
money: 1000,
children: [
{
name: "Child 1",
id: 1,
pocket_money: 500
}
]
}
and then I am sending for update:
{
id: 1,
name: "Father 1",
money: 2000,
children: [
{
name: "Child 1",
id: 1,
pocket_money: 1500
}
]
}
So I am getting an error in my validation because it starts sum_of_children_pocket_money > self.father.money comparison and sum_of_children_pocket_money is equal to 1500 but self.father.money still 1000. How can I fix this?

Moving the validation logic from Child to Father should solve the problem.
class Father < ActiveRecord::Base
has_many :children
accepts_nested_attributes_for :children
validate :validate_money
def validate_money
children = Child.where(id: id)
sum_of_children_pocket_money = my_func # function for getting sum of all pocket money of all children
if sum_of_children_pocket_money > money
errors.add(:pocket_money, "My error message!!!")
end
end
end
class Child < ActiveRecord::Base
belongs_to :father
end

Related

Get a belongs_to relationship from class Ruby on Rails

Here are my models:
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :product
end
class Order < ApplicationRecord
has_many :order_items
end
Here is my controller:
def index
orders = Order.where(nil)
render json: {'orders': orders}, include:
['order_items'], status: :ok
end
I want to also include the product in the order_items. How can I achieve this to get the following JSON:
{
"id": 2,
"order_items": [
{
"id": 1,
"product": {
"name": "abc"
},
}
]
},
You can reach this with changing
include: ['order_items']
to
include: ['order_items', 'order_items.product'].
More details you can get here.
I have been able to solve this by changing include: ['order_items'] to include: {'order_items': {include: 'product'}}

How to fix an API using jsonapi-resources that is responding with mismatch type and not allowing me to create records?

I have a problem with Mismatch types in my API everytime I want to create a new item.
It happens in 2 places:
when I try to POST a new item
POST http://localhost:8060/datasets/
{
"data": {
"type": "datasets",
"attributes": {
"doi": "10.5259/2008120816KAKA",
"version": 0,
"is-active": "1",
"datacentre": 201
}
}
Response
"errors": [
{
"title": "Internal Server Error",
"detail": "Internal Server Error",
"code": "500",
"status": "500",
"meta": {
"exception": "Datacentre(#23863440) expected, got 201 which is an instance of Fixnum(#4211740)",
"backtrace": [
"/usr/local/rvm/gems/ruby-2.3.3/gems/activerecord-5.1.1/lib/active_record/associations/association.rb:239:in `raise_on_type_mismatch!'",
"/usr/local/rvm/gems/ruby-2.3.3/gems/activerecord-5.1.1/lib/active_record/associations/belongs_to_association.rb:11:in `replace'",
"/usr/local/rvm/gems/ruby-2.3.3/gems/activerecord-5.1.1/lib/active_record/associations/singular_association.rb:15:in `writer'",
"/usr/local/rvm/gems/ruby-2.3.3/gems/activerecord-5.1.1/lib/active_record/associations/builder/association.rb:119:in `datacentre='"
I can see it happening when accessing the relationships of an item. For example this is a a GET response:
GET http://localhost:8060/datasets/
{
"data": [
{
"id": "5",
"type": "datasets",
"links": {
"self": "http://localhost:8060/datasets/5"
},
"attributes": {
"created": "2011-03-02T16:41:20.000Z",
"doi": "10.5259/20070410230000/HTTP://www.STAFFSPASTTRACK.ORG.UK/EXHIBIT/CRIMEANDPUNISHMENT/IMAGEPAGE/ZERO.HTM",
"version": 0,
"is-active": "",
"updated": "2011-03-02T16:41:20.000Z",
"datacentre": "#<Datacentre:0x000000033389b8>",
"deposited": "2012-07-31T09:12:37.000Z"
},
"relationships": {
"datacentre": {
"links": {
"self": "http://localhost:8060/datasets/5/relationships/datacentre",
"related": "http://localhost:8060/datasets/5/datacentre"
}
}
}
}
You can see how the datacentre (foreign_key) attribute is presented as an object type of the Model Datacentre. If I try to access the relantship to that model http://localhost:8060/datasets/5/datacentre I get the following:
{
* errors: [
* {
* title: "Internal Server Error",
* detail: "Internal Server Error",
* code: "500",
* status: "500",
* meta: {
* exception: "Unknown source type #<Datacentre id: 10015, comments: "", contact_email: "andrew.jackson#bl.uk", contact_name: "Andy Jackson", created: "2010-12-14 22:05:32", doi_quota_allowed: 50000, doi_quota_used: 17, domains: "webarchive.org.uk", is_active: "\x01", name: "Web Archive Programme at BL", password: "98583c1bf114bfe80105f906d800e05325307f06b93ccee6b6...", role_name: "ROLE_DATACENTRE", symbol: "BL.WAP", updated: "2012-02-17 11:49:01", version: 2, allocator: 106, experiments: nil>",
* backtrace: [
* "/usr/local/rvm/gems/ruby-2.3.3/gems/jsonapi-resources-0.9.0/lib/jsonapi/resource_serializer.rb:267:in `top_level_source_key'",
* "/usr/local/rvm/gems/ruby-2.3.3/gems/jsonapi-resources-0.9.0/lib/jsonapi/resource_serializer.rb:47:in `block in serialize_to_hash'",
* "/usr/local/rvm/gems/ruby-2.3.3/gems/jsonapi-resources-0.9.0/lib/jsonapi/resource_serializer.rb:47:in `map'",
* "/usr/local/rvm/gems/ruby-2.3.3/gems/jsonapi-resources-0.9.0/lib/jsonapi/resource_serializer.rb:47:in `serialize_to_hash'",
* "/usr/local/rvm/gems/ruby-2.3.3/gems/jsonapi-resources-0.9.0/lib/jsonapi/response_document.rb:109:in `results_to_hash'",
* "/usr/local/rvm/gems/ruby-2.3.3/gems/jsonapi-resources-0.9.0/lib/jsonapi/response_document.rb:12:in `contents’”,
I think the problem is that the API is expecting the wrong type of object. It’s expecting Datacentre when it should be expecting DatacentreResource.
My setup is as follows:
I have a Legacy database that doesn’t follow the ActiveRecord conventions for naming tables and foreign_keys.
Tables are singular and foreign_keys do not have the _id suffix.
The tables/models in which I have the problem have one_to_many relationship.
The relationship being a datacentre has_many datasets.
I am using jsonapi-resources and rails 5 api-only.
Datacentre Model
class Datacentre < ApplicationRecord
self.table_name = "datacentre"
alias_attribute :allocator_id, :allocator
has_and_belongs_to_many :prefixes, class_name: 'Prefix', join_table: "datacentre_prefixes", foreign_key: :prefixes, association_foreign_key: :datacentre
belongs_to :allocator, class_name: 'Allocator', foreign_key: :allocator
has_many :datasets
end
Dataset Model
class Dataset < ApplicationRecord
self.table_name = "dataset"
alias_attribute :datacentre_id, :datacentre
belongs_to :datacentre, class_name: 'Datacentre', foreign_key: :datacentre
end
Datacentre Resource
class DatacentreResource < JSONAPI::Resource
model_name 'Datacentre'
model_hint model: Datacentre
attributes :comments, :contact_email, :contact_name, :created, :doi_quota_allowed, :doi_quota_used, :domains, :is_active, :name, :password, :role_name, :symbol,
:updated, :version, :experiments, :allocator
has_many :datasets
has_many :prefixes
has_one :allocator, class_name: 'Allocator', foreign_key: :allocator
end
Dataset Resource
class DatasetResource < JSONAPI::Resource
model_name 'Dataset'
model_hint model: Dataset
attributes :created, :doi, :version, :is_active, :updated, :datacentre
attribute :deposited
has_one :datacentre, class_name: "Datacentre", foreign_key: :datacentre
end
So far I have got around the first problem (i.e. accessing the relationship) by modifying the methods for datacentre and its alias datacentre_id in the Dataset Resource
def datacentre(context=nil)
DatacentreResource.find_by_key(#model.datacentre.id)
end
def datacentre_id()
#model.datacentre.id
end
But this doesn’t solve the POST problem.
I think this is the thing that cause the error
in
class Dataset < ApplicationRecord
self.table_name = "dataset"
alias_attribute :datacentre_id, :datacentre
belongs_to :datacentre, class_name: 'Datacentre', foreign_key: :datacentre
end
datacentre_id and datacentre are aliased so it gives back and object datacentre back in response instead of datacentre_id
probably it will wokr if you remove this line

Rails New Field Not Appear

I just got an issue after I've done a migration for a table, I've added owner field to the project table, but it is not appear when I request it (project).
Expected response after migration:
{
projects: [
{ id: 1, name: "First proj", owner: 1 },
{ id: 2, name: "Second proj", owner: 1 }
]
}
I got:
{
projects: [
{ id: 1, name: "First proj" },
{ id: 2, name: "Second proj" }
]
}
Here is my migration file
class AddOwnerRefToProjects < ActiveRecord::Migration
def change
add_reference :projects, :owner, index: true
end
end
My projects model
class Project < ActiveRecord::Base
belongs_to :owner, :class_name => :User
end
You need to add the new attribute in your json serializer.

Saving nested JSON object to database

Lets say I have a model.
Passengers belongs to Flights.
Flights belongs to Trips
class Trip < ActiveRecord::Base
has_many :flights, :dependent => :destroy, :order => "order_num ASC"
end
class Flight < ActiveRecord::Base
belongs_to :trip, touch: true
has_many :passengers, :dependent => :destroy
end
class Passenger < ActiveRecord::Base
belongs_to :flight, touch: true
end
And I'm getting this sent back to the rails app. (when the user calls save).
*The top level is the trip
{
name: 'Hello Trip',
date: '2013-08-12',
flights: [
{
id: 1
depart_airport: 'RDU',
arrive_airport: 'RDU',
passengers: [
{
user_id: 1,
request: true
}
]
},
{
depart_airport: 'RDU',
arrive_airport: 'RDU',
passengers: [
{
user_id: 1,
request: true
},
{
user_id: 2
request:true
}
]
}
]
}
Right now I'm getting the saved json in and manually looping through the flights to see if there is an id. If there is i'm updating it. If not I'm creating a new one. Then adding the passengers.
I'm wondering if there is an automatic format that Rails takes that can do all the saving for me. I know when you submit a nested form it creates a similar pattern, and adds a _destroy property and the id is a timestamp if it's just created. Would the JSON saving be similar to that?
Thanks for any help!
Yes, you should be able to use accepts_nested_attributes_for here.
You'll need to enable accepts_nested_attributes_for on your models, e.g.,
class Trip < ActiveRecord::Base
has_many :flights, :dependent => :destroy, :order => "order_num ASC"
accepts_nested_attributes_for :flights, :allow_destroy => true
attr_accessible :flights_attributes
end
You'll also need to ensure that your JSON response uses keys that Rails will recognize. You can either modify the JSON response or do something like:
response = JSON.parse(json_string)
response[:flights_attributes] = response.delete(:flights)
# ...
Then you can just do
Trip.create(response)
You'll want to ensure that everything is created/updated as expected. For more on accepts_nested_attributes_for, see the documentation: http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for.
I think accepts_nested_attributes_for is convenient, but note that there are some that think it should be deprecated (e.g., here: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/, see here for a response: https://stackoverflow.com/a/17639029/1614607).

elastic search object association querying through params

I'm having some difficulty with Elastic Search and Tire not returning any results. I'm using Ruby 1.9.3 and Rails 3.2.11.
In my controller I'm calling:
#location_id = 1
#listings = Listing.search(params.merge!(location_id: #location_id))
In my listing model I have
mapping do
indexes :id, type: 'integer'
...
indexes :author do
indexes :location_id, :type => 'integer', :index => :not_analyzed
...
end
def self.search(params={})
tire.search(load: true, page: params[:page], per_page: 20) do |search|
search.query { string params[:query], :default_operator => "AND" } if params[:query].present?
search.filter :range, posted_at: {lte: DateTime.now}
search.filter :term, "author.location_id" => params[:location_id]
end
I have 300 results which all have the location_id of 1 in the database so I can't seem to figure out why it's returning a nil set? If I comment out the author.location_id search filter line it returns all other results as expected?
There are several things which needs to be adressed in a situation like yours. Let's start with a fully working code:
require 'active_record'
require 'tire'
require 'logger'
# Tire.configure { logger STDERR }
# ActiveRecord::Base.logger = Logger.new(STDERR)
Tire.index('articles').delete
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
ActiveRecord::Schema.define(version: 1) do
create_table :articles do |t|
t.string :title
t.integer :author_id
t.date :posted_at
t.timestamps
end
create_table :authors do |t|
t.string :name
t.integer :number, :location_id
t.timestamps
end
add_index(:articles, :author_id)
add_index(:authors, :location_id)
end
class Article < ActiveRecord::Base
belongs_to :author, touch: true
self.include_root_in_json = false
include Tire::Model::Search
include Tire::Model::Callbacks
mapping do
indexes :title
indexes :author do
indexes :location_id, type: 'integer'
end
end
def self.search(params={})
tire.search load: {include: 'author'} do |search|
search.query do |query|
query.filtered do |f|
f.query { params[:query].present? ? match([:title], params[:query], operator: 'and') : match_all }
f.filter :range, 'posted_at' => { lte: DateTime.now }
f.filter :term, 'author.location_id' => params[:location_id]
end
end
end
end
def to_indexed_json
to_json( only: ['title', 'posted_at'], include: { author: { only: [:location_id] } } )
end
end
class Author < ActiveRecord::Base
has_many :articles
after_touch do
articles.each { |a| a.tire.update_index }
end
end
# -----
Author.create id: 1, name: 'John', location_id: 1
Author.create id: 2, name: 'Mary', location_id: 1
Author.create id: 3, name: 'Abby', location_id: 2
Article.create title: 'Test A', author: Author.find(1), posted_at: 2.days.ago
Article.create title: 'Test B', author: Author.find(2), posted_at: 1.day.ago
Article.create title: 'Test C', author: Author.find(3), posted_at: 1.day.ago
Article.create title: 'Test D', author: Author.find(3), posted_at: 1.day.from_now
Article.index.refresh
# -----
articles = Article.search query: 'test', location_id: 1
puts "", "Documents with location:1", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
articles = Article.search query: 'test', location_id: 2
puts "", "Documents with location:2", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
puts "(NOTE: 'D' is missing, because is not yet posted)"
articles = Article.search query: 'test b', location_id: 1
puts "", "Documents with query:B and location:1", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
First, it's usually a good idea to create an isolated, extracted case like this.
In your example code, I assume you have a relationship Listing belongs_to :author. You need to properly define the mapping and serialization, which I again assume you did.
As for the query itself:
Unless you're using faceted navigation, use the filtered query, not top level filters, as in my example code.
Do not use the string query, unless you really want to expose all the power (and fragility!) of the Lucene query string query to your users.
Use the match query, as your "generic purpose" query -- Tire sprinkles some sugar on top of it, allowing to easily create multi_match queries, etc
The filter syntax in your example is correct. When the filter method is called multiple times in Tire, it creates and and filter.
Uncomment the Tire logging configuration (and possibly also the ActiveRecord logging), to see what the code is doing.

Resources