how to give condition to loop through a variable containing data from different models - ruby-on-rails

I have following four variables in my controller index action which are retrieving data from different models and i have joined them as follows:
#forum = Forum.where(:user_id => #users.collect(&:user_id)).all
#poll=Poll.where(:created_by => #users.collect(&:user_id)).all
#article = Article.where(:user_id => #users.collect(&:user_id)).all
#jobpost = Jobplacement.where(:user_id => #users.collect(&:user_id)).all
#post = #article + #jobpost + #forum + #poll
In the view i wanted to loop through #post so i wrote - #post.reverse.each do | post| but the problem is this post contains data from 4 different models and forum ,article,jobpost is having :user_id column whereas poll is having created_by as the column name for user_id field.because of this m getting the error undefined method `user_id' for # in following lines
- if User.find(post.user_id).basic_info or User.find(post.created_by).basic_info
- if User.find(post.user_id).basic_info.profilephoto?
= image_tag User.find(post.user_id).basic_info.profilephoto.url(:thumb)
how can i give condition like
- if User.find(post.user_id).basic_info or User.find(post.created_by).basic_info
or
- if User.find(post.user_id or post.created_by).basic_info

this seems like a messy thing to be doing.
but something like this should work:
user_id = post.user_id || post.created_by
if User.find(user_id).basic_info
BUT, you really shouldn't be doing database calls from a loop in your view.
It would be cleaner to do all the data collection in the controller and models.

Related

How to connect with different tables depending on parameter

I have model Board and BoardController where i can find all boards of my project.
All records have filled "board_layout" column with enum.
Now i have show method in BoardController and i want to load there different data from different table depending on board_layout column value.
I could do it like this:
def show
#board = Board.find(params[:id])
if #board.board_layout == 1
#tasks = Car.where(board_id: #board.id)
elsif #board.board_layout == 2
#tasks = Truck.where(board_id: #board.id)
end
end
But it's not elegant + it is not DRY (i need to use some sort of if statement anytime when i want to do something with those 2 tables).
So i have tried to create Concern and create case statement inside, now it looks like:
def show
#board = Board.find(params[:id])
#tasks = get_class_name(#board.board_layout).where(board_id: #board.id)
end
# inside my concern
def get_class_name(scope)
case scope
when 1
Car
when 2
Truck
end
end
My Question:
Is there better way to do it? Is my solution safe and clear?
What is the best solution to resolve problem like this?
I would appreciate any help.
maybe you can abstract that out into a class, so you can define multiple layout and their correspondent classes, like:
class LayoutClassGetter
CLASS_BY_LAYOUT = { '1' => Car, '2' => Truck }.freeze
def initialize(layout_number)
#layout_number = layout_number
end
def layout_class
CLASS_BY_LAYOUT[#layout_number]
end
def self.layout_class(layout_number)
new(layout_number).layout
end
end
And then use it:
def show
#board = Board.find(params[:id])
#tasks = layout_class(#board.board_layout).where(board_id: #board.id)
end
def layout_class(scope)
LayoutClassGetter.layout_class(scope)
end

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

ActiveModel::MissingAttributeError - can't write unknown attribute Rails 4

So I am still quite new to the rails framework.
I am having problems when build a resource.
I am using an AJAX query to POST a JSON array of project_materials to a cart with a line_items association
(I am following the Agile Web Development Book).
This is the error I get
ActiveModel::MissingAttributeError - can't write unknown attribute project_materials_id':
This is what my controller looks like.
in line_items controller
# POST /line_items
# POST /line_items.json
def create
#materialsArray = params[:materials]
project_id = params[:project_id]
#cart = current_cart
#pm = ProjectMaterials.find(1)
#lm = #cart.line_items.build(:project_materials => pm)
#lm.save
array = JSON::parse(materialsArray)
#puts array
array.each do |key|
pm = ProjectMaterials.find_by_svg_id(key['id'])
lm = #cart.line_items.create!(:project_materials => pm)
lm.save
end
render :js => "window.location = '#{project_path(Project.find(project_id))}'"
end
It looks like you need to examine your #cart object and make sure that you have set up accepts_nested_attributes_for line_items in that object.
I made a big mistake. I labelled "project_materials_id" as "project_material_id" in the database.

Add Conditions in the Controller to filter through records to pass the correct instance to the view

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 %>

How can I combine the results of different variables into one variable?

I have following four variables in my controller index action which are retrieving data from different models as follows:
#forum = Forum.where(:user_id => #users.collect(&:user_id)).all
#poll=Poll.where(:created_by => #users.collect(&:user_id)).all
#article = Article.where(:user_id => #users.collect(&:user_id)).all
#jobpost = Jobplacement.where(:user_id => #users.collect(&:user_id)).all
I want to join all these variables' data into a single variable #post. How can I do this?
It is not good to have different type of objects in single collection.
But as you asked try
#post = [#forum,#forum,#article,#jobpost].flatten
Update:
I wrote this answer when I was a newbie in Ruby. When I look this answer I can not control my smile. The purpose of the flatten is to make a single array from the nested arrays. The answer does not relate to the question. But I am surprised about the upvotes :)
Put them in a hash:
#post = Hash.new
#post['forum'] = Forum.where(:user_id => #users.collect(&:user_id)).all
#post['poll'] = Poll.where(:created_by => #users.collect(&:user_id)).all
#post['article'] = Article.where(:user_id => #users.collect(&:user_id)).all
#post['job_placement'] = Jobplacement.where(:user_id => #users.collect(&:user_id)).all
They are not joined, but they are in one single variable. You can access them whenever you want, and do with them whatever you want.
Something like this:
conditions = { :user_id => #users } # assuming primary_key is set correctly
# in the User model
#post = Forum.where( conditions ).all +
Poll.where( conditions ).all +
Article.where( conditions ).all +
Jobplacement.where( conditions ).all
Or if you want to get fancy:
models = [ Forum, Poll, Article, Jobplacement ]
#post = models.reduce [] do |records, model|
records.push *model.where( :user_id => #users ).all
end
Note: .all might be unnecessary in both cases since it's usually called automatically by Rails when necessary, but I'm not certain.
I think you need like view model concept. Create a simple model class without inherit from ActiveRecord::Base and add all objects as attributes in the new class and initialize that.
class Post
attr_accessor :forum, :poll, :article, :jobpost
def initialize(forum,poll,article,jobpost)
#forum = forum
#poll = poll
#article = article
#jobpost = jobpost
end
end
In the controller action add the following;
#post = Post.new(#forum,#poll,#article,#jobpost)

Resources