Fortitude’s benefits become even more evident as your application scales. Next, we’ll take a look at a much larger view and the ways Fortitude can improve code at scale.
This view has been adapted from another real-world application. It’s a view from an administrative page that shows a list of blocked users, reported users, and reporting users (i.e., those who have reported other users).
<h1>Blocked users in the last <%= @time_span_days %> days:</h1>
<table class="blocked_users user_list">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Last Login</th>
<th>Blocked Because</th>
<th>Blocked By</th>
</tr>
</thead>
<tbody>
<% @blocked_users.each do |blocked_user|
blocked_details = @blocked_user_reasons[blocked_user.id] %>
<tr>
<td><%= blocked_user.id %></td>
<td><%= blocked_user.username %></td>
<td><%= blocked_user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
<td><%= time_ago_in_words(blocked_user.last_login_at) %></td>
<td><%= blocked_details[:reason] %></td>
<td><%= blocked_details[:admin_user] %></td>
</tr>
</tbody>
</table>
<h1>Reported Users in the last <%= @time_span_days %> days:</h1>
<table class="reported_users user_list">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Last Login</th>
<th>Reported For</th>
<th>Report Text</th>
<th>Reported By</th>
</tr>
</thead>
<tbody>
<% @reported_users.each do |reported_user|
report_details = @reported_user_reasons[reported_user.id] %>
<tr>
<td><%= reported_user.id %></td>
<td><%= reported_user.username %></td>
<td><%= reported_user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
<td><%= time_ago_in_words(reported_user.last_login_at) %></td>
<td><%= report_details[:abuse_flag] %></td>
<td><%= report_details[:abuse_text] %></td>
<td><%= report_details[:abuse_reporter] %></td>
</tr>
</tbody>
</table>
<h1>Most Reporting Users in the last <%= @time_span_days %> days:</h1>
<table class="reporting_users user_list">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Last Login</th>
<th>Total Reported Users</th>
<th>Reported-For Breakdown</th>
</tr>
</thead>
<tbody>
<% @top_reporting_users.each do |reporting_user|
reporting_details = @top_reporting_user_details[reporting_user.id] %>
<tr>
<td><%= reporting_user.id %></td>
<td><%= reporting_user.username %></td>
<td><%= reporting_user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
<td><%= time_ago_in_words(reporting_user.last_login_at) %></td>
<td><%= reporting_details[:flags].uniq { |f| f.reported_user_id }.length %></td>
<td>
<% grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
grouped_by_flag.each do |flag_type, flags| %>
Flag <strong><%= flag_type %></strong>: <%= flags.length %> total flag(s)<br>
<% end %>
</td>
</tr>
</tbody>
</table>
Obviously, the most notable thing about this view is that it’s big. That’s OK — there’s no inherent reason that long views are bad. The tables in this view aren’t used anywhere else, so there’s no need to share the code with anything else.
However, the length of this view makes it pretty hard to read; if you open it on anything but a huge monitor, it’s not even obvious that there is a list of reporting users, for example.
Further, there’s a lot of duplication among these three tables, which would be nice to clean up, too.
Using standard templating engines, our options in this case are pretty limited.
One thing we could do would be to break out the three tables into their own partials. This does work, and looks pretty concise:
<%= render :partial => 'blocked_users' %>
<%= render :partial => 'reported_users' %>
<%= render :partial => 'reporting_users' %>
However, we’ve now created a subtle but insidious problem: these three new partials now require quite a few variables to be set in order to render them — specifically, @time_span_days, @blocked_users, and @blocked_user_reasons for one of them, @time_span_days, @reported_users, and @reported_user_reasons for the next, and so on. This is fine, except that nowhere in the code can you tell this— if you look at the original view, it now doesn’t mention those whatsoever, and you can only tell that the partials require those variables by carefully scouring their text to see what they use.
Even worse, nowhere in the originating view are any of these variables mentioned! If you were to look at the view, it’s literally impossible to tell which variables the controller needs to set in order to make it work. You need to go look at the partials it renders. Now, imagine we perform some further refactorings — you might need to look through several layers of partials, carefully writing down which @ variables are used and deduplicating them, just to wrap your head around the data being communicated from the controller to the view.
Although many views are written this way, this is not any better in view code than it is in any other code. We’re passing data using what are effectively implicit global variables, and that’s seriously detrimental to maintainability and readability in any application.
Further: if we do this, we haven’t done anything to factor out the commonality of these tables…and, because they’re now in three separate files, the likelihood that they’ll diverge in a bad way goes up. (For example, it’d be much easier for a developer to add a new common column to one of them, while forgetting to add it to the others — and now we have not just duplication, but duplication and inconsistency, which is pretty close to the canonical definition of poorly-factored code.)
Trying to unify the code for the three tables is a lot trickier. While the tables actually have a great deal of commonality, they also have varying numbers of columns, and that makes it considerably harder. We have a few choices here:
This last option certainly seems like the best. Let’s see what it looks like in practice. First, the header partial:
<h1><%= title %></h1>
<table class="<%= table_classes %> user_list">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Last Login</th>
<% table_header_columns.each do |table_header_column| %>
<th><%= table_header_column %></th>
<% end %>
</tr>
</thead>
<tbody>
And the body-row partial:
<tr>
<td><%= user.id %></td>
<td><%= user.username %></td>
<td><%= user.email || "<span class="no_email">(none available)</span>".html_safe %></td>
<td><%= time_ago_in_words(user.last_login_at) %></td>
<%= extra_columns %>
</tr>
Finally, here’s the caller:
<%= render :partial => 'user_reporting_table_header', :locals => {
:title => "Blocked users in the last #{time_span_days} days:",
:table_classes => "blocked_users",
:table_header_columns => [ "Blocked Because", "Blocked By" ]
} %>
<tbody>
<% @blocked_users.each do |blocked_user|
blocked_details = @blocked_user_reasons[blocked_user.id] %>
<%= render :partial => 'user_reporting_table_row_begin', :locals => {
:user => blocked_user, :extra_columns => "<td>#{h(blocked_details[:reason])}</td><td>#{h(blocked_details[:admin_user])}</td>"
} %>
<% end %>
</tbody>
</table>
<%= render :partial => 'user_reporting_table_header', :locals => {
:title => "Reported users in the last #{time_span_days} days:",
:table_classes => "reported_users",
:table_header_columns => [ "Reported For", "Report Text", "Reported By" ]
} %>
<tbody>
<% @reported_users.each do |reported_user|
report_details = @reported_user_reasons[reported_user.id] %>
<%= render :partial => 'user_reporting_table_row_begin', :locals => {
:user => reported_user, :extra_columns =>
"<td>#{h(report_details[:abuse_flag])}</td><td>#{h(report_details[:abuse_text])}</td><td>#{h(report_details[:abuse_reporter])}</td>"
} %>
<% end %>
</tbody>
</table>
<%= render :partial => 'user_reporting_table_header', :locals => {
:title => "Most Reporting Users in the last #{time_span_days} days:",
:table_classes => "reporting_users",
:table_header_columns => [ "Total Reported Users", "Reported-For Breakdown" ]
} %>
<tbody>
<% @top_reporting_users.each do |reporting_user|
reporting_details = @top_reporting_user_details[reporting_user.id]
total_flags = reporting_details[:flags].uniq { |f| f.reported_user_id }.length
grouped_by_data = grouped_by_flag.map do |flag_type, flags|
capture do %>
Flag <strong><%= flag_type %></strong>: <%= flags.length %> total flag(s)<br>
<% end
end.join("
") %>
<%= render :partial => 'user_reporting_table_row_begin', :locals => {
:user => reporting_user, :extra_columns =>
"<td>#{h(total_flags)}</td><td>#{grouped_by_data}</td>"
} %>
<% end %>
</tbody>
</table>
Ugh. We’ve managed to unify the tables, and yet only at significant expense. Let’s try to break down the issues we see here:
capture to pass HTML in, and so on. Generally speaking, it sure isn’t very readable.
#h manually around the user data we put into the extra_columns partial parameter. And if we forget, even once? We now have an XSS vulnerability.
#capture: For our last table, building the string we need to pass in as extra_columns requires enough logic that we either would have to use Ruby string concatenation multiple times, or, better, use #capture like we do here. While there’s nothing fundamentally wrong with #capture, the way it suddenly builds HTML out-of-order almost always makes code harder to read and harder to maintain and debug.
app/views/admin/users directory. Initially, this isn’t that big a deal; however, in a large application, this really adds up. Add seven more views like this, and now you have 24 separate view files or partials in that directory, with no obvious way of knowing which partials belong to which views (or even knowing for sure which partials might not even be used any more). In other words, the fact that you have to create a new file just to be able to reuse some code, even within a single view, is a heavyweight restriction that leads to more files than you otherwise might want.
Indentation (of Output): Related to this, the HTML output we get from this is now really, really poorly indented. For example, if the last table has one row, it is now going to look like this (with some sections elided for clarity):
<html>
<head>
...
</head>
<body>
...
<h1>Most Reporting Users in the last 30 days:</h1>
<table class="reporting_users user_list">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Last Login</th>
<th>Total Reported Users</th>
<th>Reported-For Breakdown</th>
</tr>
</thead>
<tbody>
<tr>
<td>12345</td>
<td>jane_doe_1</td>
<td>[email protected]</td>
<td>about 2 hours ago</td>
<td>17</td><td>Flag <strong>Spam</strong>: 27 total flag(s)<br>
Flag <strong>Personal Attack</strong>: 17 total flag(s)<br>
Flag <strong>Off-Topic</strong>: 9 total flag(s)<br>
</td>
</tr>
</tbody>
</table>
...
</body>
</html>
Is this the end of the world? Of course not.
Does it make our HTML considerably harder to read? Of course.
Should we have to put up with this as just a fact of life in 2015? Absolutely not.
Fortitude can help improve this situation a lot. However, before we refactor this, let’s take a look at what a simple Fortitude translation of the original, un-refactored view looks like in Fortitude:
class Views::Admin::Users::UserReporting < Views::Base
needs :time_span_days
needs :blocked_users, :blocked_user_reasons
needs :reported_users, :reported_user_reasons
needs :top_reporting_users, :top_reporting_user_details
def content
h1 "Blocked users in the last #{time_span_days} days: "
table(:class => "blocked_users user_list") {
thead {
tr {
th "User ID"
th "Username"
th "Email"
th "Last Login"
th "Blocked Because"
th "Blocked By"
}
}
tbody {
blocked_users.each do |blocked_user|
blocked_details = blocked_user_reasons[blocked_user.id]
tr {
td blocked_user.id
td blocked_user.username
td {
if blocked_user.email
text blocked_user.email
else
span "(none available)", :class => :no_email
end
}
td time_ago_in_words(blocked_user.last_login_at)
td blocked_details[:reason]
td blocked_details[:admin_user]
}
end
}
}
h1 "Reported Users in the last #{time_span_days} days:"
table(:class => "reported_users user_list") {
thead {
tr {
th "User ID"
th "Username"
th "Email"
th "Last Login"
th "Reported For"
th "Report Text"
th "Reported By"
}
}
tbody {
reported_users.each do |reported_user|
report_details = reported_user_reasons[reported_user.id]
tr {
td reported_user.id
td reported_user.username
td {
if reported_user.email
text reported_user.email
else
span "(none available)", :class => :no_email
end
}
td time_ago_in_words(reported_user.last_login_at)
td report_details[:abuse_flag]
td report_details[:abuse_text]
td report_details[:abuse_reporter]
}
end
}
}
h1 "Most Reporting Users in the last #{time_span_days} days:"
table(:class => "reporting_users user_list") {
thead {
tr {
th "User ID"
th "Username"
th "Email"
th "Last Login"
th "Total Reported Users"
th "Reported-For Breakdown"
}
}
tbody {
top_reporting_users.each do |reporting_user|
reporting_details = top_reporting_user_details[reporting_user.id]
tr {
td reporting_user.id
td reporting_user.username
td {
if reporting_user.email
text reporting_user.email
else
span "(none available)", :class => :no_email
end
}
td time_ago_in_words(reporting_user.last_login_at)
td(reporting_details[:flags].uniq { |f| f.reported_user_id }.length)
td {
grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
grouped_by_flag.each do |flag_type, flags|
text "Flag"; strong flag_type; text ": #{flags.length} total flag(s)"; br
end
}
}
end
}
}
end
end
Although we really haven’t used the true advantages of Fortitude here, because we haven’t refactored anything yet, we can already see a few nice things about this:
needs declarations at the top of the Fortitude class show you, at a glance, exactly what you have to pass into this view to render it. Every single Fortitude view or partial has this, and it’s guaranteed to be correct: if you don’t declare something as a need, you’ll be unable to render it in the view. (You can also supply defaults to make these optional, but we’re getting ahead of ourselves.)
do...end for multiline blocks, the convention is different in Fortitude code, and for good reason. In Fortitude code, we use braces to delimit actual HTML element contents, and do...end to delimit Ruby logic. This lets the eye scan the class easily and see exactly where there’s logic and where there’s HTML.
<%= ... %> symbols cluttering up any place there’s logic in the view makes it quite a bit cleaner to read.
However, again, the syntax isn’t really the point of Fortitude — the real point is the refactoring that the syntax allows you to do. Let’s take a look at that next.
Let’s just look at the fully-refactored view right off the bat and see what we’ve been able to do:
class Views::Admin::Users::UserReporting < Views::Base
needs :time_span_days
needs :blocked_users, :blocked_user_reasons
needs :reported_users, :reported_user_reasons
needs :top_reporting_users, :top_reporting_user_details
def content
blocked_users_table
reported_users_table
reporting_users_table
end
def blocked_users_table
users_table("Blocked Users", :blocked_users, [ "Blocked Because", "Blocked By" ], blocked_users) do |user|
blocked_details = blocked_user_reasons[user.id]
td blocked_details[:reason]
td blocked_details[:admin_user]
end
end
def reported_users_table
users_table("Reported Users", :reported_users, [ "Reported For", "Report Text", "Reported By" ], reported_users) do |user|
report_details = reported_user_reasons[user.id]
td report_details[:abuse_flag]
td report_details[:abuse_text]
td report_details[:abuse_reporter]
end
end
def reporting_users_table
users_table("Most Reporting Users", :reporting_users, [ "Total Reported Users", "Reported-For Breakdown" ], top_reporting_users) do |user|
reporting_details = top_reporting_user_details[user.id]
td(reporting_details[:flags].uniq { |f| f.reported_user_id }.length)
td {
grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
grouped_by_flag.each do |flag_type, flags|
text "Flag"; strong flag_type; text ": #{flags.length} total flag(s)"; br
end
}
end
end
def users_table(title_prefix, css_class, extra_columns, users)
h1 "#{title_prefix} in the last #{time_span_days} days: "
table(:class => "#{css_class} user_list") {
thead {
tr {
([ "User ID", "Username", "Email", "Last Login" ] + extra_columns).each do |header|
th header
end
}
}
tbody {
users.each do |user|
tr {
td user.id
td user.username
td {
if user.email
text user.email
else
span "(none available)", :class => :no_email
end
}
td time_ago_in_words(user.last_login_at)
yield user
}
end
}
}
end
end
This is such an enormous improvement over either the original ERb view or the refactored ERb view that it’s hard to know where to begin. Let’s try enumerating the advantages, over and above the ones we mentioned above that are inherent to any Fortitude code:
#content method (the entry point of every Fortitude view or partial): it contains a table of blocked users, a table of reported users, and a table of reporting users.
#blocked_users_table, #reported_users_table, and so on).
app/views/admin/users, and it’s crystal-clear exactly what each one is for. (Of course, you are more than welcome to factor out Fortitude code into separate files if you want — it just isn’t required in the same way it is with ERb, HAML, or other traditional templating engines.)
#h anywhere here, nor do we even have to think about HTML escaping. It’s all just handled for us, as it should be, with no risk of XSS attacks. (This doesn’t mean it’s impossible to create XSS vulnerabilities using Fortitude, of course — but it’s much, much more difficult, because you effectively never have to compose HTML using Ruby strings.)
Indentation (of Output): Because Fortitude understands the structure of your code, it always produces perfectly-formatted output. Here’s what the equivalent to the poorly-formatted HTML output from ERb above looks like if it’s coming from Fortitude:
<html>
<head>
...
</head>
<body>
...
<h1>Most Reporting Users in the last 30 days:</h1>
<table class="reporting_users user_list">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Last Login</th>
<th>Total Reported Users</th>
<th>Reported-For Breakdown</th>
</tr>
</thead>
<tbody>
<tr>
<td>12345</td>
<td>jane_doe_1</td>
<td>[email protected]</td>
<td>about 2 hours ago</td>
<td>17</td>
<td>
Flag <strong>Spam</strong>: 27 total flag(s)<br>
Flag <strong>Personal Attack</strong>: 17 total flag(s)<br>
Flag <strong>Off-Topic</strong>: 9 total flag(s)<br>
</td>
</tr>
</tbody>
</table>
...
</body>
</html>
(In production, Fortitude will, by default, emit this code with no whitespace at all, reducing your page weight significantly — also something that ERb cannot do.)
Altogether, Fortitude has helped us refactor this view in a way we simply couldn’t have using any traditional templating engine.
One of the reasons the complex ERb refactoring above may seem unfamiliar to many readers is simply that the refactoring ends up being so complex that most teams simply don’t do it — they leave this view un-refactored, like the original example, since it actually is cleaner in many ways than the refactored version. And, like before, this doesn’t mean that the original is actually particularly good; it contains an awful lot of duplication — it’s just that the tools traditional templating engines give you are insufficient to refactor this code well.
By providing you all the power of Ruby to refactor your code, Fortitude allows you to refactor this code into something that’s clean, clear, and actually a joy to work with.
Next, we’ll take a look at how Fortitude lets you use inheritance to beautifully and effectively factor a set of views that all have a lot in common, but differ in their exact details.