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.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +28 -22
- data/CHANGES.md +50 -0
- data/CONTRIBUTORS.md +1 -0
- data/doc/.gitignore +18 -0
- data/doc/Gemfile +21 -0
- data/doc/config.rb +92 -0
- data/doc/source/images/background.png +0 -0
- data/doc/source/images/middleman.png +0 -0
- data/doc/source/images/why/icon_button.png +0 -0
- data/doc/source/images/why/icon_button@2x.png +0 -0
- data/doc/source/images/why/modal_dialog@2x.png +0 -0
- data/doc/source/index.html.pcss +96 -0
- data/doc/source/index.html.rb +66 -0
- data/doc/source/javascripts/all.js +1 -0
- data/doc/source/javascripts/highlight.pack.js +1 -0
- data/doc/source/layouts/layout.rb +55 -0
- data/doc/source/portable/fortitude-bootstrap.rb +53 -0
- data/doc/source/shared/base.pcss +62 -0
- data/doc/source/shared/base.rb +30 -0
- data/doc/source/shared/common.rb +55 -0
- data/doc/source/shared/standard_page.rb +40 -0
- data/doc/source/stylesheets/_shared_prefix.scss +25 -0
- data/doc/source/stylesheets/all.css.scss +7 -0
- data/doc/source/stylesheets/basics.css.scss +20 -0
- data/doc/source/stylesheets/bootstrap_importer.css.scss +1 -0
- data/doc/source/stylesheets/highlight/arta.css +140 -0
- data/doc/source/stylesheets/highlight/ascetic.css +52 -0
- data/doc/source/stylesheets/highlight/atelier-dune.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-dune.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-forest.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-forest.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-heath.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-heath.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-lakeside.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-lakeside.light.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-seaside.dark.css +95 -0
- data/doc/source/stylesheets/highlight/atelier-seaside.light.css +95 -0
- data/doc/source/stylesheets/highlight/brown_paper.css +104 -0
- data/doc/source/stylesheets/highlight/brown_papersq.png +0 -0
- data/doc/source/stylesheets/highlight/codepen-embed.css +108 -0
- data/doc/source/stylesheets/highlight/color-brewer.css +168 -0
- data/doc/source/stylesheets/highlight/dark.css +104 -0
- data/doc/source/stylesheets/highlight/default.css +152 -0
- data/doc/source/stylesheets/highlight/docco.css +135 -0
- data/doc/source/stylesheets/highlight/far.css +111 -0
- data/doc/source/stylesheets/highlight/foundation.css +136 -0
- data/doc/source/stylesheets/highlight/github.css +124 -0
- data/doc/source/stylesheets/highlight/googlecode.css +147 -0
- data/doc/source/stylesheets/highlight/hybrid.css +170 -0
- data/doc/source/stylesheets/highlight/idea.css +125 -0
- data/doc/source/stylesheets/highlight/ir_black.css +109 -0
- data/doc/source/stylesheets/highlight/kimbie.dark.css +96 -0
- data/doc/source/stylesheets/highlight/kimbie.light.css +96 -0
- data/doc/source/stylesheets/highlight/magula.css +121 -0
- data/doc/source/stylesheets/highlight/mono-blue.css +69 -0
- data/doc/source/stylesheets/highlight/monokai.css +127 -0
- data/doc/source/stylesheets/highlight/monokai_sublime.css +154 -0
- data/doc/source/stylesheets/highlight/obsidian.css +153 -0
- data/doc/source/stylesheets/highlight/paraiso.dark.css +95 -0
- data/doc/source/stylesheets/highlight/paraiso.light.css +95 -0
- data/doc/source/stylesheets/highlight/pojoaque.css +107 -0
- data/doc/source/stylesheets/highlight/pojoaque.jpg +0 -0
- data/doc/source/stylesheets/highlight/railscasts.css +187 -0
- data/doc/source/stylesheets/highlight/rainbow.css +108 -0
- data/doc/source/stylesheets/highlight/school_book.css +112 -0
- data/doc/source/stylesheets/highlight/school_book.png +0 -0
- data/doc/source/stylesheets/highlight/solarized_dark.css +108 -0
- data/doc/source/stylesheets/highlight/solarized_light.css +108 -0
- data/doc/source/stylesheets/highlight/sunburst.css +164 -0
- data/doc/source/stylesheets/highlight/tomorrow-night-blue.css +95 -0
- data/doc/source/stylesheets/highlight/tomorrow-night-bright.css +94 -0
- data/doc/source/stylesheets/highlight/tomorrow-night-eighties.css +94 -0
- data/doc/source/stylesheets/highlight/tomorrow-night.css +95 -0
- data/doc/source/stylesheets/highlight/tomorrow.css +92 -0
- data/doc/source/stylesheets/highlight/vs.css +93 -0
- data/doc/source/stylesheets/highlight/xcode.css +158 -0
- data/doc/source/stylesheets/highlight/zenburn.css +118 -0
- data/doc/source/why/a_larger_view.html.rb +774 -0
- data/doc/source/why/a_simple_helper.html.rb +332 -0
- data/doc/source/why/building_a_rich_modal_dialog.html.rb +156 -0
- data/doc/source/why/commonality_and_inheritance.html.rb +564 -0
- data/doc/source/why/example_list.rb +60 -0
- data/doc/source/why/example_page.rb +116 -0
- data/doc/source/why/index.html.rb +86 -0
- data/doc/source/why/other_benefits.html.rb +110 -0
- data/fortitude.gemspec +6 -1
- data/lib/fortitude/doctypes/html4_tags_strict.rb +1 -0
- data/lib/fortitude/doctypes/html5.rb +1 -0
- data/lib/fortitude/method_templates/tag_method_template.rb.smpl +27 -14
- data/lib/fortitude/rails/helpers.rb +2 -2
- data/lib/fortitude/rendering_context.rb +10 -1
- data/lib/fortitude/tags/tag.rb +3 -2
- data/lib/fortitude/tags/tag_support.rb +8 -3
- data/lib/fortitude/tilt/fortitude_template.rb +6 -2
- data/lib/fortitude/version.rb +1 -1
- data/lib/fortitude/widget.rb +2 -0
- data/lib/fortitude/widget/convenience.rb +30 -0
- data/lib/fortitude/widget/files.rb +22 -11
- data/lib/fortitude/widget/needs.rb +5 -3
- data/spec/helpers/system_helpers.rb +1 -0
- data/spec/rails/development_mode_system_spec.rb +0 -1
- data/spec/rails/rendering_system_spec.rb +20 -1
- data/spec/rails/templates/rendering_system_spec/app/controllers/rendering_system_spec_controller.rb +13 -0
- data/spec/rails/templates/rendering_system_spec/app/views/rendering_system_spec/render_hash_subclass.rb +18 -0
- data/spec/rails/templates/rendering_system_spec/lib/my_hash.rb +5 -0
- data/spec/system/convenience_methods_system_spec.rb +166 -0
- data/spec/system/formatting_system_spec.rb +25 -1
- data/spec/system/tag_rendering_system_spec.rb +73 -0
- data/spec/system/tilt_system_spec.rb +31 -0
- data/spec/system/widget_class_from_spec.rb +20 -4
- 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
|