fortitude 0.9.1-java → 0.9.2-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|