Add Conditions in the Controller to filter through records to pass the correct instance to the view - ruby-on-rails

This should be a small question. I have a students table and a classifieds table in my schema. The model/table relationship is hook up in a way that when I do
#student = Student.first.classifieds.all
in the rails console I will get all the classifieds ad for this particular student
[#<Classified id: 3, ad_content: "在BC附近BU也可以、需要女生一起租房子、看了几处、俩人去租非常合算、限女生", ad_title: "BU和BC旁边的房子求室友一起租 ", student_id: 16, created_at: "2013-09-17 19:20:43", updated_at: "2013-09-17 19:49:31", location: "Allston">, #<Classified id: 1, ad_content: "Malden Towers 宽敞客厅出租,附带阳台,窗外是公寓的花园,客厅可用窗帘或木板隔开, 每月4...", ad_title: "Malden Towers 客厅出租 400/月", student_id: 16, created_at: nil, updated_at: "2013-09-17 19:47:55", location: "Malden">]
I am trying to filter through the records with specific conditions so only the records that satisfy this specific condition can be passed to the view therefore appear on that particular page.
I want to display the record only if the location is equal to malden.
in my students_controller.rb I have this
def malden_index
#student = Student.first
for classified in #student.classifieds.all
return classified if classified['location'] == 'Malden'
end
I have this in my view
<%= classified.ad_content %>
I am getting this error
undefined local variable or method `classified'
I have three questions
can I add the conditions in my view ? or does it have to be in my controller?
are my records returned to me in array data type?
What is the problem in my code? (I think its pretty straight forward)(the classified should be each record, then return the record only if the location key is equal to malden)

You shouldn't add this condition filtering in the view. It's much better when done in the controller.
The filtering can be done in multiple ways. It is usually best and fastest to let the database do the work:
#student.classifieds.where(:location => 'Malden').all
You can either forward variables by making them an instance variable #classifieds or pass it as local variable to your view with render 'malden_index', :locals => {:classifieds => #student.classifieds.all}
In general, the approach with return in your for-loop doesn't result in your desired filter. Either use my suggestion from #2 or build your array like this
#classifieds = []
for classified in #student.classifieds.all
#classifieds << classified if classified['location'] == 'Malden'
end
Or shorter and more 'ruby-way':
#classifieds = #student.classifieds.keep_if{|cf| cf['location'] == 'Malden'}
You could then access the #classifieds array in your view. Still, I very much suggest you rather use a database filter if possible.

First off: in the view you can only reach instance variables defined in the controller. So the for loop does not gather anything that is reachable in the view.
So you could fix that by doing
def malden_index
#student = Student.first
#classifieds = #student.classifieds.where('location="Malden"')
end
and in your view iterate over all the #classifieds.
Now notice: this is completely hardcoded.
I would solve this as follows: instead of using a separate index method, use the show action (of a student), check if a location is given, and if so, filter the classifieds accordingly.
That would look like this
def show
#student = Student.find(params[:id])
#classifieds = #student.classifieds
if params[:location]
#classifieds = #classifieds.where('location = ?', params[:location]
end
end
and then you would build the url as follows /students/1?location=malden.
If you then add the following route to config/routes.rb
get '/students/:id/:location', to: 'students#show'
you could improve that to /students/1/malden.

def malden_index
#student = Student.first
#classified = #student.classifieds.find_by_location("Malden")
end
In view:
<%= #classified.ad_content %>

Try this...
def malden_index
#student = Student.first
#classified = #student.classifieds.where(location: 'Malden').first
end
in view:
<%= #classified.ad_content %>

Related

Problem with selecting elements with the same params

What i do wrong? I want to return every products which pass condition:
def show
products = Product.select{|x| x[:category_id] == params[:id]}
render json: products
end
When i write
def show
products = Product.select{|x| x[:category_id] == 1}
render json: products
end
it works why the first example doesn't work?
I am pretty sure that there is mismatch in data type.
1=='1' #will be always false
1==1 #will be true
'1'=='1' #will be true as well
And also check for nil value from params[:id]
Please make sure to change as follows
def show
products = Product.select{|x| x.category_id == params[:id].to_i}
render json: products
end
OR
The best solution as suggested by #Eyeslandic is to use .where as it will not check for mismatch in data type. And also you don't have to take care of nil value from params[:id].
You should really be using a where to stop sql from loading all your products.
#products = Product.where('category_is = ?', params[:id])
The being said, if you are sticking to rails restful conventions, the fact you have a param called :id that is the category_id suggests you are on the category controller. So maybe consider changing your logic to:
#category = Category.includes(:products).find(params[:id])
you can then access products via
#category.products
or if your not interested in the category too much maybe
#products = Category.includes(:products).find(params[:id])&.products

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

Rails: update existing has_many through record via controller?

So two thirds of this works. Every time a User reads an Article, a History record is created (has_many through), which just says "User read Article at Read_Date_X".
The database is ok, the models are ok, the read_date param is permitted in the History controller, and the following operation works both 1) to check if a User has read an article before and 2) to create a new History record if it is the first time on this article.
But I cannot work out why the middle bit (to just update the read_date on an existing record) is not working. It doesn't matter if I try it with h.save! or h.update().
h = History.where(article_id: #article, user_id: current_user)
if h.exists?
h = History.where(article_id: #article, user_id: current_user)
h.read_date = Time.now
h.save!
else
h = History.new
h.article_id = #article.id
h.user_id = current_user.id
h.read_date = Time.now
h.save!
end
The error it throws if it finds an existing record is:
undefined method `read_date=' for #<History::ActiveRecord_Relation:0x007fe7f30a5e50>
UPDATE: working answer
So Derek was right, and this version works. The middle bit needed a single instance, not an array, which is what the top conditional (without .first) was checking for. Using that to return a single record, though, means you need to swap "exists?" to "present?" in the second part.
h = History.where(article_id: #article, user_id: current_user).first
if h.present?
h.read_date = Time.now
h.save!
else
h = History.new
h.article_id = #article.id
h.user_id = current_user.id
h.read_date = Time.now
h.save!
end
History.where(article_id: #article, user_id: current_user) is returning a History::ActiveRecord_Relation. If you want to set the read_date, you'll want to get a single record.
Here's one way you could do this with what you have currently:
h = History.where(article_id: #article, user_id: current_user).first
Another way you could handle this is by using find_by instead of where. This would return a single record. Like this:
h = History.find_by(article_id: #article, user_id: current_user)
However, if it's possible for a user to have many history records for an article, I would stick to the way you're doing things and make one change. If for some reason you have a lot of history records, this may not be very efficient though.
histories = History.where(article_id: #article, user_id: current_user)
histories.each { |history| history.update(read_date: Time.now) }
I realize this question is already answered. Here are a couple of additional thoughts and suggestions.
I would not have a separate read_date attribute. Just use updated_at instead. It's already there for you. And, the way your code works, read_date and updated_at will always be (essentially) the same.
When looking up whether the history exists, you can do current_user.histories.where(article: #article). IMO, that seems cleaner than: History.where(article_id: #article, user_id: current_user).first.
You can avoid all that exists? and present? business by just checking if the h assignment was successful. Thus, if h = current_user.histories.where(article: #article).
If you go the route of using updated_at instead of read_date, then you can set updated_at to Time.now by simply doing h.touch.
I would use the << method provided by has_many :through (instead of building the history record by hand). Again, if you use updated_at instead of read_date, then you can use this approach.
So, you could boil your code down to:
if h = current_user.histories.where(article: #article)
h.touch
else
current_user.articles << #article
end
You could use a ternary operator instead of that if then else, in which case it might look something like:
current_user.histories.where(article: #article).tap do |h|
h ? h.touch : current_user.articles << #article
end

redirecting back to different object

I have a problem for redirecting back after an action.
My condition is this:
Client, Volunteer, and staff has many next of kin. After creating a new next of kin, I want to redirect back to edit page of a particular client/volunteer/staff.
my current solution is this.
For the link to add
<%= link_to new_admin_people_next_of_kin_path(source: source,
source_id: source_id),
class: 'js-btn-add btn btn-success btn-sm' do %>
Add New Next of Kin
<% end %>
where
source = :client/:staff/:volunteer
source_id = id(primary key) or the staff/volunteer/client
my new method
def new
#person = Person.new
#person.source = params[:source]
#person.source_id = params[:source_id]
end
later I will pass source and source_id as hidden parameter.
my people_controller create method(because next of kin is a person)
def create
if params[:source] == 'client'
#client = Client.find(params[:source_id])
#pnok = #client.people_next_of_kin.build
elsif params[:source] == 'volunteer'
#volunteer = Volunteer.find(params[:source_id])
#pnok = #volunteer.people_next_of_kin.build
elsif params[:source] == 'staff'
#staff = Staff.find(params[:source_id])
#pnok = #staff.people_next_of_kin.build
else
#pnok = PeopleNextOfKin.new
end
#person = #pnok.build_next_of_kin
Person.transaction do
#person.update_attributes(create_params)
#person.save(validate: false)
end
end
as you can see, it's not really clean and hardcoded. I have read on polymorphic path, but I can't really find a way to use that for my solution as I need to build a new next of kin first and I cannot pass in an object in link_to or redirect_to, and then there's also a problem whereby the next of kin is not saved yet in database, so I cannot use person.find.
any solution?
It's a little tough to see what you are after from your example. I would guess that you want to redirect back to the staff / volunteer / client page and there would be a Parent / Child relationship with a NOK.
However, it's unclear what your models look like. For example, you might be using single table inheritance and polymorphism because these are all "People", or you might have the relationships in your models. I think the solution depends on which path you take.
For example, you might use something like this:
def Client
has_many :noks
end
If you had that, you could build the empty :nok record and then redirect back to the :client, and let the Rails / ActiveRecord internals manage the relationship. For example, the Client #show page may have places to list all Next of Kins that enumerates all of the kin.
Summary: I think you are trying to do too much in your controller without using models the way that RoR supports.

How to delete a single instance from a collection

I'm trying to delete a single instance from a database query. "l.remove" represents what i want to do but i know its wrong. I have tried delete and destroy. destroy didn't work and delete actually removed the data from the database. I just want the data removed from the variable. Can anyone help me?
<%
#owner = User.find(params[:id])
#job_list = ShoppingList.where(:user_id=>#user.user_id)
#job_list.each do |l|
#temp = FlaggedCandidate.where(:flagged_user_id=>#owner.user_id, :list_id=>l.list_id)
if !#temp.nil?
l.remove
end
end
#candidate = FlaggedCandidate.new
%>
based on the code i assume that User has many ShoppingList.
You can do something like:
#job_list = #owner.shopping_lists.where( list_id: FlaggedCandidate.where( flagged_user_id: #owner.user_id ).pluck(:list_id) )
That could save the trouble of looping around.
You are trying to remove record from db. In order to just modify collection #job_list you need reject some unsatisfied elements. You can do it with select method (to select job_lists that flagged), or reject in opposite. This is how you code should looks like:
#owner = User.find(params[:id])
#job_list = ShoppingList.where(:user_id=>#user.user_id)
#job_list.select! do |job_list|
FlaggedCandidate.where(
:flagged_user_id => #owner.user_id,
:list_id => job_list.list_id
).any?
end
#candidate = FlaggedCandidate.new
select! simply change the original collection, instead of doing #job_list = #job_list.select { ... }

Resources