I have a Rails 3 ActiveRecord that belongs_to two different ActiveRecords. Example
class Animal < ActiveRecord::Base
belongs_to: species
belongs_to: zoo
...
end
where the animals table contains a species_id, zoo_id, name, and description and the tables species with a scientific_name and zoo has address.
In the controller, I have a query
#animals = Animal.includes(:species, :zoo).order(:name)
and a list of columns I want displayed in the view,
#columns = ["name", "description", "species.scientific_name", "zoo.address"]
In the view, I want the creation of a HTML table to be driven by the list of column names, e.g.
<table>
<tbody>
<tr>
<% #animals.each do |animal| %>
<% %columns.each do |col| } %>
<td><%= animal[</td>
<% end %>
<% end %>
</tr>
</tbody>
</table>
this works great for animals's name and description, but does not work for species.scientific_name and zoo.address.
I know I could special case the loop and access the included classes directly like animal.species['scientific_name'], but I was hoping there would be a way to access the included classes by name. Something like animal['species']['scientific_name']
Approach 1
Monkey patch the ActiveRecord class. Refer to this answer for details about monkey patching AR class.
class ActiveRecord::Base
def read_nested(attrs)
attrs.split(".").reduce(self, &:send)
end
end
Sample nested attribute access:
animal.read_nested("zoos.address")
user.read_nested("contacts.first.credit_cards.first.name")
product.read_nested("industry.category.name")
For your use case:
Controller:
#columns = %w(name color zoo.address species.scientific_name)
View
<% #animals.each do |animal| %>
<% #columns.each do |col| } %>
<td><%= animal.read_nested(col)%></td>
<% end %>
<% end %>
Approach 2
Add select clause to select the columns and alias them.
#animals = Animal.includes(:species, :zoo).select("
animals.*,
species.scientific_name AS scientific_name,
zoos.address AS zoo_address").
order(:name)
Now in your view, you can access attributes like scientific_name, zoo_address like regular model attributes.
Related
In my project I have a department model. I want to add employees to the department by using a search. I want to add the result of the search to a list, then submit the list and add all searched employees in one go at the end, all in the same view.
Search function in departments_controller
def add_employees
employees = Employee.all
#searched_employee = Employee.where('name LIKE ?', "#{params[:search_by_name]}")
#searched_employee.each do |employee|
#searched_employee_name = employee.name
end
end
add_employees-view:
h1 Add employees
= form_for #department, :url => add_employees_path(:param1 => #searched_employee_name, :param2 => request.query_parameters), method: :post do
= label_tag :search_by_name
br
= search_field_tag :search_by_name, params[:name]
= submit_tag "Search"
= form_for #department, :url => add_employee_path, html: {method: "post"} do |f|
- if params[:search_by_name].present?
- #searched_employee.each do |employee|
li = employee.name
br
table
h5 Employees
thead
tr Name
tr Email
tbody
- #searched_employee.each do |employee|
tr
td = employee.name
td = request.query_parameters
Single search works fine, so I hoped to add a second param which stores the first request to be passed on for the next search and so forth.
Now I am stuck with splitting up the long query string into its unique search results and their objects, as to add them to a list where I can then work further with them (checkboxes etc).
Request.query_parameters is nested, but does not react to dig, because it says it is a string.
Any ideas on how to approach this or maybe a better solution, without the use of additional gems?
Here is how I would solve it if I had to do it without JS.
Create an M-2-M association with a join model between deparments and employees. Let departments accept nested attributes for the join model:
class Department < ApplicationRecord
has_many :positions
has_many :employees, through: :positions
accepts_nested_attributes_for :positions, reject_if: :reject_position?
private
def reject_position?(attributes)
!ActiveModel::Type::Boolean.new.cast(attributes['_keep'])
end
end
class Employee < ApplicationRecord
has_many :departments
has_many :positions, through: :departments
end
# rails g model position employee:belongs_to department:belongs_to
class Position < ApplicationRecord
belongs_to :employee
belongs_to :department
attribute :_keep, :boolean
end
Setup the routes:
# config/routes.rb
Rails.application.routes.draw do
# ...
# #todo merge this with your existing routes
resources :departments, only: [] do
resources :employees, only: [], module: :departments do
collection do
get :search
patch '/',
action: :update_collection,
as: :update
end
end
end
end
Now lets create the search form:
# /app/views/departments/employees/search.rb
<%= form_with(
url: search_department_employees_path(#department),
local: true,
method: :get
) do |form| %>
<div class="field">
<%= form.label :search_by_name %>
<%= form.text_field :search_by_name %>
</div>
<% #results&.each do |employee| %>
<%= hidden_field_tag('stored_employee_ids[]', employee.id) %>
<% end %>
<%= form.submit("Search") %>
<% end %>
Note that we are using GET instead of POST. Since this action is idempotent (it does not actually alter anything) you can use GET.
Note <%= hidden_field_tag('stored_employee_ids[]', employee.id) %>. Rack will merge any pairs where the key ends with [] into an array.
Now lets setup the controller:
module Departments
# Controller that handles employees on a per department level
class EmployeesController < ApplicationController
before_action :set_department
# Search employees by name
# This route is nested in the department since we want to exclude employees
# that belong to the department
# GET /departments/1/employees?search_by_name=john
def search
#search_term = params[:search_by_name]
#stored_ids = params[:stored_employee_ids]
if #search_term.present?
#results = not_employed.where('employees.name LIKE ?', #search_term)
end
# merge the search results with the "stored" employee ids we are passing along
if #stored_ids.present?
#results = not_employed.or(Employee.where(id: #stored_ids))
end
#positions = (#results||[]).map do |employee|
Position.new(employee: employee)
end
end
private
def not_employed
#results ||= Employee.where.not(id: #department.employees)
end
def set_department
#department = Department.find(params[:department_id])
end
end
end
This just creates a form that "loops back on itself" and just keeps adding more ids to the query string - without all that hackery.
Now lets create a second form where we actually do something with the search results as a partial:
# app/views/departments/employees/_update_collection_form.html.erb
<%= form_with(
model: #department,
url: update_department_employees_path(#department),
local: true,
method: :patch
) do |form |%>
<legend>Add to <%= form.object.name %></legend>
<table>
<thead>
<tr>
<td>Add?</td>
<td>Name</td>
<tr>
</thead>
<tbody>
<%= form.fields_for(:positions, #positions) do |p_fields| %>
<tr>
<td>
<%= p_fields.label :_keep, class: 'aria-hidden' %>
<%= p_fields.check_box :_keep %>
</td>
<td>
<%= p_fields.object.employee.name %>
<%= p_fields.hidden_field :employee_id %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= form.submit 'Add employees to department' %>
<% end %>
form.fields_for(:positions, #positions) loops through the array and creates inputs for each position.
And render the partial in app/views/departments/employees/search.html.erb:
# ...
<%= render partial: 'update_collection_form' if #positions.any? %>
You should not nest this form inside another form. That will result in invalid HTML and will not work properly.
Unlike your solution I'm not cramming everything and the bathtub into a single endoint. This form sends a PATCH request to /departments/1/employees. Using PATCH on an entire collection like this is somewhat rare as we usually just use it for individual members. But here we really are adding a bunch of stuff to the collection itself.
Now lets add the action to the controller:
module Departments
# Controller that handles employees on a per department level
class EmployeesController < ApplicationController
# ...
# Adds a bunch of employees to a department
# PATCH /departments/:department_id/employees
def update_collection
if #department.update(nested_attributes)
redirect_to action: :search,
flash: 'Employees added'
else
#postions = #department.positions.select(&:new_record?)
render :search,
flash: 'Some employees could not be added'
end
end
private
# ...
def update_collection_attributes
params.require(:department)
.permit(
positions_attributes: [
:keep,
:employee_id
]
)
end
end
end
There is almost nothing to it since accepts_nested_attributes is doing all the work on the controller layer.
I'll leave it up to you to convert this ERB to Slim or Haml and adapt it to your existing code base.
So, if anyone faces a similar problem, what I cam up with is this:
- Hash[CGI::parse(request.query_string).map{|k,v| [k,v]}].values.each do |value|
- valuename = value[0]
- employees = Employee.all
- found_employees = employees.where('name LIKE ?', "#{valuename}")
- found_employees.each do |employee|
tr
td = employee.name
First line: gets the whole query string, parses over it to look for the parameters and puts them in a hash, then reads out the key value pairs, which then are arrays. Those arrays' values are then iterated over.
Second line: since the values are still in an array, I ask the arrays to just put out the pure values with value[0], and assign them to valuename.
Folowing lines: Just querying the database to find all employee-names that match a valuename, receive objects so I can further work on them.
I have a Animal model which at the moment consists of Cats and Dog. I have a column called animal_type that will define what the animal is
When I view a record (show action) then it could be any animal type and I have created next and previous links to cycle through all the animal records:
def next_animal
animal = self.class.order('created_at desc').where('created_at > ?', self.created_at)
animal.first if animal
end
def previous_animal
animal = self.class.order('created_at desc').where('created_at < ?', self.created_at)
animal.last if animal
end
Controller
def show
#animal = Animal.find(params[:id])
end
View
<% if #animal.previous_animal %>
<%= link_to(#animal.previous_animal, {class: 'prev-page'}) do %>
<span class="glyphicon glyphicon-chevron-left"></span> Meet <span class="name"><%= #animal.previous_animal.name %></span>, the <%= animal_breed(#animal.previous_animal) %>
<% end %>
<% end %>
So if I am looking at a dog, what do I need to do to say only be able to cycle through the next and previous dogs, and not include any cat, and vice versa, so if I'm looking at a cat record, only cycle through other cats.
I've thought about a scope
scope :dog_type, -> { where(animal_type: 'Dog') }
but still unsure on how to implement.
You can do the following:
# model
def previous_animal
self.class.order('created_at desc').where('created_at < ?', self.created_at).where(animal_type: self.animal_type).first
end
# view
<% if previous_animal = #animal.previous_animal %> # local assignment in the if condition
<%= link_to(previous_animal, {class: 'prev-page'}) do %>
<span class="glyphicon glyphicon-chevron-left"></span> Meet <span class="name"><%= previous_animal.name %></span>, the <%= animal_breed(previous_animal) %>
<% end %>
<% end %>
The previous_animal method is simplified, calling .first on an ActiveRecord::Relation can't fail, but it can return nil.
I used the local variable assignment in the if condition because every time you call previous_animal on the record it trigger a SQL query. This local variable kind of act like a cache (won't trigger the SQL query multiple times).
def next_animal
animal = self.class.order('created_at desc').where('created_at > ? and animal_type = ?', created_at, animal_type)
animal.first if animal
end
Just add it in the where, if you use scopes then you're going to need an if statement in your previous and next.
I have a table Projects each with 0 or more Categories. On my view, I want to display 0 projects until a JQuery click event associated with each category--i.e. when the user clicks "Food," I want to display all projects with category Food; when the user clicks "Photos," I want to display BOTH food and photos-related projects.
So on the jQuery click event I define an ajax call:
params = 'category_name=' + cat;
$.ajax({
url: "/projects_controller/filter_list",
data: params
})
where "cat" is the names of the Categories selected (in the format "Food Photography Journal etc")
In my projects_controller I started a filter_list method:
def filter_list
#categories = []
words = params[:category_name].split(/\W+/)
words.each { |word| #categories.push(Category.where("name = ?", word)) }
#projects = ...
end
But now I'm stuck. 1) How do I get all the projects associated with any of the categories in #categories? and 2) How do I display the #projects variable on my view? Right now I just display all like this:
<% Project.all.each do |project| %>
<tr style="display:none" class="project <% project.categories.all.each do |cat| %><%= cat.name %> <% end %>">
<td><%= project.filename %></td>
<td><a href='project/<%= project.id %>'><%= project.location %></a>
<td><% project.categories.all.each do |cat| %><%= cat.name %>, <% end %></td>
<% end %>
Your instance variables $categories, #projects are already available in the view. So in the view you can use #project rather than accessing the Class Project itself.
<% #projects.each do |project| %>
...
<% end %>
But probably you did not design your models correctly. Establish the correct relationships in your model. If a project belongs to a category, you can associate them as follows:
#models/category.rb
class Category < ActiveRecord::Base
has_many :projects
end
#models/project.rb
class Project < ActiveRecord::Base
belongs_to :category
end
#controllers/categories_controller.rb
def index
#categories = Category.all #or use your own query
end
#views/categories/index.erb
<% #categories.each do |category| %>
# here you can get all projects under this category using
# category.projects
<% end %>
Note: i'm used to HAML, so sorry if my ERB syntax is wrong
The according view(s) to a controller class have have access to the class level instance variables (i.e. #my_variable).
Simply let #projects = Project.all.each in your controller and substitute Project.all with #projects in your view.
I have something like
<% #users.each do |u| %>
...
<% #interests.each do |i| %>
<% if i joins u in interests_users %> <-- That is not working that way! = Problem
...
<% i.id %>
...
<% end %>
<% end %>
...
<% end %>
I need to output each interest-id that joins users in interests_users for every user. But i cannot use a sql query for each user, because that would be too much for the server, so i query the whole interests table and want to filter it in the view.
Anyone got a simple solution for that?
You're thinking about this too view-centric - this is a data issue, so the model(s) should take care of it.
I'm guessing you have an n-m relationship between users and interests, with the interests-users join table in the middle. The models should define that relationship something like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :interests
end
class Interest ...
has_and_belongs_to_many :users
end
This is with has_and_belongs_to_many (HABTM). The models then already take care of "selecting" which interests are "on" each user, simply query them with user_instance.interests, i.e. do
<% #users.each do |u| %>
...
<% u.interests.each do |i| %>
If that generates a query for interests for each user, you can eager load the data when getting the users:
#users = User.includes(:interests).all
Edit:
Oh yeah and if you want to list all interests and mark those a user has associated, something like this should work:
#interests.each do |interest|
if u.interests.include?(interest) ...
If I have the following nested model relationships (all has_many):
Countries < Cities < Streets < Homes
In a show view, how can I check if a particular Country has any homes?
Edit:
Adding the suggested method of chaining with the map method (first try to map to streets). So far it's not restricting the records
<% #countries.each do |country| %>
<% if country.cities.map(&:streets).any? %>
....
<% end %>
<% end %>
You can call or #country.cities.map(&:streets).flatten.map(&:homes).present? or #country.cities.map(&:streets).map(&:homes).any?
<% if #country.cities.map(&:streets).flatten.map(&:homes).flatten.any? %>
Tro-lol-lo yo-lo-puki
<% end %>
Also you can wrap this long line into your model method:
class Country < ActiveRecord::Base
def any_homes?
cities.map(&:streets).flatten.map(&:homes).flatten.any?
end
end
Usage
<% if #country.any_homes? %>
Tro-lol-lo yo-lo-puki
<% end %>
And of course it looks like a good data structure for refactoring! It wants to be refactored!