ActiveModel::Errors when mass updating - ruby-on-rails

On a single instance, validation would produce something like:
foo = Foo.new(price: -2)
foo.valid?
foo.errors
=> #<ActiveModel::Errors:0x00007fc66e670430
#base=#<Foo:0x00007fc6503f8658 id: nil, price: nil,
#details={:price=>[{:error=>:greater_than_or_equal_to, :value=>-0.2e1, :count=>0}]},
#messages={:price=>["must be greater than or equal to 0"]}>
Is there a rails way of obtaining the errors when using the update method?:
Foo.update([1, 2, 3], [{ price: 10 }, { price: -20 }, { price: 3 }])
Thank you!

Here is an example how to gather errors from the Model.update(...) method:
# first create a payload with ids and attributes
payload = { 1 => { price: 10 }, 2 => { price: -20 } }
# next update records
result = Foo.update(payload.keys, payload.values)
# the update method returns processed records
# in case of array it will return array of records
# iterate over all objects and find invalid
with_errors = result.map { |r| !r.errors.any? ? nil : r }.compact
# after compact the with_errors variable contains only invalid records.

What I did in the end:
class FooUpdateService
attr_reader :errors
def update(ids, values)
self.errors = ActiveModel::Errors.new(Foo.new)
Foo.update(ids, values).select(&:invalid?).each { |invalid_foo| self.errors.merge!(invalid_foo.errors) }
end
private
attr_writer :errors
end

Related

Rails group and sum array of objects

A production has_many :production_lines,
production_line belongs_to :item,
item has_one :recipe,
recipe has_many :recipe_lines,
recipe_line belongs_to :item,
production_line and recipe line have attribute quantity. I need to group recipe_lines for a production by item, with quantity that equals to production_line.quantity * recipe_line.quantity
def item_quantities
array = production_lines.map do |p|
p.item.recipe.recipe_lines.map do |r|
{
item_id: r.item_id,
item_name: r.item.name,
quantity: r.quantity * p.quantity
}
end
end
array.flatten(1).group_by { |p| p[:item_id] }
.transform_values { |vals| vals.sum { |val| val[:quantity] } }
end
This returns:
item_quantities = {
1: 10,
2: 5
}
where key is item_id and value is quantity. Values are correct.
However I would like to return:
item_quantities = [
{
id: 1,
name: "Tomato",
quantity: 10,
},
{
id: 2,
name: "Carrot",
quantity: 5
}
]
How should I change my solution to achieve that?
First of all, your nested map followed by flatten(1) can be simplified by making the first map into flat_map. If you do this you could remove the flatten(1).
From this point your code is most of the way there, but you could make the following changes to get the desired output:
you can group by multiple attributes, name and id. In another language you might use a tuple for this. Ruby doesn't have tuples, so we can just use a len-2 array:
.group_by { |p| [p[:item_id], p[:item_name]] }
.transform_values { |vals| vals.sum { |val| val[:quantity] } }
At this point you have a hash mapping [id,name] tuple to quantity:
{ [1,"foo"] => 123, [2, "bar"] => 456 }
and you can coerce this to the desired data type using reduce (or each_with_object, if you prefer):
.reduce([]) do |memo, ((id, name), quantity)|
memo << {
id: id,
name: name,
quantity: quantity
}
end
The wierd looking ((id, name), quantity) is a kind of destructuring. See https://jsarbada.wordpress.com/2019/02/05/destructuring-with-ruby/ specifically the sections on "Destructuring Block Arguments" and "Destructuring Hashes".

convert an array of objects (created from OpenStruct) into an array of values

I am learning ruby and I am playing with a sample data. I converted following hash into an array of objects as follows.
class Openstruct
require 'JSON'
require 'ostruct'
HASH = {
items: [
{
health: [
{
goal: [
{
activity: [
{
id: "1A"
},
{
id: "2A"
}
],
id: "GA"
}
],
activity: [
{
id: "1B"
},
{
id: "2B"
}
],
id: "GB"
}
],
goal: [
{
activity: [
{
id: "1C"
},
{
id: "2C"
},
],
id: "3c"
}
],
createdAt: "2018-01-01",
updatedAt: "2018-01-01",
id: "DA"
}
],
}
def self.all
json = HASH.to_json
JSON.parse(json, object_class: OpenStruct)
end
end
Above returns me following result
#<OpenStruct items=
[#<OpenStruct health=
[#<OpenStruct goal=
[#<OpenStruct activity=
[#<OpenStruct id="1A">, #<OpenStruct id="2A">], id="GA">],
activity=[#<OpenStruct id="1B">, #<OpenStruct id="2B">], id="GB">],
goal=[#<OpenStruct activity=[#<OpenStruct id="1C">, #<OpenStruct id="2C">], id="3c">],
createdAt="2018-01-01",
updatedAt="2018-01-01",
id="DA">]>
However, i want to convert the array of object having id's into array of values of ids. e.g [#<OpenStruct id="1A">, #<OpenStruct id="2A">] --> ["1A", "2A"]. so i want the final result as follows:
#<OpenStruct items=
[#<OpenStruct health=
[#<OpenStruct goal=
[#<OpenStruct activity=
["1A","2A"], id="GA">],
activity=["1B", 2B"], id="GB">],
goal=[#<OpenStruct activity=["1C","2C"], id="3c">],
createdAt="2018-01-01",
updatedAt="2018-01-01",
id="DA">]>
Does anyone know how to do that?
You will have to check the child node recursively until you find the OpenStruct with only :id element. Following is the working code for your sample data.
def self.convert_struct_id(os)
# Get possible attributes of any open_struct
attributes = os.to_h.keys
# Only get id_value if :id is the only attribute of open_struct
if attributes.length == 1 && attributes.first == :id
id_value = os.send(:id)
return id_value
end
# Iterate through attributes
attributes.each do |attr|
# Get child elements
data = os.send(attr)
case data
when OpenStruct
convert_struct_id(data)
when Array
# Recursively process for child node
data.map! { |d| convert_struct_id(d) }
end
end
return os
end
Your self.all method will look like this
def self.all
json = HASH.to_json
os = JSON.parse(json, object_class: OpenStruct)
res = convert_struct_id(os)
end
Result:
=> #<OpenStruct items=[#<OpenStruct health=[#<OpenStruct goal=[#<OpenStruct activity=["1A", "2A"], id="GA">], activity=["1B", "2B"], id="GB">], goal=[#<OpenStruct activity=["1C", "2C"], id="3c">], createdAt="2018-01-01", updatedAt="2018-01-01", id="DA">]>

How to chaange my as_json method?

Now i have used the as_json method like this in my model
def as_json(options = {})
{
id: id,
diary_id: diary_id,
title: title,
post_date_gmt: date,
post_content: strip_tags(content),
smiley_id: smiley_id,
author_id: user_id,
author_name: user.display_name,
attachments: filter_attachments(options[:version]),
root_comments: format_comments(nested_comments.arrange(:order => :created_at)),
post_readings: post_readings.size,
is_read: read_by(options[:current_user])
}
end
I need to change this structure a bit as follows, Actually i want group this array by the date.
{
date_01: {
[post1], [post2], [post3]
},
date_02: {
[post1], [post2], [post3]
}
}
What should I do ?
I fixed the issue as follows
post_dates = (no_of_days.days.ago.to_date..(date_as_string.to_date + no_of_days.days)).map{ |date| date.strftime("%Y-%m-%d") }
# Arrange posts details under each date
i = 0
post_dates.each do |post_date|
posts_grouped_by_date[i] = {:post_date => post_date, :posts_for_date => diary.posts_for_date(Date.parse(post_date) )}
i = i + 1
end
render json: posts_grouped_by_date.sort_by {|hash| hash['post_date']}.as_json(current_user: current_user)
replace the values of data keys to an array of arrays. like below.
{
date_01: [
[post1], [post2], [post3]
],
date_02: [
[post1], [post2], [post3]
]
}

handling a webservice responce with ror

I am collecting a response from a web service in a Rails' app with
response = #api.sql_data
This causes response to equal the following.
response
#=> { "success"=>true, "totalcount"=>10, "rows"=>[
# { "ID"=>"0001", "CODE"=>"0000001", "CODE_1"=>"Alpha",
# "NAME"=>"Alpha", "Balance"=>"0" },
# { "ID"=>"0002", "CODE"=>"0000002", "CODE_1"=>"Beta",
# "NAME"=>"Beta", "Balance"=>"0" },
# { "ID"=>"0003", "CODE"=>"0000003", "CODE_1"=>"Charlie",
# "NAME"=>"Charlie", "Balance"=>"0"},
# ...
# ]
# }
I have created the following method:
def format_response(response)
response['rows'].map do |row|
{ id: row[0],
code: row[1],
code1: row[2],
balance: row[4] }
end.uniq { |x| x[:code1] }
end
When I execute this method for the above value of response, I obtain the following.
format_response(response)
#=> [{:id=>nil, :code=>nil, :code1=>nil, :balance=>nil}]
This is incorrect. I would like this expression to return the following.
{"ID"=>"0001", "CODE"=>"0000001", "CODE_1"=>"Alpha", "NAME"=>"Alpha", "Balance"=>"0"}
What is my mistake?
response['rows'] contains an array of hashes. Hashes in ruby are not associative arrays so you cannot get the first key by hash[0].
def format_response(response)
response['rows'].map do |row|
{
id: row["ID"],
code: row["CODE"],
code_1: row["CODE_1"],
balance: row["BALANCE"]
}
end.uniq { |x| x[:code1] }
end
Each row is a hash so you need to access its elements by name not by index (as you would with an array). The following should work:
def format_response(response)
formatted_response = response['rows'].map do |row|
{ id: row['ID'],
code: row['CODE'],
code1: row['CODE_1'],
balance: row['balance'] }
end
formatted_response.uniq { |x| x[:code1] }
end

Passing hashes into create action in Ruby on Rails

I'm trying to create a workout routine that gets created when a workout gets created by passing this in via ajax:
Parameters: {"utf8"=>"✓", "workout"=>{"name"=>"cool workout", "desc"=>"super cool"}, "exerciseorder"=>["4", "2"], "repssets"=>{"0"=>["40", "4"], "1"=>["60", "6"]}}
Here is what my Create action looks like in my Workout Controller:
exercise_order = params[:exerciseorder]
repssets = params[:repssets]
#workout = Workout.new(workout_params)
if #workout.save
WorkoutRoutine.create(
[
exercise_order.each_with_index.map { |x,i|
{
:exercise_id => x,
:position => i,
:workout_id => #workout.id
}
},
repssets.map { |x| x.last }.each { |y|
{
:reps => y.first,
:sets => y.last
}
}
]
)
render :nothing => true
else
render json: #workout.errors.full_messages, status: :unprocessable_entity
end
If I use an opening and closing '[]' within the WorkoutRoutine.create, it tells me:
ArgumentError: When assigning attributes, you must pass a hash as an argument.
And when I change them to '{}' it tells me:
syntax error, unexpected ',', expecting =>
I've tried a myriad of different combinations and work-arounds but can't seem to figure out why it won't correctly parse the data and save it to the database, any help is very appreciated.
EDIT:
When I remove the initial {} and [] from the WorkoutRoutine.create:
WorkoutRoutine.create(
exercise_order.each_with_index.map { |x,i|
{
:exercise_id => x,
:position => i,
:workout_id => 20
}
},
repssets.map { |x| x.last }.each { |y|
{
:reps => y.first,
:sets => y.last
}
}
)
I get this error message:
ArgumentError: wrong number of arguments (2 for 0..1)
Edit2:
This is the jQuery code that sents to the data field via ajax:
var getId = $(".exercise-list").sortable('toArray');
ary = []
$(getId).each(function () {
id = $(this[0]).selector;
var reps = $("#reps" + id).val();
var sets = $("#sets" + id).val();
ary.push([reps, sets])
});
var orderRepsSets = { exerciseorder: getId, repssets: ary }
var formData = $('#new_workout').serialize();
var data = formData + '&' + $.param(orderRepsSets);
$.ajax({
url: $("#new_workout").attr('action'),
method: 'POST',
data: data,
success: (function() {
....
});
Did I get it correctly that you want to create multiple WorkloadRoutine objects, one for each exercise with the corresponding repetitions, the position, etc. If yes, then in order to do that you will have to pass an array of hashes (one hash for each object) to the WorkoutRoutine.create() function. As engineersmnky correctly stated in his comment, the data structure you are currently passing is more like [[{...},{...},{...}],[{...},{...},{...}]], but instead it should be just [{...},{...},...]. Do achieve that, something like this should do the trick:
WorkoutRoutine.create(
exercise_order.map.with_index { |x,i|
{
:exercise_id => x,
:position => i,
:workout_id => #workout.id,
:reps => repssets[i.to_s].first,
:sets => repssets[i.to_s].last
}
}
)
If you could change repssets to an array like exercise_order you could even remove the string cast for getting the reps and sets, which would simplify the whole think even more.
If it comes for errors they are quite self explanatory. But let's start from beginning..
I assume that WorkoutRoutine is an ActiveRecord::Base model. The WorkoutRoutine.create method gets 0 or 1 argument which should be a Hash or a block.
In the first iteration you were passing an Array instead of Hash, so it looked like:
WorkoutRoutine.create([a, b]) # => ArgumentError: When assigning attributes, you must pass a hash as an argument.
On the second iteration you stripped away the square brackets, what gave you 2 arguments instead of one Hash:
WorkoutRoutine.create(a, b) # => ArgumentError: wrong number of arguments (2 for 0..1)
If you read errors carefully you will start getting the idea what's happening.
About the workout routine itself
From what you specified I would assume that you want something like:
Workout has many Routines
Routine belongs to Workout and Exercise
Routine is composed of fields like
position/order,
number of repetitions,
number of sets
If my assumption is correct, then you want to use nested_attributes and then have parameters and controller like
# given parameters as workout_params
{
workout: {
name: "workout name",
desc: "workout description",
routines_attributes: [
{ position: 1, exercise_id: 4, repetitions_number: 40, sets_number: 4 },
{ position: 2, exercise_id: 2, repetitions_number: 60, sets_number: 6 }
]
}
}
# Controller
def create
#workout = Workout.new workout_params
if #workout.save
redirect_to # ...
else
render :new
end
end
private
def workout_params
params.require(:workout).permit(:name, :desc, routines_attributes: [:position, :exercise_id, :repetitions_number, :sets_number])
end
It should be strait forward how to then create a view with fields_for and update models with proper associations

Resources