dashing-rails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +161 -0
  3. data/Rakefile +6 -0
  4. data/app/assets/fonts/dashing/fontawesome-webfont.eot +0 -0
  5. data/app/assets/fonts/dashing/fontawesome-webfont.svg +255 -0
  6. data/app/assets/fonts/dashing/fontawesome-webfont.ttf +0 -0
  7. data/app/assets/fonts/dashing/fontawesome-webfont.woff +0 -0
  8. data/app/assets/javascripts/dashing/application.js +27 -0
  9. data/app/assets/javascripts/dashing/dashing-src.coffee +110 -0
  10. data/app/assets/javascripts/dashing/dashing.coffee +18 -0
  11. data/app/assets/stylesheets/dashing/application.css +17 -0
  12. data/app/assets/stylesheets/dashing/dashing.scss +250 -0
  13. data/app/controllers/dashing/application_controller.rb +15 -0
  14. data/app/controllers/dashing/dashboards_controller.rb +31 -0
  15. data/app/controllers/dashing/events_controller.rb +25 -0
  16. data/app/controllers/dashing/widgets_controller.rb +50 -0
  17. data/app/helpers/dashing/application_helper.rb +4 -0
  18. data/app/views/dashing/default_widgets/clock/clock.coffee +18 -0
  19. data/app/views/dashing/default_widgets/clock/clock.html +2 -0
  20. data/app/views/dashing/default_widgets/clock/clock.scss +13 -0
  21. data/app/views/dashing/default_widgets/comments/comments.coffee +24 -0
  22. data/app/views/dashing/default_widgets/comments/comments.html +7 -0
  23. data/app/views/dashing/default_widgets/comments/comments.scss +33 -0
  24. data/app/views/dashing/default_widgets/graph/graph.coffee +35 -0
  25. data/app/views/dashing/default_widgets/graph/graph.html +5 -0
  26. data/app/views/dashing/default_widgets/graph/graph.scss +65 -0
  27. data/app/views/dashing/default_widgets/iframe/iframe.coffee +9 -0
  28. data/app/views/dashing/default_widgets/iframe/iframe.html +1 -0
  29. data/app/views/dashing/default_widgets/iframe/iframe.scss +8 -0
  30. data/app/views/dashing/default_widgets/image/image.coffee +9 -0
  31. data/app/views/dashing/default_widgets/image/image.html +1 -0
  32. data/app/views/dashing/default_widgets/image/image.scss +13 -0
  33. data/app/views/dashing/default_widgets/index.css +12 -0
  34. data/app/views/dashing/default_widgets/index.js +13 -0
  35. data/app/views/dashing/default_widgets/list/list.coffee +6 -0
  36. data/app/views/dashing/default_widgets/list/list.html +18 -0
  37. data/app/views/dashing/default_widgets/list/list.scss +60 -0
  38. data/app/views/dashing/default_widgets/meter/meter.coffee +14 -0
  39. data/app/views/dashing/default_widgets/meter/meter.html +7 -0
  40. data/app/views/dashing/default_widgets/meter/meter.scss +35 -0
  41. data/app/views/dashing/default_widgets/number/number.coffee +24 -0
  42. data/app/views/dashing/default_widgets/number/number.html +11 -0
  43. data/app/views/dashing/default_widgets/number/number.scss +39 -0
  44. data/app/views/dashing/default_widgets/text/text.coffee +1 -0
  45. data/app/views/dashing/default_widgets/text/text.html +7 -0
  46. data/app/views/dashing/default_widgets/text/text.scss +32 -0
  47. data/app/views/layouts/dashing/dashboard.html.erb +30 -0
  48. data/config/routes.rb +15 -0
  49. data/lib/assets/javascripts/batman.jquery.js +163 -0
  50. data/lib/assets/javascripts/batman.js +13680 -0
  51. data/lib/assets/javascripts/d3.v2.min.js +4 -0
  52. data/lib/assets/javascripts/dashing.gridster.coffee +35 -0
  53. data/lib/assets/javascripts/es5-shim.js +1021 -0
  54. data/lib/assets/javascripts/jquery.gridster.js +2890 -0
  55. data/lib/assets/javascripts/jquery.js +4 -0
  56. data/lib/assets/javascripts/jquery.knob.js +646 -0
  57. data/lib/assets/javascripts/jquery.leanModal.min.js +5 -0
  58. data/lib/assets/javascripts/jquery.timeago.js +184 -0
  59. data/lib/assets/javascripts/moment.min.js +6 -0
  60. data/lib/assets/javascripts/rickshaw.min.js +2 -0
  61. data/lib/assets/stylesheets/font-awesome.css +303 -0
  62. data/lib/assets/stylesheets/jquery.gridster.css +57 -0
  63. data/lib/dashing.rb +29 -0
  64. data/lib/dashing/configuration.rb +26 -0
  65. data/lib/dashing/engine.rb +13 -0
  66. data/lib/dashing/version.rb +3 -0
  67. data/lib/generators/dashing/install_generator.rb +32 -0
  68. data/lib/generators/dashing/job_generator.rb +15 -0
  69. data/lib/generators/templates/dashboards/sample.html.erb +28 -0
  70. data/lib/generators/templates/initializer.rb +50 -0
  71. data/lib/generators/templates/jobs/new.rb +3 -0
  72. data/lib/generators/templates/jobs/sample.rb +9 -0
  73. data/lib/generators/templates/widgets/index.css +12 -0
  74. data/lib/generators/templates/widgets/index.js +13 -0
  75. data/lib/tasks/dashing_tasks.rake +4 -0
  76. data/spec/controllers/dashing/dashboards_controller_spec.rb +42 -0
  77. data/spec/controllers/dashing/events_controller_spec.rb +11 -0
  78. data/spec/controllers/dashing/widgets_controller_spec.rb +61 -0
  79. data/spec/dummy/README.rdoc +28 -0
  80. data/spec/dummy/Rakefile +6 -0
  81. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  82. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  83. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  84. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  85. data/spec/dummy/app/views/dashing/dashboards/foo.erb +0 -0
  86. data/spec/dummy/app/views/dashing/widgets/foo/foo.coffee +0 -0
  87. data/spec/dummy/app/views/dashing/widgets/foo/foo.html +0 -0
  88. data/spec/dummy/app/views/dashing/widgets/foo/foo.scss +0 -0
  89. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  90. data/spec/dummy/app/views/layouts/dashing/dashboard.html.erb +30 -0
  91. data/spec/dummy/bin/bundle +3 -0
  92. data/spec/dummy/bin/rails +4 -0
  93. data/spec/dummy/bin/rake +4 -0
  94. data/spec/dummy/config.ru +4 -0
  95. data/spec/dummy/config/application.rb +23 -0
  96. data/spec/dummy/config/boot.rb +5 -0
  97. data/spec/dummy/config/database.yml +25 -0
  98. data/spec/dummy/config/environment.rb +5 -0
  99. data/spec/dummy/config/environments/development.rb +29 -0
  100. data/spec/dummy/config/environments/production.rb +80 -0
  101. data/spec/dummy/config/environments/test.rb +36 -0
  102. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  103. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  104. data/spec/dummy/config/initializers/inflections.rb +16 -0
  105. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  106. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  107. data/spec/dummy/config/initializers/session_store.rb +3 -0
  108. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  109. data/spec/dummy/config/locales/en.yml +23 -0
  110. data/spec/dummy/config/routes.rb +4 -0
  111. data/spec/dummy/db/test.sqlite3 +0 -0
  112. data/spec/dummy/log/test.log +27 -0
  113. data/spec/dummy/public/404.html +58 -0
  114. data/spec/dummy/public/422.html +58 -0
  115. data/spec/dummy/public/500.html +57 -0
  116. data/spec/dummy/public/favicon.ico +0 -0
  117. data/spec/lib/dashing/configuration_spec.rb +43 -0
  118. data/spec/lib/dashing_spec.rb +41 -0
  119. data/spec/spec_helper.rb +44 -0
  120. metadata +342 -0
@@ -0,0 +1,27 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // the compiled file.
9
+ //
10
+ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
+ // GO AFTER THE REQUIRES BELOW.
12
+ //
13
+ //= require jquery
14
+ //= require jquery.gridster
15
+ //= require jquery.knob
16
+ //= require jquery.leanModal.min
17
+ //= require jquery.timeago
18
+ //= require moment.min
19
+ //= require rickshaw.min
20
+ //= require batman
21
+ //= require batman.jquery
22
+ //= require d3.v2.min
23
+ //= require es5-shim
24
+ //= require_tree .
25
+ //= require dashing.gridster
26
+ //= require default_widgets
27
+ //= require widgets
@@ -0,0 +1,110 @@
1
+ Batman.config.pathPrefix = "/"
2
+ Batman.config.viewPrefix = "/dashing/widgets/"
3
+
4
+ Batman.Filters.prettyNumber = (num) ->
5
+ num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") unless isNaN(num)
6
+
7
+ Batman.Filters.dashize = (str) ->
8
+ dashes_rx1 = /([A-Z]+)([A-Z][a-z])/g;
9
+ dashes_rx2 = /([a-z\d])([A-Z])/g;
10
+
11
+ return str.replace(dashes_rx1, '$1_$2').replace(dashes_rx2, '$1_$2').replace('_', '-').toLowerCase()
12
+
13
+ Batman.Filters.shortenedNumber = (num) ->
14
+ return num if isNaN(num)
15
+ if num >= 1000000000
16
+ (num / 1000000000).toFixed(1) + 'B'
17
+ else if num >= 1000000
18
+ (num / 1000000).toFixed(1) + 'M'
19
+ else if num >= 1000
20
+ (num / 1000).toFixed(1) + 'K'
21
+ else
22
+ num
23
+
24
+ class window.Dashing extends Batman.App
25
+ @root ->
26
+ Dashing.params = Batman.URI.paramsFromQuery(window.location.search.slice(1));
27
+
28
+ class Dashing.Widget extends Batman.View
29
+ constructor: ->
30
+ # Set the view path
31
+ @constructor::source = Batman.Filters.underscore(@constructor.name)
32
+ super
33
+
34
+ @mixin($(@node).data())
35
+ Dashing.widgets[@id] ||= []
36
+ Dashing.widgets[@id].push(@)
37
+ @mixin(Dashing.lastEvents[@id]) # in case the events from the server came before the widget was rendered
38
+
39
+ type = Batman.Filters.dashize(@view)
40
+ $(@node).addClass("widget widget-#{type} #{@id}")
41
+
42
+ @accessor 'updatedAtMessage', ->
43
+ if updatedAt = @get('updatedAt')
44
+ timestamp = new Date(updatedAt * 1000)
45
+ hours = timestamp.getHours()
46
+ minutes = ("0" + timestamp.getMinutes()).slice(-2)
47
+ "Last updated at #{hours}:#{minutes}"
48
+
49
+ @::on 'ready', ->
50
+ Dashing.Widget.fire 'ready'
51
+
52
+ receiveData: (data) =>
53
+ @mixin(data)
54
+ @onData(data)
55
+
56
+ onData: (data) =>
57
+ # Widgets override this to handle incoming data
58
+
59
+ Dashing.AnimatedValue =
60
+ get: Batman.Property.defaultAccessor.get
61
+ set: (k, to) ->
62
+ if !to? || isNaN(to)
63
+ @[k] = to
64
+ else
65
+ timer = "interval_#{k}"
66
+ num = if (!isNaN(@[k]) && @[k]?) then @[k] else 0
67
+ unless @[timer] || num == to
68
+ to = parseFloat(to)
69
+ num = parseFloat(num)
70
+ up = to > num
71
+ num_interval = Math.abs(num - to) / 90
72
+ @[timer] =
73
+ setInterval =>
74
+ num = if up then Math.ceil(num+num_interval) else Math.floor(num-num_interval)
75
+ if (up && num > to) || (!up && num < to)
76
+ num = to
77
+ clearInterval(@[timer])
78
+ @[timer] = null
79
+ delete @[timer]
80
+ @[k] = num
81
+ @set k, to
82
+ , 10
83
+ @[k] = num
84
+
85
+ Dashing.widgets = widgets = {}
86
+ Dashing.lastEvents = lastEvents = {}
87
+ Dashing.debugMode = false
88
+
89
+ source = new EventSource('/dashing/events')
90
+ source.addEventListener 'open', (e) ->
91
+ console.log("Connection opened")
92
+
93
+ source.addEventListener 'error', (e)->
94
+ console.log("Connection error")
95
+ if (e.readyState == EventSource.CLOSED)
96
+ console.log("Connection closed")
97
+
98
+ source.addEventListener 'message', (e) =>
99
+ data = JSON.parse(e.data)
100
+ if lastEvents[data.id]?.updatedAt != data.updatedAt
101
+ if Dashing.debugMode
102
+ console.log("Received data for #{data.id}", data)
103
+ lastEvents[data.id] = data
104
+ if widgets[data.id]?.length > 0
105
+ for widget in widgets[data.id]
106
+ widget.receiveData(data)
107
+
108
+
109
+ $(document).ready ->
110
+ Dashing.run()
@@ -0,0 +1,18 @@
1
+ console.log("Yeah! The dashboard has started!")
2
+
3
+ Dashing.on 'ready', ->
4
+ Dashing.widget_margins ||= [5, 5]
5
+ Dashing.widget_base_dimensions ||= [300, 360]
6
+ Dashing.numColumns ||= 4
7
+
8
+ contentWidth = (Dashing.widget_base_dimensions[0] + Dashing.widget_margins[0] * 2) * Dashing.numColumns
9
+
10
+ Batman.setImmediate ->
11
+ $('.gridster').width(contentWidth)
12
+ $('.gridster ul:first').gridster
13
+ widget_margins: Dashing.widget_margins
14
+ widget_base_dimensions: Dashing.widget_base_dimensions
15
+ avoid_overlapped_widgets: !Dashing.customGridsterLayout
16
+ draggable:
17
+ stop: Dashing.showGridsterInstructions
18
+ start: -> Dashing.currentWidgetPositions = Dashing.getWidgetPositions()
@@ -0,0 +1,17 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require font-awesome
12
+ *= require jquery.gridster
13
+ *= require default_widgets
14
+ *= require widgets
15
+ *= require_self
16
+ *= require_tree .
17
+ */
@@ -0,0 +1,250 @@
1
+ // ----------------------------------------------------------------------------
2
+ // Sass declarations
3
+ // ----------------------------------------------------------------------------
4
+ $background-color: #222;
5
+ $text-color: #fff;
6
+
7
+ $background-warning-color-1: #e82711;
8
+ $background-warning-color-2: #9b2d23;
9
+ $text-warning-color: #fff;
10
+
11
+ $background-danger-color-1: #eeae32;
12
+ $background-danger-color-2: #ff9618;
13
+ $text-danger-color: #fff;
14
+
15
+ @-webkit-keyframes status-warning-background {
16
+ 0% { background-color: $background-warning-color-1; }
17
+ 50% { background-color: $background-warning-color-2; }
18
+ 100% { background-color: $background-warning-color-1; }
19
+ }
20
+ @-webkit-keyframes status-danger-background {
21
+ 0% { background-color: $background-danger-color-1; }
22
+ 50% { background-color: $background-danger-color-2; }
23
+ 100% { background-color: $background-danger-color-1; }
24
+ }
25
+ @mixin animation($animation-name, $duration, $function, $animation-iteration-count:""){
26
+ -webkit-animation: $animation-name $duration $function #{$animation-iteration-count};
27
+ -moz-animation: $animation-name $duration $function #{$animation-iteration-count};
28
+ -ms-animation: $animation-name $duration $function #{$animation-iteration-count};
29
+ }
30
+
31
+ // ----------------------------------------------------------------------------
32
+ // Base styles
33
+ // ----------------------------------------------------------------------------
34
+ html {
35
+ font-size: 100%;
36
+ -webkit-text-size-adjust: 100%;
37
+ -ms-text-size-adjust: 100%;
38
+ }
39
+
40
+ body {
41
+ margin: 0;
42
+ background-color: $background-color;
43
+ font-size: 20px;
44
+ color: $text-color;
45
+ font-family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif;
46
+ }
47
+
48
+ b, strong {
49
+ font-weight: bold;
50
+ }
51
+
52
+ a {
53
+ text-decoration: none;
54
+ color: inherit;
55
+ }
56
+
57
+ img {
58
+ border: 0;
59
+ -ms-interpolation-mode: bicubic;
60
+ vertical-align: middle;
61
+ }
62
+
63
+ img, object {
64
+ max-width: 100%;
65
+ }
66
+
67
+ iframe {
68
+ max-width: 100%;
69
+ }
70
+
71
+ table {
72
+ border-collapse: collapse;
73
+ border-spacing: 0;
74
+ width: 100%;
75
+ }
76
+
77
+ td {
78
+ vertical-align: middle;
79
+ }
80
+
81
+ ul, ol {
82
+ padding: 0;
83
+ margin: 0;
84
+ }
85
+
86
+ h1, h2, h3, h4, h5, p {
87
+ padding: 0;
88
+ margin: 0;
89
+ }
90
+ h1 {
91
+ margin-bottom: 12px;
92
+ text-align: center;
93
+ font-size: 30px;
94
+ font-weight: 400;
95
+ }
96
+ h2 {
97
+ text-transform: uppercase;
98
+ font-size: 76px;
99
+ font-weight: 700;
100
+ color: $text-color;
101
+ }
102
+ h3 {
103
+ font-size: 25px;
104
+ font-weight: 600;
105
+ color: $text-color;
106
+ }
107
+
108
+ // ----------------------------------------------------------------------------
109
+ // Base widget styles
110
+ // ----------------------------------------------------------------------------
111
+ .gridster {
112
+ margin: 0px auto;
113
+ }
114
+
115
+ .icon-background {
116
+ width: 100%!important;
117
+ height: 100%;
118
+ position: absolute;
119
+ left: 0;
120
+ top: 0;
121
+ opacity: 0.1;
122
+ font-size: 275px;
123
+ }
124
+
125
+ .list-nostyle {
126
+ list-style: none;
127
+ }
128
+
129
+ .gridster ul {
130
+ list-style: none;
131
+ }
132
+
133
+ .gs_w {
134
+ width: 100%;
135
+ display: table;
136
+ cursor: pointer;
137
+ }
138
+
139
+ .widget {
140
+ padding: 25px 12px;
141
+ text-align: center;
142
+ width: 100%;
143
+ display: table-cell;
144
+ vertical-align: middle;
145
+ }
146
+
147
+ .widget.status-warning {
148
+ background-color: $background-warning-color-1;
149
+ @include animation(status-warning-background, 2s, ease, infinite);
150
+
151
+ .icon-warning-sign {
152
+ display: inline-block;
153
+ }
154
+
155
+ .title, .more-info {
156
+ color: $text-warning-color;
157
+ }
158
+ }
159
+
160
+ .widget.status-danger {
161
+ color: $text-danger-color;
162
+ background-color: $background-danger-color-1;
163
+ @include animation(status-danger-background, 2s, ease, infinite);
164
+
165
+ .icon-warning-sign {
166
+ display: inline-block;
167
+ }
168
+
169
+ .title, .more-info {
170
+ color: $text-danger-color;
171
+ }
172
+ }
173
+
174
+ .more-info {
175
+ font-size: 15px;
176
+ position: absolute;
177
+ bottom: 32px;
178
+ left: 0;
179
+ right: 0;
180
+ }
181
+
182
+ .updated-at {
183
+ font-size: 15px;
184
+ position: absolute;
185
+ bottom: 12px;
186
+ left: 0;
187
+ right: 0;
188
+ }
189
+
190
+ #save-gridster {
191
+ display: none;
192
+ position: fixed;
193
+ top: 0;
194
+ margin: 0px auto;
195
+ left: 50%;
196
+ z-index: 1000;
197
+ background: black;
198
+ width: 190px;
199
+ text-align: center;
200
+ border: 1px solid white;
201
+ border-top: 0px;
202
+ margin-left: -95px;
203
+ padding: 15px;
204
+ }
205
+
206
+ #save-gridster:hover {
207
+ padding-top: 25px;
208
+ }
209
+
210
+ #saving-instructions {
211
+ display: none;
212
+ padding: 10px;
213
+ width: 500px;
214
+ height: 122px;
215
+ z-index: 1000;
216
+ background: white;
217
+ top: 100px;
218
+ color: black;
219
+ font-size: 15px;
220
+ padding-bottom: 4px;
221
+
222
+ textarea {
223
+ white-space: nowrap;
224
+ width: 494px;
225
+ height: 80px;
226
+ }
227
+ }
228
+
229
+ #lean_overlay {
230
+ position: fixed;
231
+ z-index:100;
232
+ top: 0px;
233
+ left: 0px;
234
+ height:100%;
235
+ width:100%;
236
+ background: #000;
237
+ display: none;
238
+ }
239
+
240
+ #container {
241
+ padding-top: 5px;
242
+ }
243
+
244
+
245
+ // ----------------------------------------------------------------------------
246
+ // Clearfix
247
+ // ----------------------------------------------------------------------------
248
+ .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
249
+ .clearfix:after { clear: both; }
250
+ .clearfix { zoom: 1; }
@@ -0,0 +1,15 @@
1
+ module Dashing
2
+ class ApplicationController < ActionController::Base
3
+
4
+ private
5
+
6
+ def check_accessibility
7
+ auth_token = params.delete(:auth_token)
8
+ if !Dashing.config.auth_token || auth_token == Dashing.config.auth_token
9
+ true
10
+ else
11
+ render nothing: true, status: 401 and return
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ module Dashing
2
+ class DashboardsController < ApplicationController
3
+
4
+ before_filter :check_dashboard_name, only: :show
5
+
6
+ rescue_from ActionView::MissingTemplate, with: :template_not_found
7
+
8
+ def index
9
+ render file: dashboard_path(Dashing.config.default_dashboard || Dashing.first_dashboard || ''), layout: Dashing.config.dashboard_layout
10
+ end
11
+
12
+ def show
13
+ render file: dashboard_path(params[:name]), layout: Dashing.config.dashboard_layout
14
+ end
15
+
16
+ private
17
+
18
+ def check_dashboard_name
19
+ raise 'bad dashboard name' unless params[:name] =~ /\A[a-zA-z0-9_\-]+\z/
20
+ end
21
+
22
+ def dashboard_path(name)
23
+ Rails.root.join(Dashing.config.dashboards_path, name)
24
+ end
25
+
26
+ def template_not_found
27
+ raise "Count not find template for dashboard '#{params[:name]}'. Define your dashboard in #{dashboard_path('')}"
28
+ end
29
+
30
+ end
31
+ end