Rails 7 how to decorate non-ActiveRecord objects - ruby-on-rails

In my Rails 7 app I've got data table which I want to decorate. The data comes from the API response so in fact it's an array of hashes. Like below:
# transactions_controller.rb
class TransactionsController < ApplicationController
def index
response = client.transactions.list(platform_id: current_user.platform_id, page: 1, per_page: 100)
#transactions = response.body['data']
end
private
def client
#client ||= TestAPI::Client.new
end
end
Now inside the transactions/index.html.erb I've got a table with #transactions data which I want to decorate:
#views/transactions/index.html.erb
<table class="table table-striped">
<thead>
<tr>
<b>
<tr>
<th>Date</th>
<th>Amount</th>
</tr>
</b>
</tr>
</thead>
<tbody>
<% #transactions.map do |transaction| %>
<tr>
<td>
<%= transaction['created_at'] %>
</td>
<td>
<%= transaction['amount_cents'] %>
</td>
</tr>
<% end %>
</tbody>
</table>
I know I could inject that logic inside of view file to be like:
(...)
<td>
<%= Date.parse(transaction['created_at']).strftime("%d.%m.%Y") %>
</td>
<td>
<%= "#{ transactions_data.last['amount_cents']/100}" "#{ transactions_data.last['currency']}" %>
</td>
(...)
But I want to get rid of that logic from the view since I'll have more and more logic in the future here.

Kudos for wanting to remove logic from the view.
You need a new object, it could be called TransactionPresenter or whatever you choose. It will implement the view logic. So in your TransactionsController:
def index
response = client.
transactions.
list(platform_id: current_user.platform_id, page: 1, per_page: 100).
map{|t| TransactionPresenter.new(t)}
#transactions = response.body['data']
end
and the TransactionPresenter model could be something like this:
class TransactionPresenter
def initialize(transaction)
# capture the fields of interest as variables
end
def amount
"$#{amount_cents.to_f/100}" # for example, whatever makes sense in your context
end
end
so all logic is removed from the view:
<table>
<% #transactions.each do |transaction| %>
<tr><%= transaction.amount %></tr>
<% end %>
</table>

Related

Controller fails to pass instance variables into view

In my home page I iterate over collections of objects, and for each object I render its attributes in a table row. There are four collections of objects, defined as instance variables in my controller, all making Guard (according to the used method) raising one of the following errors:
ActionView::Template::Error: undefined method `each_with_index' for nil:NilClass
ActionView::Template::Error: undefined method `any?' for nil:NilClass
The code in my application view raising the above errors is:
<table class="col-md-4 col-md-offset-1">
<thead>
<tr>
<th> Rank </th>
<th> Gamer </th>
<th> Points </th>
</tr>
</thead>
<tbody>
<% #atp_gamers.each_with_index do |gamer, index| %>
<tr>
<td class="index"> <%= index+1 %> </td>
<td class="gamer"> <%= gamer.name %> </td>
<td class="atppoints"> <%= gamer.atpscore %> </td>
</tr>
<% end %>
<tr class="current-user">
<td> <%= #atp_gamers.to_a.index(current_user) + 1 %> </td>
<td> <%= current_user.name %> </td>
<td> <%= current_user.atpscore %> </td>
</tr>
</tbody>
</table>
<table class="col-md-4 col-md-offset-2">
<thead>
<tr>
<th> Year </th>
<th> Champion </th>
<th> Points </th>
</tr>
</thead>
<tbody>
<% if #atp_champions.any? %>
<% #atp_champions.each do |champion| %>
<tr>
<td class="year"> <%= champion.created_at.year %> </td>
<td class="winnername"> <%= champion.name %> </td>
<td class="winnerpoints"> <%= champion.points %> </td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
The above code is part of a partial (named _gamers_home.html.erb) rendered in the original home page:
<% if logged_in? %>
<% if current_user.gamer? %>
<%= render 'static_pages/gamers_home' %>
<% else %>
<%= render 'static_pages/non_gamers_home' %>
<% end %>
<% else %>
<%= render 'static_pages/non_logged_in_home' %>
<% end %>
The logged_in? method is defined as !current_user.nil?
The instance variables that result nil are: #atp_gamers, #wta_gamers, #atp_champions and #wta_champions, defined in the controller as follows:
def home
if logged_in? && !current_user.gamer?
...l
elsif logged_in? && current_user.gamer?
#gamers = User.where(gamer: true)
#atp_gamers = #gamers.order(atpscore: :desc).limit(50)
#wta_gamers = #gamers.order(wtascore: :desc).limit(50)
#atp_champions = AtpChampion.all
#wta_champions = WtaChampion.all
...
end
end
The first instance variable raising the error (each_with_index' for nil:NilClass) is #atp_gamers. In view I tried to change it with its explicit value, that is User.where(gamer: true).order(atpscore: :desc).limit(50), and the respective code is accepted. After this change, Guard raises an error for #atp_champions.
With rails console #atp_gamers and #wta_gamers are not empty, returning 50 records out of 100 users. #atp_champions and #wta_champions are not nil, but empty arrays.
I suspect that this might be an issue raised only by Guard, because the rails server succeeds in rendering the view.
def home
if logged_in? # delete this line
...
end # delete this line
end
Delete the if logged_in?, and see what happens.
Maybe you have to use before_action :logged_in_user, only :home in controller and define the logged_in_user method as private method.
If non-logged-in users also allowed to access the home action, you need to use if statement erb in the view. Like,
<% if logged_in? %>
<% #atp_gamers.each_with_index do |gamer, index| %>
...
<% end %>
--UPDATE--
Maybe, it needs to toss variables to the partial.
Replace
<%= render 'static_pages/gamers_home' %>
to
<%= render partial: 'static_pages/gamers_home', locals: {atg_gamers: #atp_gamers, wta_gamers: #wta_gamers, atp_champions: #atp_champions, wta_champions, #wta_champions} %>
and, replace the #atp_gamers, #wta_gamers, #atp_champions, #wta_champions in the partial to atp_gamers, wta_gamers, atp_champions, wta_champions.
Try and see what happens.

How do I use lookup_context to make this view as DRY as possible?

For starters, this is the view I am trying to replicate:
This is the HTML from that layout (from the SAT portion anyway, you can extrapolate the rest):
<table class="table table-hover table-bordered">
<thead>
<td colspan="2" class="text-center">
<strong>SAT</strong>
</td>
<tr>
<th>Subject</th>
<th>Grade</th>
</tr>
</thead>
<tbody>
<tr>
<td>Reading</td>
<td>900</td>
</tr>
<tr>
<td>Math</td>
<td>700</td>
</tr>
<tr>
<td>Writing</td>
<td>800</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>2,400</strong></td>
</tr>
</tbody>
This is what my Grade.rb model looks like:
# == Schema Information
#
# Table name: grades
#
# id :integer not null, primary key
# subject :string
# result :string
# grade_type :integer
# profile_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Grade < ActiveRecord::Base
belongs_to :profile
enum grade_type: { csec: 0, cape: 1, sat: 2, g7: 3, g8: 4, g9: 5, g10: 6, g11: 7, g12: 8, g13: 9 }
end
This is what that table looks like currently, i.e. before using the lookup_context method in Rails:
<table class="table table-hover table-bordered">
<thead>
<td colspan="2" class="text-center">
<strong>SAT</strong>
</td>
<tr>
<th>Subject</th>
<th>Grade</th>
</tr>
</thead>
<tbody>
<% #sat_grades.each do |grade| %>
<tr>
<% if grade.subject.eql? "Total" %>
<td><strong><%= grade.subject %></strong></td>
<td><strong><%= grade.result %></strong></td>
<% else %>
<td><%= grade.subject %></td>
<td><%= grade.result %></td>
<% end %>
</tr>
<% end %>
</tbody>
Where #sat_grades is this: #sat_grades = #profile.grades.where(grade_type: :sat).
I want to use this lookup_context method, I was thinking like this:
<% #grades.each do |grade| %>
<% if lookup_context.template_exists?(grade.grade_type, "grades/grade_types", true) %>
<%= render partial: "grade/grade_types/#{grade.grade_type}", locals: {event: event, index: index} %>
<% end %>
<% end %>
The issue I am running into is that each grade_type has a different table. So grade_type: :sat belongs in the "SAT" table, the same for "CSEC", "g11", etc.
I can't think of a way to have each of those grade_types rendered specifically within their HTML table, without having lots of lookup_context.template_exists? calls within that view.
It almost defeats the purpose of doing it like that, if I have to have a lookup_context call for each grade_type.
What's the best way to approach this so I just have 1 lookup_context call (if possible), but it correctly renders and handles all the different grades correctly.
With the given fragment I would try the following:
# Render each grade
<%= render(partial: "grade/grade", collection: #grades, locals: {event: event, index: index}) || "There's grade to be displayed" %>
# Render Concated content
<%= content_for :all_grades %>
Within grade/_grade.html.erb:
# If a special grade template exists prepare the content to be shown
# but don't display it right now
<% if lookup_context.template_exists?(grade.grade_type, "grades/grade_types", true) %>
<%= render partial: "grade/grade_types/#{grade.grade_type}", locals: {event: event, index: index} %>
<% end %>
# Render the common stuff
...
# Display the special stuff stored for the grade
<%= content_for :grade_table %>
# Repeat previous steps
...
Within the grade template (for instance grade/grade_types/_g7.html.erb):
# remove content from previous grades
<% content_for :grade_table, flush: true do %>
...
<% end %>
<% content_for :xxx_xxx, flush: true do %>
...
<% end %>
...
# Concat content for all grades together (flush: false)
<% content_for :all_grades do %>
...
<% end %>
Another approach can be a presenter or maybe even Single Table Inheritance.

Hide table header if there is no column attribute - Ruby on rails

Here is a index.html.erb. How do I hide the table header, such as Remark, with a method when no records exists in remark attribute column? Preferably without using JavaScript.
index.html.erb
<table id = "kola" class="table listing text-center">
<% has_remark = collection_has_remark?(#aslani361s) %>
<thead>
<tr class="tr-head">
<td>Date</td>
<td>Description</td>
<td>Amount</td>
<td>Discount</td>
<td>Paid</td>
<td>Balance</td>
<td>DelnDel</td>
<% if has_remark %>
<td>Remark</td>
<% end %>
<td>Hide</td>
</tr>
</thead>
</table>
However I am able to hide the remark attribute values like as below ;
_aslani361.html.erb
<% if aslani361.remark.present? -%>
<td class="col-1"><%= aslani361.remark %></td>
<% end %>
aslani361s_helper.rb
module Aslani361sHelper
def collection_has_remark?(collection)
collection.each do |aslani361|
if aslani361.remark.present?
return true
end
end
end
end
aslani361.rb
class Aslani361 < ActiveRecord::Base
end
Any suggestions are most welcome.
Thank you in advance.
If you wish to hide the column because no records in your array has the remark value, you can do something like this:
Define a method in your helper module file for the controller:
def collection_has_remark?(collection)
collection.each do |record|
if record.remark.preset?
return true
end
end
end
Then use it in the view
<% has_remark = collection_has_remark?(#records) %>
<thead>
<tr class="tr-head">
<td>Date</td>
<td>Description</td>
<td>Amount</td>
<td>Discount</td>
<td>Paid</td>
<td>Balance</td>
<% if has_remark %>
<td>Remark</td>
<% end %>
</tr>
</thead>
Then use the same if statement inside your loop. Personally I think it's important to leave an empty column, so users know for sure it doesn't have one.

How to add attributes to a active record result in Rails 4?

Lets say that I have a model with name of User. How can I add a virtual attributes to the final result of generated query ?
class User < ActiveRecord::Base
ATTRIBUTES = %w[name email balance]
scope :main_selection, -> { select('name,email,total_bought, total_deposit') }
def balance
(total_deposit - total_bought).round
end
end
and inside my controller I have
#user = User.main_selection
#attributes = User::ATTRIBUTES
Inside the View I would like to show it in a table
<table>
<thead>
<tr>
<% #attributes.each do |a| %>
<th><%= a %><th>
<% end %>
</tr>
</thead>
<tbody>
<% #user.each do |u| %>
<tr>
<% #attributes.each do |a| %>
<td><%= u[a] %><td>
<% end %>
</tr>
<% end %>
</tbody>
<table>
The only problem is that I need to add the balance attribute into the generated result, so that the loop with u[a] could work.
I need to mention that the balance can be called by #user.first.balance, but inside the loop does not work and it shows a nil value instead.
Try u.send(a) instead of u[a]
u[a] will only work on attributes. In your example, balance is not an attribute, it's a method.

multiple models in one index page

Is there any way we can list multiple models in one index page?
Like I have 4 models:users, agencies, authorizedpersons and mentors that I would want to list them in one index page.
Is there a specific process that I could follow?
You can just to query for all of these in that controller's index action:
class MyController < ApplicationController
def index
#users = User.all
#agencies = Agency.all
#authorized_people = AuthorizedPerson.all
#mentors = Mentor.all
respond_to do |format|
format.html
end
end
# ...
end
And reference them in your views as normal:
<% #agencies.each do |agency| %>
<!-- do stuff -->
<% end %>
<% #users.each do |user| %>
<!-- do more stuff -->
<% end %>
<!-- etc. -->
Rubyuser,
Yes, this is totally normal behavior. In your controller whenever you specify a variable using #myvariable, it is an instance variable available in the scope of the controller and view. A variable without the # is a local variable only available within that method.
So in your controller when you do:
class Foos < ApplicationController
def index
#foos = Foo.all
#bars = Bar.all
end
end
You can then reference #foos and #bars from within your view.
<h1>My foos and bars</h1>
<table>
<thead>
<th>foo</th>
</thead>
<tbody>
<% #foos.each do |f| %>
<tr>
<td>f.name</td>
</tr>
<% end %>
</tbody>
</table>
<table>
<thead>
<th>bar</th>
</thead>
<tbody>
<% #bars.each do |b| %>
<tr>
<td>b.name</td>
</tr>
<% end %>
</tbody>
</table>
Now, to keep things more clean, you may want to consider using a partial. Create a file called _bars_index.html.erb and copy the table with the bars code in it.
replace it with
<%= render "bars_index" %>
and now your code is nice and tidy and easy to follow.

Resources