ActiveRecord build dynamic query conditions - ruby-on-rails

I have a piece of code that seems to work. I just think there may be a much better way to achieve the desired work. The problem is to build an ActiveRecord query with an unknown list of parameters.
Here is the code:
query_string = String.new
query_values = []
unless params[:organization][:name].blank?
query_string << 'name = ?'
query_values << params[:organization][:name]
end
unless params[:organization][:national_id].blank? && params[:organization][:vat_id].blank?
raise RequestParamsException.new('National ID or Vat ID given without country') if params[:organization][:country].nil?
country_id = Country.find_by_name(params[:organization][:country]).pluck(:id)
unless params[:organization][:national_id].blank?
query_string << ' OR ' unless query_string.empty?
query_string << '(national_id = ?'
query_values << params[:organization][:national_id]
query_string << ' AND ' << 'country_id = ?)'
query_values << country_id
end
unless params[:organization][:vat_id].blank?
query_string << ' OR ' unless query_string.empty?
query_string << '(vat_id = ?'
query_values << params[:organization][:vat_id]
query_string << ' AND ' << 'country_id = ?)'
query_values << country_id
end
end
known_organizations = query_string.blank? ? [] : Organization.where(query_string, query_values).uniq
Country is needed when a Vat or National Id are given since these are scoped in the model:
class Organization < ActiveRecord::Base
#======================VALIDATIONS=========================
validates :national_id, :uniqueness => { :scope => :country_id }, :allow_blank => true
validates :vat_id, :uniqueness => { :scope => :country_id }, :allow_blank => true
validates :country, :presence => true
end

You can take advantage of Arel. For example when you write:
posts = Post.where(author_id: 12)
this query isn't executed unless you start iterating on posts or call posts.all. So you can write something like this:
def search_posts
posts = Post.where(active: true)
posts = posts.where('body ilike ?', "%#{params[:query]%") unless params[:query].blank?
posts
end
This simple example shows how to achieve behavior you are looking for.

Related

Can not seem to pull out the attributes of this object

I have a method where an object is called but I can not seem to pull out the attributes of this object and I do not understand why.
def set_cashier
test = User.first
result = test.login
Rails.logger.debug "User is: #{result}"
as I set a breakpoint on the second line in rubymine IDE (I can see the following)
def set_cashier
test = User.first test: #<User:0x00000004809ea0>
result = test.login result: nil test: #<User:0x00000004809ea0>
Rails.logger.debug "User is: #{result}"
I know I have attributes on this object like id and login. When I run the debugger in my IDE I can see #attributes = Hash(17 elements) and I can see them listed inside as 'id' = "433" and 'login' = "firstname.lastname" etc...in the rubymine debugger it looks sort of like this...
result = nil
test = {User}#<User:0x00000004809ea0>
#attributes = Hash(17 elements)
'id' = "433"
'login' = "firstname.lastname"
...
How do I return the value of 'login'?
test = User.first seems to give an object that I can see in the debugger Variables tab that I can open and see a value for "#attributes = Hash(17 elements)" and I can see those values inside there....and yet...
"result = test.login" gives a nil result which so confusing
I would think that "test = User.first.login" should work ...
def set_cashier
test = User.first.login
result = test
Rails.logger.debug "User.first is: #{result}"
but this gives the same error ...so confusing.
(the full error displayed in the browser looks like so...)
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.map
I've been at this for a few days now. If this question does not make sense please let me know.
Thank you for your time.
UPDATED:
Per request of a commenter I'm including the code for the User model
(By the way...I inherited this code...so nothing in this file was written by me)
Note:
Rails 3.1 (updated from a rails 2.x app)
ruby 1.9.3p551
user.rb
class User < ActiveRecord::Base
acts_as_userstamp
has_and_belongs_to_many "roles"
# Virtual attribute for the unencrypted password
attr_accessor :password
validates_presence_of :login, :email
validates_presence_of :password, :if => :password_required?
validates_presence_of :password_confirmation, :if => :password_required?
validates_length_of :password, :within => 4..40, :if => :password_required?, :allow_nil => true
validates_confirmation_of :password, :if => :password_required?
validates_length_of :login, :within => 3..40, :allow_nil => true
validates_length_of :email, :within => 3..100, :allow_nil => true
validates_uniqueness_of :login, :email, :case_sensitive => false
validates_uniqueness_of :cashier_code, :if => :cashier_code
validates_format_of :login, :with => /[^0-9]/, :message => "must contain a non-numeric character"
before_save :encrypt_password
before_save :add_cashier_code
before_save :disable_reason_cannot_login_on_reenable
def disable_reason_cannot_login_on_reenable
return unless self.can_login && self.can_login_changed?
self.reason_cannot_login = "" if self.reason_cannot_login && self.reason_cannot_login.length > 0
end
belongs_to :contact
has_one :skedjulnator_access
####################################################
# I HAVE NO IDEA WHAT THIS IS HERE FOR, BUT IF YOU #
# FORGET ABOUT IT YOU WILL SPEND AN HOUR TRYING TO #
# FIGURE OUT WHAT YOU DID WRONG #
####################################################
# prevents a user from submitting a crafted form that bypasses activation
# anything else you want your user to change should be added here.
attr_accessible :login, :email, :password, :password_confirmation, :can_login, :shared
scope :can_login, {:conditions => ["can_login = 't'"]}
def self.hidden_columns
super + [:crypted_password, :salt]
end
def can_view_disciplinary_information?
!! (self.contact and self.contact.worker and self.contact.worker.worker_type_today and self.contact.worker.worker_type_today.name != 'inactive')
end
def update_skedjulnator_access_time
self.skedjulnator_access ||= SkedjulnatorAccess.new
self.skedjulnator_access.user_id_will_change!
self.skedjulnator_access.save!
end
def grantable_roles
self.roles.include?(Role.find_by_name('ADMIN')) ? Role.find(:all) : self.roles
end
def to_s
login
end
def self.reset_all_cashier_codes
self.find(:all).each{|x|
x.reset_cashier_code
x.save
}
end
def contact_display_name
self.contact ? self.contact.display_name : self.login
end
def add_cashier_code
reset_cashier_code if !self.shared and cashier_code.nil?
end
def reset_cashier_code
valid_codes = (1000..9999).to_a - User.find(:all).collect{|x| x.cashier_code}
my_code = valid_codes[rand(valid_codes.length)]
self.cashier_code = my_code
end
def merge_in(other)
for i in [:actions, :donations, :sales, :types, :users, :volunteer_tasks, :contacts, :gizmo_returns]
User.connection.execute("UPDATE #{i.to_s} SET created_by = #{self.id} WHERE created_by = #{other.id}")
User.connection.execute("UPDATE #{i.to_s} SET updated_by = #{self.id} WHERE updated_by = #{other.id}")
end
["donations", "sales", "volunteer_tasks", "disbursements", "recyclings", "contacts"].each{|x|
User.connection.execute("UPDATE #{x.to_s} SET cashier_created_by = #{self.id} WHERE cashier_created_by = #{other.id}")
User.connection.execute("UPDATE #{x.to_s} SET cashier_updated_by = #{self.id} WHERE cashier_updated_by = #{other.id}")
}
self.roles = (self.roles + other.roles).uniq
self.save!
end
# Authenticates a user by their login name and unencrypted password. Returns the user or nil.
def self.authenticate(login, password)
if login.to_i.to_s == login
u = find_by_contact_id(login.to_i)
else
u = find_by_login(login) # need to get the salt
end
return u if u && u.can_login && u.authenticated?(password)
return nil
end
# Encrypts some data with the salt.
def self.encrypt(password, salt)
Digest::SHA1.hexdigest("--#{salt}--#{password}--")
end
# Encrypts the password with the user salt
def encrypt(password)
self.class.encrypt(password, salt)
end
def authenticated?(password)
crypted_password == encrypt(password)
end
def remember_token?
remember_token_expires_at && Time.now.utc < remember_token_expires_at
end
# These create and unset the fields required for remembering users between browser closes
def remember_me
remember_me_for 2.weeks
end
def remember_me_for(time)
remember_me_until time.from_now.utc
end
def remember_me_until(time)
self.remember_token_expires_at = time
self.remember_token = encrypt("#{email}--#{remember_token_expires_at}")
save(false)
end
def forget_me
self.remember_token_expires_at = nil
self.remember_token = nil
save(false)
end
# start auth junk
def User.current_user
Thread.current['user'] || User.fake_new
end
attr_accessor :fake_logged_in
def User.fake_new
u = User.new
u.fake_logged_in = true
u
end
def logged_in
! fake_logged_in
end
def to_privileges
return "logged_in" if self.logged_in
end
def privileges
#privileges ||= _privileges
end
def _privileges
olda = []
return olda if !self.can_login
a = [self, self.contact, self.contact ? self.contact.worker : nil, self.roles].flatten.select{|x| !x.nil?}.map{|x| x.to_privileges}.flatten.select{|x| !x.nil?}.map{|x| Privilege.by_name(x)}
while olda != a
a = a.select{|x| !x.restrict} if self.shared
olda = a.dup
a << olda.map{|x| x.children}.flatten
a = a.flatten.sort_by(&:name).uniq
a = a.select{|x| !x.restrict} if self.shared
end
a = a.map{|x| x.name}
a
end
def has_privileges(*privs)
positive_privs = []
negative_privs = []
privs.flatten!
for i in privs
if i.match(/^!/)
negative_privs << i.sub(/^!/, "")
else
positive_privs << i
end
end
if positive_privs.length > 0
positive_privs << "role_admin"
end
if negative_privs.length > 0
negative_privs << "role_admin"
end
my_privs = self.privileges
#puts "NEG: #{negative_privs.inspect}, POS: #{positive_privs.inspect}, MY: #{my_privs.inspect}"
return (negative_privs & my_privs).length == 0 && ((positive_privs & my_privs).length > 0 || positive_privs.length == 0)
end
# end auth junk
protected
# before filter
def encrypt_password
return if password.blank?
self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
self.crypted_password = encrypt(password)
end
def password_required?
crypted_password.blank? || !password.blank?
end
end
Eliminating the IDE as source of problem:
So I ran Rails console and got the following error...
irb(main):001:0> User.first
User Load (0.5ms) SELECT "users".* FROM "users" LIMIT 1
(0.1ms) SHOW search_path
User Indexes (1.1ms) SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = 'users'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN ('"$user"','public') )
ORDER BY i.relname
User Indexes (0.4ms) SELECT c2.relname, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true)
FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
WHERE c.relname = 'users'
AND c.oid = i.indrelid AND i.indexrelid = c2.oid
AND i.indisprimary = 'f'
AND i.indexprs IS NOT NULL
ORDER BY 1
(Object doesn't support #inspect)
=>
irb(main):002:0>
...very confusing

Filterrific and Globalize

It appears as if filterrific does not take content in translation tables into account (Globalize).
Is there anyway to search translation tables as well? My setup works perfectly well if the content is in the actual model. However, once the fields are empty and only entered in the translation table no results are being displayed (obviously).
My Model:
class Manual < ApplicationRecord
translates :title, :content, :teaser, :slug
extend FriendlyId
friendly_id :title, :use => :globalize
belongs_to :user
belongs_to :support_category
has_many :manual_faqs
has_many :faqs, :through => :manual_faqs
validates :title, presence: true
validates :content, presence: true
validates :user_id, presence: true
update_index('manuals#manual') { self }
filterrific(
default_filter_params: { sorted_by: 'created_at_desc' },
available_filters: [
:sorted_by,
:search_query,
:with_user_id,
:with_created_at_gte
]
)
scope :with_user_id, lambda { |user_ids|
where(user_id: [*user_ids])
}
scope :search_query, lambda { |query|
# Searches the students table on the 'first_name' and 'last_name' columns.
# Matches using LIKE, automatically appends '%' to each term.
# LIKE is case INsensitive with MySQL, however it is case
# sensitive with PostGreSQL. To make it work in both worlds,
# we downcase everything.
return nil if query.blank?
# condition query, parse into individual keywords
terms = query.downcase.split(/\s+/)
# replace "*" with "%" for wildcard searches,
# append '%', remove duplicate '%'s
terms = terms.map { |e|
('%' + e.gsub('*', '%') + '%').gsub(/%+/, '%')
}
# configure number of OR conditions for provision
# of interpolation arguments. Adjust this if you
# change the number of OR conditions.
num_or_conds = 2
where(
terms.map { |term|
"(LOWER(manuals.title) LIKE ? OR LOWER(manuals.content) LIKE ?)"
}.join(' AND '),
*terms.map { |e| [e] * num_or_conds }.flatten
)
}
scope :sorted_by, lambda { |sort_option|
# extract the sort direction from the param value.
direction = (sort_option =~ /desc$/) ? 'desc' : 'asc'
case sort_option.to_s
when /^created_at_/
# Simple sort on the created_at column.
# Make sure to include the table name to avoid ambiguous column names.
# Joining on other tables is quite common in Filterrific, and almost
# every ActiveRecord table has a 'created_at' column.
order("manuals.created_at #{ direction }")
else
raise(ArgumentError, "Invalid sort option: #{ sort_option.inspect }")
end
}
scope :created_at_gte, lambda { |reference_time|
where('manuals.created_at >= ?', reference_time)
}
def self.options_for_sorted_by
[
['Date received (newest first)', 'created_at_desc'],
['Date received (oldest first)', 'created_at_asc']
]
end
end
My Controller:
def index
#filterrific = initialize_filterrific(
Manual,
params[:filterrific],
select_options: {
sorted_by: Manual.options_for_sorted_by,
with_user_id: User.options_for_select
}
) or return
#manuals = #filterrific.find.page(params[:page])
respond_to do |format|
format.html
format.js
end
rescue ActiveRecord::RecordNotFound => e
# There is an issue with the persisted param_set. Reset it.
puts "Had to reset filterrific params: #{ e.message }"
redirect_to(reset_filterrific_url(format: :html)) and return
#respond_with(#references)
end
I don't know filterrific at all but I do know Globalize, and since filterrific is based on AR scopes it should be simply a matter of joining the translation table to get results to show up.
Here's your search_query scope modified to join and search the joined translations table (without the comments for clarity):
scope :search_query, lambda { |query|
return nil if query.blank?
terms = query.downcase.split(/\s+/)
terms = terms.map { |e|
('%' + e.gsub('*', '%') + '%').gsub(/%+/, '%')
}
num_or_conds = 2
where(
('(LOWER(manual_translations.title) LIKE ? OR'\
' LOWER(manual_translations.content) LIKE ?)' * (terms.count)).join(' AND '),
*terms.map { |e| [e] * num_or_conds }.flatten
).with_translations
}
Notice I've only changed two things: (1) I've appended with_translations, a method described in this SO answer which joins the translations for the current locale, and (2) I've swapped the manuals table for the manual_translations table in the query.
So if you call this query in the English locale:
Manual.search_query("foo")
you get this SQL:
SELECT "manuals".* FROM "manuals"
INNER JOIN "manual_translations" ON "manual_translations"."manual_id" = "manuals"."id"
WHERE (LOWER(manual_translations.title) LIKE '%foo%' OR
LOWER(manual_translations.content) LIKE '%foo%')
AND "manual_translations"."locale" = 'en'"
Notice that with_translations is automatically tagging on that manual_translations.locale = 'en' so you filter out only results in your locale, which I assume is what you want.
Let me know if that works for you.

How to upload CSV with Paperclip

I have looked over the other posts about creating a CSV with Paperclip but am still a bit lost on why this isn't working. I have a method that generates a CSV string (using CSV.generate), and I try to save it to a Report in the Reports Controller with the following method:
def create(type)
case type
when "Geo"
csv_string = Report.generate_geo_report
end
#report = Report.new(type: type)
#report.csv_file = StringIO.new(csv_string)
if #report.save
puts #report.csv_file_file_name
else
#report.errors.full_messages.to_sentence
end
end
However, upon execution, I get a undefined method 'stringify_keys' for "Geo":String error. Here is the Report model:
class Report < ActiveRecord::Base
attr_accessible :csv_file, :type
has_attached_file :csv_file, PAPERCLIP_OPTIONS.merge(
:default_url => "//s3.amazonaws.com/production-recruittalk/media/avatar-placeholder.gif",
:styles => {
:"259x259" => "259x259^"
},
:convert_options => {
:"259x259" => "-background transparent -auto-orient -gravity center -extent 259x259"
}
)
def self.generate_geo_report
male_count = 0
female_count = 0
csv_string = CSV.generate do |csv|
csv << ["First Name", "Last Name", "Email", "Gender", "City", "State", "School", "Created At", "Updated At"]
Athlete.all.sort_by{ |a| a.id }.each do |athlete|
first_name = athlete.first_name || ""
last_name = athlete.last_name || ""
email = athlete.email || ""
if !athlete.sports.blank?
if athlete.sports.first.name.split(" ", 2).first.include?("Women's")
gender = "Female"
female_count += 1
else
gender = "Male"
male_count += 1
end
else
gender = ""
end
city = athlete.city_id? ? athlete.city.name : ""
state = athlete.state || ""
school = athlete.school_id? ? athlete.school.name : ""
created_at = "#{athlete.created_at.to_date.to_s[0..10].gsub(" ", "0")} #{athlete.created_at.to_s.strip}"
updated_at = "#{athlete.updated_at.to_date.to_s[0..10].gsub(" ", "0")} #{athlete.updated_at.to_s.strip}"
csv << [first_name, last_name, email, gender, city, state, school, created_at, updated_at]
end
csv << []
csv << []
csv << ["#{male_count}/#{Athlete.count} athletes are men"]
csv << ["#{female_count}/#{Athlete.count} athletes are women"]
csv << ["#{Athlete.count-male_count-female_count}/#{Athlete.count} athletes have not declared a gender"]
end
return csv_string
end
end
This is being called from a cron job rake task:
require 'csv'
namespace :reports do
desc "Geo-report"
task :generate_nightly => :environment do
Report.create("Geo")
end
end
Not sure where to begin on getting this functional. Any suggestions? I've been reading Paperclip's doc but I'm a bit of a newbie to it.
Thank you!
There's a lot going on here :)
First, it looks like you're getting your controller and model confused. In the rake task, Report is the model, but you're calling create as if it was the controller method. Models (aka ActiveRecord classes) take a key/value pair:
Report.create(type: "Geo")
Another issue is that you're using "type" for the name of your column, and this will tell ActiveRecord that you're using single table inheritance. That means that you have subclasses of Report. Unless you really want STI, you should rename this column.
Finally, you shouldn't have a controller method that takes an argument. I'm not really sure what you're trying to do there, but controller get their arguments via the params hash.

In Rails 3.1, how can I create an HTML table generator that uses block style formatting

I'm developing an application that displays tabular data in many different areas and I find myself constantly using the same HTML table structure over and over. For example a particular table looks like this:
%table.zebra-striped#user-table{ :cellspacing => "0" }
%colgroup
%col{:id => "email"}
%col{:id => "username"}
%col{:id => "sign-in-count"}
%col{:id => "last-sign-in-at"}
%thead
%tr
%th{:id => "email-head", :scope => "col"} E-mail
%th{:id => "username-head", :scope => "col"} Username
%th{:id => "sign-in-count-head", :scope => "col"} Sign Ins
%th{:id => "last-sign-in-at-head", :scope => "col"} Last Sign In
%tbody
- #users.each do |user|
%tr{ :class => zebra }
%td
=h user.email
%td
=h user.username
%td
=h user.sign_in_count
%td
=h user.last_sign_in_at
Ideally, I would like to create some kind of helper method where I could do something like:
= custom_table_for #users do
= column :email
= column :username do |user|
= link_to user.username, user_path(user)
= column "Sign Ins", :sign_in_count
= column :last_sign_in_at
This way I can change the formatting of the data in the columns and the column header names if I'm not happy with default values, but have the table generated for me.
I suppose I could create a normal helper, but I'd have to use arrays and I have no idea how I could include custom data formatting per column.
active_admin has something similar to this which you can see here: http://activeadmin.info/docs/3-index-pages/index-as-table.html
Any leads or ideas would be greatly appreciated.
I just came up with this:
A few points:
The line #columns = [] is a reset so you can call it more than once.
The yield in the custom_table_for calls the block that you pass it.
The block in the column method is stored and called in custom_table_for if it is set.
I included a sample class to show the usage too.
please note I did this outside of a rails app and you almost certainly want to use http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag instead of the p "<table>" this is merely for sample purposes when you run it in the console.
module TableHelper
def custom_table_for(items)
#columns = []
yield
p "<table>"
#columns.each do |c|
p "<th>#{c[:value]}</th>"
end
items.each do |e|
p "<tr>"
#columns.each do |c|
e[c[:name]] = c[:block].call(e[c[:name]]) if c[:block]
p "<td>#{e[c[:name]]}</td>"
end
p "</tr>"
end
p "</table>"
end
def column(name, value = nil, &block)
value = name unless value
#columns << {:name => name, :value => value, :block => block}
end
end
class ExampleTable
include TableHelper
def test
#users = [{:email => "Email 1", :username => "Test User"}, {:email => "Email 2", :username => "Test User 2"}]
custom_table_for #users do
column :email, "Email"
column :username do |user|
user.upcase
end
end
end
end
et = ExampleTable.new
et.test
UPDATE
I migrated this to rails to use content_tags
module TableHelper
def custom_table_for(items)
#columns = []
yield
content_tag :table do
thead + tbody(items)
end
end
def thead
content_tag :thead do
content_tag :tr do
#columns.each do |c|
concat(content_tag(:th, c[:value]))
end
end
end
end
def tbody(items)
content_tag :tbody do
items.each { |e|
concat(content_tag(:tr){
#columns.each { |c|
e[c[:name]] = c[:block].call(e[c[:name]]) if c[:block]
concat(content_tag(:td, e[c[:name]]))
}
})
}
end
end
def column(name, value = nil, &block)
value = name unless value
#columns << {:name => name, :value => value, :block => block}
end
end
To compliment #gazler's response, here's a way to make a table of a single resource-- column one for attribute names, column two for their values:
module TableHelper
#resource = nil
def simple_table_for(resource)
#resource = resource
content_tag :table do
content_tag :tbody do
yield
end
end
end
def row(key, label = nil, &block)
if key.is_a? String
label = key
end
content_tag(:tr) {
concat content_tag :td, label || key.capitalize
concat content_tag(:td ){
if block_given?
yield
else
#resource.send(key)
end
}
}
end
end

Bulk insert using one model

I'm trying to create a form using textarea and a submit button that will allow users to do bulk insert. For example, the input would look like this:
0001;MR A
0002;MR B
The result would look like this:
mysql> select * from members;
+------+------+------+
| id | no | name |
+------+------+------+
| 1 | 0001 | MR A |
+------+------+------+
| 2 | 0002 | MR B |
+------+------+------+
I'm very new to Rails and I'm not sure on how to proceed with this one. Should I use attr_accessor? How do I handle failed validations in the form view? Is there any example? Thanks in advance.
Update
Based on MissingHandle's comment, I created a Scaffold and replace the Model's code with this:
class MemberBulk < ActiveRecord::Base
attr_accessor :member
def self.columns
#columsn ||= []
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :data, :text
validates :data, :create_members, :presence => true
def create_members
rows = self.data.split("\r\n")
#member = Array.new
rows.each_with_index { |row, i|
rows[i] = row.strip
cols = row.split(";")
p = Member.new
p.no = cols[0]
p.name = cols[1]
if p.valid?
member << p
else
p.errors.map { |k, v| errors.add(:data, "\"#{row}\" #{v}") }
end
}
end
def create_or_update
member.each { |p|
p.save
}
end
end
I know the code is far from complete, but I need to know is this the correct way to do it?
class MemberBulk < ActiveRecord::Base
#Tells Rails this is not actually tied to a database table
# or is it self.abstract_class = true
# or #abstract_class = true
# ?
abstract_class = true
# members holds array of members to be saved
# submitted_text is the data submitted in the form for a bulk update
attr_accessor :members, :submitted_text
attr_accessible :submitted_text
before_validation :build_members_from_text
def build_members_from_text
self.members = []
submitted_text.each_line("\r\n") do |member_as_text|
member_as_array = member_as_text.split(";")
self.members << Member.new(:number => member_as_array[0], :name => member_as_array[1])
end
end
def valid?
self.members.all?{ |m| m.valid? }
end
def save
self.members.all?{ |m| m.save }
end
end
class Member < ActiveRecord::Base
validates :number, :presence => true, :numericality => true
validates :name, :presence => true
end
So, in this code, members is an array that is a collection of the individual Member objects. And my thinking is that as much as possible, you want to hand off work to the Member class, as it is the class that will actually be tied to a database table, and on which you can expect standard rails model behavior. In order to accomplish this, I override two methods common to all ActiveRecord models: save and valid. A MemberBulk will only be valid if all it's members are valid and it will only count as saved if all of it's members are saved. You should probably also override the errors method to return the errors of it's underlying members, possibly with an indication of which one it is in the submitted text.
In the end I had to change from using Abstract Class to Active Model (not sure why, but it stoppped working the moment I upgrade to Rails v3.1). Here's the working code:
class MemberBulk
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :input, :data
validates :input, presence: true
def initialize(attributes = {})no
attributes.each do |name, value|
send("#{name}=", value) if respond_to?("#{name}=")
end
end
def persisted?
false
end
def save
unless self.valid?
return false
end
data = Array.new
# Check for spaces
input.strip.split("\r\n").each do |i|
if i.strip.empty?
errors.add(:input, "There shouldn't be any empty lines")
end
no, nama = i.strip.split(";")
if no.nil? or nama.nil?
errors.add(:input, "#{i} doesn't have no or name")
else
no.strip!
nama.strip!
if no.empty? or nama.empty?
errors.add(:input, "#{i} doesn't have no or name")
end
end
p = Member.new(no: no, nama: nama)
if p.valid?
data << p
else
p.errors.full_messages.each do |error|
errors.add(:input, "\"#{i}\": #{error}")
end
end
end # input.strip
if errors.empty?
if data.any?
begin
data.each do |d|
d.save
end
rescue Exception => e
raise ActiveRecord::Rollback
end
else
errors.add(:input, "No data to be processed")
return false
end
else
return false
end
end # def
end

Resources