Call to a collection path keeps passing an id - ruby-on-rails

I have a search method in a rails app I'm writing. Here is an excerpt.
The intent is to use regex to match all items with a description which matches a search query . Then, all matching item ids are passed as a string to another page. (this all takes place in a Search controller).
The item controller then catches that string of ids, splits them up into an array and displays a list of the matching objects.
I'm currently getting a problem however, whereby rails is attaching to the redirect_to statement another paramater (which according to the trace is {"id" => "search"} and is then searching for an Item with ID ":a1". It obviously can't find one (ids are all numeric) and therefore the app crashes. Can anyone work out why this is the case?
IN THE SEARCH CONTROLLER
rquery = Regexp.new(Regexp.escape(#query), Regexp::IGNORECASE)
item_list = []
Item.all.each{|item| item_list << item if rquery.match(item.shortdescr)}
unless item_list == nil
ids = ""
maybe_matters.each do |matter|
ids += item.id.to_s + " "
end
redirect_to search_items_path(ids: ids)
return
end
IN THE ITEM CONTROLLER
def search
authorize! :show, Item
#id = params[:ids].split
#search = []
#id.each do |id|
#search << id.to_i
end
#items = Item.accessible_by(current_ability).where(id: #search).order('completed desc nulls last').page params[:page]
end
EDIT
In case it's relevant, this is (a very small) part of the routes file:
resources :items, only: [:index, :show] do
collection do
post :search
end
end

I'd recommend making search a GET request. Make sure the path to that action is a collection action in your routes.rb. If your controller is named ItemsController:
resources :items do
get 'search', on: :collection
end
You can pass your search params as a URL parameter:
GET /items/search?query=searchterm
EDIT
Just realized the POST in your routes. You can't redirect to a POST path which is probably why you're getting weird behavior. Change search action to a GET and the issue will at least be partially resolved.
Also, how are you handling URL encoding? Characters with spaces will get encoded to weird values like %20 which might give the unexpected results you're seeing when you call stuff like params[:ids].split.
If possible, I'd recommend consolidating this into a single controller action rather than redirecting.

Related

Ruby on Rails 5: Find index of post_id and display in view (post # of n)

I have a resource :posts, which I show one at a time in show.html.erb
Suppose I have ten posts, each with an :id going from 1-10. If I delete post #2, then my posts will be 1,3,4,5,6,7,8,9,10. If I create ten posts and delete them all, then the next post :id would be [1,3..10,21] but I would only have 11 posts.
I want to show the post number that's in the application and put it in the view against a total number of posts. So if you were looking at post #3, it might have an :id of 3, but it is post #2 in the database.
Here's what I tried so far:
posts_controller.rb
def show
...
#post = Post.friendly.find(params[:id])
#total_posts = Post.all.count.to_i
#posts_array = Post.pluck(:id).to_a
...
end
views/posts/show.html.erb
<%= #post.id %> of <%= #total_posts %> /
models/post.rb
def next
Post.where("id > ?", id).order(id: :asc).limit(1).first
end
def prev
Post.where("id < ?", id).order(id: :desc).limit(1).first
end
However, showing the :id of a resource is a security issue so I don't know how to do it better.
How can I make it so the show.html.erb view only shows the current index order of the total amount of resources as compared to the post_id?
An efficient way to do this could be
# app/controllers/posts_controller.rb
def show
#post = Post.friendly.find(params[:id])
#total_posts = Post.count
#post_index = Post.where("id <= ?", #post.id).count
end
# app/views/posts/show.html.erb
. . .
<%= #post_index %> of <%= #total_posts %>
. . .
You should avoid loading all posts (or even their id) if you can. This will become more and more expensive as the number of posts grows and will eventually become a bad bottleneck for performance.
If you're trying to find the 'array index' of a record (so to speak) you can do this:
Agency.order(id: :asc).offset(params[:index]).limit(1)
You don't really want to do any other way because then it will load EVERY record into rails which will be very slow. It's better to ask the database for only a single record (which is what 'offset' does). Just replace params[:index] with whatever the name of the params is, whether its params[:id], etc.
I did just want to address one thing you said:
However, showing the :id of a resource is a security issue so I don't know how to do it better
That's not a security issue. The app should be designed in a way where the ID of a resource is not special or "secret." If you have an ID of a record, your controller should work such that it "authorizes" certain actions and won't let you do something you're not supposed to (like a user deleting a post).
If you REALLY need to do this, then just hide the ID and use a slug instead, like example.com/this-is-a-post-slug. This can be done quite easily
Edit To answer your specific question...
ids = Agency.order(id: :asc).pluck(:id)
#post_index = ids.find_index(#post.id)
#next_post = ids[#post_index + 1]
#prev_post = ids[#post_index - 1]
You can now use #post_index in your view.
Note: #prev_post and #next_post will be nil when the page doesn't exist (i.e. the "next post" when you're on the last page), so you will need to check that.
Just try it:
def show
...
#post = Post.friendly.find(params[:id])
#total_posts = Post.count # this will return integer type data
#posts_array = Post.pluck(:id) # you don't need to_a as .pluck returns array
...
For the next part you could write:
def next
self.class.where("id > ?", id).limit(1).first # this use of id is secured.
end
def prev
self.class.where("id < ?", id).order(id: :desc).limit(1).first
end

Doing search by count in json with params results

I'm trying to implement a counter filter in my json. When I access the url api/v1/contacts?Results=2 [sic], I would like to get only two results from my json.
For this, I created two methods in my controller: an index that takes the information from the database and turns render into json, and a method that returns the number of times the json is returned.
class Api::V1::ContactsController < ApplicationController
before_action :results, only: [:index]
def index
#contacts = Contact.all
render json: {results: #contacts[0..#number_results]}
end
def results
if params[:results]
results = params[:results]["results"]
#number_results = results.to_i
else
#number_results = 3
end
end
end
Regardless of the value entered for results =, the value of #number_results is set to 0, so whenever I type results = I, get only the first result of json. If I do not type results = in the url, it sets #number_results to 3, and shows four results on the screen.
Can anyone help?
First, in the url you propose, "Results" is capitalized. If that is how you intend to submit it, then you'll need to search for it that way on the back end. But since all your code uses lowercase "results", we'll go with that. You should modify your url in the same way: api/v1/contacts?results=2.
If that's what your url looks like then the number you pass in is accessible in params[:results]. If you pass no parameter, then params[:results] will return nil. Don't call #to_i on the param before you check for its existence, because nil.to_i is 0.
All that said, here's what you probably want:
class Api::V1::ContactsController < ApplicationController
def index
number_results = params[:results] || 3
#contacts = Contact.all.limit(number_results.to_i)
render json: {results: #contacts}
end
end
If the result of params[:results] is nil, then number_results is assigned 3.
Now we use #limit to return only the number of contacts that was requested. This allows us to do away with the #results method entirely. You can also get rid of the before_action callback.

[Ruby on Rails]Redirect to RESTful URL

I am working on a movie rails project and I have to redirect the url to be RESTful, but I have no idea how to make it done. The following information may be useful:
I fail to: GET /movies when selecting a movie rating should redirect to a RESTful route
rake routes:
Prefix Verb URI Pattern Controller#Action
root GET / movies#index
movies GET /movies(.:format) movies#index
POST /movies(.:format) movies#create
new_movie GET /movies/new(.:format) movies#new
edit_movie GET /movies/:id/edit(.:format) movies#edit
movie GET /movies/:id(.:format) movies#show
PATCH /movies/:id(.:format) movies#update
PUT /movies/:id(.:format) movies#update
DELETE /movies/:id(.:format) movies#destroy
The UI of the webpage
Some descriptions of what I have to do: (generally saying if the user intentionally deletes some attributes from the url, I have redirect it to the correct url):
To be RESTful, we want to preserve the property that a URI that results in a sorted/filtered view always contains the corresponding sorting/filtering parameters.
The code I have implemented to do the task:
if !(params.has_key?(:sort_by)) #check the url has this parameter
if session[:sort_by] #has cookie
redirect_to movies_path(:sort_by => session[:sort_by]) #redirect using cookie, not sure need to add :ratings
else
session[:sort_by] = "id" # default sorting
end
end
if !(params.has_key?(:ratings))
if session[:ratings]
redirect_to movies_path(:ratings => session[:ratings]) # error: AbstractController::DoubleRenderError
else
session[:rating] = {"G"=>"1", "PG"=>"1", "PG-13"=>"1", "R"=>"1"}
end
end
:sort_by is the attribute to control how is the movies sorted, e.g. session[:sort_by] = "date" means sorted on the release date.
:ratings is something like: "ratings"=>{"G"=>"1", "PG-13"=>"1", "R"=>"1"}
An example of URL: /movies?ratings%5BG%5D=1&ratings%5BPG%5D=1&ratings%5BPG-13%5D=1&ratings%5BR%5D=1&sort_by=title
if I type something like .../movies, the url should be generated from cookie(if any). But I am having some problems:
for example we check that sort_by is missing, and there is session for sort_by, do we still need to supply session[:ratings] to movies_path()? And what to do if it is the first time I come to this website (no session[:ratings]/session[:sort_by]) but I still need the RESTful URL(all checked boxes are 'clicked' by default)
Some more instructions:
So you should redirect when parameters are missing in URL, instead of in URL + session.
It means that when ‘sort_by’ is not in the URL, but in the session, you should complete this URL with session data, and redirect to the complete URL (so fill in if ‘ratings’ is also missing).
Now what if a parameter is missing in both URL and session? You should rely on some initialization rules. For ‘ratings’, you should check all ratings; and for ‘sort_by’, you can set the keyword to ‘id’, which is how items are sorted by default (the order of movies in the array returned by Movie.all).
Something like this should work
def index
session[:sort_by] = if params[:sort_by].present?
params[:sort_by]
else
session[:sort_by] || 'id'
end
session[:rating] = if params[:rating].present?
params[:rating]
else
session[:rating] || { 'G' => '1', 'PG' => '1', 'PG-13' => '1', 'R' => '1' }
end
if !(params[:sort_by].present? && params[:rating].present?)
redirect_to movies_path(sort_by: session[:sort_by], rating: session[:rating])
return
end
end
First couple lines set up the session to the parameter value if one was passed in, otherwise checks for an existing session value, and then falls back to the default.
Then, we look to see if either the sort_by or the rating parameter were missing, we'll redirect to include both of them from the new values in the session.
I believe this hits all your criteria. The main problem you were having is that as soon as you call redirect_to, it's going to try and redirect, it doesn't like having code (especially other redirect or render calls) after it. So you need to check any conditions you need and then lump it all into one redirect call and then return from the method so it doesn't try to also render.

Rspec testing a controller search method

I'm trying to test the behavior of a custom search method in my controller:
#RecordingsController
def search
# raise params.inspect
#search = params[:search]
searches = []
searches2 = []
for n in 1..5
searches << #search["date(#{n}i)"].to_i
searches2 << #search["date2(#{n}i)"].to_i
end
start_date = date_format(*searches)
end_date = date_format(*searches2)
conditions = []
conditions << "agent like '%#{#search["agent"]}%'" unless #search["agent"].blank?
conditions << "phone like '%#{#search["phone"]}%'" unless #search["phone"].blank?
conditions << "date between '#{start_date}' and '#{end_date}'"
#recordings = Recording.where(conditions.join(" AND ")).order('date ASC')
if #recordings.blank?
redirect_to("/", alert: "No results were found for this search. Please try again.")
else
render "recordings/search"
end
end
using the following layout:
#recordings_controller_spec.rb
describe RecordingsController do
describe "POST #search" do
context "with valid attributes" do
it "assigns a new search to #search" do
search = #recording_search
get :search, #recording_search
assigns(:search).should eq(search)
end
it "populates an array of recordings"
it "renders the :search view"
end
end
end
The furthest I've gotten is trying to build a hash that mimics what my params hash would be for the form
#params hash
params = {"search" => { "date_1i" => "2012", "date_2i" => "1", ... "date2_5i" => "00" } }
where date_#{n}i is the start date [year, month, day, hour, minute], and date2_#{n}i is the end date. I'm trying to follow the answer posted here, mimicking the params hash with just a regular hash. As you can see from my controller, I don't actually pass parameters to my #search method. Should I be? Or is there a way to mock a params hash in an rspec test and determine if my #search, #recordings, and redirect_to/render variables/actions are being performed? I'm already kind of testing the render/redirect in my request spec, but I'd like to fully test this method if I can.
You should be able to generate a controller spec that GETs the search action with a given set of parameters. This will cause those parameters to be available to the params hash. You can then verify how the search is constructed and which results are returned.
describe RecordingsController do
describe '#search' do
it 'should return results' do
get :search, "search" => { "date_1i" => "2012", "date_2i" => "1", ... "date2_5i" => "00" }
response.should be_ok
#recordings.map(&:name).should == ['expected1', 'expected2']
end
end
end
This example executes a search with some search criteria as query parameters, verifies the response is successful (http 200 status), and then extracts the list of recordings returned and tries to map them to a list of friendly recording names (you can use any key on this model) to compare them to an expected list of results.
It'll be easier to write/test this code if you separate the concerns in this controller - you could write a helper that processes the query parameters and builds a search filter, which it then passes to the Recording model in the controller:
class RecordingController
def search
#search_filter = SearchFilter.for_params(params[:search])
#recordings = Recording.where(#search_filter).order('date ASC')
render "recordings/search"
end
end
class SearchFilter
# Return search params as a hash for given request params hash
def self.for_params(params)
...
end
end
This would let you write a unit test for the logic that generates search filters and only verify that the controller is doing the more simple operation of passing information between the search logic and the Recording model collection. I'd also recommend moving your logic about displaying empty results into the view on the results page and out of the controller.

How to chain optional Mongoid criteria in separate statements?

I'm trying to chain criteria based on optional rails
parameters.
I want to be able to simultaneously filter based on selected tags as
well as searching.
Here is the current code that works in all situations:
if params[:tag] and params[:search]
#notes = Note.tagged_with_criteria(params[:tag]).full_text_search(params[:search])
elsif params[:tag]
#notes = Note.tagged_with_criteria(params[:tag])
elsif params[:search]
#notes = Note.full_text_search(params[:search])
end
I tried simply using
#notes = Note.tagged_with_criteria(params[:tag]).full_text_search(params[:search])
without the if statement, but then if only one of the params was
given, then all notes are returned.
Each of the methods (tagged_with_criteria and full_text_search) are
returning Note.criteria if their parameter is nil / empty.
Is there a simpler, more elegant way to chain Mongoid criteria like this?
I'd rather keep tacking on criteria one-by-one instead of having to do
the weird "if params[...] and params[...]" thing..
UPDATE: here are the current methods:
def tagged_with_criteria(_tags)
_tags = [_tags] unless _tags.is_a? Array
if _tags.empty?
criteria
else
criteria.in(:tags => _tags)
end
end
def self.full_text_search(query)
if query
begin
regex = /#{query}/
# supplied string is valid regex (without the forward slashes) - use it as such
criteria.where(:content => regex)
rescue
# not a valid regexp -treat as literal search string
criteria.where(:content => (/#{Regexp.escape(query)}/))
end
else
# show all notes if there's no search parameter
criteria
end
end
In a situation like that, I would modify the scopes to do nothing when passed in blank values.
I think what might be happening is you are getting empty strings from the params hash, which is causing your code to think that something was entered. Try the scopes with these edits.
def tagged_with_criteria(_tags)
_tags = Array.wrap(_tags).reject(&:blank?)
if _tags.empty?
criteria
else
criteria.in(:tags => _tags)
end
end
def self.full_text_search(query)
if query.present?
begin
regex = /#{query}/
# supplied string is valid regex (without the forward slashes) - use it as such
criteria.where(:content => regex)
rescue
# not a valid regexp -treat as literal search string
criteria.where(:content => (/#{Regexp.escape(query)}/))
end
else
# show all notes if there's no search parameter
criteria
end
end

Resources