attr_accessor variable nil when doing before_validation callback - ruby-on-rails

I am doing a before_validation as follows:
event.rb
attr_accessor :start_date
attr_accessible :start_time #recorded in database as a datetime
before_validation :build_start_time
...
def build_start_time
begin
self.start_time = DateTime.parse(start_date)
rescue
errors.add(:start_date, "invalid date")
return false
end
end
and the controller looks like:
def create
#event = events.build(params[:event])
if #event.save
# some other method calls
redirect_to #event
else
redirect_to :root
end
end
start_date is being set by a <%= f.text_field :start_date %> call in a form view, and when I check the params it is being passed to the 'Create' method of the model controller correctly, but in the build_start_time method it is nil, so self.start_time is not being set. Can you explain why it would be nil and what the solution would be? I also tried referring to it as self.start_date but that didn't make a difference.
Thanks

Have you tried making start_date also accessible?

Either you call attr_accessible with start_date so build() can actually set it, or you can change your controller to:
def create
#event = events.build(params[:event])
#event.start_date = params[:event][:start_date]
if #event.save
# some other method calls
redirect_to #event
else
redirect_to :root
end
end

tente assim.
#app/models/adm/video.rb
class Adm::Video < ActiveRecord::Base
validates :titulo, :url_codigo, presence: true
before_validation(on: [ :create, :update ]) do
self.url_codigo = parse_youtube(url_codigo) #url_codigo = params[:adm_video][:url_codigo]
end
private
# pega só o codigo do link youtube para inserir no banco
def parse_youtube(url)
if !url.blank?
regex = /(?:.be\/|\/watch\?v=|\/(?=p\/))([\w\/\-]+)/
return url.match(regex)[1] # https://www.youtube.com/watch?v=iX_rKHnKJSg = iX_rKHnKJSg
end
end
end
grava no banco de dados sò código do video = iX_rKHnKJSg = https://www.youtube.com/watch?v=iX_rKHnKJSg = iX_rKHnKJSg.
records in the database sò code iX_rKHnKJSg video = # = https://www.youtube.com/watch?v=iX_rKHnKJSg iX_rKHnKJSg

Related

How can I test these RSS parsing service objects?

I have some service objects that use Nokogiri to make AR instances. I created a rake task so that I can update the instances with a cron job. What I want to test is if it's adding items that weren't there before, ie:
Create an Importer with a url of spec/fixtures/feed.xml, feed.xml having 10 items.
Expect Show.count == 1 and Episode.count == 10
Edit spec/fixtures/feed.xml to have 11 items
Invoke rake task
Expect Show.count == 1 and Episode.count == 11
How could I test this in RSpec, or modify my code to be more testable?
# models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_importer
validates :title, presence: true
validates :url, presence: true
validates :feed_format, presence: true
private
def parse_importer
Parser.new(self)
end
end
# models/show.rb
class Show < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
has_many :episodes
attr_accessor :entries
end
# models/episode.rb
class Episode < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
belongs_to :show
end
#lib/tasks/admin.rake
namespace :admin do
desc "Checks all Importer URLs for new items."
task refresh: :environment do
#importers = Importer.all
#importers.each do |importer|
Parser.new(importer)
end
end
end
# services/parser.rb
class Parser
def initialize(importer)
feed = Feed.new(importer)
show = Show.where(rss_link: importer.url).first
if show # add new episodes
new_episodes = Itunes::Channel.refresh(feed.origin)
new_episodes.each do |new_episode|
show.episodes.create feed.episode(new_episode)
end
else # create a show and its episodes
new_show = Show.new(feed.show) if (feed && feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create feed.episode(entry)
end
end
end
end
end
# services/feed.rb
class Feed
require "nokogiri"
require "open-uri"
require "formats/itunes"
attr_reader :params, :origin, :show, :episode
def initialize(params)
#params = params
end
def origin
#origin = Nokogiri::XML(open(params[:url]))
end
def format
#format = params[:feed_format]
end
def show
case format
when "iTunes"
Itunes::Channel.fresh(origin)
end
end
def episode(entry)
#entry = entry
case format
when "iTunes"
Itunes::Item.fresh(#entry)
end
end
end
# services/formats/itunes.rb
class Itunes
class Channel
def initialize(origin)
#origin = origin
end
def title
#origin.xpath("//channel/title").text
end
def description
#origin.xpath("//channel/description").text
end
def summary
#origin.xpath("//channel/*[name()='itunes:summary']").text
end
def subtitle
#origin.xpath("//channel/*[name()='itunes:subtitle']/text()").text
end
def rss_link
#origin.xpath("//channel/*[name()='atom:link']/#href").text
end
def main_link
#origin.xpath("//channel/link/text()").text
end
def docs_link
#origin.xpath("//channel/docs/text()").text
end
def release
#origin.xpath("//channel/pubDate/text()").text
end
def image
#origin.xpath("//channel/image/url/text()").text
end
def language
#origin.xpath("//channel/language/text()").text
end
def keywords
keywords_array(#origin)
end
def categories
category_array(#origin)
end
def explicit
explicit_check(#origin)
end
def entries
entry_array(#origin)
end
def self.fresh(origin)
#show = Itunes::Channel.new origin
return {
description: #show.description,
release: #show.release,
explicit: #show.explicit,
language: #show.language,
title: #show.title,
summary: #show.summary,
subtitle: #show.subtitle,
image: #show.image,
rss_link: #show.rss_link,
main_link: #show.main_link,
docs_link: #show.docs_link,
categories: #show.categories,
keywords: #show.keywords,
entries: #show.entries
}
end
def self.refresh(origin)
#show = Itunes::Channel.new origin
return #show.entries
end
private
def category_array(channel)
arr = []
channel.xpath("//channel/*[name()='itunes:category']/#text").each do |category|
arr.push(category.to_s)
end
return arr
end
def explicit_check(channel)
string = channel.xpath("//channel/*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(channel)
keywords = channel.xpath("//channel/*[name()='itunes:keywords']/text()").text
arr = keywords.split(",")
return arr
end
def entry_array(channel)
arr = []
channel.xpath("//item").each do |item|
arr.push(item)
end
return arr
end
end
class Item
def initialize(origin)
#origin = origin
end
def description
#origin.xpath("*[name()='itunes:subtitle']").text
end
def release
#origin.xpath("pubDate").text
end
def image
#origin.xpath("*[name()='itunes:image']/#href").text
end
def explicit
explicit_check(#origin)
end
def duration
#origin.xpath("*[name()='itunes:duration']").text
end
def title
#origin.xpath("title").text
end
def enclosure_url
#origin.xpath("enclosure/#url").text
end
def enclosure_length
#origin.xpath("enclosure/#length").text
end
def enclosure_type
#origin.xpath("enclosure/#type").text
end
def keywords
keywords_array(#origin.xpath("*[name()='itunes:keywords']").text)
end
def self.fresh(entry)
#episode = Itunes::Item.new entry
return {
description: #episode.description,
release: #episode.release,
image: #episode.image,
explicit: #episode.explicit,
duration: #episode.duration,
title: #episode.title,
enclosure_url: #episode.enclosure_url,
enclosure_length: #episode.enclosure_length,
enclosure_type: #episode.enclosure_type,
keywords: #episode.keywords
}
end
private
def explicit_check(item)
string = item.xpath("*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(item)
keywords = item.split(",")
return keywords
end
end
end
Before anything else, good for you for using service objects! I've been using this approach a great deal and find POROs preferable to fat models in many situations.
It appears the behavior you're interested in testing is contained in Parser.initialize.
First, I'd create a class method for Parser called parse. IMO, Parser.parse(importer) is clearer about what Parser is doing than is Parser.new(importer). So, it might look like:
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
end
end
Then add the create_new_episodes and create_show_and_episodes class methods.
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: #importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
def create_new_episodes(new_episodes)
new_episodes.each do |new_episode|
#show.episodes.create #feed.episode(new_episode)
end
end # create_new_episodes
def create_show_and_episodes
new_show = Show.new(#feed.show) if (#feed && #feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create #feed.episode(entry)
end
end
end # create_show_and_episodes
end
end
Now you have a Parser.create_new_episodes method that you can test independently. So, your test might look something like:
require 'rspec_helper'
describe Parser do
describe '.create_new_episodes' do
context 'when an initial parse has been completed' do
before(:each) do
first_file = Nokogiri::XML(open('spec/fixtures/feed_1.xml'))
#second_file = Nokogiri::XML(open('spec/fixtures/feed_2.xml'))
Parser.create_show_and_episodes first_file
end
it 'changes Episodes.count by 1' do
expect{Parser.create_new_episodes(#second_file)}.to change{Episodes.count}.by(1)
end
it 'changes Show.count by 0' do
expect{Parser.create_new_episodes(#second_file)}.to change{Show.count}.by(0)
end
end
end
end
Naturally, you'll need feed_1.xml and feed_2.xml in the spec\fixtures directory.
Apologies for any typos. And, I didn't run the code. So, might be buggy. Hope it helps.

Rails class methods validations

I have a few class methods that help with querying the database but I'd like to add some sort of validations for the params sent to these methods. For example,
def self.get_ayahs_by_range(surah_id, from, to)
self.where('quran.ayah.surah_id = ?', surah_id)
.where('quran.ayah.ayah_num >= ?', from)
.where('quran.ayah.ayah_num <= ?', to)
.order('quran.ayah.surah_id, quran.ayah.ayah_num')
end
which is called from the controller by passing params[:surah_id], params[:to] and params[:from] to this function.
At times, for some reason, we have :surah_id being undefined which causes a mess. How can I fix by validations prior to?
Any suggestions for params validation other than strong_params which didn't work for an index action I felt?
controller:
def index
unless valid_params?
return render json: {message: 'Params are wrong.'}
end
params_hash = (params[:range] || ("#{params[:from]}-#{params[:to]}")) + "/#{params[:quran]}/#{params[:audio]}/#{params[:content]}"
if params.key?(:range)
range = params[:range].split('-')
elsif params.key?(:from) && params.key?(:to)
range = [params[:from], params[:to]]
else
range = ['1', '10']
end
if (range.last.to_i - range.first.to_i) > 50
return render json: {error: "Range invalid, use a string (maximum 50 ayat per request), e.g. '1-3'"}
end
#results = Rails.cache.fetch("surahs/#{params[:surah_id]}/ayahs/#{params_hash}", expires_in: 12.hours) do
ayahs = Quran::Ayah.get_ayahs_by_range(params[:surah_id], range[0], range[1])
Quran::Ayah.merge_resource_with_ayahs(params, ayahs)
end
render json: #results
end
Make a new simple class in your models (it doesn't have to be a table in your database)
require 'ostruct'
class SearchOptions < OpenStruct
include ActiveModel::Validations
validates :surah_id, presence: true
validates :from, presence: true
...
end
Then in the controller
#search_option = SearchOption.new(seach_params)
#search_option.valid?
# here you put the "invalid" processing
# maybe re-render the search parameters view
end

More ruby-like way of writing simple ActiveRecord code

Here is some fairly standard Ruby on Rails 4 ActiveRecord code:
def hide(user)
self.hidden = true
self.hidden_on = DateTime.now
self.hidden_by = user.id
end
def unhide
self.hidden = false
self.hidden_on = nil
self.hidden_by = nil
end
def lock(user)
self.locked = true
self.locked_on = DateTime.now
self.locked_by = user.id
end
def unlock
self.locked = false
self.locked_on = nil
self.locked_by = nil
end
# In effect this is a soft delete
def take_offline(user)
hide(user)
lock(user)
end
The code is easy to understand and doesn't try to be clever. However it feels verbose. What would be a more succinct or canonical way of specifying this code/behaviour?
Well, it's a trade-off, but if you want to be more clever, you can do something like:
def self.def_toggle(type, field)
define_method(type) do |user|
send("#{field}=", true)
send("#{field}_on=", DateTime.now)
send("#{field}_by=", user.id)
end
define_method("un#{type}") do
send("#{field}=", false)
send("#{field}_on=", nil)
send("#{field}_by=", nil)
end
end
def_toggle(:hide, :hidden)
def_toggle(:lock, :locked)
It's a bit extreme unless you have a lot of these or you want to encapsulate a bit more logic. But you can do something like the following using composed_of
class Model < ActiveRecord::Base
composed_of :hidden, class_name: 'State', mapping: %w(hidden, hidden_on, hidden_by)
composed_of :locked, class_name: 'State', mapping: %w(locked, locked_on, locked_by)
def hide(user)
hidden.on
end
def unhide
hidden.off
end
def lock(user)
locked.on
end
def unlock
locked.off
end
end
class State < Struct.new(:state, :on, :by)
def on(user)
set(true, user)
end
def off
set(false, nil, nil)
end
def on?
state
end
def off?
!on
end
private
def set(state, by, on = Time.current)
self.state = state
self.by = by
self.on = on
end
end

How can I lessen the verbosity of my populate method?

I wrote a form object to populate an Order, Billing, and Shipping Address objects. The populate method looks pretty verbose. Since the form fields don't correspond to Address attributes directly, I'm forced to manually assign them. For example:
shipping_address.name = params[:shipping_name]
billing_address.name = params[:billing_name]
Here's the object. Note that I snipped most address fields and validations, and some other code, for brevity. But this should give you an idea. Take note of the populate method:
class OrderForm
attr_accessor :params
delegate :email, :bill_to_shipping_address, to: :order
delegate :name, :street, to: :shipping_address, prefix: :shipping
delegate :name, :street, to: :billing_address, prefix: :billing
validates :shipping_name, presence: true
validates :billing_name, presence: true, unless: -> { bill_to_shipping_address }
def initialize(item, params = nil, customer = nil)
#item, #params, #customer = item, params, customer
end
def submit
populate
# snip
end
def order
#order ||= #item.build_order do |order|
order.customer = #customer if #customer
end
end
def shipping_address
#shipping_address ||= order.build_shipping_address
end
def billing_address
#billing_address ||= order.build_billing_address
end
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
if order.bill_to_shipping_address?
billing_address.name = params[:shipping_name]
billing_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
else
billing_address.name = params[:billing_name]
billing_address.street = params[:billing_street]
# Repeat for city, state, post, code, etc...
end
end
end
Here's the controller code:
def new
#order_form = OrderForm.new(#item)
end
def create
#order_form = OrderForm.new(#item, params[:order], current_user)
if #order_form.submit
# handle payment
else
render 'new'
end
end
Noe I am not interested in accepts_nested_attributes_for, which presents several problems, hence why I wrote the form object.
def populate
order.email = params[:email]
shipping_params = %i[shipping_name shipping_street]
billing_params = order.bill_to_shipping_address? ?
shipping_params : %i[billing_name billing_street]
[[shipping_address, shipping_params], [billing_address, billing_params]]
.each{|a, p|
a.name, a.street = params.at(*p)
}
end
How about
class Order < ActiveRecord::Base
has_one :shipping_address, class_name: 'Address'
has_one :billing_address, class_name: 'Address'
accepts_nested_attributes_for :shipping_address, :billing_address
before_save :clone_shipping_address_into_billing_address, if: [check if billing address is blank]
Then when you set up the form, you can have fields_for the two Address objects, and side step the populate method entirely.
A possible fix would be to use a variable for retrieving those matching params, like so:
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# etc...
#set a default state
shipping_or_billing = "shipping_"
#or use a ternary here...
shipping_or_billing = "billing_" if order.bill_to_shipping_address?
billing_address.name = params["shipping_or_billing" + "name"]
billing_address.street = params["shipping_or_billing" + "street"]
...
end
Your address classes should probably have a method that would set the values for all the address properties from a hash that it would receive as an argument.
That way your populate method would only check for order.bill_to_shipping_address? and them pass the correct dictionary to the method I'm suggesting.
That method on the other hand, would just assign the values from the hash to the correct properties, without the need for a conditional check.

Custom validation help needed

validate :check_product_stock
def check_product_stock
#thisproduct = product.id
#productbeingchecked = Product.find_by_id(#thisproduct)
#stocknumber = #productbeingchecked.stock_number
if producto.en_stock == 0
raise "El Producto no tiene stock suficiente para completar la venta"
#errors.add :venta, "Producto con pedos de stock"
return false
end
true
end
end
i need to be able to validate on creation of a model (sale), if it's association (product), hasn't reached zero in a product.column called stock_number.
i think i need to rewrite the entire thing but now with validate :check_product_stock then built a method from scratch that checks if product hasn't reached zero and it it has it should throw a flash notice and stay in the same place (sales/new)
class Venta < ActiveRecord::Base
hobo_model # Don't put anything above this
belongs_to :cliente, :accessible => true
belongs_to :producto, :accessible => true
belongs_to :marca, :accessible => true
belongs_to :vendedor
belongs_to :instalador
has_many :devolucions
fields do
numero_de_serie :string
precio_de_venta :integer
precio_de_instalacion :integer, :default => "0"
forma_de_pago enum_string(:contado, :tarjeta)
status enum_string(:activa, :cancelada)
timestamps
end
validates_presence_of :cliente, :precio_de_venta, :vendedor, :precio_de_instalacion
validate_on_create :check_product_stock
after_save :descontar_precio_de_instalacion_si_el_instalador_es_a_destajo
#def stock_error
#flash[:notice] = "Producto con pedos de stock"
# redirect_to :controller => :venta, :action => :stock_error
#errors.add_to_base("Producto con pedos de stock")
# end
def check_product_stock
if producto.en_stock == 0
raise "El Producto no tiene stock suficiente para completar la venta"
#errors.add :venta, "Producto con pedos de stock"
return false
end
true
end
#def check_product_stock
# if producto.en_stock == 0
# errors.add :producto, "El Producto no tiene stock suficiente para completar la venta"
# return false
# end
# true # guards against returning nil which is interpreted as false.
#end
def descontar_precio_de_instalacion_si_el_instalador_es_a_destajo
#este_instalador_id = instalador.id
#instalador = Instalador.find_by_id(#este_instalador_id)
if #instalador.a_destajo?
#last_venta = Venta.find(:last)
#vid = #last_venta.id
#precio_de_instalacion_original = precio_de_instalacion
#mitad_de_instalacion = #precio_de_instalacion_original/2
#Venta.update(#vid, :precio_de_instalacion => #mitad_de_instalacion)
ActiveRecord::Base.connection.execute "UPDATE ventas SET precio_de_instalacion = #{#mitad_de_instalacion} WHERE id = #{#vid};"
end
end
#after_save :reduce_product_stock_number
# def reduce_product_stock_number
# Producto.decrement_counter(:en_stock, producto.id)
# end
# --- Permissions --- #
def create_permitted?
true
end
def update_permitted?
false
end
def destroy_permitted?
false
end
def view_permitted?(field)
true
end
end
And this is my observer that diminishes the en_stock column from producto:
class VentaObserver < ActiveRecord::Observer
def after_save(venta)
#venta_as_array = venta
if venta.producto_id?
#pid = #venta_as_array[:producto_id]
Producto.decrement_counter(:en_stock, #pid)
end
if venta.cart_id
#cid = #venta_as_array[:cart_id]
#cart = Cart.find_by_id(#cid)
for item in #cart.cart_items do
# #pid = #cart.cart_items.producto.id
Producto.decrement_counter(:en_stock, item.producto.id)
end
#si el instalador es a destajo se debe descontar la mitad del rpecio de instalacion
end
end
end
It seems you've got a lot going wrong here.
First of all, you're doing way too much work. This is all you really need.
before_save :check_product_stock
def check_product_stock
if product.stocknumber == 0
flash[:notice] = "Producto con pedos de stock"
end
end
Secondly, the flash hash is not available in models. You can use the ActiveRecord error object to make errors available to the controller and views by replacing
flash[:notice] = with errors.add_to_base
I used errors.add_to_base because the error is not exactly a part of this model, yet is still blocking the save.
Thirdly, it seems like you're reducing product.stocknumber at some point. Probably as a before_validation so it's highly possible that product.stocknumber is less than 0 during the check if product.stocknumber was 0 before the save call.
So let's change the if condition to reflect that.
unless product.stocknumber > 0
Finally, you're using a before_save callback, so just adding an error will not cancel the transaction. You need to return false for a before/after save/create/update/valdiaiton callback to cancel the transaction.
Putting this all together gives you
before_save :check_product_stock
def check_product_stock
unless product.stocknumber > 0
errors.add_to_base "Producto con pedos de stock"
return false
end
true # guards against returning nil which is interpreted as false.
end
As for displaying these errors you can either use the nice error_messages_for helper on the object in the view. Or copy the errors to the flash hash in the controller.
In the view:
<%= error_mesages_for :order %>
Or, in the controller in the else block of if #order.save:
flash[:errors] = #order.errors.full_messages.join "<br />"
I prefer the errors over notice when it comes to passing errors in the flash hash because it makes it easy to distinguish the two via CSS if an action happens to produce both a notices or an error. Also it's more DRY to always display flash[:errors] with a class that gives red text, the write the logic into your view to determine if the contents of flash[:notice] are an error or a notice.
flash is not available in the model. You need to do something like this:
errors.add :stocknumber, "Producto con pedos de stock"
And then work with flash in the controller and views.

Resources