In my application, users describe buildings. A user should be able to specify in which neighborhood a building exists using a grouped select. The models look like:
class Building
include Mongoid::Document
belongs_to :neighborhood
end
class Neighborhood
include Mongoid::Document
field :name, type: String, default: nil
field :borough, type: String, default: nil
field :city, type: String, default: nil
end
Using simple_form, I'm trying to generate a grouped select represent a list of neighborhoods the building might belong to.
= building_form.association :neighborhood, as: :grouped_select, collection: Neighborhood.where(city: city), group_method: :borough
Which ideally creates something like:
Borough #1
Downtown
Uptown
Borough #2
Suburbs
...
However, I get this error:
undefined method `map' for "Borough #1":String
It appears it is calling Neighborhood.borough.map, and because a String doesn't have a map function, it errors out. How do I fix this?
I've struggled with this for sometime, and unfortunately the intuitive 'Rails' magic I was hoping to get from association doesn't seem to exist. It's using the underlying Rails grouped_collection_select, which doesn't seem to handle objects/models very well.
Instead, it appears to handle Arrays much better. According to this documentation, the collection input should be in the form of:
[
['group_name',
[
['item-name','item-value'],
['item2-name','item2-value'],
...(more items)...
]
],
['group2_name',
[
['item3-name','item3-value'],
['item4-name','item4-value'],
...(more items)...
]
],
...(more groups)...
]
MongoDB models don't lend themselves to this format naturally, so I wrote a helper method on my Neighborhood class:
def self.grouped_by_borough(city)
groups = []
Neighborhood.where(city: city).distinct(:borough).each_with_index do |borough, index|
groups << [borough, Neighborhood.where(city: city, borough: borough)]
end
return groups
end
Then my association looks like:
= building_form.association :neighborhood, as: :grouped_select, collection: Neighborhood.grouped_by_borough(city), group_method: :last, option_key_method: :name, option_value_method: :id
This also automatically selects any previously selected neighborhood, which is convenient for 'edit' forms.
If any Rails forms/Mongoid gurus have a cleaner way of handling this, I'd love to hear about it.
Related
Rails 4.2.5, Mongoid 5.1.0
I have three models - Mailbox, Communication, and Message.
mailbox.rb
class Mailbox
include Mongoid::Document
belongs_to :user
has_many :communications
end
communication.rb
class Communication
include Mongoid::Document
include Mongoid::Timestamps
include AASM
belongs_to :mailbox
has_and_belongs_to_many :messages, autosave: true
field :read_at, type: DateTime
field :box, type: String
field :touched_at, type: DateTime
field :import_thread_id, type: Integer
scope :inbox, -> { where(:box => 'inbox') }
end
message.rb
class Message
include Mongoid::Document
include Mongoid::Timestamps
attr_accessor :communication_id
has_and_belongs_to_many :communications, autosave: true
belongs_to :from_user, class_name: 'User'
belongs_to :to_user, class_name: 'User'
field :subject, type: String
field :body, type: String
field :sent_at, type: DateTime
end
I'm using the authentication gem devise, which gives access to the current_user helper, which points at the current user logged in.
I have built a query for a controller that satisfied the following conditions:
Get the current_user's mailbox, whose communication's are filtered by the box field, where box == 'inbox'.
It was constructed like this (and is working):
current_user.mailbox.communications.where(:box => 'inbox')
My issue arrises when I try to build upon this query. I wish to chain queries so that I only obtain messages whose last message is not from the current_user. I am aware of the .last method, which returns the most recent record. I have come up with the following query but cannot understand what would need to be adjusted in order to make it work:
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
This query produces the following result:
undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>
I am currently able to accomplish this by doing the following, which I know is very inefficient and want to change immediately:
mb = current_user.mailbox.communications.inbox
comms = mb.reject {|c| c.messages.last.from_user == current_user}
I wish to move this logic from ruby to the actual database query. Thank you in advance to anyone who assists me with this, and please let me know if anymore information is helpful here.
Ok, so what's happening here is kind of messy, and has to do with how smart Mongoid is actually able to be when doing associations.
Specifically how queries are constructed when 'crossing' between two associations.
In the case of your first query:
current_user.mailbox.communications.where(:box => 'inbox')
That's cool with mongoid, because that actually just desugars into really 2 db calls:
Get the current mailbox for the user
Mongoid builds a criteria directly against the communication collection, with a where statement saying: use the mailbox id from item 1, and filter to box = inbox.
Now when we get to your next query,
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
Is when Mongoid starts to be confused.
Here's the main issue: When you use 'where' you are querying the collection you are on. You won't cross associations.
What the where(:messages.last.from_user => {'$ne' => current_user}) is actually doing is not checking the messages association. What Mongoid is actually doing is searching the communication document for a property that would have a JSON path similar to: communication['messages']['last']['from_user'].
Now that you know why, you can get at what you want, but it's going to require a little more sweat than the equivalent ActiveRecord work.
Here's more of the way you can get at what you want:
user_id = current_user.id
communication_ids = current_user.mailbox.communications.where(:box => 'inbox').pluck(:_id)
# We're going to need to work around the fact there is no 'group by' in
# Mongoid, so there's really no way to get the 'last' entry in a set
messages_for_communications = Messages.where(:communications_ids => {"$in" => communications_ids}).pluck(
[:_id, :communications_ids, :from_user_id, :sent_at]
)
# Now that we've got a hash, we need to expand it per-communication,
# And we will throw out communications that don't involve the user
messages_with_communication_ids = messages_for_communications.flat_map do |mesg|
message_set = []
mesg["communications_ids"].each do |c_id|
if communication_ids.include?(c_id)
message_set << ({:id => mesg["_id"],
:communication_id => c_id,
:from_user => mesg["from_user_id"],
:sent_at => mesg["sent_at"]})
end
message_set
end
# Group by communication_id
grouped_messages = messages_with_communication_ids.group_by { |msg| mesg[:communication_id] }
communications_and_message_ids = {}
grouped_messages.each_pair do |k,v|
sorted_messages = v.sort_by { |msg| msg[:sent_at] }
if sorted_messages.last[:from_user] != user_id
communications_and_message_ids[k] = sorted_messages.last[:id]
end
end
# This is now a hash of {:communication_id => :last_message_id}
communications_and_message_ids
I'm not sure my code is 100% (you probably need to check the field names in the documents to make sure I'm searching through the right ones), but I think you get the general pattern.
I have two questions:
In Rails 3 you can update multiple records using
Product.update(params[:products].keys, params[:products].values)
How do you do the same thing in Rails 4 using Strong Parameters? How about creating multiple records at the same time? Can you please elaborate your solution with an example in a format like the following:
params = ActionController::Parameters.new(...)
Product.create!(params.require(...)permit(...)
Also, my products model has a column called number which is equal to the order that they are updated. Is there a way to pass a counter value to the number while updating?
Thanks.
Solution w/o accepts_nested_attributes_for
This is my 2nd ever answer on SO, but I came across this problem multiple times, couldn't as of this writing find a reasonable solution and finally figured it out myself after doing a little nested_attributes hypothesizing and wanted to share my best understanding of it. I believe the strong params line you're after is this:
product_params = params.require(:products).permit(product_fields: [:value1, :value2, etc...])
I don't know exactly what your form looks like, but you will need to have a fields_for to nest the params in the product_fields(or any_name):
form_for :products do |f|
f.fields_for product_fields[] do |pf|
pf.select :value1
pf.select :value2
...
end
end
This will allow the permit() to accept a single explicit hash
product_fields => {0 => {value1: 'value', value2: 'value'}}
instead of the key/value pairs
0 => {value1: 'value', value2: 'value'}, 1 => {value1: 'value', value2: 'value'}, etc...
which you'd otherwise have to name individually:
.permit(0 => [value1: 'value', value2: 'value'], 1 => [...], 2 => [...], ad infinitum)
This allows you to update multiple products without having to use a parent model accepting nested attributes. I just tested this on my own project in Rails 4.2 and it worked like a charm. As for creating multiple: How would I save multiple records at once in Rails?.
As for a counter, you may need to iterate over each model individually:
product_params[:product_fields].keys.each_index do |index|
Product.create!(product_params.merge(counter: index))
end
Thought it's been so long you probably resolved that on your own. :-)
Maybe you're thinking about using nested_attributes?
That would look something like this:
params.require(:parent_model).permit(
:id,
child_models_attributes: [
:id,
:parent_model_id,
:child_model_attribute_1,
:child_model_attribute_2
]
)
with
params = {
id: parent_model.id,
child_models_attributes: {
'0' => {
id: child_model_1.id,
parent_model_id: parent_model.id,
child_model_attribute_1: 'value 1',
child_model_attribute_2: 12312
}
}
}
You would need to allow nested_attributes for the parent model like this though:
class ChildModel < Activerecord::Base
belongs_to :parent_model
end
class ParentModel < Activerecord::Base
has_many :child_models
accepts_nested_attributes_for :child_models
end
[I'm new to rails, and I hope it's not a silly question, seen a similar question but it's for PHP and doesn't help in my case]
To explain my problem, I'm using a analogy to users here. Lets say I have users table in my app, I have added a field called user_type to users table. Now I want to specify which type of user is.
lets say I have 5 types of users eg. moderator, administrator, consumer etc.
I don't want to make user_type field to be string type to store user type. Instead I want to make user_type to store integer and then map these integer values to respective string values.
Advantage to this approach is that I can change what a user type is called. Suppose that I no longer wish to call consumer a consumer and instead wish to call it something else.
I believe storing integer in db is better and gives some flexibility.
I know I can create select menu using formtastic(I'm using active_admin as admin panel, formtastic is used for forms)
<%= f.input :user_type, :as => :select, :collection => {
0 => "Admin",
1 => "Moderator",
2 => "Consumer",
} %>
and then store values in db, and then select these users from db.
I want to know Is there a better way or approach to do it in rails or there is some gem available to do this or some other approach you prefer and why you recommend it.
I'm using postgresql as database.
Thanks!!
I personally like the active_enum gem combined to simple_form because it's really simple to implement and they work fine together.
In your case, you would have to define an enum class like this :
class Type < ActiveEnum::Base
value 1 => 'Admin'
value 2 => 'Moderator'
value 3 => 'Consumer'
end
Then in your User model, you simply add this :
enumerate :user_type, :with => Type
And what is really great with simple_form is that you simply have to call :
<%= f.input :user_type =>
to get a select with all your values.
Try this
# user.rb
USER_TYPES = { moderator: 1, superuser: 2, admin: 3, client: 4 }
# views
select :user, :user_type, User::USER_TYPES
This saves the integer values to the database. If you want to get the the string equivalent, use User::USER_TYPES.key(#user.user_type)
EDIT: forgot to add scopes
scope :moderators, where(user_type: USER_TYPES[:moderator])
scope :superusers, where(user_type: USER_TYPES[:superuser])
...
or
USER_TYPES.each do |user_type, value|
scope :"#{user_type}s", where(user_type: USER_TYPES[user_type])
end
I'd like to update a massive set of document on an hourly basis.
Here's the
fairly simple Model:
class Article
include Mongoid::Document
field :article_nr, :type => Integer
field :vendor_nr, :type => Integer
field :description, :type => String
field :ean
field :stock
field :ordered
field :eta
so every hour i get a fresh stock list, where :stock,:ordered and :eta "might" have changed
and i need to update them all.
Edit:
the stocklist contains just
:article_nr, :stock, :ordered, :eta
wich i parse to a hash
In SQL i would have taken the route to foreign keying the article_nr to a "stock" table, dropping the whole stock table, and running a "collection.insert" or something alike
But that approach seems not to work with mongoid.
Any hints? i can't get my head around collection.update
and changing the foreign key on belongs_to and has_one seems not to work
(tried it, but then Article.first.stock was nil)
But there has to be a faster way than iterating over the stocklist array of hashes and doing
something like
Article.where( :article_nr => stocklist['article_nr']).update( stock: stocklist['stock'], eta: stocklist['eta'],orderd: stocklist['ordered'])
UPDATING
You can atomically update multiple documents in the database via a criteria using Criteria#update_all. This will perform an atomic $set on all the attributes passed to the method.
# Update all people with last name Oldman with new first name.
Person.where(last_name: "Oldman").update_all(
first_name: "Pappa Gary"
)
Now I can understood a bit more. You can try to do something like that, assuming that your article nr is uniq.
class Article
include Mongoid::Document
field :article_nr
field :name
key :article_nr
has_many :stocks
end
class Stock
include Mongoid::Document
field :article_id
field :eta
field :ordered
belongs_to :article
end
Then you when you create stock:
Stock.create(:article_id => "123", :eta => "200")
Then it will automaticly get assign to article with article_nr => "123"
So you can always call last stock.
my_article.stocks.last
If you want to more precise you add field :article_nr in Stock, and then :after_save make new_stock.article_id = new_stock.article_nr
This way you don't have to do any updates, just create new stocks and they always will be put to correct Article on insert and you be able to get latest one.
If you can extract just the stock information into a separate collection (perhaps with a has_one relationship in your Article), then you can use mongoimport with the --upsertFields option, using article_nr as your upsertField. See http://www.mongodb.org/display/DOCS/Import+Export+Tools.
I have the following model
class Person
include Mongoid::Document
embeds_many :tasks
end
class Task
include Mongoid::Document
embedded_in :commit, :inverse_of => :tasks
field :name
end
How can I ensure the following?
person.tasks.create :name => "create facebook killer"
person.tasks.create :name => "create facebook killer"
person.tasks.count == 1
different_person.tasks.create :name => "create facebook killer"
person.tasks.count == 1
different_person.tasks.count == 1
i.e. task names are unique within a particular person
Having checked out the docs on indexes I thought the following might work:
class Person
include Mongoid::Document
embeds_many :tasks
index [
["tasks.name", Mongo::ASCENDING],
["_id", Mongo::ASCENDING]
], :unique => true
end
but
person.tasks.create :name => "create facebook killer"
person.tasks.create :name => "create facebook killer"
still produces a duplicate.
The index config shown above in Person would translate into for mongodb
db.things.ensureIndex({firstname : 1, 'tasks.name' : 1}, {unique : true})
Can't you just put a validator on the Task?
validates :name, :uniqueness => true
That should ensure uniqueness within parent document.
Indexes are not unique by default. If you look at the Mongo Docs on this, uniqueness is an extra flag.
I don't know the exact Mongoid translation, but you're looking for something like this:
db.things.ensureIndex({firstname : 1}, {unique : true, dropDups : true})
I don't believe this is possible with embedded documents. I ran into the same issue as you and the only workaround I found was to use a referenced document, instead of an embedded document and then create a compound index on the referenced document.
Obviously, a uniqueness validation isn't enough as it doesn't guard against race conditions. Another problem I faced with unique indexes was that mongoid's default behavior is to not raise any errors if validation passes and the database refuses to accept the document. I had to change the following configuration option in mongoid.yml:
persist_in_safe_mode: true
This is documented at http://mongoid.org/docs/installation/configuration.html
Finally, after making this change, the save/create methods will start throwing an error if the database refuses to store the document. So, you'll need something like this to be able to tell users about what happened:
alias_method :explosive_save, :save
def save
begin
explosive_save
rescue Exception => e
logger.warn("Unable to save record: #{self.to_yaml}. Error: #{e}")
errors[:base] << "Please correct the errors in your form"
false
end
end
Even this isn't really a great option because you're left guessing as to which fields really caused the error (and why). A better solution would be to look inside MongoidError and create a proper error message accordingly. The above suited my application, so I didn't go that far.
Add a validation check, comparing the count of array of embedded tasks' IDs, with the count of another array with unique IDs from the same.
validates_each :tasks do |record, attr, tasks|
ids = tasks.map { |t| t._id }
record.errors.add :tasks, "Cannot have the same task more than once." unless ids.count == ids.uniq.count
end
Worked for me.
You can define a validates_uniqueness_of on your Task model to ensure this, according to the Mongoid documentation at http://mongoid.org/docs/validation.html this validation applies to the scope of the parent document and should do what you want.
Your index technique should work too, but you have to generate the indexes before they brought into effect. With Rails you can do this with a rake task (in the current version of Mongoid its called db:mongoid:create_indexes). Note that you won't get errors when saving something that violates the index constraint because Mongoid (see http://mongoid.org/docs/persistence/safe_mode.html for more information).
You can also specify the index in your model class:
index({ 'firstname' => 1, 'tasks.name' => 1}, {unique : true, drop_dups: true })
and use the rake task
rake db:mongoid:create_indexes
you have to run :
db.things.ensureIndex({firstname : 1, 'tasks.name' : 1}, {unique : true})
directly on the database
You appear to including a "create index command" inside of your "active record"(i.e. class Person)