Ruby on Rails: Handling invalid dates with multiparameter dates - ruby-on-rails

I've added a form to my rails app which asks for a date. Rather than use the (IMO) clunky date_select helper, or a date popup solution, I'd like to use seperate input fields for date, month and year (as specified in the GDS service manual). I've written a custom input for simple_form here:
class TextDateInput < SimpleForm::Inputs::Base
def input(wrapper_options)
input_html_options[:pattern] = '[0-9]*'
#value = #builder.object.send(attribute_name)
#merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
"#{date_field} #{month_field} #{year_field}".html_safe
end
def date_field
#builder.label(attribute_name, class: 'grouped-date date') do
output = template.content_tag(:span, 'Date')
output += #builder.text_field(attribute_name,
#merged_input_options.merge(
name: "#{#builder.object_name}[#{attribute_name}(3i)]",
maxlength: 2,
value: #value&.day
))
output
end
end
def month_field
#builder.label(attribute_name, class: 'grouped-date month') do
output = template.content_tag(:span, 'Month')
output += #builder.text_field(attribute_name,
#merged_input_options.merge(
name: "#{#builder.object_name}[#{attribute_name}(2i)]",
maxlength: 2,
value: #value&.month
))
output
end
end
def year_field
#builder.label(attribute_name, class: 'grouped-date year') do
output = template.content_tag(:span, 'Year')
output += #builder.text_field(attribute_name,
#merged_input_options.merge(
name: "#{#builder.object_name}[#{attribute_name}(1i)]",
maxlength: 4,
value: #value&.year
))
output
end
end
end
And it works perfectly in the frontend, however, if the user enters an invalid date (for example 99/99/9999), Rails raises an ActiveRecord::MultiparameterAssignmentErrors error. Is there a clean way to handle this so rather than raising an error I can apply a validation error to the database object and show an invalid date error to the user?

You can parse date in before_filter callback or introduce params object which will replace invalid date with date that can trigger AR validation.

I decided to have a stab at this myself and added the following to my base model class (I'm using Rails 5):
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def update_attributes(attrs = {})
super parse_dates(attrs)
end
def parse_dates(attrs)
# First fetch any nested attributes
attrs.clone.each do |k, v|
next unless v.is_a?(Hash)
# If this is a hash, it's a nested attribute, so check for dates
attrs = parse_nested_dates(k, attrs)
end
# Now marshal the rest of the dates
marshal_dates(self.class, attrs)
end
def parse_nested_dates(key, attrs)
klass = Object.const_get key.split('_attributes').first.classify
attrs[key] = marshal_dates(klass, attrs[key])
attrs
end
def marshal_dates(klass, attrs)
# Get all the columns in the class that have a date type
date_columns = klass.columns_hash.select { |_k, value| value.type == :date }.keys
date_columns.each { |c| attrs = parse_date(attrs, c) }
attrs
end
def parse_date(attrs, property)
# Gather up all the date parts
keys = attrs.keys.select { |k| k =~ /#{property}/ }.sort
return attrs if keys.empty?
values = []
keys.each do |k|
values << attrs[k]
attrs.delete(k)
end
# Set the date as a standard ISO8601 date
attrs[property] = values.join('-')
attrs
end
end
This seems to work perfectly for both standard attributes and nested attributes, and means it automatically works for all date columns without me having to do anything.

Related

How to convert string to existing attribute in model when creation

I got a array of strings, I want to retrieve for each the attribute during the creation of the post.
My array = ["_646_maturity", "_660_maturity", "_651_maturity", "_652_maturity", "_641_maturity"]
class Audit < ApplicationRecord
belongs_to :user
before_save :calculate_scoring
def calculate_scoring
scoring = []
models = ActiveRecord::Base.connection.tables.collect{|t| t.underscore.singularize.camelize.constantize rescue nil}
columns = models.collect{|m| m.column_names rescue nil}
columns[2].each do |c|
if c.include? "maturity"
Rails.logger.debug 'COLUMN : '+c.inspect
scoring.push(c)
end
end
getMaturity = ""
scoring.each do |e|
getMaturity = e.to_sym.inspect
Rails.logger.debug 'MATURITY : '+getMaturity
end
end
end
The log print > 'MATURITY : :_651_maturity'
I'm looking to the value of :_651_maturity who is a attribute of my post.
I tried .to_sym but it's not working..
Thanks for the help!
Inside calculate_scoring you can use self to point to the record you are saving. So self._651_maturity = <some_value>, self[:_651_maturity] = <some_value> and self['_651_maturity'] are all valid methods to set _651_maturity.
Also, you can do something like:
my_attrib = '_651_maturity'
self[my_attrib] = 'foo'

Trigger rails controller function - Paypal Website Standard IPN

I've got a Paypal IPN that comes into a PaymentNotificationsController in my app. However, some variables depend on the number of items in a cart, so i want to extract them before creating the PaymentNotification.
So far, i've got:
class PaymentNotificationsController < ApplicationController
protect_from_forgery except: [:create]
def create
PaymentNotification.create!(params: params,
item_number: params[:item_number], item_name: params[:item_name], quantity: params[:quantity]
render nothing: true
end
end
However, when the notification comes from PayPal, it comes in the form of item_name1, item_number1, quantity1, item_name2, item_number2, quantity2 and so on.
Even if its just one item, it would come as item_name1, item_number1, quantity1, option1 and so on.
I have this function to try and extract the variables, but i don't know how to trigger the function. I tried using a before_action at the top of the controller but it didn't work. Returned wrong number of arguments(0 for 1):
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity"]
def extract_ipn_items_params(params)
item_params = []
loop do
item_num_to_test = item_params.length + 1
item_num_suffix = item_num_to_test.to_s
possible_param_name = ITEM_PARAM_PREFIXES[0] + item_num_suffix
if params.include?(possible_param_name)
this_item_params = {}
ITEM_PARAM_PREFIXES.each do |prefix|
this_item_params[prefix] = params[prefix + item_num_suffix]
end
item_params.push this_item_params
else
return item_params
end
end
end
So i'm asking, how do i trigger the function to extract the variables and put them into params[:item_number], params[:item_name], params[:quantity] for each item in the cart so if there are two items, two separate PaymentNotifications would be created?
Note: Both methods are in the same PaymentNotificationsController.
Any help would be appreciated. Thanks in advance!
I assume your method extract_ipn_items_params already fetches the data you require, you can remove the params argument to the method, as the params is always available in the actions/methods of the controller.
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity"]
def extract_ipn_items_params
mod_params = Hash.new{|k, v| k[v] = {} }
ITEM_PARAM_PREFIXES.each do |item_data_key|
key_tracker = 1
loop do
current_key = (item_data_key + key_tracker.to_s).to_sym
if params.include? current_key
mod_params[key_tracker][item_data_key] = params[current_key]
else
break
end
key_tracker += 1
end
end
mod_params
end
The method returns a hash of hashes like:
{1 => {item_name: 'Item 1', item_number: 1084, quantity: 15}}, if you have nested attributes set up for a user, I think you should be able to do something like, not really sure, but should be possible:
user.update(payment_notifications_attributes: extract_ipn_items_params)
Let me know if that works for you.
UPDATE
Based on the Github Gist, here's something I was able to come up with:
class PaymentNotificationsController < ApplicationController
protect_from_forgery except: [:create]
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity", "option_name"]
def create
extract_ipn_items_params.each do |key, values|
# this approach loops through all the returned results, nested attributes may help abstract this though
PaymentNotification.create(values)
render nothing: true
end
def details
# params.extract_ipn_items_params #this doesn't exist as params is an instance of ActionController::Parameters
PaymentNotification.update_attributes(line_item_id: params[:item_number], product_title: params[:item_name], option_name: params[:option_name], quantity: params[:quantity])
end
private
def additional_attributes
# create this for additional merge attributes. A better place for these would be the parent of this
{
params: params,
cart_id: params[:invoice],
status: params[:payment_status],
transaction_id: params[:txn_id],
first_name: params[:first_name],
last_name: params[:last_name],
email: params[:payer_email],
address_name: params[:address_name],
address_street: params[:address_street],
address_city: params[:address_city],
address_state: params[:address_state],
address_zip: params[:address_zip],
address_country: params[:address_country]
}
end
def extract_ipn_items_params
mod_params = Hash.new{|k, v| k[v] = {}.merge(additional_attributes) }
ITEM_PARAM_PREFIXES.each do |item_data_key|
key_tracker = 1
loop do
current_key = (item_data_key + key_tracker.to_s).to_sym
if params.include? current_key
mod_params[key_tracker][item_data_key] = params[current_key]
else
break
end
key_tracker += 1
end
end
mod_params
end
end
Let me know if that fixes your problem.
You should have payment_id so you can find it by using gem 'paypal-sdk-rest'
payment = PayPal::SDK::REST::Payment.find payment_id
then you could see all details in payment object

Instance Variables in a Rails Model

I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method

How to store method attributes in variable

class Dates
ATTRS = {date_from: '', date_to: ''}
def set_dates(ATTRS)
#date_from = date_from
#date_to = date_to
end
def show_dates(ATTRS)
p date_from
p date_to
end
end
Dates.new.set_dates(date_from: Time.current, date_to: Time.current)
#-:4: formal argument cannot be a constant
# def set_dates(ATTRS)
# ^
#-:9: formal argument cannot be a constant
# def show_dates(ATTRS)
SO question is: Is it possible to store method attributes in variable ?
You can use the new keyword arguments syntax in Ruby 2.0:
class Dates
def set_dates(date_from: '', date_to: '')
#date_from = date_from
#date_to = date_to
end
def show_dates
p #date_from
p #date_to
end
end
Or the hash arguments before Ruby 2.0:
class Dates
ATTRS = {date_from: '', date_to: ''}
def set_dates(attr)
attr = ATTRS.merge(attr)
#date_from = attr[:date_from]
#date_to = attr[:date_to]
end
def show_dates
p #date_from
p #date_to
end
end
For the show_dates method, I guess you meant showing the status of the Dates instance, so I made some modification to it.
Besides, in Ruby, variables started with capitalized letter are treated as constants, you cannot use it as formal arguments of the methods.
I would write your code as below using Ruby (2.0.0 > =) new keyword rest argument(**) operator :
class Dates
def set_dates(**attr)
#date_from,#date_to = attr.values_at(:date_from,:date_to)
end
def show_dates
p #date_from
p #date_to
end
end

How can I make a delayed job for this custom method?

Here is my Lesson model:
#encoding: utf-8
class Lesson < ActiveRecord::Base
attr_accessible :content, :title, :parsed_content, :html_content, :user_id
serialize :parsed_content, Array
serialize :html_content, Array
serialize :pinyin_content, Array
serialize :defined_content, Array
serialize :literal_content, Array
validates :title, :presence => true
validates :content, :presence => true
belongs_to :user
before_update do |lesson|
lesson.makesandwich
end
before_save do |lesson|
lesson.delay.makesandwich
end
def makesandwich
require 'rmmseg'
#require 'to_lang'
require 'bing_translator'
require 'ruby-pinyin'
self.parsed_content = []
RMMSeg::Dictionary.load_dictionaries
content = self.content
paragraphs = content.split(/\r\n\r\n/) #convert to array of paragraphs
self.parsed_content = paragraphs
paragraphs.each_with_index do |text, ti|
text = text.gsub("。", "^^.")
text = text.gsub("?", "~~?")
text = text.gsub("!", "||!")
text = text.gsub(":", ":") #fix missing colons
text = text.split(/[.?!]/u) #convert to an array
text.each do |s|
s.gsub!("^^", "。")
s.gsub!("~~", "?")
s.gsub!("||", "!")
#s.gsub!("———————————",":")
end
text.each_with_index do |val, index|
algor = RMMSeg::Algorithm.new(text[index])
splittext = []
loop do
tok = algor.next_token
break if tok.nil?
tex = tok.text.force_encoding('UTF-8')
splittext << tex
text[index] = splittext
end
paragraphs[ti] = text
end
end
bing = BingTranslator.new(BING_API)
self.parsed_content = paragraphs
textarray = Marshal.load(Marshal.dump(paragraphs))
self.defined_content = Marshal.load(Marshal.dump(paragraphs))
self.literal_content = Marshal.load(Marshal.dump(paragraphs))
self.pinyin_content = Marshal.load(Marshal.dump(paragraphs))
textarray.each_with_index do |paragraph, pi|
paragraph.each_with_index do |sentence, si|
sentence.each_with_index do |word, wi|
if DictionaryEntry.find_by_simplified(word) != nil
self.defined_content[pi][si][wi] = DictionaryEntry.find_by_simplified(word).definition
#self.literal_content is down below
self.pinyin_content[pi][si][wi] = DictionaryEntry.find_by_simplified(word).pinyin
else
self.defined_content[pi][si][wi] = bing.translate(word, :from => 'zh-CHS', :to => 'en')
#self.defined_content[pi][si][wi] = word
#self.literal_content is down below
if PinYin.of_string(word, true).length > 1 #for punctuation
self.pinyin_content[pi][si][wi] = PinYin.of_string(word, true).join(" ").downcase
else
self.pinyin_content[pi][si][wi] = word
end
end
end
end
end
#Literal
literalarray = Marshal.load(Marshal.dump(paragraphs))
literalarray.each_with_index do |paragraph, pi|
paragraph.each_with_index do |sentence, si| #iterate array of sentence
literalarray[pi][si] = []
sentence.each_with_index do |word, wi| #iterate sentence's array of words
entrytobesliced = DictionaryEntry.find_by_simplified(word)
slicedentry = []
if entrytobesliced == nil
if word.length > 1 && word !~ /\w/ #/^\s*\w\d+\s*$/ #number regex #for cases where there is no DictionaryEntry
split = []
wordarray = word.split("").each_with_index() do |ws, wsi|
split << [DictionaryEntry.find_by_simplified(ws).definition]
end
literalarray[pi][si] << split
else
literalarray[pi][si] << [word] #in case none of the above work
end
else
entrytobesliced.simplified.each_char do |w|
singlechar = DictionaryEntry.find_by_simplified(w)
slicedentry << singlechar.definition.split("\", \"")
end
literalarray[pi][si] << slicedentry
end
self.literal_content = literalarray #slicedentry #literalarray
end
end
end
end
end
When I try to create a new lesson it errors like this: Jobs cannot be created for records before they've been persisted
But if I change it to after_save instead of before_save then I can see the work run, but it doesn't update the serialized arrays in the database.
Can someone please help me implement delayed_jobs for this? It was working when I had:
before_save do |lesson|
lesson.makesandwich #no delay
end
I think you're getting these errors:
Jobs cannot be created for records before they've been persisted
because your Lesson instances won't have an id until they've been saved and without an id, DJ has no way to know which instance it should be working with. So you have to use an after_save so that your Lesson has an id and can be uniquely identified. But then your updates from the delayed job won't be saved because, well, nothing asks for them to be saved. You should be able to get around that simply by adding a self.save or self.save! call at the end of makesandwich.

Resources