Rails: capture method - ruby-on-rails

The following code is from a Ryan Bates' RailsCasts in which he turns the front page of a blog into a calendar, so that articles show up as links on days. The following helper module creates the Calendar. I have two questions about this code
In the day_cell method, he uses a method called capture. I found some docs on it but I still can't figure out how capture is working in this context. Also, what is the &callback that's passed as an argument to capture? Would it be the same :callback that's passed to Struct.new? If so, how does it get into capture? What is the :callback that's passed to Struct?
def day_cell(day)
content_tag :td, view.capture(day, &callback), class: day_classes(day)
end
source code
module CalendarHelper
def calendar(date = Date.today, &block)
binding.pry
Calendar.new(self, date, block).table
end
class Calendar < Struct.new(:view, :date, :callback)
HEADER = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]
START_DAY = :sunday
delegate :content_tag, to: :view
def table
content_tag :table, class: "calendar" do
header + week_rows
end
end
def header
content_tag :tr do
HEADER.map { |day| content_tag :th, day }.join.html_safe
end
end
def week_rows
weeks.map do |week|
content_tag :tr do
week.map { |day| day_cell(day) }.join.html_safe
end
end.join.html_safe
end
def day_cell(day)
content_tag :td, view.capture(day, &callback), class: day_classes(day)
end
def day_classes(day)
classes = []
classes << "today" if day == Date.today
classes << "notmonth" if day.month != date.month
classes.empty? ? nil : classes.join(" ")
end
def weeks
first = date.beginning_of_month.beginning_of_week(START_DAY)
last = date.end_of_month.end_of_week(START_DAY)
(first..last).to_a.in_groups_of(7)
end
end
end

I have done my research, and I've finally unraveled the mystery.
So, a couple of things to start with; as usual the documentation isn't very clear,
the capture(*args) method is supposed to grab a piece of template into a variable, but it doesn't dig deeper into explaining that you may pass variables to the grabbed piece of template, that of course comes in the form of a block
source code from Ryan Bate's Calendar Screen-cast:
<div id="articles">
<h2 id="month">
<%= link_to "<", date: #date.prev_month %>
<%= #date.strftime("%B %Y") %>
<%= link_to ">", date: #date.next_month %>
</h2>
<%= calendar #date do |date| %>
<%= date.day %>
<% if #articles_by_date[date] %>
<ul>
<% #articles_by_date[date].each do |article| %>
<li><%= link_to article.name, article %></li>
<% end %>
</ul>
<% end %>
<% end %>
</div>
In the code above, the block would be exclusively this part:
do |date| %>
<%= date.day %>
<% if #articles_by_date[date] %>
<ul>
<% #articles_by_date[date].each do |article| %>
<li><%= link_to article.name, article %></li>
<% end %>
</ul>
<% end %>
<% end %>
So, When he makes this call:
content_tag :td, view.capture(day, &callback), class: day_classes(day)
particularly:
view.capture(day, &callback)
What's happening here is that he's passing the day argument to the Block above as the |date| parameter (in the block).
What needs to be understood here, is that in the context of the Problem (making a 30-day Calendar); each day of the Month is passed to the capture method, along with the piece of template (&callback), doing so.. in consequence renders the block above for each day of a Given Month. The final step, of course is.. Placing that rendered content (for each day) as the content for the content_tag :td
A final note; Ryan is calling the capture method on a view variable, it isn't stated in the documentation either, but he does mention during the ScreenCast that he needs this view as a "proxy" to access the view, and of course the view is the only one that has access to ViewHelper methods.
So, in summary, it's very beautiful code, but it's only beautiful once you understand what it does, So, I agree is very confusing at first sight.
Hope this helps, it's the best explanation I could come up with. :)

Related

Helper method within each do loop not working

I have a loop that looks like this
<% #user.collections.each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= collection.stories.count %>
<% end %>
It works perfectly to show the Collections that belongs to a User, and then show how many Stories are in each Collection.
However, I want to use a helper that does this.
in the view
<% #user.collections.each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= number_of_stories_in_collection %>
<% end %>
in the helper
module CollectionsHelper
def number_of_stories_in_collection
collection.stories.count
end
def render_stories_count
if number_of_stories_in_collection.zero?
'No stories in this collection yet'
else
"#{number_of_stories_in_collection} #{'story'.pluralize(number_of_stories_in_collection)}"
end
end
end
I get an error that says
undefined method `stories' for #<Collection::ActiveRecord_Relation:0x007f510f504af8>
Any help is appreciated, thanks!
The 'collection' variable isn't an instance variable, so the helper can't see it.
Change your view to this:
<% #user.collections.each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= number_of_stories_in(collection) %>
<% end %>
And your helper method to:
def number_of_stories_in(collection)
collection.stories.count
end
This way you are passing the variable to the helper correctly.
extending #Richard's answer and little bit of optimisation to avoid n+1 queries..
<% #user.collections.includes(:stories).each do |collection| %>
<h1 class="impact"> <%= collection.name %><br></h1>
<%= render_stories_count(collection) %>
<% end %>
helper:
module CollectionsHelper
def number_of_stories_in(collection)
collection.stories.length
end
def render_stories_count(collection)
if (count = number_of_stories_in(collection)).zero?
'No stories in this collection yet'
else
"#{count} #{'story'.pluralize(count)}"
end
end
end

Rails Calendar Helper: New Calendar Instance For Each User

I'm trying to build a staff rostering/scheduling app in Rails 5. The idea is for each staff member to have a user account where they can log in to enter the days they are available for work, then for the boss to book shifts based on availability. A good example of such a site is https://www.findmyshift.com/.
The main part of the site would be a calendar, with each user having their own instance of the calendar. Each user would have their own row in the calendar table, with each row displaying the days of the week.
I've looked at a few plugins, including FullCalendar and dhtmlxScheduler, but decided to try doing it from scratch. I've managed to build a basic calendar based on this Railscast, which works just fine. The 3 main parts of this are as follows:
home_controller.rb:
class HomeController < ApplicationController
def index
#date = params[:date] ? Date.parse(params[:date]) : Date.today
end
end
index.html.erb:
<div id="roster">
<h2 id="month">
<%= link_to "<", date: #date.prev_week %>
<%= #date.strftime("%B %Y") %>
<%= link_to ">", date: #date.next_week %>
</h2>
<%= calendar #date do |date| %>
<%= date.day %>
<% end %>
</div>
calendar_helper.rb:
module CalendarHelper
def calendar(date = Date.today, &block)
Calendar.new(self, date, block).table
end
class Calendar < Struct.new(:view, :date, :callback)
HEADER = %w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday]
START_DAY = :monday
delegate :content_tag, to: :view
def table
content_tag :table, class: "calendar" do
header + week_rows
end
end
def header
content_tag :tr do
HEADER.map { |day| content_tag :th, day }.join.html_safe
end
end
def week_rows
weeks.map do |week|
content_tag :tr do
week.map { |day| day_cell(day) }.join.html_safe
end
end.join.html_safe
end
def day_cell(day)
content_tag :td, view.capture(day, &callback), class: day_classes(day)
end
def day_classes(day)
classes = []
classes << "today" if day == Date.today
classes << "notmonth" if day.month != date.month
classes.empty? ? nil : classes.join(" ")
end
def weeks
first = date.beginning_of_week(START_DAY)
last = date.end_of_week(START_DAY)
(first..last).to_a.in_groups_of(7)
end
end
end
Again, all of this works fine. It displays 1 week row, with arrows to move backwards and forwards in time. What I would like is to have 1 week row PER USER. I'm aware that I could simply render a calendar for each user in my view template, but this would include the whole thing - header, week selector and week row. I just want 1 header + week selector, then a week row for each user as part of the same table.
My guess is that I should iterate over each user somewhere in calendar_helper.rb, adding a row per user, but I'm not too sure how to proceed. Any help would be much appreciated :)
EDIT:
The first image is what I have now, the second image is what I would like to achieve - multiple instances of the calendar week row, rendered dynamically based on number of users.

Rails 4 - Helper not returning anything

I am on rails 4 trying to build a simple helper to reduce some of the code in my view.
Here is the view code (show.html.erb) before using a helper:
<% unless #article.long_effects.blank? %>
<ul>
<% #article.long_effects.split(';').each do |effect| %>
<li><%= effect %></li>
<% end %>
</ul>
<% end %>
and here is the helper I built for the above code:
def list(attribute)
unless attribute.blank?
content_tag(:ul) do
attribute.split(';').each do |a|
content_tag(:li, a)
end
end
end
end
which I then call from the view like so
<%= list(#article.long_effects) %>
Unfortunately, the helper is not returning anything. Any suggestions? This is my first time writing a helper that returns HTML, so maybe I am doing something wrong? Thank you for the help.
from
def list(attribute)
unless attribute.blank?
content_tag(:ul) do
attribute.split(';').each do |a|
content_tag(:li, a)
end
end
end
end
to
def list(attribute)
unless attribute.blank?
content_tag(:ul) do
attribute.split(';').each do |a|
concat content_tag(:li, a)
end
end
end
end
concat method will be useful to join the collection object from looping conditions.

Am I supposed to do this in helper? or does this make slower?

In this case which pattern will be faster?
Obviously Pattern1 with helper looks much more sophisticated and looks clean.
But it send SQL every time when user_link method is called.
Here it calls up to 100times at one page loading.
Which way would be better for benchmark performance?
Pattern1. With helper
application_helper
def user_link(username)
link_to User.find_by_username(username).user_profile.nickname, show_user_path(username)
end
view
<% #topics.order("updated_at DESC").limit(100).each do |topic| %>
<%= user_link(topic.comment_threads.order("id").last.user.username) if topic.comment_threads.present? %>
<% end %>
Pattern2. Without helper. Just only view
<% #topics.order("updated_at DESC").limit(100).each do |topic| %>
<%= link_to(topic.comment_threads.order("id").last.user.nickname, show_user_path(topic.comment_threads.order("id").last.user.username) ) if topic.comment_threads.present? %>
<% end %>
try
# Topics model
#via scope
scope :get_topic_list, lambda do
order("updated_at DESC").joins(:comment_threads => :user).limit(100)
end
#via method
def self.get_topic_list
Topic.order("updated_at DESC").joins(:comment_threads => :user).limit(100)
end
# in your controller or move to model itself (recommened)
#topics = Topic.get_topic_list
# in you view
<% #topics.each do |topic| %>
<%= link_to(topic.comment_threads.order("id").last.user.nickname, show_user_path(topic.comment_threads.order("id").last.user.username) ) if topic.comment_threads.present? %>
<% end %>

Ruby on Rails getting wrong number of arguments (1 for 0)

I'm using similar code to Railscast 213 to display a calendar with records.
The do line is causing a "getting wrong number of arguments (1 for 0):
<%= calendar #date do |date| %>
<%= date.day %>
<% if #wolabors_by_date[date] %>
<ul>
<% #wolabors_by_date[date].each do |wolabor| %>
<li><%= link_to wolabor.name, wolabor %></li>
<% end %>
</ul>
<% end %>
<% end %>
The calendar_helper.rb starts out with:
module CalendarHelper
def calendar(date = Date.today, &block)
Calendar.new(self, date, block).table
end
wolabors_controller.rb has
class WolaborsController < ApplicationController
def index
#wolabors = Wolabor.all
#wolabors_by_date = #wolabors.group_by(&:date)
#date = params[:date] ? Date.parse(params[:date]) : Date.today
end`
I think that first line is supposed to be
<% calendar_for #date do |date| %>
That railscast has been revised and it does not use that table_builder plugin in the new version.
http://railscasts.com/episodes/213-calendars-revised
I've found in the discussion about this Railscast , that the statement :
first = date.beginning_of_month.beginning_of_week(START_DAY)
gives the same arguments error . It seems methods
beginning_of_month
and
beginning_of_week
are Rails 3.2 specific and if you are on lower version , you should upgrade.

Resources