I have code to check if a CSV file is correct. I would like to write idiomatic code to check if the columns are correct. I don't want to write in block check if we have got first line check columns.
CSV.foreach(#csv) { |person|
first_name, last_name, person_id, email, title, phone, mobile, department, address, city = person[0..9]
zip_code, state, country, manager_id =person[10..13]
#managers << manager_id
#persons << person_id
validate = false unless validate_email(email)
validate = false unless validate_first_name(first_name)
validate = false unless validate_last_name(last_name)
validate = false unless validate_person_id(person_id)
}
Does the CSV have headers or can you add them? If yes you can do CSV.foreach(#csv, :headers => true) and get the column values like person['first_name']. Then the checks at the end become
validate = false unless validate_email(person['email'])
That said, it seems like your entire validation at the end could be written as
validate_email(person['email']) && validate_first_name(person['first_name']) etc.
+1 for Michael's advice for :headers => true
But if you want to validate all of the named fields in your example and are aiming for DRY (though, admittedly, maybe a bit too clever for maintainability), you could use Enumerable#inject:
CSV.foreach(#cvs, :headers => true) { |person|
#managers << person[:manager_id]
#persons << person[:person_id]
# Array of columns to be validated
validate_cols = [:first_name, :last_name, :person_id, :email, :title, :phone,
:mobile, :department, :address, :city, :zip_code, :state,
:country, :manager_id]
valid = validate_cols.inject(true){|valid_sum, col|
valid_sum && send("validate_#{col}", person[col])
}
}
This assumes you have validate_* methods for every column named in the validate_cols array.
Related
I want to generate forms for a resource that has a postgres jsonb column :data, and I want the schema for these forms to be stored in a table in the database. After a lot of research I am 90% there but my method fails in ActiveAdmin forms upon create (not update). Can anyone explain this?
Sorry for the long code snippets. This is a fairly elaborate setup but I think it would be of some interest since if this works one could build arbitrary new schemas dynamically without hard-coding.
I am following along this previous discussion with Rails 6 and ActiveAdmin 2.6.1 and ruby 2.6.5.
I want to store Json Schemas in a table SampleActionSchema that belong_to SampleAction (using the json-schema gem for validation)
class SampleActionSchema < ApplicationRecord
validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
validate :schema_is_json_schema
private
def schema_is_json_schema
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
unless JSON::Validator.validate(metaschema, schema)
errors.add :schema, 'not a compliant json schema'
end
end
end
class SampleAction < ActiveRecord::Base
belongs_to :sample
validate :is_sample_action
validates :name, uniqueness: { case_sensitive: false }
after_initialize :add_field_accessors
before_create :add_field_accessors
before_update :add_field_accessors
def add_store_accessor field_name
singleton_class.class_eval {store_accessor :data, field_name.to_sym}
end
def add_field_accessors
num_fields = schema_properties.try(:keys).try(:count) || 0
schema_properties.keys.each {|field_name| add_store_accessor field_name} if num_fields > 0
end
def schema_properties
schema_arr=SampleActionSchema.where(category: category)
if schema_arr.size>0
sc=schema_arr[0]
if !sc.schema.empty?
props=sc.schema["properties"]
else
props=[]
end
else
[]
end
end
private
def is_sample_action
sa=SampleActionSchema.where(category: category)
errors.add :category, 'not a known sample action' unless (sa.size>0)
errors.add :base, 'incorrect json format' unless (sa.size>0) && JSON::Validator.validate(sa[0].schema, data)
end
end
This all works correctly; For example, for a simple schema called category: "cleave", where :data looks like data: {quality: "good"}, I can create a resource as follows in the rails console:
sa=SampleAction.new(sample_id: 6, name: "test0", data: {}, category: "cleave" )
=> #<SampleAction id: nil, name: "test0", category: "cleave", data: {}, created_at: nil, updated_at: nil, sample_id: 6>
sa.quality = "good" => true
sa.save => true
To make this system work in AA forms, I call the normal path (new or edit)_admix_sample_action_form with params: {category: "cleave"} and then I generate permit_params dynamically:
ActiveAdmin.register SampleAction, namespace: :admix do
permit_params do
prms=[:name, :category, :data, :sample_id, :created_at, :updated_at]
#the first case is creating a new record (gets parameter from admix/sample_actions/new?category="xxx"
#the second case is updating an existing record
#falls back to blank (no extra parameters)
categ = #_params[:category] || (#_params[:sample_action][:category] if #_params[:sample_action]) || nil
cat=SampleActionSchema.where(category: categ)
if cat.size>0 && !cat[0].schema.empty?
cat[0].schema["properties"].each do |key, value|
prms+=[key.to_sym]
end
end
prms
end
form do |f|
f.semantic_errors
new=f.object.new_record?
cat=params[:category] || f.object.category
f.object.category=cat if cat && new
f.object.add_field_accessors if new
sas=SampleActionSchema.where(category: cat)
is_schema=(sas.size>0) && !sas[0].schema.empty?
if session[:active_sample]
f.object.sample_id=session[:active_sample]
end
f.inputs "Sample Action" do
f.input :sample_id
f.input :name
f.input :category
if !is_schema
f.input :data, as: :jsonb
else
f.object.schema_properties.each do |key, value|
f.input key.to_sym, as: :string
end
end
end
f.actions
end
Everything works fine if I am editing an existing resource (as created in the console above). The form is displayed and all the dynamic fields are updated upon submit. But when creating a new resource where e.g. :data is of the form data: {quality: "good"} I get
ActiveModel::UnknownAttributeError in Admix::SampleActionsController#create
unknown attribute 'quality' for SampleAction.
I have tried to both add_accessors in the form and to override the new command to add the accessors after initialize (these should not be needed because the ActiveRecord callback appears to do the job at the right time).
def new
build_resource
resource.add_field_accessors
new!
end
Somehow when the resource is created in the AA controller, it seems impossible to get the accessors stored even though it works fine in the console. Does anyone have a strategy to initialize the resource correctly?
SOLUTION:
I traced what AA was doing to figure out the minimum number of commands needed. It was necessary to add code to build_new_resource to ensure that any new resource AA built had the correct :category field, and once doing so, make the call to dynamically add the store_accessor keys to the newly built instance.
Now users can create their own original schemas and records that use them, without any further programming! I hope others find this useful, I certainly will.
There are a couple ugly solutions here, one is that adding the parameters to the active admin new route call is not expected by AA, but it still works. I guess this parameter could be passed in some other way, but quick and dirty does the job. The other is that I had to have the form generate a session variable to store what kind of schema was used, in order for the post-form-submission build to know, since pressing the "Create Move" button clears the params from the url.
The operations are as follows: for a model called Move with field :data that should be dynamically serialized into fields according to the json schema tables, both
admin/moves/new?category="cleave" and admin/moves/#/edit find the "cleave" schema from the schema table, and correctly create and populate a form with the serialized parameters. And, direct writes to the db
m=Move.new(category: "cleave") ==> true
m.update(name: "t2", quality: "fine") ==> true
work as expected. The schema table is defined as:
require "json-schema"
class SampleActionSchema < ApplicationRecord
validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
validate :schema_is_json_schema
def self.schema_keys(categ)
sas=SampleActionSchema.find_by(category: categ)
schema_keys= sas.nil? ? [] : sas[:schema]["properties"].keys.map{|k| k.to_sym}
end
private
def schema_is_json_schema
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
unless JSON::Validator.validate(metaschema, schema)
errors.add :schema, 'not a compliant json schema'
end
end
end
The Move table that employs this schema is:
class Move < ApplicationRecord
after_initialize :add_field_accessors
def add_field_accessors
if category!=""
keys=SampleActionSchema.schema_keys(category)
keys.each {|k| singleton_class.class_eval{store_accessor :data, k}}
end
end
end
Finally, the working controller:
ActiveAdmin.register Move do
permit_params do
#choice 1 is for new records, choice 2 is for editing existing
categ = #_params[:category] || (#_params[:move][:category] if #_params[:move]) || ""
keys=SampleActionSchema.schema_keys(categ)
prms = [:name, :data] + keys
end
form do |f|
new=f.object.new_record?
f.object.category=params[:category] if new
if new
session[:current_category]=params[:category]
f.object.add_field_accessors
else
session[:current_category] = ""
end
keys=SampleActionSchema.schema_keys(f.object.category)
f.inputs do
f.input :name
f.input :category
keys.each {|k| f.input k}
end
f.actions
end
controller do
def build_new_resource
r=super
r.assign_attributes(category: session[:current_category])
r.add_field_accessors
r
end
end
end
I have one problem when I try to save some data into my database, imported from a CSV file (uploaded).
My environment is about a classroom reservation. I have the following code for my model Reservation:
class Reservation < ActiveRecord::Base
require 'csv'
belongs_to :classroom
validates :start_date, presence: true
validates :end_date, presence: true
validates :classroom_id, presence: true
validate :validate_room
scope :filter_by_room, ->(room_id) { where 'classroom_id = ?' % room_id }
def self.import(file)
CSV.foreach(file, headers: true ) do |row|
room_id = Classroom.where(number: row[0]).pluck(:id)
Reservation.create(classroom_id: room_id, start_date: row[1], end_date: row[2])
end
end
private
def validate_room
if Reservation.filter_by_room(classroom_id).nil?
errors.add(:classroom_id, ' has already booked')
end
end
end
The CSV file comes with these three headers: "classroom number", "start date", "end date".
Note that "classroom number" header came from a column of classroom table.
My job is to get the classroom.id using the "number" and create the row in the database of the reservation table.
Ok, but the problem is when I get the classroom_id in "self.import" method and print on the console, he exists. When I use the scope to filter the classroom_id, he is empty.
Expect I've expressed myself like I want.
Sorry for my bad English :/
Edit: Discovered that classroom_id before Reservation.create become nil when I use inside the create method. If I use row[0] works, but I need to use classroom_id.
{ where 'classroom_id = ?' % room_id }
Should be
{ where 'classroom_id = ?', room_id }
The answer is simple, I forgot to use .first after pluck(:id) method.
The pluck method returns a value wrapped in an array:
room_id = Classroom.where(number: row[0]).pluck(:id).first
I have a rails question that I have been unable to find an answer for on my own. I apologize if it's very simple or obvious, I'm a complete beginner here.
So I have a column in my db called :client_code, which is defined in the model as the down-cased concatenation of the first character of :first_name, and :last_name. So for instance, if I pull up a new form and enter 'John' for :first_name, and 'Doe' for :last_name, :client_code is automatically given the value of 'jdoe'. Here's the relevant portion of the model code:
class Client < ActiveRecord::Base
before_validation :client_code_default_format
validates_presence_of :first_name, :last_name, :email
validates_uniqueness_of :client_code
...
def client_code_default_format
self.client_code = "#{first_name[0]}#{last_name}".downcase
end
end
I would like to add something to this code so that, in the event that someone enters a different client with the same exact name, it does't fail the uniqueness validation but instead creates a slightly modified :client_code ('jdoe2', for example). I could probably figure out how to add an index to all of them, but I would prefer to only include numbers as a failsafe in the case of duplicates. Can anyone point me in the right direction?
Calculating the number of current matching Client objects with the same client_code should work
def client_code_default_format
preferred_client_code = "#{first_name[0]}#{last_name}".downcase
count = Client.count(:conditions => "client_code = #{preferred_client_code}")
self.client_code = count == 0 ? preferred_client_code : preferred_client_code + count
end
Many thanks to #Dieseltime for his response. I took his suggestion and was able to get the functionality I wanted with some minor changes:
before_validation :format_client_code
validates_presence_of :first_name, :last_name, :email, :company_id
validates_uniqueness_of :client_code
...
def format_client_code
unless self.client_code != nil
default_client_code = "#{first_name[0]}#{last_name}".downcase
count = Client.count(:conditions => "client_code = '#{default_client_code}'")
if count == 0
self.client_code = default_client_code
else
self.client_code = default_client_code + (count + 1).to_s
end
end
end
I understand that validating uniqueness of a standard, single field like "username" is easy. However, for something that has an unlimited number of inputs like, for example, "Favorite Movies" where a user can add as many favorite movies, is something I can't figure out.
They can choose to add or remove fields via the builder, but how do I ensure that no two or more entries are duplicates?
I think the easiest way to accomplish something like this is to validate the uniqueness of something in a scope. I can't say for sure how it would fit in your scenario since you did not describe you model associations but here is an example of how it could work in a FavoriteMovie model:
class FavoriteMovie < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :movie_name, :scope => :user_id
end
This makes sure that there can't be two movie names that are the same for one specific user.
It turns out that when using nested attributes, you can only validate what's already in the database and not new duplicate occurrences. So, a validation extension (below) with memory validation is really the only option, unfortunately.
#user.rb
class User
has_many :favorite_movies
validate :validate_unique_movies
def validate_unique_movies
validate_uniqueness_of_in_memory(
favorite_movies, [:name, :user_id], 'Duplicate movie.')
end
end
#lib/extensions.rb
module ActiveRecord
class Base
def validate_uniqueness_of_in_memory(collection, attrs, message)
hashes = collection.inject({}) do |hash, record|
key = attrs.map {|a| record.send(a).to_s }.join
if key.blank? || record.marked_for_destruction?
key = record.object_id
end
hash[key] = record unless hash[key]
hash
end
if collection.length > hashes.length
self.errors.add_to_base(message)
end
end
end
end
A very un-rails like solution to the problem would be to add a unique key constraint on the columns that in combination are required to be unique:
create unique index names_idx on yourtable (id, name);
you could easly check it like:
params[:user][:favourite_movies].sort.uniq == params[:user][:favourite_movies].sort
or in model:
self.favourite_movies.sort.uniq == self.favourite_movies.sort
irb(main):046:0> movies = ['terminator', 'ninja turtles', 'titanic', 'terminator' ].map {|movie| movie.downcase }
=> ["terminator", "ninja turtles", "titanic", "terminator"]
irb(main):047:0> movies.sort.uniq == movies.sort
=> false
You can try to create virtual attribute and check it uniqueness:
def full_name
[first_name, last_name].joun(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
You can check uniqueness on the database level by fix your migration:
CREATE TABLE properties (
namespace CHAR(50),
name CHAR(50),
value VARCHAR(100),
);
execute <<-SQL
ALTER TABLE properties
ADD CONSTRAINT my_constraint UNIQUE (namespace, name)
SQL
Little more modern approach: validates method
validates :movie_name, :uniqueness => {:scope => : user_id}
I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""