Rails custom validate - uniqueness of a property across models - ruby-on-rails

I'm stuck at defining a custom validation method that's purpose is to verify uniqueness of a property across two models
I realize this is bad code, but i wanted to get the test passing before refactor
here is a model with the custom validation to check another model property, error undefined local variable or method `params' (be gentle I'm still trying to figure out RoR)
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models
def uniqueness_of_a_slug_across_models
#sprocket = Sprocket.where(slug: params[:widget_slug]).first
if #sprocket.present?
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
end
end

You don't have access to params in a model. It belongs to controller and view. What you could do is to call custom method in widgets controller (instead of regular save) in order to pass params to a model:
class WidgetsController < ActionController::Base
def create
#widget = Widget.new(widget_params)
if #widget.save_with_slug_validation(params)
redirect_to widgets_path
else
render :new
end
end
end
and define it:
class Widget < ActiveRecord::Base
# ...
def save_with_slug_validation(params)
sprocket = Sprocket.find_by(slug: params[:widget_slug])
if sprocket
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
save
end
end
I didn't test it but it should work.
P.S. Rails 4 style is used.
UPD
I should have tested it, sorry. Please use another approach.
Widgets controller:
# POST /widgets
# POST /widgets.json
def create
#widget = widget.new(widget_params)
#widget.has_sprocket! if Sprocket.find_by(slug: params[:widget_slug])
respond_to do |format|
if #widget.save
format.html { redirect_to [:admin, #widget], notice: 'widget was successfully created.' }
format.json { render action: 'show', status: :created, location: #widget }
else
format.html { render action: 'new' }
format.json { render json: #widget.errors, status: :unprocessable_entity }
end
end
end
Widget model:
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models, if: 'has_sprocket?'
def uniqueness_of_a_slug_across_models
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
def has_sprocket!
#has_sprocket = true
end
def has_sprocket?
!!#has_sprocket
end
end
It would be better to move has_sprocket! and has_sprocket? methods and maybe validation itself to Slugable concern.

Related

Rails default parems for id set to new?

I am getting a 404 error on my RoR app and i found that it was because one of the method in the controller, which should only triggers when the record is not new, triggered when the record is new.
I do by that checking if the id of that record nil in my controller.
before_action :create_record, if: proc { not params[:id].nil? }
I was confused by it was triggered so i went head and checked my front-end network, which show following:
Request
Parameters:
{"format"=>"json", "id"=>"new"} <----Set to new by default
My completely controller looks like this:
class Api::MyController < ApplicationController
before_action :create_recotrd, if: proc { not params[:id].nil? }
def show
end
def index
#my_model = MyModel.all
end
def create
#my_model = MyModel.new(my_model_params)
respond_to do |format|
if #my_model.save
format.json { render json: #my_model, status: :created}
else
format.json { render json: #my_model.errors, status: :unprocessable_entity}
end
end
end
def update
#my_model = MyModel.update
end
private
def create_record
#my_model = MyModel.find(params[:id])
end
def my_model_params
params.require(:my_model).permit(
:city,
:state,
:county,
:zip,
:telephone,
:email,
)
end
end
I cant seem to find out why the id in the parameters is set to "new" instead of "nil".
I tried in the console by doing MyModel.new, the default id was nil, but then when i do the GET request, the id was set to "new"
This is a really weird approach to set a new record. I think the problem lies in your routes. You are probably trying to access yoursite.com/your_model/new and your routes are configured to look for
get "your_model/:id" => "your_controller#show"
You are probably missing
get "your_model/new" => "your_controller#new"
So when you try to visit your_model/new the routes map the "new" as the :id param in your url.
I don't see a new action in your controller as well. You should read up on basic resource set up for rails here: http://guides.rubyonrails.org/routing.html.

Proper model set up

I am new to stackoverflow, so my apologies if this is formatted poorly.
In my current project I have a model Driver, which has many trips. Those trips have many mileages, backhauls, picks, drops and hours. When I create a new trip, i want to be able to associate it to the driver, but I also want to be able to add the mileages, backhauls, picks and drops and hours on the same page. I am unsure how to structure my routes for this. I have been successful in creating a trip for a driver without adding on the additional models to the trip but from there I am stumped. I have only created the mileage model/controller so far for what needs to be associated with the trip. Any nudge in the right direction would be greatly appreciated.
Driver Model
class Driver < ApplicationRecord
has_many :trips
end
Trip Model
class Trip < ApplicationRecord
belongs_to :driver
has_many :mileages
accepts_nested_attributes_for :mileages
default_scope {order(date: :asc)}
validates :total, presence: true
validates :date, presence: true
validates_uniqueness_of :trip_number, :allow_nil => true, :allow_blank =>
true
end
Mileage Model
class Mileage < ApplicationRecord
belongs_to :trip
end
Trips controller
def index
#trips = Trip.all
end
def show
end
def new
#driver = Driver.find(params[:driver_id])
#trip = Trip.new
end
def edit
end
def create
#driver = Driver.find(params[:driver_id])
#trip = Trip.new(trip_params)
#driver.trips.create(trip_params)
respond_to do |format|
if #driver.trips.create(trip_params)
flash[:notice] = "Trip successfully created"
redirect_to new_driver_trip_path(#driver)
else
flash[:warning] = "Unable to create trip"
redirect_to new_driver_trip_path(#driver)
end
end
private
def set_trip
#trip = Trip.find(params[:id])
end
def trip_params
params.require(:trip).permit(:trip_number, :date, :driver_id, :total)
end
end
Mileage Controller
def new
#mileage = Mileage.new
end
def create
#mileage.create(mileage_params)
end
private
def mileage_params
params.require(:mileage).permit(:miles, :rate, :total)
end
end
end
Driver Controller
def index
#drivers = Driver.all
end
def show
end
def new
#driver = Driver.new
end
def edit
end
def create
#driver = Driver.new(driver_params)
respond_to do |format|
if #driver.save
format.html { redirect_to #driver, notice: 'Driver was
successfully created.' }
format.json { render :show, status: :created, location: #driver }
else
format.html { render :new }
format.json { render json: #driver.errors, status:
:unprocessable_entity }
end
end
end
private
def set_driver
#driver = Driver.find(params[:id])
end
def driver_params
params.require(:driver).permit(:first_name, :last_name, :email, :unit)
end
end
If you want to create nested models on the same page. i.e. milages within trip page using accepts_nested_attributes_for, You can use cocoon gem.
https://github.com/nathanvda/cocoon
Drifting Ruby has a video that shows the process in detail that is easy to follow.
https://www.youtube.com/watch?v=56xjUOAAZY8
You can do it manually as well but it will require a little bit more work.
With cocoon you will do have a Driver Controller and Trip controller but you don't need a Milage controller since it is handled with nested_attributes via Trip Controller.
If you want to do it manually, you will need a bit of JavaScript. You can follow Ryan Bates RailsCast on this topic.
railscasts.com/episodes/196-nested-model-form-revised

undefined method `marked_for_destruction?' CarrierWave, RMagick

I am passing the ability to crop images, uploaded by Carrierwave. Here is RailsCast video on Youtube which I am following.
But after including RMagick in uploader, I received:
undefined method `marked_for_destruction?' for #<ImageUploader:0x007fe86634fcf0>
What a heck is this I thought. I haven't called this method anywhere. But if it is not defined, lets define it! And it worked! But later I checked more about this method and found that it is built in in Active Record Autosave Association module. And from docs, about this method:
Returns whether or not this record will be destroyed as part of the
parents save transaction.
Only useful if the :autosave option on the parent is enabled for this
associated model.
But I didn't passed autosave: true to any object!
So, my first question - was it done by default somehow?
2 - on RailsCast tutorial he didn't defined this method. Why I had to?
3 - I pass my code bellow. Any errors?
4 - if possible, could anyone explain how this process works, in general?
Many thanks!
product.rb:
has_one :image
validates :image, presence: true
mount_uploader :image, ImageUploader
products_controller.rb:
def create
#product = Product.new(product_params)
#product.category_id = params[:category_id]
#product.user_id = current_user.id
respond_to do |format|
if #product.save
if params[:product][:image].present?
format.html { render :crop }
else
format.html { redirect_to #product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: #product }
end
else
format.html { render :new }
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
image_uploader.rb:
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
def marked_for_destruction?
#marked_for_destruction
end
def mark_for_destruction
#marked_for_destruction = true
end
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
version :large do
resize_to_limit(600,600)
end
end
If you're using Rails 5:
Open up new_framework_defaults.rb and change:
Rails.application.config.active_record.belongs_to_required_by_default = true
to
Rails.application.config.active_record.belongs_to_required_by_default = false
config.active_record.belongs_to_required_by_default is a boolean
value and controls whether a record fails validation if belongs_to
association is not present.
Inside your the *_uploader.rb file just write the function:
def marked_for_destruction?
end

Rails: Validate params before they are handled in controller?

I'm making a rails app where user can paste a soundcloud link in an input field. Then, this link is sent to the create action in my post_controller and used to get the JSON file for that song.
# app/controllers/post_controller.rb
def create
require 'open-uri'
raw_link = params[:post][:link]
raw_link.downcase.include? "soundcloud.com/"
tmp_media_json = JSON.load(open("http://soundcloud.com/oembed?format=json&url=#{raw_link}"))
if tmp_media_json['thumbnail_url']["placeholder"]
tmp_media_json['thumbnail_url'] = JSON.load(open("http://soundcloud.com/oembed?format=json&url=#{tmp_media_json['author_url']}"))['thumbnail_url']
end
media_thumbnail = tmp_media_json['thumbnail_url'].gsub('t500x500.jpg', 't80x80.jpg')
media_title = tmp_media_json['title']
media_iframe = tmp_media_json['html']
media_type = params[:post][:media_type]
#post = Post.new(link: media_iframe, title: media_title, thumbnail: media_thumbnail, media_type: media_type)
respond_to do |format|
if #post.save
format.js { render :file => "/pages/create_new_post.js.erb" }
format.html { redirect_to #post, notice: 'Post was successfully created.' }
format.json { render action: 'show', status: :created, location: #post }
else
format.html { render action: 'new' }
format.json { render json: #post.errors, status: :unprocessable_entity }
end
end
end
In the Post model, I'm trying to run validates :link, presence: true, but the problem is that it seems to be done after all the code in the create action. I want the validation to be done before all the code in the create action. (Since if there isn't a valid link, the code in the create action won't work).
How can I do this or is there a better practice?
class YourController < ApplicationController
before_filter :my_filter
before_filter :my_filter2, :except => [:index, :new, :create]
def my_filter
# your code gose here
end
def my_filter2
# your code gose here
end
........
end
You need to move the json fetching code out of the controller and into a model. Think Single Responsibility Principle(SRP): http://en.wikipedia.org/wiki/Single_responsibility_principle
Also, This question is slightly confusing because you are validating the "link" attribute which is the result of the JSON load from soundcloud, so this kind of makes your question impossible to achieve.
That being said, keep the controller lean
# controller...
def create
#post = Post.new_from_link(params[:post][:link], params[:post][:media_type])
#post.save
...render...
end
# post model...
class Post < ActiveRecord::Base
attr_accessor :raw_link, :raw_media_type
def new_from_link(raw_link, raw_media_type)
#raw_link = raw_link
#raw_media_type = raw_media_type
end
def assign_soundcloud_attributes
fetcher = SoundCloudFetcher.new(raw_link, raw_media_type).fetch
self.link = fetcher.link
self.media_type = fetcher.media_type
self.media_thumbnail = fetcher.media_thumbnail
self.media_iframe = fetcher.media_iframe
end
end
class SoundCloudFetcher
attr_accessor :link, :media_type, :media_thumbnail, :media_title, :media_iframe
def self.initialize(raw_link, raw_media_type)
#raw_link = raw_link
#raw_media_type = raw_media_type
end
def fetch
...go get and set the data...
return self
end
end
So the code above is not complete. Its missing the actual call to #assign_soundcloud_attributes. This design lets you move around where you want to do the call. You can place the call in #new_from_link, or you can place it in a before_validation or before_create callback, depending on your needs.
The question to clear this up is are you intending to validate the raw_link thats passed in, or do you want to validate the link that comes back from the soundcloud api call.
If you are validating the raw link, move the call to #assign_soundcloud_attributes to a before_create callback.
If you are validating the actual link attribute that is retrieved from the SoundCloud api call, then put it in the #new_from_link or #before_validation callback.
You can use a before_filter with rails_parms gem.
See https://github.com/nicolasblanco/rails_param
It works like this:
param! :q, String, required: true
param! :categories, Array
param! :sort, String, default: "title"
param! :order, String, in: %w(asc desc), transform: :downcase, default: "asc"
param! :price, String, format: /[<\=>]\s*\$\d+/

Nil class error

I want to display the related products to a certain need, just the picture and the name nothing more depending on the categorie
here is mu controller
class RelatedneedsController < ApplicationController
def index
#relatedneeds = RelatedNeed.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #relatedneeds }
end
end
def show
s1 = '#need.category.name'
s2 = '#relatedneed.category.name'
if s1.eql?(s2)
#relatedneed = relatedneed.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #need }
end
end
end
def get_name
#relatedneed.name
end
end
and this my model
class Relatedneed
include Mongoid::Document
include Mongoid::Paperclip
mapping do
indexes :name
end
field :name, type: String
belongs_to :category
belongs_to :user
end
and this is show.haml file
%h1
%b= #need.name
#container{:style => "width:1000px"}
#desc{:style => "height:400px;width:400px;float:left;"}
=image_tag #relatedneed.photo.url(:normal)
this is my index.haml file
%h1= #relatedneed.get_name
#container{:style => "width:1000px"}
#desc{:style => "background-color:#EEEEEE;height:400px;width:400px;float:left;"}
= link_to "Check Need", new_need_path
I don't know if their is something missing and i get this error
NoMethodError in RelatedneedsController#index
undefined method `key?' for nil:NilClass
Your index.haml has #relatedneed.get_name but you have not set #relatedneed in your controller, only #relatedneeds. Is it as simple as that?
Also, your show method in the controller makes no sense to me. You have put your instance variables inside strings! You need to set your instance variables with a database query via the model first. Your get_name method looks like it belongs in a model as well rather than a controller.

Resources