"IS NOT NULL" conditions for associations in Thinking Sphinx search - ruby-on-rails

How do you add an "IS NOT NULL" condition for associations in Thinking Sphinx search? For example if we have an article model which has the following index..
ThinkingSphinx::Index.define :article, :with => :active_record do
indexes subject, :sortable => true
indexes content
has pictures(:id), as: :picture_ids
end
..and we want to search for all articles which contain a certain keyword and have a picture. Articles and pictures are related by a simple has_many relationship
class Article < ActiveRecord::Base
has_many :pictures, -> { where 'pictures.type' => 'ArticlePicture' }
The following line used to work, as it is described here, but it no longer seems to work :-(
Article.search(keyword, without: {picture_ids: 0})
What is the correct way to do it? I am using Sphinx 2.2.10 and thinking-sphinx 3.2.0

You can add an additional attribute with a SQL snippet:
has "COUNT(DISTINCT pictures.id)", :as => :picture_count, :type => :integer
And then - once you've run rake ts:rebuild - I'd expect the following to work:
Article.search(keyword, :without => {:picture_count => 0})
It's important to note that you'll still need a reference to the pictures association in your index definition to ensure there's a SQL join. This is done by your existing attribute (picture_ids), or otherwise you can force the join using the following line within the index definition:
join pictures

Related

The method .joins() must contain arguments (Syntax problem from Rails v4.2.1 to Rails v6.1.3.1)

I have been working on an App called CtrlPanel for the company I work for.
This app was originally running on Ruby v2.2.2 and rails v4.2.1. I could not get that environment to work on ANYTHING; I tried both PC and Linux. Since I couldn't get that environment running and since it needed to be updated to the newest version anyway I figured I would just get it working on the latest version.
I had no idea what I was in for, that was a little over a month ago. I am happy to report I now have everything in the program working with one exception. There is a catalog that displays all of the items and it uses a scope in the model with a lambda expression that is rather complicated (at least to me). I have had to update the syntax ALL over this application due to the older version of Rails and now being on the newest version and this is the only one I can't seem to figure out. I am pretty sure again that it is just a Syntax problem from Rails v4.2.1 to Rails v6.1.3.1 but I just can't seem to figure it out and I am sure people who are more experienced than myself will know what it is.
Here is the model in question:
category.rb
# == Schema Information
#
# Table name: categories
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Category < ActiveRecord::Base
attr_accessible :name
# accepts_nested_attributes_for :product_categories
has_many :products, through: :product_categories
has_many :product_categories, dependent: :destroy
default_scope { order("id") }
scope :with_published_products, -> { joins{product_categories.product}.where{products.status.eq "published"}.uniq }
scope :with_matched_search_terms, ->(search_terms) { joins{product_categories.product}.where{products.name.like search_terms}.uniq }
end
The line with the issue is:
scope :with_published_products, -> { joins{product_categories.product}.where{products.status.eq "published"}.uniq }
It is giving the error:
The method .joins() must contain arguments
I had other joins statements that I had to fix (numerous). It was due to syntax for a very BASIC example from joins{arguments} to joins(arguments). I assume that is the same case here. In this case there are many more elements and that lambda expression thrown into the mix which is making it much harder (for me at least) to get it corrected. I have tried every version of changing that line around I can think of and probably many that do not make sense. I have one version where it will get past the line but then when that "with_published_products" is called later in the view it errors out with a bad PG statement and when I traced it back it was due to the very same query. Here is the view involved:
- if current_user
.pull-left{style: "position:absolute; top: 10px; left: 20px;"}
= link_to products_path do
%button.btn{style: "margin-top: 0px; "}
%i.icon-arrow-left.icon-large
%b back to products
.row-fluid
.span12
%h1.bigred DPF Product Catalog
%hr
.row-fluid
.span12
- unless #categories.empty?
.tabbable.tabbable-left
%ul.nav.nav-tabs{style: "text-align:right;"}
%li.active
%a{"data-toggle" => "tab", :href => "#tab-#{#categories.first.id}"} #{#categories.first.name}
- #categories.each do |category|
-unless category == #categories.first
%li
%a{"data-toggle" => "tab", :href => "#tab-#{category.id}"} #{category.name}
.tab-content
- #categories.each_with_index do |category, i|
.tab-pane.fade{id: "tab-#{category.id}", class: "#{ 'in active' if i.zero? }"}
%h1.bigred.professional{style: "text-align:left;"} #{category.name}
%hr
- #params_name.blank? ? #products = category.products.published.includes(:pictures).order("name asc") : #products = category.products.searched(#params_name).published.includes(:pictures).order("name asc")
= render partial: 'product', collection: #products, cache: true
- else
%h2
There were no products matching
%b #{#params_name.gsub(/%/, '')}
Here is the Controller:
class StaticPagesController < ApplicationController
#skip_before_filter :authenticate_user!, :only => [:catalog, :roi]
skip_before_action :authenticate_user!, :only => [:catalog, :roi]
#skip_authorization_check
before_action
def home
end
def help
end
def catalog
if params[:name].present?
#params_name = "%#{params[:name]}%"
#categories = Category.with_published_products.with_matched_search_terms(#params_name)
else
#categories = Category.with_published_products
end
render layout: "catalog"
end
def roi
render layout: "roi"
end
end
Best I can tell the original author is trying to display product_catagoires that have the status of "published". I might be over simplfying the statement but I beleive that is what I understand it to be doing. I know from other joins I had to fix I had to eliminate the .eq and put => in its place which I tried many versions of and it always complained about the syntax. As an example of what I mean this was another section I fixed:
FROM:
cds_item = item.variants.joins{vendor}.where{ vendor.code.eq 'cds' }.first
TO:
cds_item = item.variants.joins(:vendor).where( :vendor_id => 'cds' ).first
I tried to change the line I am having the problem with to a similar format and every combination I could think of I just can't seem to speak to Rails the way it wants.
If there are any other files I need to attach please let me know.
I appreciate any input anyone has to offer.
I am willing to make the scopes less complicated and more verbose if it solves the problem I just am too new to figure this last piece out.
Thank You,
Scott
Thank You #engineersmnky,
I am now getting the following error:
Cannot have a has_many :through association 'Category#products' which goes through 'Category#product_categories' before the through association is defined.
It is referencing this line in the view:
- #params_name.blank? ? #products = category.products.published.includes(:pictures).order("name asc") : #products = category.products.searched(#params_name).published.includes(:pictures).order("name asc")
Sorry if this is not the right way to add more information, I couldn't post the line dealing with the code in a comment.
My guess is that your legacy app used a gem called Squeel which monkeypatched core methods in ActiveRecord like where, joins etc to take a block argument:
Person.joins(:articles => {:comments => :person})
Becomes:
Person.joins{articles.comments.person}
The authors ambition was that Squeel would be incorporated into ActiveRecord but that didn't pass and the fact that the monkeypatches broke with every Rails release burned out the maintainers. Squeel was abandoned after Rails 4.2. But parts of it live on in the baby_squeel gem.
Some of the easier cases are going to be relatively straight forward to unfurl into modern ActiveRecord code. Others like a LIKE query will require some Arel:
class Category < ActiveRecord::Base
# Do not use attr_accessible
# - model level whitelisting was replaced by strong params in Rails 4
# attr_accessible :name
# accepts_nested_attributes_for :product_categories
# You need to declare the relations you are joining through first
has_many :product_categories, dependent: :destroy
has_many :products, through: :product_categories
# default scope is evil!
# default_scope { order("id") }
# use `lambda do` for multi-line lambdas to keep reasonable line lengths
# or just write regular class methods
scope :with_published_products, lambda do
joins(product_categories: :product)
.where( products: { status: "published" } )
.distinct
end
scope :with_matched_search_terms, lambda do |search_terms|
joins(product_categories: :product)
.where(
Product.arel_attribute(:name)
.lower
.matches("%#{search_terms.downcase}%")
.distinct
)
end
end
I'm not sure what #uniq was supposed to do in the Squeel - but I believe it may have added a distinct clause - like ActiveRecord::Relation#uniq which was deprechiated in Rails 5.0.

ActiveAdmin, polymorphic associations, and custom filters

Rails 3.1, ActiveAdmin 0.3.4.
My question is somewhat similar to this one but different enough in terms of data modeling that I think it warrants its own response. Models:
class CheckoutRequest < ActiveRecord::Base
has_one :request_common_data, :as => :requestable, :dependent => :destroy
end
class RequestCommonData < ActiveRecord::Base
belongs_to :requestable, :polymorphic => true
end
The RequestCommonData model has a completed field (boolean) that I'd like to be able to filter in ActiveAdmin's CheckoutRequest index page. I've tried a few different approaches to no avail, including the following:
filter :completed, :collection => proc { CheckoutRequest.all.map { |cr| cr.request_common_data.completed }.uniq }
which results in no filter being displayed. Adding :as => :select to the line, as follows:
filter :completed, :as => :select, :collection => proc { CheckoutRequest.all.map { |cr| cr.request_common_data.completed }.uniq }
results in the following MetaSearch error message:
undefined method `completed_eq' for #<MetaSearch::Searches::CheckoutRequest:0x007fa4d8faa558>
That same proc returns [true, false] in the console.
Any suggestions would be quite welcome. Thanks!
From the meta_search gem page you can see that for boolean values the 'Wheres' are:
is_true - Is true. Useful for a checkbox like “only show admin users”.
is_false - The complement of is_true.
so what you need is to change the generate input name from 'completed_eq' to be 'completed_is_true' or 'completed_is_false'.
The only way I have found this possible to do is with Javascript, since by looking at the Active Admin code, the 'Wheres' are hardcoded for each data type.
I would usually have a line like this in my activeadmin.js file (using jQuery)
$('#q_completed_eq').attr('name', 'q[completed_is_true]');
or
$('#q_completed_eq').attr('name', 'q[completed_is_false]');
Terrible and ugly hack but have found no other solution myself.
Be careful to enable this only in the pages you want.
--- NEW FOR VERSION 0.4.2 and newer ---
Now Active Admin uses separate modules for each :as => ... option in the filters.
So for example you can place the code below inside an initializer file
module ActiveAdmin
module Inputs
class FilterCustomBooleanInput < ::Formtastic::Inputs::SelectInput
include FilterBase
def input_name
"#{#method}_is_true"
end
def input_options
super.merge(:include_blank => I18n.t('active_admin.any'))
end
def method
super.to_s.sub(/_id$/,'').to_sym
end
def extra_input_html_options
{}
end
end
end
end
and the use
:as => :custom_boolean
where you specify your filter.

how to index associated models using thinkingtank and indextank

We are using thinkingtank gem and having trouble indexing model associations, even simple ones. For example, a profile belongs to an institution, which has a name – we would like to do something like:
class Profile < ActiveRecord::Base
#model associations
define_index do
indexes institution(:name), :as => :institution_name
end
end
but that doesn't work. This must be very simple – what am I doing wrong?
a possible solution to this issue would be adding a method returning the element to index. For the profile.institution.name case:
# profile.rb
# ...
belongs_to :institution
# ...
define_index do
indexes institution_name
end
def institution_name
self.institution.name
end
# ...
Also the ", :as => ..." syntax is not supported on thinkingtank.
I would also recommend giving a try to Tanker: https://github.com/kidpollo/tanker
Regards.
Adrian

Mongoid named scope comparing two time fields in the same document

I need to create a named scope in Mongoid that compares two Time fields within the same document. Such as
scope :foo, :where => {:updated_at.gt => :checked_at}
This obviously won't work as it treats :checked_at as a symbol, not the actual field. Any suggestions on how this can be done?
Update 1
Here is my model where I have this scope declared, with a lot of extra code stripped out.
class User
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Timestamps
field :checked_at, :type => Time
scope :unresolved, :where => { :updated_at.gt => self.checked_at }
end
This gives me the following error:
'<class:User>': undefined method 'checked_at' for User:Class (NoMethodError)
As far as I know, mongodb doesn't support queries against dynamic values.
But you could use a javascript function:
scope :unresolved, :where => 'this.updated_at >= this.checked_at'
To speed this up you could add an attribute like "is_unresolved" which will be set to true on update when this condition is matched ( and index that ).
scope :foo, :where => {:updated_at.gt => self.checked_at}
For example, this will work:
scope :foo, where(:start_date.lte=>Date.today.midnight)
Not sure if you'll like this method, it's not the best, but it should work.
class User
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Timestamps
field :checked_at, :type => Time
scope :unresolved, lambda{ |user| where(:updated_at.gt => user.checked_at) }
end
You call it with User.unresolved(my_user_object)
It seems now after rereading your post that this probably won't do what you want. If this is true, then you will probably need to use MapReduce or possibly Baju's method (have not tested it)

Custom getters in Ruby on Rails

I have a MailingList model that has_may :people
For most of my application, I only want to get people that are active
So #mailing_list.people should only return people that are active
In my model, I can't do
def people
self.people.find_all{ |p| !p.activated_at.nil? }
end
because that keeps calling itself. What is the ruby/rails way to automatically filter the people. Another possible issue is that I think self.people returns an array of active record objects where self.people.find_all... will return an array. This will cause some of my code to break. It's easy fixes but is there a way to return active record objects? It would be nice to have the option.
Thanks!
This is a perfect example for a named scope:
class Person < ActiveRecord::Base
named_scope :active, :conditions => 'activated_at is not null'
end
Then just call it:
# equivalent to Person.find(:all, :conditions => 'activated_at is not null')
#active_people = Person.active
You can also filter at the association level.
has_many :people, :conditions => {:activated => true}
You can used the standard find method or a dynamic finder. Your find might read as follows:
people.find(:all, :conditions => "activated_at = nil")
OR
people.find_all(:conditions => "activated_at = nil")
A dynamic version of this might read as:
people.find_by_activated_at(nil)

Resources