Exclude draft articles from Solr index with Sunspot - ruby-on-rails

I have an indexed model called Article and I don't want solr to index unpublished articles.
class Article < ActiveRecord::Base
searchable do
text :title
text :body
end
end
How can I specify that article that is not #published? should not be indexed?

Be sure to index the published status.
class Article < ActiveRecord::Base
searchable do
text :title
text :body
boolean :is_published, :using => :published?
end
end
Then add a filter to your query
Sunspot.search(Article) do |search|
search.with(:is_published, true)
# ...
end

If you want to make sure unpublished articles are never included in the search index, you can do it this way instead:
class Article < ActiveRecord::Base
searchable :if => :published? do
text :title
text :body
end
end
The model will then only be indexed when published.
My approach is less interesting if you also want admins to be able to search for articles, including unpublished ones, however.
Note: calling article.index! will add the instance to the index regardless of the :if => :method param.

A small look into the code base of sunspot_rails reveals a method called maybe_mark_for_auto_indexing which will be added to the models that include solr. You could override that method and set #marked_for_auto_indexing based on your criteria in the specific model. Its monkey patching but can help you solve the problem. The code for ur reference is in lib/sunspot/searchable.rb.

Related

Rails: Select options

I asked a similar question about select options a while ago but I still can't seem to wrap my head around it. I'm rather new to rails but here's what I'm trying to do
I have a Post table & in it, I have a "post_status" column. I
would like to give each post 3 options:
Draft
Pending
Publish
How would I go about creating these 3 options in Rails? (I was advised not to use booleans for this)
Thank you in advance
Elaborating on #Alexander Kobelev answer, I'd put it all in the model:
class Post < ActiveRecord::Base
STATUS_OPTIONS = {
:draft => 'Draft',
:pending => 'Pending',
:published => 'Published'
}
validates_inclusion_of :post_status, :in => STATUS_OPTIONS.keys
end
in your view:
Post Status: <%= select(:post, :post_status, Post::STATUS_OPTIONS.invert) %>
In this particular instance they look like status flags that could be handled a few ways, but you've asked about select options so here's a solution for that method.
Because you don't specify if you need to keep the values already in the table I've detailed a method that allows you to keep them by converting them to IDs (assuming they are currently strings), if this is not relevant then follow only the bold instructions.
Create a PostStatus resource (model, migrate, controller/view if you need the ability to change them).
Define the relationships
PostStatus
has_many :posts
Post
belongs_to :post_status
Add values to your PostStatus table (if you have a live system with strings in the table you should match the existing post status strings here to allow you to convert the data (detailed below).
Change column name to post_status_id in the Post table, change its type to int. If this isn't live then just redo the migrate with the column as integer. If it is a live system you'll need to convert your data into a new column instead of just changing its type, the below is a suggested method.
add_column :posts, :post_status_id_tmp, :int
Post.reset_column_information # make the new column available to model methods
Post.all.each do |post|
# Assuming you have a string with the option text currently:
post.post_status_id_tmp = PostStatus.find_by_name(post.post_status).id
post.save
end
remove_column :posts, :post_status
rename_column :posts, :post_status_tmp, :post_status_id
In your post form add a selectbox.
<%= form.collection_select :post_status_id, PostStatus.all, :id, :name %>
That should at the least get you started!
You can try something like this:
class Post < ActiveRecord::Base
validates_inclusion_of :status, :in => [:draft, :pending, :publish]
def status
read_attribute(:status).to_sym
end
def status= (value)
write_attribute(:status, value.to_s)
end
end
where status is :string, limit: 20 (it's just for example) in migration
or you can try to use https://github.com/jeffp/enumerated_attribute

Ordering a has_many list in view

I have a model called Person that the user selects five personality Traits for. However, the order they pick them for matters (they are choosing most descriptive to least descriptive).
I know how to create a join table with a poison an do ordering that way. I'm using acts_as_list as well.
But I can't seem to find any help on, is how to create a way for the user of my app to set the order of the traits. That is I want to have say five select boxes on in the HTML and have them pick each one, and use something like jQuery UI Sortable to allow them to move them around if they like.
Here is a basic idea of my models (simplified for the purpose of just getting the concept).
class Person < ActiveRecord::Base
has_many :personalizations
has_many :traits, :through => :personalizations, :order => 'personalizations.position'
end
class Personalization < ActiveRecord::Base
belongs_to :person
belongs_to :trait
end
class Trait < ActiveRecord::Base
has_many :persons
has_many :persons, :through => :personalizations
end
I just have no idea how to get positioning working in my view/controller, so that when submitting the form it knows which trait goes where in the list.
After a lot of research I'll post my results up to help someone else encase they need to have list of records attached to a model via many-to-many through relationship with being able to sort the choices in the view.
Ryan Bates has a great screencast on doing sorting with existing records: http://railscasts.com/episodes/147-sortable-lists-revised
However in my case I needed to do sorting before my Person model existed.
I can easily add an association field using builder or simple_form_for makes this even easier. The result will be params contains the attribute trait_ids (since my Person has_many Traits) for each association field:
#view code (very basic example)
<%= simple_form_for #character do |f| %>
<%= (1..5).each do |i| %>
<%= f.association :traits %>
<% end %>
<% end %>
#yaml debug output
trait_ids:
- ''
- '1'
- ''
- '2'
- ''
- '3'
- ''
- '4'
- ''
- '5'
So then the question is will the order of the elements in the DOM be respected whenever the form is submitted. Specially if I implement jQuery UI draggable? I found this Will data order in post form be the same to it in web form? and I agree with the answer. As I suspected, too risky to assume the order will always be preserved. Could lead to a bug down the line even if it works in all browsers now.
Therefore after much looking I've concluded jQuery is a good solution. Along with a virtual attribute in rails to handle the custom output. After a lot of testing I gave up on using acts_as_list for what I am trying to do.
To explain this posted solution a bit. Essentially I cache changes to a virtual property. Then if that cache is set (changes were made) I verify they have selected five traits. For my purposes I am preserving the invalid/null choices so that if validation fails when they go back to the view the order will remain the same (e.g. if they skipped the middle select boxes).
Then an after_save call adds these changes to the database. Any error in after_save is still wrapped in a transaction so if any part were to error out no changes will be made. It was easiest therefore to just delete all the endowments and save the new ones (there might be a better choice here, not sure).
class Person < ActiveRecord::Base
attr_accessible :name, :ordered_traits
has_many :endowments
has_many :traits, :through => :endowments, :order => "endowments.position"
validate :verify_list_of_traits
after_save :save_endowments
def verify_list_of_traits
return true if #trait_cache.nil?
check_list = #trait_cache.compact
if check_list.nil? or check_list.size != 5
errors.add(:ordered_traits, 'must select five traits')
elsif check_list.uniq{|trait| trait.id}.size != 5
errors.add(:ordered_traits, 'traits must be unique')
end
end
def ordered_traits
list = #trait_cache unless #trait_cache.nil?
list ||= self.traits
#preserve the nil (invalid) values with '-1' placeholders
list.map {|trait| trait.nil?? '-1' : trait.id }.join(",")
end
def ordered_traits=(val)
#trait_cache = ids.split(',').map { |id| Trait.find_by_id(id) }
end
def save_endowments
return if #trait_cache.nil?
self.endowments.each { |t| t.destroy }
i = 1
for new_trait in #trait_cache
self.endowments.create!(:trait => new_trait, :position => i)
i += 1
end
end
Then with simple form I add a hidden field
<%= f.hidden :ordered_traits %>
I use jQuery to move the error and hint spans to the correct location inside
the div of five select boxes I build. Then I had a submit event handler on the form and convert the selection from the five text boxes in the order they are in the DOM to an array of comma separated numbers and set the value on the hidden field.
For completeness here is the other classes:
class Trait < ActiveRecord::Base
attr_accessible :title
has_many :endowments
has_many :people, :through => :endowments
end
class Endowment < ActiveRecord::Base
attr_accessible :person, :trait, :position
belongs_to :person
belongs_to :trait
end

How do I get Index-tank to do conditional indexing?

After a long conversation with the fine people at IndexTank, I was not really sure on how to fix my problem, and I was wondering if anyone could help me.
I have an article model that belongs to a user model. This article model also has a boolean attribute called anonymous, which, if set to true gives the user the option to post the article without his name being shown.
Article
belongs_to :user
attr_accesible :anonymous, :user_id
User
has_many :articles
My problem is that if the article is posted as anonymous. I don't want tanker to search within the author name field, but I want it searching every other field. I tried to do this with an if else statement where I would normally put the "tankit" block, but that does not work.
Is there a way I could put the tankit block into a model method and use a validation call back like this?
def anon_index
if self.anonymous
tankit 'my_index' do
indexes VARIABLES ETC BUT NOT the user_ attributes
end
else # if anonymous is false
tankit 'my_index' do
indexes :title
indexes :body
indexes :user_penname
indexes :user_firstname
indexes :user_lastname
end
end
end
I was thinking either this or putting an if else statement where the "tankit" block declaration goes, but neither of those seem to, unless I'm doing something wrong.
how does this look?:
class Article < ActiveRecord::Base
tankit 'my_index' do
indexes :title
indexes :body
indexes :custom_penname
indexes :custom_firstname
indexes :custom_lastname
end
def custom_penname
if self.anonymous
'anonymous'
else
self.user_penname
end
end
def custom_firstname
#same for first name
end
def custom_lastname
#same for last name
end
end
Same approach, different scenario:
https://github.com/adrnai/rails-3-tanker-demo/blob/master/app/models/comment.rb

Update owner tags via form

I would like to uniquely use owner tags in my app. My problem is that when I create / update a post via a form I only have f.text_field :tag_list which only updates the tags for the post but has no owner. If I use f.text_field :all_tags_list it doesn't know the attribute on create / update. I could add in my controller:
User.find(:first).tag( #post, :with => params[:post][:tag_list], :on => :tags )
but then I have duplicate tags, for post and for the owner tags. How can I just work with owner tags?
The answer proposed by customersure (tsdbrown on SO) on https://github.com/mbleigh/acts-as-taggable-on/issues/111 works for me
# In a taggable model:
before_save :set_tag_owner
def set_tag_owner
# Set the owner of some tags based on the current tag_list
set_owner_tag_list_on(account, :tags, self.tag_list)
# Clear the list so we don't get duplicate taggings
self.tag_list = nil
end
# In the view:
<%= f.text_field :tag_list, :value => #obj.all_tags_list %>
I used an observer to solve this. Something like:
in /app/models/tagging_observer.rb
class TaggingObserver < ActiveRecord::Observer
observe ActsAsTaggableOn::Tagging
def before_save(tagging)
tagging.tagger = tagging.taggable.user if (tagging.taggable.respond_to?(:user) and tagging.tagger != tagging.taggable.user)
end
end
Don't forget to declare your observer in application.rb
config.active_record.observers = :tagging_observer
Late to the party, but I found guillaume06's solution worked well, and I added some additional functionality to it:
What this will enable: You will be able to specify the tag owner by the name of the relationship between the tagged model and the tag owner model.
How: write a module and include in your lib on initialization (require 'lib/path/to/tagger'):
module Giga::Tagger
extend ActiveSupport::Concern
included do
def self.tagger owner
before_save :set_tag_owner
def set_tag_owner
self.tag_types.each do |tag|
tag_type = tag.to_s
# Set the owner of some tags based on the current tag_list
set_owner_tag_list_on(owner, :"#{tag_type}", self.send(:"#{tag_type.chop}_list"))
# Clear the list so we don't get duplicate taggings
self.send(:"#{tag_type.chop}_list=",nil)
end
end
end
end
end
Usage Instructions:
Given: A model, Post, that is taggable
A model, User, that is the tag owner
A post is owned by the user through a relationship called :owner
Then add to Post.rb:
include Tagger
acts_as_taggable_on :skills, :interests, :tags
tagger :owner
Make sure Post.rb already has called acts_as_taggable_on, and that User.rb has acts_as_tagger
Note: This supports multiple tag contexts, not just tags (eg skills, interests)..
the set_tag_owner before_save worked for me. But as bcb mentioned, I had to add a condition (tag_list_changed?) to prevent the tags from being deleted on update:
def set_tag_owner
if tag_list_changed?
set_owner_tag_list_on(account, :tags, tag_list)
self.tag_list = nil
end
end
When working with ownership the taggable model gets its tags a little different. Without ownership it can get its tags like so:
#photo.tag_list << 'a tag' # adds a tag to the existing list
#photo.tag_list = 'a tag' # sets 'a tag' to be the tag of the #post
However, both of these opperations create taggins, whose tagger_id and tagger_type are nil.
In order to have these fields set, you have to use this method:
#user.tag(#photo, on: :tags, with: 'a tag')
Suppose you add this line to the create/update actions of your PhotosController:
#user.tag(#photo, on: :tags, with: params[:photo][:tag_list])
This will create two taggings (one with and one without tagger_id/_type), because params[:photo][:tag_list] is already included in photo_params. So in order to avoid that, just do not whitelist :tag_list.
For Rails 3 - remove :tag_list from attr_accessible.
For Rails 4 - remove :tag_list from params.require(:photo).permit(:tag_list).
At the end your create action might look like this:
def create
#photo = Photo.new(photo_params) # at this point #photo will not have any tags, because :tag_list is not whitelisted
current_user.tag(#photo, on: :tags, with: params[:photo][:tag_list])
if #photo.save
redirect_to #photo
else
render :new
end
end
Also note that when tagging objects this way you cannot use the usual tag_list method to retrieve the tags of a photo, because it searches for taggings, where tagger_id IS NULL. You have to use instead
#photo.tags_from(#user)
In case your taggable object belongs_to a single user you can also user all_tags_list.
Try using delegation:
class User < ActiveRecord::Base
acts_as_taggable_on
end
class Post < ActiveRecord::Base
delegate :tag_list, :tag_list=, :to => :user
end
So when you save your posts it sets the tag on the user object directly.
I ended up creating a virtual attribute that runs the User.tag statement:
In my thing.rb Model:
attr_accessible :tags
belongs_to :user
acts_as_taggable
def tags
self.all_tags_list
end
def tags=(tags)
user = User.find(self.user_id)
user.tag(self, :with => tags, :on => :tags, :skip_save => true)
end
The only thing you have to do is then change your views and controllers to update the tag_list to tags and make sure you set the user_id of the thing before the tags of the thing.

How do I handle nils in views?

I have the following models set up:
class Contact < ActiveRecord::Base
belongs_to :band
belongs_to :mode
validates_presence_of :call, :mode
validates_associated :mode, :band
validates_presence_of :band, :if => :no_freq?
validates_presence_of :freq, :if => :no_band?
protected
def no_freq?
freq.nil?
end
def no_band?
band.nil?
end
end
class Band < ActiveRecord::Base
has_many :logs
end
class Mode < ActiveRecord::Base
has_many :logs
end
When I enter a frequency on my new view it allows for no band to be specified if a freq is entered. This creates a problem in my other views though because band is now nil. How do I allow for band not to be specified and just show up as empty on my index and show views, and then in the edit view allow one to be specified at a later point in time.
I have been able to get my index to display a blank by doing:
contact.band && contact.band.name
But I'm not sure if this is a best approach, and I'm unsure of how to apply a similar solution to my other views.
Many thanks from a rails newb!
In my views, I use the following for potentially nil objects in my views:
<%= #contact.band.name unless #contact.band.blank? %>
if your object is an array or hash, you can use the empty? function instead.
<%= unless #contacts.empty? %>
..some code
<% end %>
Hope this helps!
D
A couple years old but still a top Google result for "rails view handle nil" so I'll add my suggestion for use with Rails 3.2.3 and Ruby 1.9.3p0.
In application_helper.rb, add this:
def blank_to_nbsp(value)
value.blank? ? " ".html_safe : value
end
Then to display a value in a view, write something like this:
<%= blank_to_nbsp contact.band %>
Benefits:
"blank" catches both nil values and empty strings (details).
Simply omitting a nil object, or using an empty string, may cause formatting issues. pushes a non-breaking space into the web page and preserves formatting.
With the "if" and "unless" suggestions in other answers, you have to type each object name twice. By using a helper, you only have to type each object name once.
<%= #contact.try(:band).try(:name) %>
This will return nil if band or name do not exist as methods on their respective objects.
You can use Object#andand for this:
<%= #contact.band.andand.name %>
<%= #contact.band if #contact.band %> also works

Resources