I have a model posts, which belongs_to category (which uses friendly_id). Now i want to list all Posts in an Category. To get the index page i want to use a link like: http://mysite/posts/category/_category_slug_, for that i made the following route:
match 'posts/category/:category/' => 'posts#index'
And in my post controller i got:
def index
if params[:category]
#posts = Post.all(:joins => :category, :conditions => {"categories.cached_slug" => params[:category]})
else
#posts = Post.all.reverse
end
...
It works like it should, but i dont think its the friedndly_id way to do it.
Is there a better way to achive this? thanks for your help.
FriendlyId adds the ability to do a find on a model using the slug, and is smart enough to check the cached_slug column first.
You can achieve the same result by performing a find on the Category model first then getting all the posts.
This assumes there is a has_many and belongs_to association in place with the referencing ID columns (or HABTM)
def index
if params[:category]
#posts = Category.find(params[:category]).posts
else
#posts = Post.all.reverse
end
...
Since you're passing in a category param (being friendly_id), it makes sense to reference it via the Category model.
-- more info added --
ALSO: Historical finds will still work ..
So, if you have generated a new slug by renaming a category, the old url will behave correctly (great if you're avoiding 404's)
Related
I have a problem with ordering search results on rails.
I want to order the posts after I searched or filtered but what I'm doing doesn't seem to work.
Here is my code.
posts_controller.rb
display
#posts = Post.where("user_id IN (?)", current_user.following_ids)
#posts = #posts.find(Like.group(:post_id).pluck(:post_id)).order(params[:change]) if params[:change].present?
end
I have a posts_controller which displays the posts user are following to, it works fine. However, when I added an extra code trying to order the posts by the number of likes, error occurs.
It seems that the second line in post controller extracts all the posts with like instead of only the posts the user are following. If I replace #posts.find with Post.find, all the posts result will be shown and ordered by the number of likes.
by the way, this, #posts.order(created_at: :desc), works fine. It orders only the posts user are following.
like.rb
class Like < ApplicationRecord
belongs_to :post, foreign_key: "post_id"
belongs_to :user
validates_uniqueness_of :post_id, scope: :user_id
end
like_controller.rb
class LikesController < ApplicationController
def create
#like = current_user.likes.create(post_id: params[:post_id])
redirect_back(fallback_location: root_path)
end
def destroy
#like = Like.find_by(post_id: params[:post_id], user_id: current_user.id)
#like.destroy
redirect_back(fallback_location: root_path)
end
end
I'm very new at programming so please forgive me if I'm asking a stupid question. Thank you all very much.
The error message is below:
Note that find, when given an array, requires that all of the id's in the array be present or you get an error. In the documentation, it states:
If one or more records can not be found for the requested ids, then RecordNotFound will be raised.
I would use a different mechanism to retrieve the records:
#posts = #posts.where(id: Like.group(:post_id).pluck(:post_id)).
order(params[:change]) if params[:change].present?
This will locate just those records that satisfy the condition.
This feels a little clunky. There might be a cleaner way of getting this result depending upon what relationship might exist in your models between Like, Post, and Group.
I have a Model called Category and another called Articles. Categories are "sections" that have many Articles, for instance News and Events. Both Categories use the kind of Articles, except they're shown under a different section of my website.
Right now I'm creating the News controller (NewsController), and I'd like to visit /news/new to add News. Likewise, the same would apply to EventsController and /events/new.
What do I have to use on my routes to do this?
My first attempt was to use:
resources :categories do
resources :articles, path: '/news'
end
But this forces me to use /categories/1/news/new, which is kinda ugly.
If News will always be category_id 1 and Events will always be 2, how would I specify this on my routes, so I can easily access them with the URLs I mentioned?
Explained Differently
I have an Articles model. I'd like to have a controller called NewsController to handle Articles, so that /news/new (and the rest of the paths) would work with Article. I'd also like to have a controller called EventsController that would also handle Articles, so that /events would also work with Article. The difference between them is that they have different category_id.
Is this possible to do via routes?
Update
Made some progress.
resources :categories do
resources :articles
end
get 'news/new' => 'articles#new', defaults: {category_id: 1}
get 'events/new' => 'articles#new', defaults: {category_id: 2}
This fixes what I wanted to do with /news/new and /events/new, but I'd be missing the rest of the routes (edit, show, update, etc). Also, this makes me use the Articles controller, which currently does not exist and would also make the News controller obsolete/useless.
My logic may be wrong, it's kinda evident with what I just made, but perhaps with this update I can better illustrate what I'm trying to do.
Update 2
I'm currently testing the following:
resources :articles, path: '/news', controller: 'news'
resources :articles, path: '/events', controller: 'events'
So far it makes sense, it makes the routes I wanted, it uses both controllers with their own configurations, and it hasn't spat any errors when I visit both /news and /events (yet).
It's also possible to do:
resources :articles, path: '/news', defaults: {category_id: 1}
resources :articles, path: '/events', defaults: {category_id: 2}
But this would depend on an Article controller, which could handle both types of Categories. Either solution works (theoretically), though I'd incline more on the first since the individual controllers would allow more specific configuration to both cases. The second, though, is more adequate when there're not that many difference between the Articles being created. The defaults property isn't explicitly necessary either, I just put it there for convenience.
Your question is asking something that I question as not making sense and maybe your design is flawed.
Why would you have news resources related to category resources if they are not related?
Is categories just a name space?
If news records really are always going to be related to the same first category as your question implies then you can not use ID's as you have no control over what the id will be for the first category and the first category could have an ID of anything in which case you could just use the top level news resources and do a find first category in your model in a before create then you don't have to worry about an ugly url.
If news records really are related to categories then the you must supply the relevant category id and nest your routes but you could pretty up the url using the following from
https://gist.github.com/jcasimir/1209730
Which states the following
Friendly URLs
By default, Rails applications build URLs based on the primary key --
the id column from the database. Imagine we have a Person model and
associated controller. We have a person record for Bob Martin that has
id number 6. The URL for his show page would be:
/people/6
But, for aesthetic or SEO purposes, we want Bob's name in the URL. The
last segment, the 6 here, is called the "slug". Let's look at a few
ways to implement better slugs. Simple Approach
The simplest approach is to override the to_param method in the Person
model. Whenever we call a route helper like this:
person_path(#person)
Rails will call to_param to convert the object to a slug for the URL.
If your model does not define this method then it will use the
implementation in ActiveRecord::Base which just returns the id.
For this method to succeed, it's critical that all links use the
ActiveRecord object rather than calling id. Don't ever do this:
person_path(#person.id) # Bad!
Instead, always pass the object:
person_path(#person)
Slug Generation
Instead, in the model, we can override to_param to include a
parameterized version of the person's name:
class Person < ActiveRecord::Base def to_param
[id, name.parameterize].join("-") end end
For our user Bob Martin with id number 6, this will generate a slug
6-bob_martin. The full URL would be:
/people/6-bob-martin
The parameterize method from ActiveSupport will deal with converting
any characters that aren't valid for a URL. Object Lookup
What do we need to change about our finders? Nothing! When we call
Person.find(x), the parameter x is converted to an integer to perform
the SQL lookup. Check out how to_i deals with strings which have a mix
of letters and numbers:
"1".to_i
=> 1
"1-with-words".to_i
=> 1
"1-2345".to_i
=> 1
"6-bob-martin".to_i
=> 6
The to_i method will stop interpreting the string as soon as it hits a
non-digit. Since our implementation of to_param always has the id at
the front followed by a hyphen, it will always do lookups based on
just the id and discard the rest of the slug. Benefits / Limitations
We've added content to the slug which will improve SEO and make our
URLs more readable.
One limitation is that the users cannot manipulate the URL in any
meaningful way. Knowing the url 6-bob-martin doesn't allow you to
guess the url 7-russ-olsen, you still need to know the ID.
And the numeric ID is still in the URL. If this is something you want
to obfuscate, then the simple scheme doesn't help. Using a Non-ID
Field
Sometimes you want to get away from the ID all together and use
another attribute in the database for lookup. Imagine we have a Tag
object that has a name column. The name would be something like ruby
or rails. Link Generation
Creating links can again override to_param:
class Tag < ActiveRecord::Base validates_uniqueness_of :name
def to_param
name end end
Now when we call tag_path(#tag) we'd get a URL like /tags/ruby. Object
Lookup
The lookup is harder, though. When a request comes in to /tags/ruby
the ruby will be stored in params[:id]. A typical controller will call
Tag.find(params[:id]), essentially Tag.find("ruby"), and it will fail.
Option 1: Query Name from Controller
Instead, we can modify the controller to
Tag.find_by_name(params[:id]). It will work, but it's bad
object-oriented design. We're breaking the encapsulation of the Tag
class.
The DRY Principle says that a piece of knowledge should have a single
representation in a system. In this implementation of tags, the idea
of "A tag can be found by its name" has now been represented in the
to_param of the model and the controller lookup. That's a maintenance
headache. Option 2: Custom Finder
In our model we could define a custom finder:
class Tag < ActiveRecord::Base validates_uniqueness_of :name
def to_param
name end
def self.find_by_param(input)
find_by_name(input) end end
Then in the controller call Tag.find_by_param(params[:id]). This layer
of abstraction means that only the model knows exactly how a Tag is
converted to and from a parameter. The encapsulation is restored.
But we have to remember to use Tag.find_by_param instead of Tag.find
everywhere. Especially if you're retrofitting the friendly ID onto an
existing system, this can be a significant effort. Option 3:
Overriding Find
Instead of implementing the custom finder, we could override the find
method:
class Tag < ActiveRecord::Base #... def self.find(input)
find_by_name(input) end end
It will work when you pass in a name slug, but will break when a
numeric ID is passed in. How could we handle both?
The first temptation is to do some type switching:
class Tag < ActiveRecord::Base #... def self.find(input)
if input.is_a?(Integer)
super
else
find_by_name(input)
end end end
That'll work, but checking type is very against the Ruby ethos.
Writing is_a? should always make you ask "Is there a better way?"
Yes, based on these facts:
Databases give the id of 1 to the first record
Ruby converts strings starting with a letter to 0
class Tag < ActiveRecord::Base #... def self.find(input)
if input.to_i != 0
super
else
find_by_name(input)
end end end
Or, condensed down with a ternary:
class Tag < ActiveRecord::Base #... def self.find(input)
input.to_i == 0 ? find_by_name(input) : super end end
Our goal is achieved, but we've introduced a possible bug: if a name
starts with a digit it will look like an ID. If it's acceptable to our
business domain, we can add a validation that names cannot start with
a digit:
class Tag < ActiveRecord::Base #... validates_format_of :name,
:without => /^\d/ def self.find(input)
input.to_i == 0 ? find_by_name(input) : super end end
Now everything should work great! Using the FriendlyID Gem
Does implementing two additional methods seem like a pain? Or, more
seriously, are you going to implement this kind of functionality in
multiple models of your application? Then it might be worth checking
out the FriendlyID gem: https://github.com/norman/friendly_id Setup
The gem is just about to hit a 4.0 version. As of this writing, you
want to use the beta. In your Gemfile:
gem "friendly_id", "~> 4.0.0.beta8"
Then run bundle from the command line. Simple Usage
The minimum configuration in your model is:
class Tag < ActiveRecord::Base extend FriendlyId friendly_id :name
end
This will allow you to use the name column or the id for lookups using
find, just like we did before. Dedicated Slug
But the library does a great job of maintaining a dedicated slug
column for you. If we were dealing with articles, for instance, we
don't want to generate the slug over and over. More importantly, we'll
want to store the slug in the database to be queried directly.
The library defaults to a String column named slug. If you have that
column, you can use the :slugged option to automatically generate and
store the slug:
class Tag < ActiveRecord::Base extend FriendlyId friendly_id
:name, :use => :slugged end
Usage
You can see it in action here:
t = Tag.create(:name => "Ruby on Rails")
=> #
Tag.find 16
=> #
Tag.find "ruby-on-rails"
=> #
t.to_param
=> "ruby-on-rails"
We can use .find with an ID or the slug transparently. When the object
is converted to a parameter for links, we'll get the slug with no ID
number. We get good encapsulation, easy usage, improved SEO and easy
to read URLs.
If you are sure there will be only 2 categories, why not simply add a boolean to the articles?
Like: article.event = true if events category, false if news
Then you can add a scopes to Article class for both categories
class Article
scope :events, -> { where(event: true) }
scope :news, -> { where(event: false) }
end
Create controllers, for example:
class EventsController < ApplicationController
def index
#articles = Article.events
end
def create
#article.new(params)
#article.event = true
#article.save
end
...
end
and routes: resources :events
You should try to use dynamic segments: http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
Add some slug attribute to Category, it should be unique and add index to it.
# routes
resources :articles, except: [:index, :new]
get '*category_slug/new', to: 'articles#new'
get '*category_slug', to: 'articles#index'
# controller
class ArticlesController < ApplicationController
def index
#category = Category.find_by slug: params[:category_slug]
#articles = #category.articles
end
def new
#category = Category.find_by slug: params[:category_slug]
#article = #category.articles.build
end
...
end
Remember to put a category in a hidden field in the form_for #article
I have a Post model combined with gem Acts_as_Taggable_on.
I would like to display all posts with all their tags, but the tags should be sorted by number of their use (number of posts tagged with certain tag).
To do that I looped through ActiveRecord_Relation and did a sort on Tags column:
def index
temp_posts = Post.all.order('updated_at DESC')
temp_posts.each_with_index do |temp_post, index|
temp_posts[index].tags = temp_post.tags.sort_by {|tag| -tag.taggings_count}
end
#show = temp_posts.first.tags.sort_by {|tag| -tag.taggings_count} # according to this control output it should work
#posts = temp_posts
end
When looking through the control output #show, the tags are sorted as required, but they are not saved into the temp_posts variable. The output is thus unsorted.
What can I do to 'save' the changes I made in the loop?
Since you have Tag#taggings_count, you can order your associations by it. I don't know if this will conflict with what ActsAsTaggable is up to, but this is what it would look like in vanilla Rails. Perhaps ActsAsTaggable has some options to accomplish the same thing.
class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, through: :taggings, -> { order(taggings_count: :desc) }
end
For more details, see Scopes for has_many.
If you don't want the order applied globally, andrykonchin's idea is a good one. Write a Post#sorted_tags method and you can access it on the Post when you want it. Memoizing it into an instance variable will prevent extra database queries.
class Post < ActiveRecord::Base
def sorted_tags
#sorted_tags ||= tags.sort_by(&:taggings_count).reverse
end
end
The problem was in the end only with using an invalid variable for saving the sorted tags.
Acts as Taggable on uses variable tag_list to store the tags associated with the Tag model. Instead I have wrongly used variable tags.
The full correct version of my code:
def index
temp_posts = Post.all.order('updated_at DESC')
temp_posts.each_with_index do |temp_post, index|
// CHANGE: temp_posts[index].tags => temp_posts[index].tag_list
temp_posts[index].tag_list = temp_post.tags.sort_by {|tag| -tag.taggings_count}
end
#posts = temp_posts
end
How would one correctly search multiple models in SunSpot Solr?
Profile model
has_one :match
searchable do
string :country
string :state
string :city
end
Match model
belongs_to :profile
searchable do
string :looking_for_education
integer :age_from
integer :age_to
end
ProfilesController#Index
def index
#search = Sunspot.search Profile, Match do
with(:country, params[:country])
with(:state, params[:state])
with(:looking_for_education, params[:looking_for_education]) <= from the 2nd model
end
#profiles = #search.results
end
This fails with:
Using a with statement like
with(:age).between(params[:age_from]..params[:age_to])
undefined method `gsub' for nil:NilClass
Removing the
with(:age).between(params[:age_from]..params[:age_to]) line then it tries to
then it tries to load the
view app/views/educators/educator.html.haml
which does not exist ( im only using
/app/views/profiles/_profile.html.haml
to show profiles
EDIT #1:
What are good opensource projects in ruby on rails that use sunspot and solr in a bit more advanced way to have a look at? Maybe I can find the answer there. Any answer in this direction will also be accepted the bounty if it yields in resulting this issue, thx!
The method you've found for searching multiple models is correct. However, it appears that the meaning of your search is not what you intended. It looks as if you're trying to say:
Give me all Profile records with these country and state values, and whose Match record has this looking_for_education value
Your search, however, says:
Give me all records of type Profile or Match that have all of these country, state and looking_for_education values
Because neither Profile nor Match have all of these fields in their respective searchable blocks, no single record can match the conditions you specify.
If I'm correct about your intended behaviour above, then you need to include the profile's associated match information in the profile's searchable block, like so:
class Profile < ActiveRecord::Base
has_one :match
searchable do
string(:country)
string(:state)
string(:city)
string(:looking_for_education) { match.looking_for_education }
integer(:age_from) { match.age_from }
integer(:age_to) { match.age_to }
end
end
Here, we've told Sunspot to index properties of the profile's match association as if they lived on the profile itself. In the respective blocks, we've told Sunspot how to populate these values when the profile is indexed.
This will allow you to write your search using only the Profile model:
def index
#search = Sunspot.search Profile do
with(:country, params[:country])
with(:state, params[:state])
with(:looking_for_education, params[:looking_for_education])
with(:age).between(params[:age_from]..params[:age_to])
end
#profiles = #search.results
end
This search will return only Profile records, while still reflecting the properties of each profile's match association, because we stored them when the profile was indexed.
Note that this increases complexity when you index your models. If a Match record changes, its associated profile now needs to be reindexed to reflect those changes.
This is what i am doing when i have to search for multiple models
Sunspot.search [Model1, Model2] do
....
end
#moises-zaragoza answered correctly your question but you have more issues than you think with what you want to do.
The first error:
Using a with statement like
with(:age).between(params[:age_from]..params[:age_to])
undefined method `gsub' for nil:NilClass
Is most likely produced because params[:age_from] and/or params[:age_to] are nil. I can't assure it because you haven't shown the stacktrace. You can fix by filter only when they are present:
with(:age).between(params[:age_from]..params[:age_to]) if params[:age_from].present? and params[:age_to].present?
The second error
Related to your views. I am assuming you are rendering a collection or object with one of the rails helper partial object helpers, without specifying the partial (again, without the code this is more of a good guess than anything else):
<%= render #results %>
or
<% #results.each do |result| %>
<%= render result %>
<% end %>
When Rails does not have a partial specified, it guesses the partial name depending on the object type. In this case, if your object is for example of class Educator, which might be a subclass of Profile, Rails will look for the partial 'educators/_educator.html.erb. Make sure you render the proper partial depending on the object type and this will ensure you render what you want.
Here the answer for search different model on matching string using with
searchable(:auto_index => AppConfig.solr.auto_index) do
string :category_name, :stored => true
text :content, :stored => true
text :title
string :company_id, :stored => true
time :published_on
end
search do |q|
if params[:keyword].present?
q.fulltext params[:keyword] do
fields(:deal_data)
end
end
if (ids = params["company_id"]).present?
ids = ids.split(",")
q.with(:company_id,ids) #here company id formate should be ["***","***"]
end
end
I'm trying to create a multi criteria search form. I want to submit all of the pieces of the search via GET and if they have a value assigned I would like them evaluated. The thing that I'm having trouble grasping is building a query that will allow me to layer more queries on top when you're doing it with a through association.
Just to give you an idea of how my models are set up:
class Client < ActiveRecord::Base
has_many :campaigns
has_many :pieces, :through => :campaigns
end
class Campaign < ActiveRecord::Base
belongs_to :client
has_many :pieces
end
class Piece < ActiveRecord::Base
belongs_to :client
end
Now, with that model in mind, I'm using the collect method to grab the pieces that have an organization in common.
if params.has_key?(:criteria)
#selected_client = Client.where(:organization => "Org1")
#pieces = #selected_client.collect{ |c| c.pieces }.flatten
end
Is there some way of formatting that query string so that I can narrow #pieces down, a couple more times? Let's say I wanted to use that through association again, to get pieces that have another of the same Client criteria...
Thanks a ton! My brain is a pretzel at this point.
I'm not sure if i undestand very well what you're trying to do. If you want to get all pieces matching your client criteria, in rails 3, you can do this :
#pieces = Piece.joins(:campaign => :client).where(:clients => {:organization => criteria})
to get all pieces belonging to clients from organization "Org1".
You can stack as many where statements as you want to add new conditions, as #Andrew said. See this for more information.
Your first paragraph got cut off, just FYI.
If #pieces is an array, you can use a find block to narrow your search further. Although this will put the load on your server CPU rather than the SQL database.
You can stack where statements and Rails will automatically create a single query for you. Here is some sample code from the app store section of our website:
#platform = params[:platform]
#category = params[:category]
#search = params[:app_q]
# Get initial SQL, narrow the search, and finally paginate
#apps = App.live
#apps = #apps.where("platform = ?", AppPlatforms.value(#platform)) if AppPlatforms.value(#platform)
#apps = #apps.where("(name LIKE ? OR description LIKE ?)", "%#{#search}%", "%#{#search}%") if #search
#apps = #apps.where("id IN(?)", #cat.apps) if #category && #cat = Category.find_by_id(#category)
#apps = #apps.paginate(:page => params[:page], :per_page => 10, :order => "name")
Instead of using collect you should be able to use #selected_client.pieces.where(...) to narrow your search down via SQL.
I hope this is a point in the right direction!