I am getting the following error when trying to update a Rails model via the ActiveModel update method:
ActiveModel::ForbiddenAttributesError
I am aware of the strong parameters requirement in Rails 4 per the link below, but how do I whitelist the params in my case - an array of hashes? I cannot make sense of the documentation.
http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters
Here are the json params that I am trying to process:
{
id: 1,
month: 'April',
measurements: [
{ id: 1, name: 'PT', location_1: '1.1', location_2: '1.2' },
{ id: 1, name: 'OT', location_1: '1.1', location_2: '1.2' },
.
.
]
}
Controller action:
def update
#Trying to update all measurements associated with this parent object
#params.permit(measurements: [{ :name, :location_1, :location_2 } ])
#This attempt causes a syntax error
measurements = params[:measurements]
measurements.each do |measurement|
current_measurement = Measurement.find(measurement[:id])
new_measurement = measurement.except(:id)
current_measurement.update(new_measurement)
end
.
.
end
to whitelist an array of attributes you'd code it this way...
params.permit(measurements: [ :name, :location_1, :location_2 ])
Related
I am working on rails 6 with ruby-2.6.5 and i am working on the API. I am using nested attributes for my order as follows:-
orders_controller.rb
# frozen_string_literal: true
module Api
module V1
class OrdersController < Api::V1::ApiApplicationController
before_action :validate_token
def create
debugger
order = OrderInteractor.create(order_params, #user_id)
if order.save
render json: { 'message' => 'Order Placed' }, status: :ok
else
render_errors(order)
end
end
private
def order_params
params.require(:data)
.require(:attributes)
.require(:order)
.permit(:user_id, :address_id, :total_price, :payment_status,
:order_number, :delivery_time_slot,
order_details_attributes:
%i[price quantity order_detail_status product_id
order_number variant_id],
payment_details_attributes:
%i[payment_data payment_id])
end
end
end
end
Api Request:-
{
"data": {
"attributes": {
"order": {
"address_id": "82",
"delivery_time_slot": "5:00 PM - 8:00 PM(Today)",
"order_details_attributes": [{
"price": "76.0",
"product_id": "46",
"quantity": "4",
"variant_id": "47"
}, {
"price": "9.9",
"product_id": "30",
"quantity": "1",
"variant_id": "29"
}],
"payment_details_attributes": [{
"payment_data": {
"data": {
"nameValuePairs": {
"razorpay_payment_id": "pay_HiHceX2p6450Wa",
"org_logo": "",
"org_name": "Razorpay Software Private Ltd",
"checkout_logo": "https://cdn.razorpay.com/logo.png",
"custom_branding": false
}
},
"paymentId": "pay_HiHceX2p6450Wa",
"userContact": "+916494949494",
"userEmail": "dailyferia#gmail.com"
}
}],
"total_price": "354"
}
},
"type": "orders"
}
}
While placing order i am getting the error Unpermitted parameter: :payment_data but it's working fine for the order_details. Please help me to fix it? I also tried the below ways to fix it but nothing worked:-
payment_details_attributes: %i[:payment_data payment_id]) and `payment_details_attributes: ['payment_data', 'payment_id'])`
Your payment_data is a complex object, rather than the scalars that are found in your order_details_attributes
You will need to add more to the permitted parameters, I believe the simplest solution would be:
payment_details_attributes: [payment_data: {}]
This should accept all parameters under payment_details_attributes, but it would also permit any other keys as well. You may want to be more strict and only allow the parameters specified above, in which case you could do:
payment_details_attributes: [
payment_data: {
data: {
nameValuePairs:
%i[razorpay_payment_id org_logo org_name checkout_logo custom_branding]
},
:paymentId, :userContact, :userEmail
}
]
which should restrict the parameters to just the format used in your example.
A few other notes:
You have %i[payment_data payment_id] in your original sample, but there is no payment_id in your payload. The attribute in the sample is paymentId, and on top of that, it is an attribute of the payment_data, not the payment_details_attributes
you wouldn't use %i and a colon, the %i is a shorthand for creating an array of ruby symbols, so %i[:payment_data payment_id] would create the array [:":payment_data", :payment_id] (note the extra colon at the beginning of payment_data)
Lastly, I haven't tested my code above, so there could be a syntax or other error, but hopefully this points you in the right direction.
This is my query method in model file:
def self.sum_by_brand_category
result = Product.joins(:brand, :category)
.select("brands.id as brand_id, categories.id as category_id, sum(products.quantity) as count")
.group("brands.id, categories.id")
return result
end
Here is the sample database query result I get:
[
{
"id":null,
"brand_id":43,
"category_id":1,
"count":2
},
{
"id":null,
"brand_id":43,
"category_id":2,
"count":5
},
{
"id":null,
"brand_id":43,
"category_id":3,
"count":4
},
....
]
I would expect the final JSON result to be used in views should be like this:
[
{
"id":null,
"brand_id":43,
"quantity": [
{
"category_id": 1,
"count": 2
},
{
"category_id": 2,
"count": 5
},
{
"category_id": 3,
"count": 4
}
]
},
....
]
How can I achieve it? Change the model method? Rebuild the result in the controller before sending it to the view? and how?
Any suggestions will be appreciated. Thank you.
Updated:
Based on #cmrichards 's answer, I come up with this private method to be called in controller and then used in views. I am including my work here, although these are not so DRY codes:
private
def get_sum_by_brand_category
query_results = Product.sum_by_brand_category
results = []
query_results.group_by(&:brand_id).each do |brand_id, query_result|
result = {}
result[:id] = nil
result[:brand_id] = brand_id
quantity_array = []
query_result.each do |data|
quantity_block = {}
quantity_block[:category_id] = data.category_id
quantity_block[:count] = data.count
quantity_array.push(quantity_block)
end
result[:quantity] = quantity_array
results.push(result)
end
return results
end
Please DRYing them out if you'd like to, by editing my question. ;)
One option is to use jBuilder, a popular gem maintained by the Rails team that provides a simple DSL (domain-specific language) allowing you to define JSON structures in your views.
https://github.com/rails/jbuilder
Using your existing method you could do it like this
controller
def index
#data = Product.sum_by_brand_category
end
views/products/index.json.jbuilder
json.array! #data.group_by(&:brand_id) do |grouped|
brand_id, category_data = grouped
json.id nil
json.brand_id brand_id
json.quantity category_data, :category_id, :count
end
I know I can skip validations for an individual save, like this:
User.new(name: 'John').save(validate: false)
But how can I do that when saving multiple objects at once? Like this:
Category.create([
{ name: 'Apps' },
{ name: 'Songs' },
{ name: 'Movies' }
])
I found this gem: https://github.com/zdennis/activerecord-import
It works like this:
categories = [
Category.new(name: 'Apps'),
Category.new(name: 'Songs'),
Category.new(name: 'Movies')
]
Category.import(categories, validate: false)
It is also possible to use plain arrays instead of ActiveRecord objects.
I guess it generates pure SQL when validate is set to false so it can skip validations.
You can't do that with create. If you really must skip validations you can do something like this:
[
{ name: 'Apps' },
{ name: 'Songs' },
{ name: 'Movies' }
].each do |attributes|
c = Category.new(attributes)
s.save(validate: false)
end
I have the following code which is expected to return complex JSON containing data selected from DB by multiple parameters and includes data from other models:
def search
params[:beacon] = [{ 'id' => 1, 'prox_uuid' => 12345453453 }, { 'id' => 2, 'prox_uuid' => 5345634564536435 }]
beacons = Beacon.includes(:ads, :venue).where("id in (?) and proximity_uuid in (?)", params[:beacon][:ids], params[:beacon][:prox_uuids])
data = beacons.map { |beacon| {
id: beacon.id,
name: beacon.name,
:venue => {
id: beacon.venue.id,
name: beacon.venue.name,
logo: URI.join(request.base_url, beacon.venue.logo.url).to_s
},
ads: beacon.ads.inject([]) do |sum, add|
sum << {
id: add.id,
title: add.name,
cover: URI.join(request.base_url, add.file_url.url).to_s,
price: add.price,
description: add.description
}
end
}
}
render json: data.to_json
end
params[:beacon] = [{ 'id' => 1, 'prox_uuid' => 12345453453 }, { 'id' => 2, 'prox_uuid' => 5345634564536435 }] is a placeholder for array of parameters coming from POST request.
The problem is that I receive the following error at the query line:
no implicit conversion of Symbol into Integer
I also tried going through the array of parameters in the loop and querying each record however json generates only the last one.
You have params[:beacon][:ids] and params[:beacon][:prox_uuids] but params[:beacon] is an array, and so should be indexed by integer, eg. params[:beacon][0]. So the error is telling you that :ids (a Symbol) cannot be implicitly converted to an Integer in order to correctly reference into the params[:beacon] array.
I have mongodb document like this and want to remove one element form unpublished array
which is in resource_well
{
"_id": ObjectId("4fa77c808d0a6c287d00000a"),
"production_status": "Unscheduled",
"projected_air_date": null,
"published": false,
"resource_well": {
"published": [
],
"unpublished": {
"0": ObjectId("4fa795038d0a6c327e00000e"),
"1": ObjectId("4fa795318d0a6c327e00000f"),
"2": ObjectId("4fa795508d0a6c327e000011"),
"3": ObjectId("4fa796f48d0a6c327e000013")
}
},
"segment_status": "Draft",
}
Code in controller
segment = Segment.find(params[:segment_id])
# segment.resource_well[params['resource_well_type']].class => Array
# segment.resource_well[params['resource_well_type']].inspect => [BSON::ObjectId('4fa795038d0a6c327e00000e'), BSON::ObjectId('4fa795318d0a6c327e00000f'), BSON::ObjectId('4fa795508d0a6c327e000011'), BSON::ObjectId('4fa796f48d0a6c327e000013')]
segment.resource_well[params['resource_well_type']].delete(params[:asset_ids])
segment.save
Not able remove an element form array
Your ODM (or ORM) provides associations for you so that you can take advantage of having the ODM manage the links for you. So you should use the associations, otherwise you risk hanging yourself. Just make sure to specify the relationship correctly, and use the generated method named by the association and its methods, e.g. resource_well.unpublished <<, resource_well.unpublished.delete. The following models and tests work for me. The inconvenience is that the delete method on the association takes an object, e.g., other.delete(object) and not a string or conditions, so if you start with a string, you have to supply an object in order to delete it. Note that other.delete_all(conditions) or other.where(conditions).delete_all remove both actual documents as well as the association, which is not what you were looking for. Anyway, hope that this helps.
Models
class Segment
include Mongoid::Document
field :production_status, type: String
field :projected_air_date, type: Date
field :published, type: Boolean
field :segment_status, type: String
embeds_one :resource_well
end
class ResourceWell
include Mongoid::Document
embedded_in :segment
has_and_belongs_to_many :published, :class_name => 'Resource'
has_and_belongs_to_many :unpublished, :class_name => 'Resource'
end
class Resource
include Mongoid::Document
field :name, type: String
end
Test
require 'test_helper'
class Object
def to_pretty_json
JSON.pretty_generate(JSON.parse(self.to_json))
end
end
class SegmentTest < ActiveSupport::TestCase
def setup
Segment.delete_all
end
test 'resource_well unpublished delete' do
res = (0..3).collect{|i| Resource.create(name: "resource #{i}")}
seg = Segment.create(
production_status: 'Unscheduled',
projected_air_date: nil,
published: false,
resource_well: ResourceWell.new(unpublished: res[0..2]),
segment_status: 'Draft')
seg.resource_well.unpublished << res[3] #just an append example
puts seg.to_pretty_json
id = res[0]['_id'].to_s
puts "id: #{id}"
resource_obj = Resource.find(id)
puts "resource: #{resource_obj.inspect}"
Rails.logger.debug("delete: #{resource_obj.inspect}")
seg.resource_well.unpublished.delete(resource_obj)
puts Segment.find(:all).to_pretty_json
end
end
Result
# Running tests:
{
"_id": "4fa839197f11ba80a9000006",
"production_status": "Unscheduled",
"projected_air_date": null,
"published": false,
"resource_well": {
"_id": "4fa839197f11ba80a9000005",
"published_ids": [
],
"unpublished_ids": [
"4fa839197f11ba80a9000001",
"4fa839197f11ba80a9000002",
"4fa839197f11ba80a9000003",
"4fa839197f11ba80a9000004"
]
},
"segment_status": "Draft"
}
id: 4fa839197f11ba80a9000001
resource: #<Resource _id: 4fa839197f11ba80a9000001, _type: nil, name: "resource 0">
[
{
"_id": "4fa839197f11ba80a9000006",
"production_status": "Unscheduled",
"projected_air_date": null,
"published": false,
"resource_well": {
"_id": "4fa839197f11ba80a9000005",
"published_ids": [
],
"unpublished_ids": [
"4fa839197f11ba80a9000002",
"4fa839197f11ba80a9000003",
"4fa839197f11ba80a9000004"
]
},
"segment_status": "Draft"
}
]
.
Finished tests in 0.016026s, 62.3986 tests/s, 0.0000 assertions/s.
1 tests, 0 assertions, 0 failures, 0 errors, 0 skips
db.collection.update(
{_id : ObjectId("4fa77c808d0a6c287d00000a")},
{ $unset : { "resource_well.unpublished.1" : 1 }}
);
This will remove element [1] of the array.
Also you can read how to implement it using mongoid: http://mongoid.org/docs/upgrading.html