fortitude 0.9.1-java → 0.9.2-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +28 -22
  4. data/CHANGES.md +50 -0
  5. data/CONTRIBUTORS.md +1 -0
  6. data/doc/.gitignore +18 -0
  7. data/doc/Gemfile +21 -0
  8. data/doc/config.rb +92 -0
  9. data/doc/source/images/background.png +0 -0
  10. data/doc/source/images/middleman.png +0 -0
  11. data/doc/source/images/why/icon_button.png +0 -0
  12. data/doc/source/images/why/icon_button@2x.png +0 -0
  13. data/doc/source/images/why/modal_dialog@2x.png +0 -0
  14. data/doc/source/index.html.pcss +96 -0
  15. data/doc/source/index.html.rb +66 -0
  16. data/doc/source/javascripts/all.js +1 -0
  17. data/doc/source/javascripts/highlight.pack.js +1 -0
  18. data/doc/source/layouts/layout.rb +55 -0
  19. data/doc/source/portable/fortitude-bootstrap.rb +53 -0
  20. data/doc/source/shared/base.pcss +62 -0
  21. data/doc/source/shared/base.rb +30 -0
  22. data/doc/source/shared/common.rb +55 -0
  23. data/doc/source/shared/standard_page.rb +40 -0
  24. data/doc/source/stylesheets/_shared_prefix.scss +25 -0
  25. data/doc/source/stylesheets/all.css.scss +7 -0
  26. data/doc/source/stylesheets/basics.css.scss +20 -0
  27. data/doc/source/stylesheets/bootstrap_importer.css.scss +1 -0
  28. data/doc/source/stylesheets/highlight/arta.css +140 -0
  29. data/doc/source/stylesheets/highlight/ascetic.css +52 -0
  30. data/doc/source/stylesheets/highlight/atelier-dune.dark.css +95 -0
  31. data/doc/source/stylesheets/highlight/atelier-dune.light.css +95 -0
  32. data/doc/source/stylesheets/highlight/atelier-forest.dark.css +95 -0
  33. data/doc/source/stylesheets/highlight/atelier-forest.light.css +95 -0
  34. data/doc/source/stylesheets/highlight/atelier-heath.dark.css +95 -0
  35. data/doc/source/stylesheets/highlight/atelier-heath.light.css +95 -0
  36. data/doc/source/stylesheets/highlight/atelier-lakeside.dark.css +95 -0
  37. data/doc/source/stylesheets/highlight/atelier-lakeside.light.css +95 -0
  38. data/doc/source/stylesheets/highlight/atelier-seaside.dark.css +95 -0
  39. data/doc/source/stylesheets/highlight/atelier-seaside.light.css +95 -0
  40. data/doc/source/stylesheets/highlight/brown_paper.css +104 -0
  41. data/doc/source/stylesheets/highlight/brown_papersq.png +0 -0
  42. data/doc/source/stylesheets/highlight/codepen-embed.css +108 -0
  43. data/doc/source/stylesheets/highlight/color-brewer.css +168 -0
  44. data/doc/source/stylesheets/highlight/dark.css +104 -0
  45. data/doc/source/stylesheets/highlight/default.css +152 -0
  46. data/doc/source/stylesheets/highlight/docco.css +135 -0
  47. data/doc/source/stylesheets/highlight/far.css +111 -0
  48. data/doc/source/stylesheets/highlight/foundation.css +136 -0
  49. data/doc/source/stylesheets/highlight/github.css +124 -0
  50. data/doc/source/stylesheets/highlight/googlecode.css +147 -0
  51. data/doc/source/stylesheets/highlight/hybrid.css +170 -0
  52. data/doc/source/stylesheets/highlight/idea.css +125 -0
  53. data/doc/source/stylesheets/highlight/ir_black.css +109 -0
  54. data/doc/source/stylesheets/highlight/kimbie.dark.css +96 -0
  55. data/doc/source/stylesheets/highlight/kimbie.light.css +96 -0
  56. data/doc/source/stylesheets/highlight/magula.css +121 -0
  57. data/doc/source/stylesheets/highlight/mono-blue.css +69 -0
  58. data/doc/source/stylesheets/highlight/monokai.css +127 -0
  59. data/doc/source/stylesheets/highlight/monokai_sublime.css +154 -0
  60. data/doc/source/stylesheets/highlight/obsidian.css +153 -0
  61. data/doc/source/stylesheets/highlight/paraiso.dark.css +95 -0
  62. data/doc/source/stylesheets/highlight/paraiso.light.css +95 -0
  63. data/doc/source/stylesheets/highlight/pojoaque.css +107 -0
  64. data/doc/source/stylesheets/highlight/pojoaque.jpg +0 -0
  65. data/doc/source/stylesheets/highlight/railscasts.css +187 -0
  66. data/doc/source/stylesheets/highlight/rainbow.css +108 -0
  67. data/doc/source/stylesheets/highlight/school_book.css +112 -0
  68. data/doc/source/stylesheets/highlight/school_book.png +0 -0
  69. data/doc/source/stylesheets/highlight/solarized_dark.css +108 -0
  70. data/doc/source/stylesheets/highlight/solarized_light.css +108 -0
  71. data/doc/source/stylesheets/highlight/sunburst.css +164 -0
  72. data/doc/source/stylesheets/highlight/tomorrow-night-blue.css +95 -0
  73. data/doc/source/stylesheets/highlight/tomorrow-night-bright.css +94 -0
  74. data/doc/source/stylesheets/highlight/tomorrow-night-eighties.css +94 -0
  75. data/doc/source/stylesheets/highlight/tomorrow-night.css +95 -0
  76. data/doc/source/stylesheets/highlight/tomorrow.css +92 -0
  77. data/doc/source/stylesheets/highlight/vs.css +93 -0
  78. data/doc/source/stylesheets/highlight/xcode.css +158 -0
  79. data/doc/source/stylesheets/highlight/zenburn.css +118 -0
  80. data/doc/source/why/a_larger_view.html.rb +774 -0
  81. data/doc/source/why/a_simple_helper.html.rb +332 -0
  82. data/doc/source/why/building_a_rich_modal_dialog.html.rb +156 -0
  83. data/doc/source/why/commonality_and_inheritance.html.rb +564 -0
  84. data/doc/source/why/example_list.rb +60 -0
  85. data/doc/source/why/example_page.rb +116 -0
  86. data/doc/source/why/index.html.rb +86 -0
  87. data/doc/source/why/other_benefits.html.rb +110 -0
  88. data/fortitude.gemspec +6 -1
  89. data/lib/fortitude/doctypes/html4_tags_strict.rb +1 -0
  90. data/lib/fortitude/doctypes/html5.rb +1 -0
  91. data/lib/fortitude/method_templates/tag_method_template.rb.smpl +27 -14
  92. data/lib/fortitude/rails/helpers.rb +2 -2
  93. data/lib/fortitude/rendering_context.rb +10 -1
  94. data/lib/fortitude/tags/tag.rb +3 -2
  95. data/lib/fortitude/tags/tag_support.rb +8 -3
  96. data/lib/fortitude/tilt/fortitude_template.rb +6 -2
  97. data/lib/fortitude/version.rb +1 -1
  98. data/lib/fortitude/widget.rb +2 -0
  99. data/lib/fortitude/widget/convenience.rb +30 -0
  100. data/lib/fortitude/widget/files.rb +22 -11
  101. data/lib/fortitude/widget/needs.rb +5 -3
  102. data/spec/helpers/system_helpers.rb +1 -0
  103. data/spec/rails/development_mode_system_spec.rb +0 -1
  104. data/spec/rails/rendering_system_spec.rb +20 -1
  105. data/spec/rails/templates/rendering_system_spec/app/controllers/rendering_system_spec_controller.rb +13 -0
  106. data/spec/rails/templates/rendering_system_spec/app/views/rendering_system_spec/render_hash_subclass.rb +18 -0
  107. data/spec/rails/templates/rendering_system_spec/lib/my_hash.rb +5 -0
  108. data/spec/system/convenience_methods_system_spec.rb +166 -0
  109. data/spec/system/formatting_system_spec.rb +25 -1
  110. data/spec/system/tag_rendering_system_spec.rb +73 -0
  111. data/spec/system/tilt_system_spec.rb +31 -0
  112. data/spec/system/widget_class_from_spec.rb +20 -4
  113. metadata +91 -4
@@ -0,0 +1,93 @@
1
+ /*
2
+
3
+ Visual Studio-like style based on original C# coloring by Jason Diamond <jason@diamond.name>
4
+
5
+ */
6
+ .hljs {
7
+ display: block;
8
+ overflow-x: auto;
9
+ padding: 0.5em;
10
+ background: white;
11
+ color: black;
12
+ -webkit-text-size-adjust: none;
13
+ }
14
+
15
+ .hljs-comment,
16
+ .hljs-annotation,
17
+ .diff .hljs-header,
18
+ .hljs-chunk,
19
+ .apache .hljs-cbracket {
20
+ color: #008000;
21
+ }
22
+
23
+ .hljs-keyword,
24
+ .hljs-id,
25
+ .hljs-built_in,.css
26
+ .smalltalk .hljs-class,
27
+ .hljs-winutils,
28
+ .bash .hljs-variable,
29
+ .tex .hljs-command,
30
+ .hljs-request,
31
+ .hljs-status,
32
+ .nginx .hljs-title,
33
+ .xml .hljs-tag,
34
+ .xml .hljs-tag .hljs-value {
35
+ color: #00f;
36
+ }
37
+
38
+ .hljs-string,
39
+ .hljs-title,
40
+ .hljs-parent,
41
+ .hljs-tag .hljs-value,
42
+ .hljs-rules .hljs-value,
43
+ .ruby .hljs-symbol,
44
+ .ruby .hljs-symbol .hljs-string,
45
+ .hljs-template_tag,
46
+ .django .hljs-variable,
47
+ .hljs-addition,
48
+ .hljs-flow,
49
+ .hljs-stream,
50
+ .apache .hljs-tag,
51
+ .hljs-date,
52
+ .tex .hljs-formula,
53
+ .coffeescript .hljs-attribute {
54
+ color: #a31515;
55
+ }
56
+
57
+ .ruby .hljs-string,
58
+ .hljs-decorator,
59
+ .hljs-filter .hljs-argument,
60
+ .hljs-localvars,
61
+ .hljs-array,
62
+ .hljs-attr_selector,
63
+ .hljs-pseudo,
64
+ .hljs-pi,
65
+ .hljs-doctype,
66
+ .hljs-deletion,
67
+ .hljs-envvar,
68
+ .hljs-shebang,
69
+ .hljs-preprocessor,
70
+ .hljs-pragma,
71
+ .userType,
72
+ .apache .hljs-sqbracket,
73
+ .nginx .hljs-built_in,
74
+ .tex .hljs-special,
75
+ .hljs-prompt {
76
+ color: #2b91af;
77
+ }
78
+
79
+ .hljs-phpdoc,
80
+ .hljs-dartdoc,
81
+ .hljs-javadoc,
82
+ .hljs-xmlDocTag {
83
+ color: #808080;
84
+ }
85
+
86
+ .hljs-type,
87
+ .hljs-typename { font-weight: bold; }
88
+
89
+ .vhdl .hljs-string { color: #666666; }
90
+ .vhdl .hljs-literal { color: #a31515; }
91
+ .vhdl .hljs-attribute { color: #00b0e8; }
92
+
93
+ .xml .hljs-attribute { color: #f00; }
@@ -0,0 +1,158 @@
1
+ /*
2
+
3
+ XCode style (c) Angel Garcia <angelgarcia.mail@gmail.com>
4
+
5
+ */
6
+
7
+ .hljs {
8
+ display: block;
9
+ overflow-x: auto;
10
+ padding: 0.5em;
11
+ background: #fff;
12
+ color: black;
13
+ -webkit-text-size-adjust: none;
14
+ }
15
+
16
+ .hljs-comment,
17
+ .hljs-javadoc {
18
+ color: #006a00;
19
+ }
20
+
21
+ .hljs-keyword,
22
+ .hljs-literal,
23
+ .nginx .hljs-title {
24
+ color: #aa0d91;
25
+ }
26
+ .method,
27
+ .hljs-list .hljs-title,
28
+ .hljs-tag .hljs-title,
29
+ .setting .hljs-value,
30
+ .hljs-winutils,
31
+ .tex .hljs-command,
32
+ .http .hljs-title,
33
+ .hljs-request,
34
+ .hljs-status {
35
+ color: #008;
36
+ }
37
+
38
+ .hljs-envvar,
39
+ .tex .hljs-special {
40
+ color: #660;
41
+ }
42
+
43
+ .hljs-string {
44
+ color: #c41a16;
45
+ }
46
+ .hljs-tag .hljs-value,
47
+ .hljs-cdata,
48
+ .hljs-filter .hljs-argument,
49
+ .hljs-attr_selector,
50
+ .apache .hljs-cbracket,
51
+ .hljs-date,
52
+ .hljs-regexp {
53
+ color: #080;
54
+ }
55
+
56
+ .hljs-sub .hljs-identifier,
57
+ .hljs-pi,
58
+ .hljs-tag,
59
+ .hljs-tag .hljs-keyword,
60
+ .hljs-decorator,
61
+ .ini .hljs-title,
62
+ .hljs-shebang,
63
+ .hljs-prompt,
64
+ .hljs-hexcolor,
65
+ .hljs-rules .hljs-value,
66
+ .hljs-symbol,
67
+ .hljs-symbol .hljs-string,
68
+ .hljs-number,
69
+ .css .hljs-function,
70
+ .hljs-function .hljs-title,
71
+ .coffeescript .hljs-attribute {
72
+ color: #1c00cf;
73
+ }
74
+
75
+ .hljs-class .hljs-title,
76
+ .smalltalk .hljs-class,
77
+ .hljs-javadoctag,
78
+ .hljs-yardoctag,
79
+ .hljs-phpdoc,
80
+ .hljs-dartdoc,
81
+ .hljs-type,
82
+ .hljs-typename,
83
+ .hljs-tag .hljs-attribute,
84
+ .hljs-doctype,
85
+ .hljs-class .hljs-id,
86
+ .hljs-built_in,
87
+ .setting,
88
+ .hljs-params,
89
+ .clojure .hljs-attribute {
90
+ color: #5c2699;
91
+ }
92
+
93
+ .hljs-variable {
94
+ color: #3f6e74;
95
+ }
96
+ .css .hljs-tag,
97
+ .hljs-rules .hljs-property,
98
+ .hljs-pseudo,
99
+ .hljs-subst {
100
+ color: #000;
101
+ }
102
+
103
+ .css .hljs-class,
104
+ .css .hljs-id {
105
+ color: #9b703f;
106
+ }
107
+
108
+ .hljs-value .hljs-important {
109
+ color: #ff7700;
110
+ font-weight: bold;
111
+ }
112
+
113
+ .hljs-rules .hljs-keyword {
114
+ color: #c5af75;
115
+ }
116
+
117
+ .hljs-annotation,
118
+ .apache .hljs-sqbracket,
119
+ .nginx .hljs-built_in {
120
+ color: #9b859d;
121
+ }
122
+
123
+ .hljs-preprocessor,
124
+ .hljs-preprocessor *,
125
+ .hljs-pragma {
126
+ color: #643820;
127
+ }
128
+
129
+ .tex .hljs-formula {
130
+ background-color: #eee;
131
+ font-style: italic;
132
+ }
133
+
134
+ .diff .hljs-header,
135
+ .hljs-chunk {
136
+ color: #808080;
137
+ font-weight: bold;
138
+ }
139
+
140
+ .diff .hljs-change {
141
+ background-color: #bccff9;
142
+ }
143
+
144
+ .hljs-addition {
145
+ background-color: #baeeba;
146
+ }
147
+
148
+ .hljs-deletion {
149
+ background-color: #ffc8bd;
150
+ }
151
+
152
+ .hljs-comment .hljs-yardoctag {
153
+ font-weight: bold;
154
+ }
155
+
156
+ .method .hljs-id {
157
+ color: #000;
158
+ }
@@ -0,0 +1,118 @@
1
+ /*
2
+
3
+ Zenburn style from voldmar.ru (c) Vladimir Epifanov <voldmar@voldmar.ru>
4
+ based on dark.css by Ivan Sagalaev
5
+
6
+ */
7
+
8
+ .hljs {
9
+ display: block;
10
+ overflow-x: auto;
11
+ padding: 0.5em;
12
+ background: #3f3f3f;
13
+ color: #dcdcdc;
14
+ -webkit-text-size-adjust: none;
15
+ }
16
+
17
+ .hljs-keyword,
18
+ .hljs-tag,
19
+ .css .hljs-class,
20
+ .css .hljs-id,
21
+ .lisp .hljs-title,
22
+ .nginx .hljs-title,
23
+ .hljs-request,
24
+ .hljs-status,
25
+ .clojure .hljs-attribute {
26
+ color: #e3ceab;
27
+ }
28
+
29
+ .django .hljs-template_tag,
30
+ .django .hljs-variable,
31
+ .django .hljs-filter .hljs-argument {
32
+ color: #dcdcdc;
33
+ }
34
+
35
+ .hljs-number,
36
+ .hljs-date {
37
+ color: #8cd0d3;
38
+ }
39
+
40
+ .dos .hljs-envvar,
41
+ .dos .hljs-stream,
42
+ .hljs-variable,
43
+ .apache .hljs-sqbracket {
44
+ color: #efdcbc;
45
+ }
46
+
47
+ .dos .hljs-flow,
48
+ .diff .hljs-change,
49
+ .python .exception,
50
+ .python .hljs-built_in,
51
+ .hljs-literal,
52
+ .tex .hljs-special {
53
+ color: #efefaf;
54
+ }
55
+
56
+ .diff .hljs-chunk,
57
+ .hljs-subst {
58
+ color: #8f8f8f;
59
+ }
60
+
61
+ .dos .hljs-keyword,
62
+ .hljs-decorator,
63
+ .hljs-title,
64
+ .hljs-type,
65
+ .diff .hljs-header,
66
+ .ruby .hljs-class .hljs-parent,
67
+ .apache .hljs-tag,
68
+ .nginx .hljs-built_in,
69
+ .tex .hljs-command,
70
+ .hljs-prompt {
71
+ color: #efef8f;
72
+ }
73
+
74
+ .dos .hljs-winutils,
75
+ .ruby .hljs-symbol,
76
+ .ruby .hljs-symbol .hljs-string,
77
+ .ruby .hljs-string {
78
+ color: #dca3a3;
79
+ }
80
+
81
+ .diff .hljs-deletion,
82
+ .hljs-string,
83
+ .hljs-tag .hljs-value,
84
+ .hljs-preprocessor,
85
+ .hljs-pragma,
86
+ .hljs-built_in,
87
+ .hljs-javadoc,
88
+ .smalltalk .hljs-class,
89
+ .smalltalk .hljs-localvars,
90
+ .smalltalk .hljs-array,
91
+ .css .hljs-rules .hljs-value,
92
+ .hljs-attr_selector,
93
+ .hljs-pseudo,
94
+ .apache .hljs-cbracket,
95
+ .tex .hljs-formula,
96
+ .coffeescript .hljs-attribute {
97
+ color: #cc9393;
98
+ }
99
+
100
+ .hljs-shebang,
101
+ .diff .hljs-addition,
102
+ .hljs-comment,
103
+ .hljs-annotation,
104
+ .hljs-pi,
105
+ .hljs-doctype {
106
+ color: #7f9f7f;
107
+ }
108
+
109
+ .coffeescript .javascript,
110
+ .javascript .xml,
111
+ .tex .hljs-formula,
112
+ .xml .javascript,
113
+ .xml .vbscript,
114
+ .xml .css,
115
+ .xml .hljs-cdata {
116
+ opacity: 0.5;
117
+ }
118
+
@@ -0,0 +1,774 @@
1
+ require 'source/why/example_page'
2
+
3
+ module Views
4
+ module Why
5
+ class ALargerView < Views::Why::ExamplePage
6
+ def example_intro
7
+ p {
8
+ text "Fortitude’s benefits become even more evident as your application scales. Next, we’ll take a look at "
9
+ text "a much larger view and the ways Fortitude can improve code at scale."
10
+ }
11
+ end
12
+
13
+ def example_description
14
+ p {
15
+ text "This view has been adapted from another real-world application. It’s a view from an administrative page "
16
+ text "that shows a list of blocked users, reported users, and "; em "reporting"; text " users ("
17
+ em "i.e."; text ", those who have reported other users)."
18
+ }
19
+
20
+ erb "/app/views/admin/users/user_reporting.html.erb", <<-EOS
21
+ <h1>Blocked users in the last <%= @time_span_days %> days:</h1>
22
+
23
+ <table class="blocked_users user_list">
24
+ <thead>
25
+ <tr>
26
+ <th>User ID</th>
27
+ <th>Username</th>
28
+ <th>Email</th>
29
+ <th>Last Login</th>
30
+ <th>Blocked Because</th>
31
+ <th>Blocked By</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ <% @blocked_users.each do |blocked_user|
36
+ blocked_details = @blocked_user_reasons[blocked_user.id] %>
37
+ <tr>
38
+ <td><%= blocked_user.id %></td>
39
+ <td><%= blocked_user.username %></td>
40
+ <td><%= blocked_user.email || "<span class=\"no_email\">(none available)</span>".html_safe %></td>
41
+ <td><%= time_ago_in_words(blocked_user.last_login_at) %></td>
42
+ <td><%= blocked_details[:reason] %></td>
43
+ <td><%= blocked_details[:admin_user] %></td>
44
+ </tr>
45
+ </tbody>
46
+ </table>
47
+
48
+ <h1>Reported Users in the last <%= @time_span_days %> days:</h1>
49
+ <table class="reported_users user_list">
50
+ <thead>
51
+ <tr>
52
+ <th>User ID</th>
53
+ <th>Username</th>
54
+ <th>Email</th>
55
+ <th>Last Login</th>
56
+ <th>Reported For</th>
57
+ <th>Report Text</th>
58
+ <th>Reported By</th>
59
+ </tr>
60
+ </thead>
61
+ <tbody>
62
+ <% @reported_users.each do |reported_user|
63
+ report_details = @reported_user_reasons[reported_user.id] %>
64
+ <tr>
65
+ <td><%= reported_user.id %></td>
66
+ <td><%= reported_user.username %></td>
67
+ <td><%= reported_user.email || "<span class=\"no_email\">(none available)</span>".html_safe %></td>
68
+ <td><%= time_ago_in_words(reported_user.last_login_at) %></td>
69
+ <td><%= report_details[:abuse_flag] %></td>
70
+ <td><%= report_details[:abuse_text] %></td>
71
+ <td><%= report_details[:abuse_reporter] %></td>
72
+ </tr>
73
+ </tbody>
74
+ </table>
75
+
76
+ <h1>Most Reporting Users in the last <%= @time_span_days %> days:</h1>
77
+ <table class="reporting_users user_list">
78
+ <thead>
79
+ <tr>
80
+ <th>User ID</th>
81
+ <th>Username</th>
82
+ <th>Email</th>
83
+ <th>Last Login</th>
84
+ <th>Total Reported Users</th>
85
+ <th>Reported-For Breakdown</th>
86
+ </tr>
87
+ </thead>
88
+ <tbody>
89
+ <% @top_reporting_users.each do |reporting_user|
90
+ reporting_details = @top_reporting_user_details[reporting_user.id] %>
91
+ <tr>
92
+ <td><%= reporting_user.id %></td>
93
+ <td><%= reporting_user.username %></td>
94
+ <td><%= reporting_user.email || "<span class=\"no_email\">(none available)</span>".html_safe %></td>
95
+ <td><%= time_ago_in_words(reporting_user.last_login_at) %></td>
96
+ <td><%= reporting_details[:flags].uniq { |f| f.reported_user_id }.length %></td>
97
+ <td>
98
+ <% grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
99
+ grouped_by_flag.each do |flag_type, flags| %>
100
+ Flag <strong><%= flag_type %></strong>: <%= flags.length %> total flag(s)<br>
101
+ <% end %>
102
+ </td>
103
+ </tr>
104
+ </tbody>
105
+ </table>
106
+ EOS
107
+
108
+ p {
109
+ text "Obviously, the most notable thing about this view is that it’s "; em "big"; text ". That’s OK — "
110
+ text "there’s no inherent reason that long views are bad. The tables in this view aren’t used anywhere "
111
+ text "else, so there’s no need to share the code with anything else."
112
+ }
113
+
114
+ p {
115
+ text "However, the length of this view makes it pretty hard to read; if you open it on anything but a huge "
116
+ text "monitor, it’s not even obvious that there "; em "is"; text " a list of reporting users, for example. "
117
+ }
118
+
119
+ p {
120
+ text "Further, there’s a lot of duplication among these three tables, which would be nice to clean up, too."
121
+ }
122
+ end
123
+
124
+ def using_standard_engines
125
+ p {
126
+ text "Using standard templating engines, our options in this case are pretty limited."
127
+ }
128
+
129
+ h5 "Splitting Apart the View"
130
+
131
+ p {
132
+ text "One thing we could do would be to break out the three tables into their own partials. This does work, "
133
+ text "and looks pretty concise:"
134
+ }
135
+
136
+ erb "/app/views/admin/users/user_reporting.html.erb", <<-EOS
137
+ <%= render :partial => 'blocked_users' %>
138
+ <%= render :partial => 'reported_users' %>
139
+ <%= render :partial => 'reporting_users' %>
140
+ EOS
141
+
142
+ p {
143
+ text "However, we’ve now created a subtle but insidious problem: these three new partials now require quite "
144
+ text "a few variables to be set in order to render them — specifically, "; code "@time_span_days"; text ", "
145
+ code "@blocked_users"; text ", and "; code "@blocked_user_reasons"; text " for one of them, "
146
+ code "@time_span_days"; text ", "; code "@reported_users"; text ", and "; code "@reported_user_reasons"
147
+ text " for the next, and so on. This is fine, except that "; em "nowhere in the code can you tell this"
148
+ text "— if you look at the original view, it now doesn’t mention those whatsoever, and you can only tell "
149
+ text "that the partials require those variables by carefully scouring their text to see what they use."
150
+ }
151
+
152
+ p {
153
+ text "Even worse, "; em "nowhere in the originating view are any of these variables mentioned"; text "! "
154
+ text "If you were to look at the view, it’s literally impossible to tell which variables the controller "
155
+ text "needs to set in order to make it work. You need to go look at the partials it renders. Now, imagine "
156
+ text "we perform some further refactorings — you might need to look through several layers of partials, "
157
+ text "carefully writing down which "; code "@"; text " variables are used and deduplicating them, just to "
158
+ text "wrap your head around the data being communicated from the controller to the view."
159
+ }
160
+
161
+ p {
162
+ text "Although many views are written this way, this is not any better in view code than it is in any other "
163
+ text "code. We’re passing data using what are effectively implicit global variables, and that’s seriously "
164
+ text "detrimental to maintainability and readability in any application."
165
+ }
166
+
167
+ p {
168
+ text "Further: if we do this, we haven’t done anything to factor out the commonality of these tables…and, "
169
+ text "because they’re now in three separate files, the likelihood that they’ll diverge in a bad way goes up. "
170
+ text "(For example, it’d be much easier for a developer to add a new common column to one of them, while "
171
+ text "forgetting to add it to the others — and now we have not just duplication, but duplication "; em "and"
172
+ text " inconsistency, which is pretty close to the canonical definition of poorly-factored code.)"
173
+ }
174
+
175
+ h5 "Unifying the Tables"
176
+
177
+ p {
178
+ text "Trying to unify the code for the three tables is a lot trickier. While the tables actually have a "
179
+ text "great deal of commonality, "
180
+ text "they also have varying numbers of columns, and that makes it considerably harder. We have a few "
181
+ text "choices here:"
182
+ }
183
+
184
+ ol {
185
+ li {
186
+ text "We could create one partial for the entire table, and then pass in two arrays: one of the extra "
187
+ text "column names, and the second an array of arrays of HTML fragments, one outer array for each row, "
188
+ text "one element in the inner array for each column. Trying to even write that calling code seems like "
189
+ text "an exercise in sheer mess, and prospects for maintainability are slim at best."
190
+ }
191
+ li {
192
+ text "We could create a partial for the table header, and one for the body, and pass in the above data, "
193
+ text "structured just as mentioned above; this cleans it up a little more, but doesn’t really make any "
194
+ text "real fundamental difference, since the big mess will be in the caller, not the partials."
195
+ }
196
+ li {
197
+ text "We could create a partial for the table header, one for the first chunk of the table body, "
198
+ text "and then one that renders a single row of the body, "
199
+ text "passing in HTML for the cells, and loop over the body partial."
200
+ }
201
+ }
202
+
203
+ p {
204
+ text "This last option certainly seems like the best. Let’s see what it looks like in practice. First, the "
205
+ text "header partial:"
206
+ }
207
+
208
+ erb "/app/views/admin/users/_user_reporting_table_header.html.erb", <<-EOS
209
+ <h1><%= title %></h1>
210
+
211
+ <table class="<%= table_classes %> user_list">
212
+ <thead>
213
+ <tr>
214
+ <th>User ID</th>
215
+ <th>Username</th>
216
+ <th>Email</th>
217
+ <th>Last Login</th>
218
+ <% table_header_columns.each do |table_header_column| %>
219
+ <th><%= table_header_column %></th>
220
+ <% end %>
221
+ </tr>
222
+ </thead>
223
+ <tbody>
224
+ EOS
225
+
226
+ p "And the body-row partial:"
227
+
228
+ erb "/app/views/admin/users/_user_reporting_table_row_begin.html.erb", <<-EOS
229
+ <tr>
230
+ <td><%= user.id %></td>
231
+ <td><%= user.username %></td>
232
+ <td><%= user.email || "<span class=\"no_email\">(none available)</span>".html_safe %></td>
233
+ <td><%= time_ago_in_words(user.last_login_at) %></td>
234
+ <%= extra_columns %>
235
+ </tr>
236
+ EOS
237
+
238
+ p "Finally, here’s the caller:"
239
+
240
+ erb "/app/views/admin/users/user_reporting.html.erb", <<-EOS
241
+ <%= render :partial => 'user_reporting_table_header', :locals => {
242
+ :title => "Blocked users in the last \#{time_span_days} days:",
243
+ :table_classes => "blocked_users",
244
+ :table_header_columns => [ "Blocked Because", "Blocked By" ]
245
+ } %>
246
+ <tbody>
247
+ <% @blocked_users.each do |blocked_user|
248
+ blocked_details = @blocked_user_reasons[blocked_user.id] %>
249
+ <%= render :partial => 'user_reporting_table_row_begin', :locals => {
250
+ :user => blocked_user, :extra_columns => "<td>\#{h(blocked_details[:reason])}</td><td>\#{h(blocked_details[:admin_user])}</td>"
251
+ } %>
252
+ <% end %>
253
+ </tbody>
254
+ </table>
255
+
256
+ <%= render :partial => 'user_reporting_table_header', :locals => {
257
+ :title => "Reported users in the last \#{time_span_days} days:",
258
+ :table_classes => "reported_users",
259
+ :table_header_columns => [ "Reported For", "Report Text", "Reported By" ]
260
+ } %>
261
+ <tbody>
262
+ <% @reported_users.each do |reported_user|
263
+ report_details = @reported_user_reasons[reported_user.id] %>
264
+ <%= render :partial => 'user_reporting_table_row_begin', :locals => {
265
+ :user => reported_user, :extra_columns =>
266
+ "<td>\#{h(report_details[:abuse_flag])}</td><td>\#{h(report_details[:abuse_text])}</td><td>\#{h(report_details[:abuse_reporter])}</td>"
267
+ } %>
268
+ <% end %>
269
+ </tbody>
270
+ </table>
271
+
272
+ <%= render :partial => 'user_reporting_table_header', :locals => {
273
+ :title => "Most Reporting Users in the last \#{time_span_days} days:",
274
+ :table_classes => "reporting_users",
275
+ :table_header_columns => [ "Total Reported Users", "Reported-For Breakdown" ]
276
+ } %>
277
+ <tbody>
278
+ <% @top_reporting_users.each do |reporting_user|
279
+ reporting_details = @top_reporting_user_details[reporting_user.id]
280
+ total_flags = reporting_details[:flags].uniq { |f| f.reported_user_id }.length
281
+
282
+ grouped_by_data = grouped_by_flag.map do |flag_type, flags|
283
+ capture do %>
284
+ Flag <strong><%= flag_type %></strong>: <%= flags.length %> total flag(s)<br>
285
+ <% end
286
+ end.join("\n") %>
287
+
288
+ <%= render :partial => 'user_reporting_table_row_begin', :locals => {
289
+ :user => reporting_user, :extra_columns =>
290
+ "<td>\#{h(total_flags)}</td><td>\#{grouped_by_data}</td>"
291
+ } %>
292
+ <% end %>
293
+ </tbody>
294
+ </table>
295
+ EOS
296
+ end
297
+
298
+ def standard_engine_issues
299
+ p {
300
+ text "Ugh. We’ve managed to unify the tables, and yet only at significant expense. Let’s try to break down "
301
+ text "the issues we see here:"
302
+ }
303
+
304
+ ul {
305
+ li {
306
+ strong "Caller Mess"; text ": By far the most apparent issue here: "
307
+ em "now our caller is almost inscrutable"; text ". It doesn’t even look at first glance like it has "
308
+ text "tables in it, the actual HTML involved is strewn in little tiny chunks that are difficult to find "
309
+ text "(and often is now embedded as Ruby strings, not ERb HTML), we’ve had to resort to "; code "capture"
310
+ text " to pass HTML in, and so on. Generally speaking, it sure isn’t very readable."
311
+ }
312
+ li {
313
+ strong { em "Dangerous (XSS Potential)" }; text ": Once again, we’re back in the land of having to be "
314
+ text "very, very careful about HTML escaping. We have to call "; code "#h"; text " manually around the "
315
+ text "user data we put into the "; code "extra_columns"; text " partial parameter. And if we forget, "
316
+ text "even once? We now have an XSS vulnerability."
317
+ }
318
+ li {
319
+ strong { text "Falling back to "; code "#capture" }; text ": For our last table, building the string "
320
+ text "we need to pass in as "; code "extra_columns"; text " requires enough logic that we either would have "
321
+ text "to use Ruby string concatenation multiple times, or, better, use "; code "#capture"
322
+ text " like we do here. While there’s nothing fundamentally wrong with "; code "#capture"
323
+ text ", the way it suddenly builds HTML out-of-order almost always makes code harder to read and harder "
324
+ text "to maintain and debug."
325
+ }
326
+ li {
327
+ strong "File Clutter"; text ": Our single view is now represented using three files, all of which properly "
328
+ text "live in the "; code "app/views/admin/users"; text " directory. Initially, this isn’t that big a deal; "
329
+ text "however, in a large application, this really adds up. Add seven more views like this, and now you have "
330
+ text "24 separate view files or partials in that directory, with no obvious way of knowing which partials "
331
+ text "belong to which views (or even knowing for sure which partials might not even be used any more). "
332
+ text "In other words, the fact that you have to create a new file just to be able to reuse some code, "
333
+ text "even within a single view, is a heavyweight restriction that leads to more files than you otherwise "
334
+ text "might want."
335
+ }
336
+ li {
337
+ strong "Indentation (of Source)"; text ": This is one of those issues that can seem minor, but really adds up over "
338
+ text "the course of a larger application. How do we indent lines in the above example? It’s not entirely "
339
+ text "clear, and there really isn’t a great solution. Basically, the natural indentation of the HTML itself "
340
+ text "is fighting with the indentation of our interlaced Ruby code. There are many different ways to handle "
341
+ text "this, but none of them are great, and none of them are accepted as “the right way”; every developer "
342
+ text "or team will have its own preferences, and your editor probably won’t be of much help."
343
+ }
344
+ li {
345
+ p {
346
+ strong "Indentation (of Output)"; text ": Related to this, the HTML output we get from this is now really, "
347
+ text "really poorly indented. For example, if the last table has one row, it is now going to look like this "
348
+ text "(with some sections elided for clarity):"
349
+ }
350
+
351
+ html_source <<-EOS
352
+ <html>
353
+ <head>
354
+ ...
355
+ </head>
356
+ <body>
357
+ ...
358
+ <h1>Most Reporting Users in the last 30 days:</h1>
359
+
360
+ <table class="reporting_users user_list">
361
+ <thead>
362
+ <tr>
363
+ <th>User ID</th>
364
+ <th>Username</th>
365
+ <th>Email</th>
366
+ <th>Last Login</th>
367
+ <th>Total Reported Users</th>
368
+ <th>Reported-For Breakdown</th>
369
+ </tr>
370
+ </thead>
371
+ <tbody>
372
+ <tr>
373
+ <td>12345</td>
374
+ <td>jane_doe_1</td>
375
+ <td>jdoe@example.com</td>
376
+ <td>about 2 hours ago</td>
377
+ <td>17</td><td>Flag <strong>Spam</strong>: 27 total flag(s)<br>
378
+ Flag <strong>Personal Attack</strong>: 17 total flag(s)<br>
379
+ Flag <strong>Off-Topic</strong>: 9 total flag(s)<br>
380
+ </td>
381
+ </tr>
382
+ </tbody>
383
+ </table>
384
+ ...
385
+ </body>
386
+ </html>
387
+ EOS
388
+ p "Is this the end of the world? Of course not."
389
+ p "Does it make our HTML considerably harder to read? Of course. "
390
+ p "Should we have to put up with this as just a fact of life in 2015? Absolutely not."
391
+ }
392
+ }
393
+ end
394
+
395
+ def using_fortitude
396
+ h5 "Before Refactoring, a Translation"
397
+
398
+ p {
399
+ text "Fortitude can help improve this situation "; em "a lot"; text ". However, before we refactor this, let’s "
400
+ text "take a look at what a simple Fortitude translation of the original, un-refactored view looks like in "
401
+ text "Fortitude:"
402
+ }
403
+
404
+ fortitude '/app/views/admin/users/user_reporting.html.rb', <<-EOS
405
+ class Views::Admin::Users::UserReporting < Views::Base
406
+ needs :time_span_days
407
+ needs :blocked_users, :blocked_user_reasons
408
+ needs :reported_users, :reported_user_reasons
409
+ needs :top_reporting_users, :top_reporting_user_details
410
+
411
+ def content
412
+ h1 "Blocked users in the last \#{time_span_days} days: "
413
+
414
+ table(:class => "blocked_users user_list") {
415
+ thead {
416
+ tr {
417
+ th "User ID"
418
+ th "Username"
419
+ th "Email"
420
+ th "Last Login"
421
+ th "Blocked Because"
422
+ th "Blocked By"
423
+ }
424
+ }
425
+
426
+ tbody {
427
+ blocked_users.each do |blocked_user|
428
+ blocked_details = blocked_user_reasons[blocked_user.id]
429
+ tr {
430
+ td blocked_user.id
431
+ td blocked_user.username
432
+ td {
433
+ if blocked_user.email
434
+ text blocked_user.email
435
+ else
436
+ span "(none available)", :class => :no_email
437
+ end
438
+ }
439
+ td time_ago_in_words(blocked_user.last_login_at)
440
+ td blocked_details[:reason]
441
+ td blocked_details[:admin_user]
442
+ }
443
+ end
444
+ }
445
+ }
446
+
447
+ h1 "Reported Users in the last \#{time_span_days} days:"
448
+
449
+ table(:class => "reported_users user_list") {
450
+ thead {
451
+ tr {
452
+ th "User ID"
453
+ th "Username"
454
+ th "Email"
455
+ th "Last Login"
456
+ th "Reported For"
457
+ th "Report Text"
458
+ th "Reported By"
459
+ }
460
+ }
461
+
462
+ tbody {
463
+ reported_users.each do |reported_user|
464
+ report_details = reported_user_reasons[reported_user.id]
465
+ tr {
466
+ td reported_user.id
467
+ td reported_user.username
468
+ td {
469
+ if reported_user.email
470
+ text reported_user.email
471
+ else
472
+ span "(none available)", :class => :no_email
473
+ end
474
+ }
475
+ td time_ago_in_words(reported_user.last_login_at)
476
+ td report_details[:abuse_flag]
477
+ td report_details[:abuse_text]
478
+ td report_details[:abuse_reporter]
479
+ }
480
+ end
481
+ }
482
+ }
483
+
484
+ h1 "Most Reporting Users in the last \#{time_span_days} days:"
485
+
486
+ table(:class => "reporting_users user_list") {
487
+ thead {
488
+ tr {
489
+ th "User ID"
490
+ th "Username"
491
+ th "Email"
492
+ th "Last Login"
493
+ th "Total Reported Users"
494
+ th "Reported-For Breakdown"
495
+ }
496
+ }
497
+
498
+ tbody {
499
+ top_reporting_users.each do |reporting_user|
500
+ reporting_details = top_reporting_user_details[reporting_user.id]
501
+ tr {
502
+ td reporting_user.id
503
+ td reporting_user.username
504
+ td {
505
+ if reporting_user.email
506
+ text reporting_user.email
507
+ else
508
+ span "(none available)", :class => :no_email
509
+ end
510
+ }
511
+ td time_ago_in_words(reporting_user.last_login_at)
512
+ td(reporting_details[:flags].uniq { |f| f.reported_user_id }.length)
513
+ td {
514
+ grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
515
+ grouped_by_flag.each do |flag_type, flags|
516
+ text "Flag"; strong flag_type; text ": \#{flags.length} total flag(s)"; br
517
+ end
518
+ }
519
+ }
520
+ end
521
+ }
522
+ }
523
+ end
524
+ end
525
+ EOS
526
+
527
+ p {
528
+ text "Although we really haven’t used the true advantages of Fortitude here, because we haven’t refactored "
529
+ text "anything yet, we can already see a few nice things about this:"
530
+ }
531
+
532
+ ul {
533
+ li {
534
+ strong "Parameter Declarations"; text ": The "; code "needs"; text " declarations at the top of the "
535
+ text "Fortitude class show you, at a glance, exactly what you have to pass into this view to render it. "
536
+ text "Every single Fortitude view or partial has this, and it’s guaranteed to be correct: if you don’t "
537
+ text "declare something as a "; code "need"; text ", you’ll be unable to render it in the view. (You can "
538
+ text "also supply defaults to make these optional, but we’re getting ahead of ourselves.)"
539
+ }
540
+ li {
541
+ strong "Logic vs. Rendering"; text ": Although a common convention in Rails code is to use braces "
542
+ text "for single-line blocks and "; code "do...end"; text " for multiline blocks, the convention is "
543
+ text "different in Fortitude code, and for good reason. In Fortitude code, we use braces to delimit actual "
544
+ text "HTML element contents, and "; code "do...end"; text " to delimit Ruby logic. This lets the eye scan "
545
+ text "the class easily and see exactly where there’s logic and where there’s HTML."
546
+ }
547
+ li {
548
+ strong "Cleanliness"; text ": This is, of course, a matter of personal preference, and not all people "
549
+ text "will feel the same way. However, to your author’s eye, the lack of all of the ERb "
550
+ code "<%= ... %>"; text " symbols cluttering up any place there’s logic in the view makes it quite a bit "
551
+ text "cleaner to read."
552
+ }
553
+ }
554
+
555
+ p {
556
+ text "However, again, the syntax isn’t really the point of Fortitude — the real point is the refactoring that "
557
+ text "the syntax allows you to do. Let’s take a look at that next."
558
+ }
559
+
560
+ h5 "A Beautifully-Refactored View"
561
+
562
+ p "Let’s just look at the fully-refactored view right off the bat and see what we’ve been able to do:"
563
+
564
+ fortitude '/app/views/admin/users/user_reporting.html.rb', <<-EOS
565
+ class Views::Admin::Users::UserReporting < Views::Base
566
+ needs :time_span_days
567
+ needs :blocked_users, :blocked_user_reasons
568
+ needs :reported_users, :reported_user_reasons
569
+ needs :top_reporting_users, :top_reporting_user_details
570
+
571
+ def content
572
+ blocked_users_table
573
+ reported_users_table
574
+ reporting_users_table
575
+ end
576
+
577
+ def blocked_users_table
578
+ users_table("Blocked Users", :blocked_users, [ "Blocked Because", "Blocked By" ], blocked_users) do |user|
579
+ blocked_details = blocked_user_reasons[user.id]
580
+ td blocked_details[:reason]
581
+ td blocked_details[:admin_user]
582
+ end
583
+ end
584
+
585
+ def reported_users_table
586
+ users_table("Reported Users", :reported_users, [ "Reported For", "Report Text", "Reported By" ], reported_users) do |user|
587
+ report_details = reported_user_reasons[user.id]
588
+ td report_details[:abuse_flag]
589
+ td report_details[:abuse_text]
590
+ td report_details[:abuse_reporter]
591
+ end
592
+ end
593
+
594
+ def reporting_users_table
595
+ users_table("Most Reporting Users", :reporting_users, [ "Total Reported Users", "Reported-For Breakdown" ], top_reporting_users) do |user|
596
+ reporting_details = top_reporting_user_details[user.id]
597
+
598
+ td(reporting_details[:flags].uniq { |f| f.reported_user_id }.length)
599
+ td {
600
+ grouped_by_flag = reporting_details[:flags].group_by { |f| f.flag_type }
601
+ grouped_by_flag.each do |flag_type, flags|
602
+ text "Flag"; strong flag_type; text ": \#{flags.length} total flag(s)"; br
603
+ end
604
+ }
605
+ end
606
+ end
607
+
608
+ def users_table(title_prefix, css_class, extra_columns, users)
609
+ h1 "\#{title_prefix} in the last \#{time_span_days} days: "
610
+
611
+ table(:class => "\#{css_class} user_list") {
612
+ thead {
613
+ tr {
614
+ ([ "User ID", "Username", "Email", "Last Login" ] + extra_columns).each do |header|
615
+ th header
616
+ end
617
+ }
618
+ }
619
+
620
+ tbody {
621
+ users.each do |user|
622
+ tr {
623
+ td user.id
624
+ td user.username
625
+ td {
626
+ if user.email
627
+ text user.email
628
+ else
629
+ span "(none available)", :class => :no_email
630
+ end
631
+ }
632
+ td time_ago_in_words(user.last_login_at)
633
+
634
+ yield user
635
+ }
636
+ end
637
+ }
638
+ }
639
+ end
640
+ end
641
+ EOS
642
+ end
643
+
644
+ def fortitude_benefits
645
+ p {
646
+ text "This is such an enormous improvement over either the original ERb view or the refactored ERb view "
647
+ text "that it’s hard to know where to begin. Let’s try enumerating the advantages, over and above the "
648
+ text "ones we mentioned above that are inherent to any Fortitude code:"
649
+ }
650
+
651
+ ul {
652
+ li {
653
+ strong "Readability of Overall Structure"; text ": The overall organization of this view is instantly "
654
+ text "obvious with one glance at the "; code "#content"; text " method (the entry point of every "
655
+ text "Fortitude view or partial): it contains a table of blocked users, a table of reported users, "
656
+ text "and a table of reporting users."
657
+ }
658
+ li {
659
+ strong "Readability of the Table"; text ": Our table is now in "; em "just one method"; text ". It’s "
660
+ text "completely clear what the structure of that table is, exactly how it works, and exactly where "
661
+ text "callers can pass in data or code that might change it."
662
+ }
663
+ li {
664
+ strong "Variation in Each Table"; text ": Similarly, it’s easy to see exactly how each table "
665
+ text "differs from the others: it’s exactly the code present in the method ("; em "e.g."; text ", "
666
+ code "#blocked_users_table"; text ", "; code "#reported_users_table"; text ", and so on)."
667
+ }
668
+ li {
669
+ strong "Lack of File Clutter"; text ": Our view is now "; em "one single file"; " again. The way we "
670
+ text "refactor our code is completely independent of the number of files we create for it, allowing us "
671
+ text "to keep like code together. If we have eight views in this controller, we’ll have just eight "
672
+ text "files in "; code "app/views/admin/users"; text ", and it’s crystal-clear exactly what each one is "
673
+ text "for. (Of course, you are more than welcome to factor out Fortitude code into separate files if you "
674
+ text "want — it just isn’t "; em "required"; text " in the same way it is with ERb, HAML, or other "
675
+ text "traditional templating engines.)"
676
+ }
677
+ li {
678
+ strong "Consistent"; text ": At no point do we build HTML by interpolating Ruby strings; it’s all just "
679
+ text "Fortitude code."
680
+ }
681
+ li {
682
+ strong { em "Safe" }; text ": We "; em "never"; text " have to call "; code "#h"; text " anywhere here, "
683
+ text "nor do we even have to think about HTML escaping. It’s all just handled for us, as it should be, "
684
+ text "with no risk of XSS attacks. (This doesn’t mean it’s "; em "impossible"; text " to create XSS "
685
+ text "vulnerabilities using Fortitude, of course — but it’s much, much more difficult, because you "
686
+ text "effectively never have to compose HTML using Ruby strings.)"
687
+ }
688
+ li {
689
+ strong "Indentation (of Source)"; text ": It’s obvious how the source should be indented, there’s only "
690
+ text "one reasonable way to do it, and any editor "
691
+ text "capable of indenting Ruby properly will automatically indent Fortitude properly."
692
+ }
693
+ li {
694
+ p {
695
+ strong "Indentation (of Output)"; text ": Because Fortitude understands the structure of your code, "
696
+ text "it "; em "always"; text " produces perfectly-formatted output. Here’s what the equivalent to the "
697
+ text "poorly-formatted HTML output from ERb above looks like if it’s coming from Fortitude:"
698
+ }
699
+
700
+ html_source <<-EOS
701
+ <html>
702
+ <head>
703
+ ...
704
+ </head>
705
+ <body>
706
+ ...
707
+ <h1>Most Reporting Users in the last 30 days:</h1>
708
+
709
+ <table class="reporting_users user_list">
710
+ <thead>
711
+ <tr>
712
+ <th>User ID</th>
713
+ <th>Username</th>
714
+ <th>Email</th>
715
+ <th>Last Login</th>
716
+ <th>Total Reported Users</th>
717
+ <th>Reported-For Breakdown</th>
718
+ </tr>
719
+ </thead>
720
+ <tbody>
721
+ <tr>
722
+ <td>12345</td>
723
+ <td>jane_doe_1</td>
724
+ <td>jdoe@example.com</td>
725
+ <td>about 2 hours ago</td>
726
+ <td>17</td>
727
+ <td>
728
+ Flag <strong>Spam</strong>: 27 total flag(s)<br>
729
+ Flag <strong>Personal Attack</strong>: 17 total flag(s)<br>
730
+ Flag <strong>Off-Topic</strong>: 9 total flag(s)<br>
731
+ </td>
732
+ </tr>
733
+ </tbody>
734
+ </table>
735
+ ...
736
+ </body>
737
+ </html>
738
+ EOS
739
+
740
+ p {
741
+ text "(In production, Fortitude will, by default, emit this code with no whitespace at all, reducing your "
742
+ text "page weight significantly — also something that ERb cannot do.)"
743
+ }
744
+ }
745
+ }
746
+
747
+ p {
748
+ text "Altogether, Fortitude has helped us refactor this view in a way we simply couldn’t have using any "
749
+ text "traditional templating engine. "
750
+ }
751
+
752
+ p {
753
+ text "One of the reasons the complex ERb refactoring above may seem unfamiliar to many readers "
754
+ text "is simply that the refactoring ends up being so complex that most teams simply don’t do it — they leave "
755
+ text "this view un-refactored, like the original example, since it actually is cleaner in many ways than "
756
+ text "the refactored version. And, like before, this doesn’t mean that the original is actually particularly "
757
+ em "good"; text "; it contains an awful lot of duplication — it’s just that the tools traditional templating "
758
+ text "engines give you are insufficient to refactor this code well."
759
+ }
760
+
761
+ p {
762
+ text " By providing you all the power of Ruby "
763
+ text "to refactor your code, Fortitude allows you to refactor this code into something that’s clean, clear, "
764
+ text "and actually a joy to work with."
765
+ }
766
+
767
+ p {
768
+ text "Next, we’ll take a look at how Fortitude lets you use inheritance to beautifully and effectively factor "
769
+ text "a set of views that all have a lot in common, but differ in their exact details."
770
+ }
771
+ end
772
+ end
773
+ end
774
+ end