I'm referring to this blog on how to use postgresql arrays in Rails 4. To be more precise, I'm using Rails 4.1.8.
I would like to enforce that the array should not be empty(should have at least one or more values) wrt the below table definition for the phones attribute.
Setting null: false as done below prevents me from doing a Contacts.create(phones: nil), but Contacts.create(phones: []) saves to the database.
class CreateContacts < ActiveRecord::Migration
def change
create_table :contacts do |t|
t.string :name
t.text :phones, array: true, null: false
end
end
end
What constraint can I use to ensure that Contacts.create(phones: []) raises an error saying that there should be at least one entry within the array?
A regular presence validation should meet your requirement, since the empty array is not present?.
class Contact < ActiveRecord::Base
validates :phones, presence: true
end
I suggest you use a serialized attribute instead.
class CreateContacts < ActiveRecord::Migration
def change
create_table :contacts do |t|
t.string :name
t.text :phones, null: false
end
end
end
And in your model:
class Contact < ActiveRecord::Base
serialize :phones
validate :phones, presence: true
before_validation :clean_empty_array
private
def clean_empty_array
self.phones = phones.presence
end
end
Note that I added a before_validation cleaning method, to turn empty arrays into nil using presence ([].presence => nil; ["1"].presence => ["1"]). As an alternative, you could use attribute_normalizer gem to handle this kind of data normalization if you have this requirement in more places of your application.
EDIT: after discussion in the comments, serialize seems to be not the best way to go, since it carries important caveats such as querying issues. Thanks to #muistooshort for pointing these out. Also, the before_validation is not needed, since a presence: true validation is equivalent to what was proposed in the before_validation.
So the final solution to the question would involve keeping Postgresql array support and just validating against phones presence.
In short: keep array: true in your migration file and add this validation to your model:
class Contact < ActiveRecord::Base
validate :phones, presence: true
end
Related
I'm trying to add an array of objects (tasks in this case) as a return type in my controller for the root object (project). One project has many tasks and I'd like to save it all at once, but I keep getting the following error indicating that the return type isn't correct.
ActiveRecord::AssociationTypeMismatch (Task(#47457277775360) expected, got {"name"=>"Some task", "start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z"} which is an instance of ActiveSupport::HashWithIndifferentAccess(#47457266882220))
The whitelisted parameters I have are like this
# whitelist params
params.permit(:name, :description, tasks: [:name, :start, :end])
The data being returned for the above example:
{"name"=>"asdf", "description"=>"zxvccxvzzxcvxcvcxvzxcvz", "tasks"=>[{"start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z", "name"=>"Some task"}]}
[Edit] - Here are the models we're working with
# app/models/task.rb
class Task < ApplicationRecord
# model association
belongs_to :project
# validation
validates_presence_of :name
end
# app/models/project.rb
class Project < ApplicationRecord
# model association
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks
# validations
validates_presence_of :name
end
As per rails documentation Nested attributes allow you to save attributes on associated records through the parent. So in your strong params you need to pass attributes like this.
params.permit(:name, :description, task_attributes: [:name, :start, :end])
I suggest you to bind all params under one attribute like this
params.require(:project).permit(:name, :description, task_attributes: [:name, :start, :end])
so you must send params from frontend like
{"project": {"name"=>"asdf", "description"=>"zxvccxvzzxcvxcvcxvzxcvz", "task_attributes"=>[{"start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z", "name"=>"Some task"}]}}
You can read the documentation from https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
So there doesn't appear to be any clean way to generically allow Hash field with strong parameters. This may of course be a strong parameters issue but I'm curious if there is a workaround. I have a model with some fields...
field :name, type: String
field :email, type: String
field :other_stuff, type: Hash, default: {}
Now I could just permit everything:
params.require(:registration).permit!
But that isn't really a great idea and what I'd like to do is something like...
params.require(:registration).permit(:name, :email, { other_stuff: {} })
However this doesn't seem to be possible with strong parameters, it isn't possible to just whitelist a hash as a property (yay for SQL centric ActiveRecord APIs!). Any ideas how this might be done, or is my best bet to submit a Rails patch to allow for this scenario.
Ok, after researching this, I found an elegant solution that I will start using too:
params.require(:registration).permit(:name).tap do |whitelisted|
whitelisted[:other_stuff] = params[:registration][:other_stuff]
end
source: https://github.com/rails/rails/issues/9454#issuecomment-14167664
If necessary nested attributes can also be permitted as follows:
def create_params
params[:book]["chapter"].permit(:content)
end
For a field that allows nested hashes, I use the following solution:
def permit_recursive_params(params)
params.map do |key, value|
if value.is_a?(Array)
{ key => [ permit_recursive_params(value.first) ] }
elsif value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
{ key => permit_recursive_params(value) }
else
key
end
end
end
To apply it to for example the values param, you can use it like this:
def item_params
params.require(:item).permit(values: permit_recursive_params(params[:item][:values]))
end
I've just started out using MongoDB and, in particular, Mongoid.
naturally I'd like to ensure my User's passwords are kept nice and secure, and previously I'd have done this with ActiveRecord and used bcrypt. I'm looking for a nice, clean, secure, simple way to implement the same sort of thing using Mongoid.
I've taken a look at mongoid-encryptor but I've not quite got my head around how to use it.
Assume my simplified User looks like this, as per the example in mongoid-encryptor's Readme file.
class User
include Mongoid::Document
include Mongoid::Encryptor
field :name
field :password
encrypts :password
end
And in my WebApp (using Sinatra in this case) I'd define a helper such as
def login (name, cleartxtpass)
return User.where(name: name, password: cleartxtpass).first
end
How do I get it to use bcrypt?
Is there any pre-processing I need to do with cleartxtpass or will Mongoid::Encryptor just handle that? It's not clear from the docs.
Okay well after some digging I decided to not bother using Mongoid::Encryptor but to stick to the tried and tested way I used to do these things when using ActiveRecord.
So now my User looks like
class User
include Mongoid::Document
field :name, type: String
field :password_hash, type: String
index({name: 1}, {unique: true, name: 'user_name_index'})
include BCrypt
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
end
and my authenticate helper method looks like
def auth_user(username, password)
user = User.where(name: username).first
return user if user && user.password == password
return nil
end
That works a treat.
The simply way to do it is:
class User
include Mongoid::Document
include ActiveModel::SecurePassword
field :name, type: String
field :password_digest, type: String
has_secure_password
end
I cant seem to find answer for this here or with the google, any help would be awesome.
The Building saves correctly, but the embedded doc PriorityArea doesnt get updated...
I want to eventually have it ajax a new form for new priority areas evenutally, but need it to update first.
Mongoid::Errors::InvalidFind in BuildingsController#update
Calling Document#find with nil is invalid
class Building
include Mongoid::Document
embeds_many :priority_areas
accepts_nested_attributes_for :priority_areas, :allow_destroy => true, :autosave => true
end
class PriorityArea
include Mongoid::Document
embedded_in :building, :inverse_of => :priority_areas
end
#view
= form_for [#customer, #building] do |f|
...
...
= f.fields_for :priority_areas do |pa|
= f.name
...
...
#controller
#building.update_attributes(params[:building])
It correctly yeilds the correct data from the db, but fails to error above on building#update. Any help is greatly appreciated.
update
in the building#update im
puts params[:building][:priority_areas_attributes].to_yaml
which yeilds
--- !map:ActiveSupport::HashWithIndifferentAccess
"0": !map:ActiveSupport::HashWithIndifferentAccess
name: area 51
location: near front door
notes: ""
priority: "1"
id: ""
"1": !map:ActiveSupport::HashWithIndifferentAccess
name: area 52
location: near rear door
notes: ""
priority: "2"
id: ""
im guessing the problem is the null id:""
The problem was the null id
it needed to have an ObjectId to work correctly. stupid error on my part.
I encountered the exact problem. simple_form was automatically passing an id parameter to my controller, but it was blank.
Why was the id for my embedded document blank? I'm guessing it's because I imported the parent document via mongoimport. If I manually generate a parent document via web forms, then the embedded documents have IDs as expected.
Here was my workaround:
class Foo
include Mongoid::Document
embeds_many :bars
accepts_nested_attributes_for :bars
####
# simple_form_for / embedded document workaround
#
# Because simple_form wants to provide the ID for an existing object,
# it will output a blank ID because imported embedded documents
# have an ID of nil.
#
# Intercept it to avoid
# Mongoid::Errors::InvalidFind in FoosController#update
def bars_attributes=(attribs)
attribs.each do |key, value|
index = key.to_i
fixed_attrib = value.delete_if { |k,v| k=="id" and v=="" }
self.bars[index].update_attributes(fixed_attrib)
end
end
end
class Bar
include Mongoid::Document
embedded_in :foo
end
I had to track the dirty objects. And it works fine with the parent
doc. But when I change the embedded or referenced in doc, the dirty
has to be accessed via the embedded/referenced in doc itself.
How can I track the dirty on the parent doc itself??
I have put together mongoid extension to solve this problem https://github.com/versative/mongoid_relations_dirty_tracking. It simply tracks changes on document relations in the similar way as mongoid do it for attributes so the timestamps and versions are updated correctly.
I don't think Mongoid's own dirty attributes feature tracks changes to embedded documents. But it's not hard to implement basic dirty tracking of embedded documents yourself with something like this:
class Bar
include Mongoid::Document
embeds_many :foos
attr_accessor :foos_changed
alias_method :foos_changed?, :foos_changed
# ...
end
class Foo
include Mongoid::Document
embedded_in :bar, :inverse_of => :foos
after_save :notify_parent
def notify_parent
bar.foos_changed = true
end
# ...
end
b = Bar.create
b.foos_changed? # => nil
b.foos.create
b.foos_changed? # => true
If you need more options, like including embedded documents changes in Bar.changes or tracking the specific changes, you'll have to override the changes and changed? methods, which I wouldn't do unless you absolutely need those features.
Here's a way to track what changed in the embedded documents:
class Bar
include Mongoid::Document
embeds_many :foos
attr_writer :embedded_changes
def embedded_changes
#embedded_changes ||= begin
self._children.inject({}) do |memo, child|
memo.merge(
{child.collection_name => {child.id => child.changes}}
) if child.changed?
end
end
end
def changes
original_value = super
if original_value.blank?
embedded_changes
else
original_value.merge(embedded_changes)
end
end
def changed?
super || self._children.any?(&:changed?)
end
# ...
end
class Foo
include Mongoid::Document
embedded_in :bar, :inverse_of => :foos
# ...
field :hat
end
b = Bar.create
f = b.foos.create
b.changes # => nil
b.changed? # => false
f.hat = 1
f.changes # => {"hat"=>[nil, "1"]}
b.changes # => {"foos"=>{BSON::ObjectId('4cf...')=>{"hat"=>[nil, "1"]}}}
b.changed? # => true