Need help validating query parameters - ruby-on-rails

I have multiple methods within my controller that takes in query parameters. How can I validate that I am being passed in valid parameters? For example, for the index method, how can I make sure that I am getting an array of authorIds.
def index
author_ids_array = params[:authorIds].to_s.split(',')
posts = Post
.get_posts_by_user_id(author_ids_array)
.order(sort_column => sort_direction)
if posts
render json: { posts: posts }, status: :ok
else
render json: {error: posts.errors}, status: :unprocessable_entity
end
end
Or in this update method. How can I validate that I am getting a valid postId
def update
post = current_user.posts.find_by(id: params[:id])
if post.update!(post_params)
post_hash = post.as_json
post_hash.merge!(authorIds: params[:authorIds])
render json: {post: post_hash}, status: :ok
else
render json: {error: post.errors}, status: :unprocessable_entity
end
end
Update:
Post Model:
class Post < ApplicationRecord
# Associations
has_many :user_posts
has_many :users, through: :user_posts, dependent: :destroy
# Validations
validates :text, presence: true, length: { minimum: 3 }
validates :popularity, inclusion: { in: 0.0..1.0 }
def tags
if super
super.split(",")
end
end
def tags=(value)
if value.kind_of? Array
super value.join(",")
else
super value
end
end
def self.get_posts_by_user_id(user_id)
Post.joins(:user_posts).where(user_posts: { user_id: user_id })
end
end
User Model:
class User < ApplicationRecord
has_secure_password
# Associations
has_many :user_posts
has_many :posts, through: :user_posts, dependent: :destroy
# Validations
validates :username, :password, presence: true
validates :password, length: { minimum: 6 }
validates :username, uniqueness: true
end
User_post Model:
class UserPost < ApplicationRecord
belongs_to :user
belongs_to :post
end

You can use specific render like below user this in any method like
def index
return render body: params.inspect
.
.
end
user below code
return render body: params.inspect
so when you use index it will give you params which is passing
OR you can user below code in your application.html.erb above <%= yield%>
<%= debug(params) if Rails.env.development? %>

After your clarifications, your question remains unclear to me and it is difficult to guess what you're doing. But I understood that you want to ensure that params[:authorIds] or anything else is an array.
You can see if a given variable is an array the following way:
a = ["1","2"]
if a.is_a?(Array)
puts "is an array"
end
With params: params[:authorIds].is_a?(Array)
You can use byebug (before Rails 7) or debugger (for Rails 7) to inspect what a param is. As an example:
(ruby#whatever: cluster worker 1: 42779 [MyApp]#42793) params[:ids].class
Array

Related

Why does the JSON patch data from relationships not get saved via Ruby on Rails?

I have an Ruby on Rails api where the data is handled in JSON. When I want to update an entity all the attributes are getting updated persistently but changed relationships arent' getting handled correctly, the entity stays the same as before.
JSON data before and after the PATCH:
{"data":{"id":"26","type":"candidate","attributes":
{"place":"Ort","zip_code":"PLZ","address":"Adresse",
"date_of_birth":"2019-01-01T00:00:00.000Z","title":"Frau",
"first_name":"Vorname","last_name":"Nachname",
"email_address":"email#example.ch",
"confirm_terms_and_conditions":true},"relationships":
{"occupational_fields":{"data":[]}}}}
PATCH input:
Started PATCH "/candidates/26" for 127.0.0.1 at 2019-01-22
19:40:53 +0100
Processing by CandidatesController#update as JSON
Parameters: {"data"=>{"id"=>"26", "attributes"=>{"place"=>"Ort",
"zip_code"=>"PLZ", "address"=>"Adresse", "title"=>"Frau",
"first_name"=>"Vorname", "last_name"=>"Nachname",
"email_address"=>"email#example.ch",
"confirm_terms_and_conditions"=>true, "date_of_birth"=>"2019-01-
01T00:00:00.000Z"}, "relationships"=>{"occupational_fields"=>
{"data"=>[{"type"=>"occupational-fields", "id"=>“4“}]}},
"type"=>"candidates"}, "id"=>"26", "candidate"=>{}}
This are my models, Candidates and OccupationalFields are related via a has_many belongs_to_many relationship through one CandidatesOccupationalField:
class Candidate < ApplicationRecord
has_many :candidates_occupational_fields, dependent: :destroy
has_many :occupational_fields, through:
:candidates_occupational_fields, dependent: :nullify
end
class CandidatesOccupationalField < ApplicationRecord
belongs_to :candidate
belongs_to :occupational_field
end
class OccupationalField < ApplicationRecord
has_many :candidates_occupational_fields, dependent: :destroy
has_many :candidates, through: :candidates_occupational_fields,
dependent: :nullify
end
This is the used controller:
class CandidatesController < ApplicationController
before_action :set_candidate, only: %i[show update destroy]
# GET /candidates
def index
#candidates = Candidate.all
render json: CandidateSerializer.new(#candidates).serialized_json
end
# GET /candidates/1
def show
#candidate = Candidate.find(params[:id])
render json: CandidateSerializer.new(#candidate).serialized_json
end
# POST /candidates
def create
#candidate = Candidate.new(candidate_params)
if #candidate.save
render json: CandidateSerializer.new(#candidate), status: :created
else
render json: #candidate.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /candidates/1
def update
#candidate = Candidate.find(params[:id])
if #candidate.update(candidate_params)
render json: CandidateSerializer.new(#candidate)
else
render status: :unprocessable_entity
end
end
# DELETE /candidates/1
def destroy
#candidate.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_candidate
#candidate = Candidate.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def candidate_params
params.require(:data)[:attributes]
.permit(:place, :zip_code, :address,
:date_of_birth, :title, :first_name,
:last_name, :email_address,
:confirm_terms_and_conditions,
occupational_field_ids: [])
end
end
The JSON formatting is handled by fastjsonapi, this are the used serializers:
class CandidateSerializer
include FastJsonapi::ObjectSerializer
attributes :place, :zip_code, :address, :date_of_birth,
:title, :first_name, :last_name, :email_address,
:confirm_terms_and_conditions
has_many :occupational_fields
end
class OccupationalFieldSerializer
include FastJsonapi::ObjectSerializer
attributes :field
has_many :candidates
end
Thank you for your help.
The problem was, that the used serializer fast_jsonapi can't be used as deserializer and the Rail's strong parameters can't handle the json input. It works with the gem restful-jsonapi and modified params as shown in the example of the readme of restful-jsonapi.

Rails use an instance variable in controller or generate within a helper?

So I'm trying to build out on an Invoice page the past_due_amount where I'm trying to find only the invoices for the current account, that are not paid off, and should be in the past.
So roughly I have:
past_due_amount = Invoice.where(account: invoice.account, status: :unpaid).where('date < ? ', invoice.date).map(&:due).sum
For additional context here are the models involved:
Invoice:
class Invoice < ApplicationRecord
belongs_to :account
has_many :line_items, dependent: :destroy
has_many :payment_destinations, dependent: :destroy
has_many :prorated_fees, dependent: :nullify
enum status: [:unpaid, :paid]
validates :date, presence: true
validates :period_start, :period_end,
uniqueness: { scope: :account, allow_blank: true }, on: :create
validate :start_is_before_end
DAYS_DUE_AFTER_DATE = 14.days
scope :descending, -> { order(date: :desc) }
scope :ascending, -> { order(date: :asc) }
scope :due, -> { unpaid.where(arel_table[:date].lteq(Time.zone.today - DAYS_DUE_AFTER_DATE)) }
def total
if persisted?
line_items.sum(:amount)
else
line_items.map(&:amount).sum
end
end
end
Account:
class Account < ApplicationRecord
belongs_to :customer
belongs_to :property_address,
class_name: Address.to_s,
dependent: :destroy,
required: false
[:products, :account_changes, :equipments,
:payments, :invoices].each do |assoc|
has_many assoc, dependent: :destroy
end
accepts_nested_attributes_for :property_address
delegate :street, :city, :state, :zip,
to: :property_address, allow_nil: true
delegate :email, :full_name, to: :customer
enum status: [:staged, :active, :inactive]
scope :active_or_staged, -> { where(status: [:staged, :active]) }
scope :past_due, lambda {
joins(:invoices)
.where(
Invoice.arel_table[:status].eq(:unpaid)
.and(Invoice.arel_table[:date].lt(Time.zone.today - 14.days))
).distinct
}
scope :search, lambda { |term|
joins(:customer)
.where(
arel_table[:account_num].matches("%#{term}%")
.or(Customer.arel_search(term))
)
}
end
With the rough code in place I decided to build out a instance variable on the InvoicesController within the show method as below:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
No errors appear but that's not saying much since the examples I have are poor, at best. But my question is...should I actually be putting this in a helper instead of the show method on an InvoicesController or even in the model?
EDIT:
I've also tried putting in my Invoice model:
def self.past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
Then in my InvoicesController:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.past_due_amount
end
End up getting undefined method `date' for #invoice.date.
The best way is to create a method past_due_amount in the InvoicesHelper
module InvoicesHelper
def past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date <?', #invoice.date).map(&:due).sum
end
end
In you controller just initialize all the instance variables
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
end
In the view you should use: <%= past_due_amount > to show your data
Create an instance method in Account model
def past_due_amount
invoices.map(&:due).sum
end
and then from view you can all it #account.past_due_amount. no need to create extra instance variable in controller action
So I sort of used Patrick's answer but it was actually failing so I switched to passing invoice as params.
Helper
module InvoicesHelper
def past_due_amount(invoice)
Invoice.where(account: invoice.account, status: :unpaid).where('date < ?', invoice.date).map(&:due).sum
end
end
Then in my view:
<% if past_due_amount(invoice).positive? %>
<p><%= number_to_currency past_due_amount(invoice) %></p>
<% end %>

Rails accepts_nested_attributes_for associated models not created

I have two models (Company and User) that have a belongs_to/has_many relationship.
class Company < ActiveRecord::Base
attr_accessor :users_attributes
has_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end
class User < ActiveRecord::Base
belongs_to :company
end
In my CompaniesController I want to create a new instance of Company along with a group of Users.
class Cms::CompaniesController < ApplicationController
def create
company = Company.new(company_params)
respond_to do |format|
if company.save
format.json { render json: company, status: :ok }
else
format.json { render json: company.errors.messages, status: :bad_request }
end
end
end
private
def company_params
params.require(:company).permit(
:id,
:name,
users_attributes: [
:id,
:_destroy,
:first_name,
:last_name,
:email
]
)
end
end
When I call company.save, I would expect a new instance of Company along with several new instances of User to be saved, depending on how many users I have in my params, however no users are persisted.
Here is a sample of what company_params looks like:
{"id"=>nil, "name"=>"ABC", "users_attributes"=>[{"first_name"=>"Foo", "last_name"=>"Bar", "email"=>"foo#bar.com"}]}
What am I missing here?
Remove attr_accessor:
class Company < ActiveRecord::Base
has_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end
Everything else should work.
--
attr_accessor creates getter/setter methods in your class.
It's mostly used for virtual attributes (ones which aren't saved to the database). Your current setup is preventing you from being able to save the users_attributes param, thus your users are not saving.

How to create nested models from API request?

I've a Rails API and I've two models:
class Event < ActiveRecord::Base
belongs_to :category
has_many :event_categories
has_many :events, through: :event_categories
attr_accessible :title, :description, :event_categories_attributes
accepts_nested_attributes_for :event_categories
end
and
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
attr_accessible :category_id, :event_id, :principal
validates :event, :presence => true
validates :category, :presence => true
validates_uniqueness_of :event_id, :scope => :category_id
end
In a first moment, EventCategory didn't exist so I created Event resources sending params like event[title]='event1', event[description] = 'blablbla' thought POST REST request.
My API EventsController was like this (I haven't a new method because I don't need views):
def create
#event = Event.create(params[:event])
if #event
respond_with #event
else
respond_with nil, location: nil, status: 404
end
end
This way worked correctly for me. Now, with the new EventCategory model I don't know how I could create EventCategories models at the same time.
I've trying this... but it doesn't work:
def create
#event = Event.new(params[:event])
#event.event_categories.build
if #event.save
respond_with #event
else
respond_with nil, location: nil, status: 404
end
end
Rails told me:
{
"event_categories.event": [
"can't be blank"
],
"event_categories.category": [
"can't be blank"
]
}
I send the category_id like this:
event[event_categories_attributes][0][category_id] = 2
Any ideas?
In your create action, instead of this:
#event.event_categories.build
Try this:
#event.event_categories = EventCategory.new do |ec|
ec.event = #event
ec.category = the_cattegory_you_want_to_specify
# You need both of these as you are validating the presence of event AND category
end

Carrierwave not uploading - no error shown

I've been working on this all night and it makes no sense. I'm adapting an old photo web app to have albums in it. I made "fails" (basically images) a nested resource of albums. I am using carrierwave to upload files to an S3 bucket.
the weird thing is: the upload works perfectly fine for the album model (album image), but it doesn't upload for the fail model.
I don't see why it'd be a problem that it's a nested resource now. It's not a problem displaying it's that for some reason, it goes through the form fine, passes validations fine, no errors are thrown, it redirects to fails#index like it was successful, but there is nothing in the db or in S3.
Code is below. All code at https://github.com/spq24/failboard
Fail Model
class Fail < ActiveRecord::Base
attr_accessible :description, :image, :remote_image_url, :fail_title, :tag_list, :processed, :youtube_url, :album_id
make_voteable
acts_as_taggable
belongs_to :album
mount_uploader :image, ImageUploader
validates :description, length: { :maximum => 200 }
validates :album_id, presence: true
validates :image, presence: true
validates :fail_title, presence: true, length: { :maximum => 50 }
validate :maximum_amount_of_tags
def maximum_amount_of_tags
number_of_tags = tag_list_cache_on("tags").uniq.length
errors.add(:base, "Please only add up to 5 tags") if number_of_tags > 5
end
before_save :update_attachment_attributes
def update_attachment_attributes
if image.present? && image_changed?
self.content_type = image.file.content_type
self.file_size = image.file.size
end
end
def next
user.fails.where("id > ?", id).order("id ASC").first
end
def prev
user.fails.where("id < ?", id).order("id DESC").first
end
end
Album Model
class Album < ActiveRecord::Base
attr_accessible :name, :image, :image_url, :created_at
belongs_to :user
has_many :fails, dependent: :destroy
mount_uploader :image, ImageUploader
validates :user_id, presence: true
validates :image, presence: true
validates :name, presence: true, length: { :maximum => 50 }
before_save :update_attachment_attributes
def update_attachment_attributes
if image.present? && image_changed?
#self.content_type = image.file.content_type
#self.file_size = image.file.size
end
end
def next
user.fails.where("id > ?", id).order("id ASC").first
end
def prev
user.fails.where("id < ?", id).order("id DESC").first
end
end
Fails Controller
def new
#fail = Fail.new(:album_id => params[:album_id])
respond_to do |format|
format.html # new.html.erb
format.json { render json: #fail }
end
end
def create
#fail = Fail.new(params[:fail])
respond_to do |format|
if #fail.save
format.html { redirect_to #fail.album, notice: 'You added a new photo!' }
format.json { render json: #fail, status: :created, location: #fail }
else
format.html { render action: "new" }
format.json { render json: #fail.errors, status: :unprocessable_entity }
end
end
end
routes.rb
resources :albums do
get 'tags/:tag', to: 'fails#index', as: :tag
resources :fails do
member do
post :up_vote
end
end
Debug Hash (this turns red when I try to upload, but I don't see anything that would cause the error)
Here is the debug info:
{"utf8"=>"✓", "authenticity_token"=>"Hz6Gl95ultYDNIEjQioIckB8JXQwhiMxXIM9jrfqd5Q=", "fail"=>{"fail_title"=>"tester", "image"=>#<ActionDispatch::Http::UploadedFile:0x56195e8 #original_filename="pic19.jpg", #content_type="image/jpeg", #headers="Content-Disposition: form-data; name=\"fail[image]\"; filename=\"pic19.jpg\"\r\nContent-Type: image/jpeg\r\n", #tempfile=#<File:C:/Users/Kinertia/AppData/Local/Temp/RackMultipart20131125-10428-m2ktp2>>, "description"=>"", "tag_list"=>"test"}, "commit"=>"Create Fail", "controller"=>"fails", "action"=>"index"}
If there is anything else needed please let me know and I will put it here. Thank you for all the help!
Have you tried validating the integrity or processing of the fail image?
validates_integrity_of :avatar
validates_processing_of :avatar
validates_download_of :avatar
By default it fails silently, which kinda sucks.
I also recommend trying to create the record in the rails console, which can help to isolate the problem to either the model or view/controller layers. In your case this would look something like:
Fail.create!(
image: File.open('path/to/known/file.jpg'),
album_id: 1,
fail_title: 'Title'
)

Resources