How to configure a pg_search multisearch on associated models in Rails? - ruby-on-rails

I'm adding pg_search into a Rails app. I'm not completely understanding the configuration, and would appreciate a gentle nudge in the right direction.
First, I already have a multi model site more or less set up and running on my app. But I want to extend it to also search on associated models.
For example, I have Manufacturer, Car, Model classes. Currently if I search for "Ford", only the manufacturer is returned. I'd also like to return all the associated Cars (which belong to Manufacturer) and Models (which belong to Car).
I can see how to do this as a scoped search
class Car
pg_search_scope :manufactured_by, :associated_against => {
:manufacturer => [:name]
}
end
But if I try to do this on a multisearch it doesn't work
class Car
include PgSearch
multisearchable :against => [:name],
:associated_against => {
:manufacturer => [:name]
}
end
It doesn't generate an error, it simply doesn't pick up the associated records.
I have a feeling I'm missing something fundamental in my understanding of how this all fits together. I'd really appreciate if someone could help me understand this, or point me towards a good source of info. I've been through the info on github and the related Railscast, but I'm still missing something.

It is impossible to search associated records with multisearch, due to how polymorphic associations work in Rails and SQL.
I will add an error that explains the situation so that in the future it won't be as confusing.
Sorry for the confusion.
What you could do instead is define a method on Car that returns the text you wish to search against.
class Car < ActiveRecord::Base
include PgSearch
multisearchable :against => [:name, manufacturer_name]
belongs_to :manufacturer
def manufacturer_name
manufacturer.name
end
end
Or to be even more succinct, you could delegate:
class Car < ActiveRecord::Base
include PgSearch
multisearchable :against => [:name, manufacturer_name]
belongs_to :manufacturer
delegate :name, :to => :manufacturer, :prefix => true
end
But you have to make sure the pg_search_documents table gets updated if you ever make a name change to a Manufacturer instance, so you should add :touch => true to its association:
class Manufacturer < ActiveRecord::Base
has_many :cars, :touch => true
end
This way it will call the Active Record callbacks on all the Car records when the Manufacturer is updated, which will trigger the pg_search callback to update the searchable text stored in the corresponding pg_search_documents entry.

Related

Adding belongs to relationship to Ruby Gem Mailboxer

I am building an e-com application and would like to implement something like a messaging system. In the application, all conversation will be related to either a Product model or an Order model. In that case, I would like to store the relating object (type + id, I supposed) to the Conversation object.
To add the fields, of course I can generate and run a migration, however, since the Model and Controller are included within the gem, how can I declare the relationship? (belongs_to :linking_object, :polymorphic) and the controller? Any idea?
Thank you.
I ended up customizing the Mailboxer gem to allow for a conversationable object to be attached to a conversation.
In models/mailboxer/conversation.rb
belongs_to :conversationable, polymorphic: true
Add the migration to make polymorphic associations work:
add_column :mailboxer_conversations, :conversationable_id, :integer
add_column :mailboxer_conversations, :conversationable_type, :string
In lib/mailboxer/models/messageable.rb you add the conversationable_object to the parameters for send_message:
def send_message(recipients, msg_body, subject, sanitize_text=true, attachment=nil, message_timestamp = Time.now, conversationable_object=nil)
convo = Mailboxer::ConversationBuilder.new({
:subject => subject,
:conversationable => conversationable_object,
:created_at => message_timestamp,
:updated_at => message_timestamp
}).build
message = Mailboxer::MessageBuilder.new({
:sender => self,
:conversation => convo,
:recipients => recipients,
:body => msg_body,
:subject => subject,
:attachment => attachment,
:created_at => message_timestamp,
:updated_at => message_timestamp
}).build
message.deliver false, sanitize_text
end
Then you can have conversations around objects:
class Pizza < ActiveRecord::Base
has_many :conversations, as: :conversationable, class_name: "::Mailboxer::Conversation"
...
end
class Photo < ActiveRecord::Base
has_many :conversations, as: :conversationable, class_name: "::Mailboxer::Conversation"
...
end
Assuming you have some users set up to message each other
bob = User.find(1)
joe = User.find(2)
pizza = Pizza.create(:name => "Bacon and Garlic")
bob.send_message(joe, "My Favorite", "Let's eat this", true, nil, Time.now, pizza)
Now inside your Message View you can refer to the object:
Pizza Name: <%= #message.conversation.conversationable.name %>
Although rewriting a custom Conversation system will be the best long-term solution providing the customization requirement (Like linking with other models for instance), to save some time at the moment I have implement the link with a ConversationLink Model. I hope it would be useful for anyone in the future who are at my position.
Model: conversation_link.rb
class ConversationLink < ActiveRecord::Base
belongs_to :conversation
belongs_to :linkingObject, polymorphic: true
end
then in each models I target to link with the conversation, I just add:
has_many :conversation_link, as: :linkingObject
This will only allow you to get the related conversation from the linking object, but the coding for reverse linking can be done via functions defined in a Module.
This is not a perfect solution, but at least I do not need to monkey patch the gem...
The gem automatically take care of this for you, as they have built a solution that any model in your own domain logic can act as a messagble object.
Simply declaring
acts_as_messagable
In your Order or Product model will accomplish what you are looking for.
You could just use something like:
form_helper :products
and add those fields to the message form
but mailboxer comes with attachment functionality(carrierwave) included
this might help if you need something like attachments in your messages:
https://stackoverflow.com/a/12199364/1230075

Editing a has_one association in ActiveAdmin - avoid saving when nothing is entered

I've got a model in which a very small percentage of the objects will have a rather large descriptive text. Trying to keep my database somewhat normalized, I wanted to extract this descriptive text to a separate model, but I'm having trouble creating a sensible workflow in ActiveAdmin.
My models look like this:
class Person < ActiveRecord::Base
has_one :long_description
end
class LongDescription < ActiveRecord::Base
attr_accessible :text, :person_id
belongs_to :person
validates :text, presence: true
end
Currently I've created a form for editing the Person model, looking somewhat like this:
form do |f|
...
f.inputs :for => [
:long_description,
f.object.long_description || LongDescription.new
] do |ld_f|
ld_f.input :text
end
f.actions
end
This works for adding/editing the LongDescription object, but I still have an issue: I'd like to avoid validating/creating the LongDescription object if no text is entered.
Anyone with better ActiveAdmin skills than me know how to achieve this?
Are you using accepts_nested_attributes_for :long_description? If so, you can add a :reject_if option:
class Person < ActiveRecord::Base
has_one :long_description
accepts_nested_attributes_for :long_description, reject_if: proc { |attrs| attrs['text'].blank? }
end
Note that this is a Rails thing, not an ActiveAdmin thing, and so it will simply skip assignment and update/create of the nested object if that attribute is missing.
More here: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

How to inherit from models in Rails, where one type extends another without intertwining

I'm aware of the number of posts on this, but still I can't figure out how to do this. I have a model "InspirationItem", which is basically a blog posts. Now I also want a second model, "Special". Specials are like inspiration items but they have extra properties, such as an "excerpt" and a "theme". So I want to extend the "InspirationPost" model.
I've tried to create a model "Post", which both "InspirationItem" and "Special" extend, but "InspirationItem" doesn't really add any properties to. Then, I create a "has_one" relation from InspirationItem/Special and try to use "delegate" to handle all logics in the "Post" model. However this does not work like I'd expect at all.
Here's some of my code. This would be my InspirationItem:
class InspirationItem < ActiveRecord::Base
has_one :post, :as => :item
delegate :title, :title=,
:body, :body=,
:category_names, :category_names=,
:hide_from_overview, :hide_from_overview=,
:to => :post, :allow_nil => true
end
And this is a short version of post:
class Post < ActiveRecord::Base
attr_accessible :title, :body, :embed, :hide_from_overview, :visual, :thumbnail, :category_names
# All sorts of logics
end
What's important is that I don't want InspirationItem.all to return Specials too, that's why I use the Post model. I also want regular error handling to work for all models. Thanks in advance!
If you want an ActiveRecord subclass of a model, but don't want the parent to search any of the children, then something like this should work (I'll use your InspirationItem class):
class InspirationItem < ActiveRecord::Base
def self.descendants
super.reject {|klass| klass == Special}
end
end
class Special < InspirationItem
end
This is a bit hacky, but will force ActiveRecord to only return InspirationItems when you search InspirationItem.all. And this shouldn't affect validations.
EDIT: Re: What the tables would look like for this.
create_table :inspiration_items do |t|
t.string :type # needed for the Single Table Inheritance mechanism
# whatever other columns you need for InspirationItems
end

embeds_many and embeds_one from same model with Mongoid

I have two models, Blog and Theme. A Blog embeds_many :themes and Theme embedded_in :blog. I also have Blog embeds_one :theme (for the activated theme). This does not work. When creating a theme with blog.themes.create it's not stored. If I change the collections so they're not embedded everything works.
# This does NOT work!
class Blog
embeds_many :themes
embeds_one :theme
end
class Theme
embedded_in :blog
end
BUT
# This DOES work!
class Blog
has_many :themes
has_one :theme
end
class Theme
belongs_to :blog
end
Anyone know why this is?
UPDATE
Also there is a problem with assigning one of themes to (selected) theme.
blog.themes = [theme_1, theme_2]
blog.save!
blog.theme = blog.themes.first
blog.save!
blog.reload
blog.theme # returns nil
With this approach you'll embed the same document twice: once in the themes collection and then in the selected theme.
I'd recommend removing the second relationship and use a string attribute to store the current theme name. You can do something like:
class Blog
include Mongoid::Document
field :current_theme_name, type: String
embeds_many :themes
def current_theme
themes.find_by(name: current_theme_name)
end
end
class Theme
include Mongoid::Document
field :name, type: String
embedded_in :blog
end
Note that mongoid embeded documents are initialized at the same time that the main document and doesn't require extra queries.
OK, so I had the same problem and think I have just stumbled across the solution (I was checking out the code for the Metadata on relations).
Try this:
class Blog
embeds_many :themes, :as => :themes_collection, :class_name => "Theme"
embeds_one :theme, :as => :theme_item, :class_name => "Theme"
end
class Theme
embedded_in :themes_collection, :polymorphic => true
embedded_in :theme_item, :polymorphic => true
end
What I have discerned guessed is that:
the first param (e.g. :themes) actually becomes the method name.
:as forges the actual relationship, hence the need for them to match in both classes.
:class_name seems pretty obvious, the class used to actually serialise the data.
Hope this helps - I am obviously not an expert on the inner workings on mongoid, but this should be enough to get you running. My tests are now green and the data is serialising as expected.
Remove embeds_one :theme and instead put its getter and setter methods in Blog class:
def theme
themes.where(active: true).first
end
def theme=(thm)
theme.set(active: false)
thm.set(active: true)
end
There is no need to call blog.save! after blog.theme = blog.themes.first because set performs an atomic operation.
Also, don't forget to add field :active, type: Boolean, default: false in your Theme model.
Hope this works with you.

Multiple has_many_polymorphs in one model

I'm trying to define multiple polymorphic relations (has_many_polymorphs plugin) from a single parent to same children.
Note has many viewers
Note has many editors
Viewers could be either Users or Groups
Editors could be either Users or Groups
Permission is the association model with note_id, viewer_id, viewer_type, editor_id, editor_type fields
Everything works out as expect as long as I have only one has_many_polymorphs relation defined in Note model
class User < ActiveRecord::Base; end
class Group < ActiveRecord::Base; end
class Note < ActiveRecord::Base
has_many_polymorphs :viewers, :through => :permissions, :from => [:users, :groups]
end
class Permission < ActiveRecord::Base
belongs_to :note
belongs_to :viewer, :polymorphic => true
end
Note.first.viewers << User.first # => [#<User id: 1, ....>]
Note.first.viewers << Group.first # => [#<User id: 1, ....>, #<Group ...>]
Note.first.viewers.first # => #<User ....>
Note.first.viewers.second # => #<Group ....>
Now, problems start to appear when I add the second relation
class Note < ActiveRecord::Base
has_many_polymorphs :viewers, :through => :permissions, :from => [:users, :groups]
has_many_polymorphs :editors, :through => :permissions, :from => [:users, :groups]
end
class Permission < ActiveRecord::Base
belongs_to :note
belongs_to :viewer, :polymorphic => true
belongs_to :editor, :polymorphic => true
end
Note.first.viewers << User.first # => [#<User id: ....>]
# >>>>>>>>
Note.first.editors << User.first
NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.constantize
... vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/base.rb:18:in `instantiate'
I've tried refining the definition of has_many_polymorphs but it didn't work. Not even with an STI model for ViewPermission < Permission, and EditPermission < Permission.
Any thoughts / workarounds / issue pointers are appreciated.
Rails 2.3.0
Dont you need to add
has_many :permissions
to your Note.
FYI. I used has_many_polymorphs once but then dropped it, it wasn't working as expected.
Can you post the schema that you are using for Permission? My guess is the root of the problem lies there, you need to have multiple type, id pairs in the schema since you have two different belongs_to in the definition.
Edit:
I see you have posted the question on github as well. Not sure if you tried using the Double sided polymorphism. You probably have... like I said, I was not impressed by this plugin, as it brought in some instability when I used it.
== Double-sided polymorphism
Double-sided relationships are defined on the join model:
class Devouring < ActiveRecord::Base
belongs_to :guest, :polymorphic => true
belongs_to :eaten, :polymorphic => true
acts_as_double_polymorphic_join(
:guests =>[:dogs, :cats],
:eatens => [:cats, :birds]
)
end
Now, dogs and cats can eat birds and cats. Birds can't eat anything (they aren't <tt>guests</tt>) and dogs can't be
eaten by anything (since they aren't <tt>eatens</tt>). The keys stand for what the models are, not what they do.
#Tamer
I was getting the same error. The problem was that has_many_polymorphs creates the record in the join table using mass association and was failing. I added attr_accessible :note_id, :editor_id, and :editor_type to my Permission class and it worked afterwards. (Note: I substituted your model names for mine.)
I haven't looked too much into it, but I'd be curious if there's a way to alter this behavior. I'm fairly new to this framework and letting anything sensitive (like an Order-Payment association) be mass-assigned seems like asking to shoot myself in the foot. Let me know if this fixed your problem, and if you figure anything else out.
Best,
Steve

Resources