concerto_template_scheduling 0.0.1

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +13 -0
  3. data/README.md +24 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/concerto_template_scheduling/application.js +13 -0
  6. data/app/assets/javascripts/concerto_template_scheduling/schedules.js +25 -0
  7. data/app/assets/stylesheets/concerto_template_scheduling/application.css +13 -0
  8. data/app/assets/stylesheets/concerto_template_scheduling/schedules.css +4 -0
  9. data/app/assets/stylesheets/scaffold.css +56 -0
  10. data/app/controllers/concerto_template_scheduling/application_controller.rb +4 -0
  11. data/app/controllers/concerto_template_scheduling/schedules_controller.rb +106 -0
  12. data/app/helpers/concerto_template_scheduling/application_helper.rb +4 -0
  13. data/app/helpers/concerto_template_scheduling/schedules_helper.rb +4 -0
  14. data/app/models/concerto_template_scheduling/schedule.rb +181 -0
  15. data/app/views/concerto_template_scheduling/schedules/_form.html.erb +118 -0
  16. data/app/views/concerto_template_scheduling/schedules/edit.html.erb +8 -0
  17. data/app/views/concerto_template_scheduling/schedules/index.html.erb +82 -0
  18. data/app/views/concerto_template_scheduling/schedules/new.html.erb +8 -0
  19. data/app/views/concerto_template_scheduling/schedules/show.html.erb +54 -0
  20. data/app/views/concerto_template_scheduling/screens/_screen_link.html.erb +66 -0
  21. data/app/views/concerto_template_scheduling/templates/_in_use_by.html.erb +11 -0
  22. data/config/locales/en.yml +62 -0
  23. data/config/routes.rb +5 -0
  24. data/db/migrate/20140118205731_create_concerto_template_scheduling_schedules.rb +13 -0
  25. data/lib/concerto_template_scheduling.rb +4 -0
  26. data/lib/concerto_template_scheduling/engine.rb +69 -0
  27. data/lib/concerto_template_scheduling/version.rb +3 -0
  28. data/lib/tasks/concerto_template_scheduling_tasks.rake +4 -0
  29. data/test/concerto_template_scheduling_test.rb +7 -0
  30. data/test/dummy/README.rdoc +28 -0
  31. data/test/dummy/Rakefile +6 -0
  32. data/test/dummy/app/assets/javascripts/application.js +13 -0
  33. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  34. data/test/dummy/app/controllers/application_controller.rb +5 -0
  35. data/test/dummy/app/helpers/application_helper.rb +2 -0
  36. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  37. data/test/dummy/bin/bundle +3 -0
  38. data/test/dummy/bin/rails +4 -0
  39. data/test/dummy/bin/rake +4 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +23 -0
  42. data/test/dummy/config/boot.rb +5 -0
  43. data/test/dummy/config/database.yml +25 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +29 -0
  46. data/test/dummy/config/environments/production.rb +80 -0
  47. data/test/dummy/config/environments/test.rb +36 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  50. data/test/dummy/config/initializers/inflections.rb +16 -0
  51. data/test/dummy/config/initializers/mime_types.rb +5 -0
  52. data/test/dummy/config/initializers/secret_token.rb +12 -0
  53. data/test/dummy/config/initializers/session_store.rb +3 -0
  54. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  55. data/test/dummy/config/locales/en.yml +23 -0
  56. data/test/dummy/config/routes.rb +4 -0
  57. data/test/dummy/public/404.html +58 -0
  58. data/test/dummy/public/422.html +58 -0
  59. data/test/dummy/public/500.html +57 -0
  60. data/test/dummy/public/favicon.ico +0 -0
  61. data/test/fixtures/concerto_template_scheduling/schedules.yml +11 -0
  62. data/test/functional/concerto_template_scheduling/schedules_controller_test.rb +51 -0
  63. data/test/integration/navigation_test.rb +10 -0
  64. data/test/test_helper.rb +15 -0
  65. data/test/unit/concerto_template_scheduling/schedule_test.rb +9 -0
  66. data/test/unit/helpers/concerto_template_scheduling/schedules_helper_test.rb +6 -0
  67. metadata +202 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19a1b3329508cae93b9159a4f5a799a5cc657305
4
+ data.tar.gz: dfcd729f0c7eef50ef11cbbba06252a8e09ce1fa
5
+ SHA512:
6
+ metadata.gz: bc389fe4c6c5dd23fe2c840bfd09d8f81865d39ec2547db3ac2f575f398aeecdd8f47011f5ebfe7aaeafa53190c5fc8dac55fd1f8e023927a5f55bfe4b4bf851
7
+ data.tar.gz: 8cc002b04ee118f3f66b2902ddcfdfa96e6cc0ef21c0d8910b0d3f4313fe270c4a12b43fc2b10f1c9f85a00e8835272279e84605a4a91792dae12a95f2ff57f6
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014 Concerto Authors
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Concerto Template Scheduling
2
+
3
+ A Rails Engine for scheduling templates for your screens in Concerto.
4
+
5
+ Templates can be scheduled for a specific time frame for specific days. A template will be made active when this
6
+ scheduling criteria is met. A template can also be made active when content exists on a specified feed-- such as when
7
+ you want travel advisories or weather alerts to show at the bottom of your screen while still having your other content
8
+ shown.
9
+
10
+ To use this engine, add the following to the Concerto Gemfile:
11
+ ```
12
+ gem 'concerto_template_scheduling'
13
+ ```
14
+
15
+ To create the proper migrations, run:
16
+ ```
17
+ rails generate concerto_template_scheduling
18
+ ```
19
+
20
+ ## Security
21
+ If a user can update a screen, they have the ability to manage the scheduled templates for that screen.
22
+
23
+ ## Requirements
24
+ `start_time` and `end_time` setters (in the model) don't work well with ruby 1.8.7.
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ConcertoTemplateScheduling'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,13 @@
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
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,25 @@
1
+ // Place all the behaviors and hooks related to the matching controller here.
2
+ // All this logic will automatically be available in application.js.
3
+
4
+ function attachConcertoTemplateSchedulingHandlers() {
5
+ $('select#schedule_config_display_when').on('change', toggleCtsFormFields);
6
+
7
+ function toggleCtsFormFields() {
8
+ var dw = $('select#schedule_config_display_when').val();
9
+ if (dw == 3) { // 'content exists'
10
+ $('#feed_selection').show();
11
+ $('#scheduling_criteria').hide();
12
+ } else if (dw == 2) { // 'by criteria'
13
+ $('#feed_selection').hide();
14
+ $('#scheduling_criteria').show();
15
+ } else {
16
+ $('#feed_selection').hide();
17
+ $('#scheduling_criteria').hide();
18
+ }
19
+ }
20
+
21
+ toggleCtsFormFields();
22
+ }
23
+
24
+ $(document).ready(attachConcertoTemplateSchedulingHandlers);
25
+ $(document).on('page:change', attachConcertoTemplateSchedulingHandlers);
@@ -0,0 +1,13 @@
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_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,4 @@
1
+ /*
2
+ Place all the styles related to the matching controller here.
3
+ They will automatically be included in application.css.
4
+ */
@@ -0,0 +1,56 @@
1
+ body { background-color: #fff; color: #333; }
2
+
3
+ body, p, ol, ul, td {
4
+ font-family: verdana, arial, helvetica, sans-serif;
5
+ font-size: 13px;
6
+ line-height: 18px;
7
+ }
8
+
9
+ pre {
10
+ background-color: #eee;
11
+ padding: 10px;
12
+ font-size: 11px;
13
+ }
14
+
15
+ a { color: #000; }
16
+ a:visited { color: #666; }
17
+ a:hover { color: #fff; background-color:#000; }
18
+
19
+ div.field, div.actions {
20
+ margin-bottom: 10px;
21
+ }
22
+
23
+ #notice {
24
+ color: green;
25
+ }
26
+
27
+ .field_with_errors {
28
+ padding: 2px;
29
+ background-color: red;
30
+ display: table;
31
+ }
32
+
33
+ #error_explanation {
34
+ width: 450px;
35
+ border: 2px solid red;
36
+ padding: 7px;
37
+ padding-bottom: 0;
38
+ margin-bottom: 20px;
39
+ background-color: #f0f0f0;
40
+ }
41
+
42
+ #error_explanation h2 {
43
+ text-align: left;
44
+ font-weight: bold;
45
+ padding: 5px 5px 5px 15px;
46
+ font-size: 12px;
47
+ margin: -7px;
48
+ margin-bottom: 0px;
49
+ background-color: #c00;
50
+ color: #fff;
51
+ }
52
+
53
+ #error_explanation ul li {
54
+ font-size: 12px;
55
+ list-style: square;
56
+ }
@@ -0,0 +1,4 @@
1
+ module ConcertoTemplateScheduling
2
+ class ApplicationController < ::ApplicationController
3
+ end
4
+ end
@@ -0,0 +1,106 @@
1
+ require_dependency "concerto_template_scheduling/application_controller"
2
+
3
+ module ConcertoTemplateScheduling
4
+ class SchedulesController < ApplicationController
5
+ # since scheduled templates are basically an extended feature of a screen
6
+ # if the user can update the screen then they can crud scheduled templates
7
+
8
+ # GET /schedules
9
+ # GET /schedules.json
10
+ def index
11
+ @schedules = Schedule.all
12
+ # ignore the schedules that belong to screens we cant read
13
+ # or schedules where the template has been deleted
14
+ @schedules.reject! { |s| !can?(:read, s.screen) || s.template.nil? }
15
+
16
+ respond_to do |format|
17
+ format.html # index.html.erb
18
+ format.json { render json: @schedules }
19
+ end
20
+ end
21
+
22
+ # GET /schedules/1
23
+ # GET /schedules/1.json
24
+ def show
25
+ @schedule = Schedule.find(params[:id])
26
+ auth! :action => :read, :object => @schedule.screen
27
+
28
+ respond_to do |format|
29
+ format.html # show.html.erb
30
+ format.json { render json: @schedule }
31
+ end
32
+ end
33
+
34
+ # GET /schedules/new
35
+ # GET /schedules/new.json
36
+ def new
37
+ @schedule = Schedule.new
38
+ if !params[:screen_id].nil?
39
+ # TODO: Error handling
40
+ @schedule.screen = Screen.find(params[:screen_id])
41
+ end
42
+ auth! :action => :update, :object => @schedule.screen
43
+
44
+ respond_to do |format|
45
+ format.html # new.html.erb
46
+ format.json { render json: @schedule }
47
+ end
48
+ end
49
+
50
+ # GET /schedules/1/edit
51
+ def edit
52
+ @schedule = Schedule.find(params[:id])
53
+ auth! :action => :update, :object => @schedule.screen
54
+ end
55
+
56
+ # POST /schedules
57
+ # POST /schedules.json
58
+ def create
59
+ @schedule = Schedule.new(schedule_params)
60
+ auth! :action => :update, :object => @schedule.screen
61
+ respond_to do |format|
62
+ if @schedule.save
63
+ format.html { redirect_to @schedule, notice: 'Schedule was successfully created.' }
64
+ format.json { render json: @schedule, status: :created, location: @schedule }
65
+ else
66
+ format.html { render action: "new" }
67
+ format.json { render json: @schedule.errors, status: :unprocessable_entity }
68
+ end
69
+ end
70
+ end
71
+
72
+ # PUT /schedules/1
73
+ # PUT /schedules/1.json
74
+ def update
75
+ @schedule = Schedule.find(params[:id])
76
+ auth! :action => :update, :object => @schedule.screen
77
+
78
+ respond_to do |format|
79
+ if @schedule.update_attributes(schedule_params)
80
+ format.html { redirect_to @schedule, notice: 'Schedule was successfully updated.' }
81
+ format.json { head :no_content }
82
+ else
83
+ format.html { render action: "edit" }
84
+ format.json { render json: @schedule.errors, status: :unprocessable_entity }
85
+ end
86
+ end
87
+ end
88
+
89
+ # DELETE /schedules/1
90
+ # DELETE /schedules/1.json
91
+ def destroy
92
+ @schedule = Schedule.find(params[:id])
93
+ auth! :action => :update, :object => @schedule.screen
94
+ @schedule.destroy
95
+
96
+ respond_to do |format|
97
+ format.html { redirect_to schedules_url }
98
+ format.json { head :no_content }
99
+ end
100
+ end
101
+
102
+ def schedule_params
103
+ params.require(:schedule).permit(*ConcertoTemplateScheduling::Schedule.form_attributes)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,4 @@
1
+ module ConcertoTemplateScheduling
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ConcertoTemplateScheduling
2
+ module SchedulesHelper
3
+ end
4
+ end
@@ -0,0 +1,181 @@
1
+ module ConcertoTemplateScheduling
2
+ class Schedule < ActiveRecord::Base
3
+ include ActiveModel::ForbiddenAttributesProtection
4
+
5
+ DISPLAY_NEVER=0
6
+ DISPLAY_ALWAYS=1
7
+ DISPLAY_AS_SCHEDULED=2
8
+ DISPLAY_CONTENT_EXISTS=3
9
+
10
+ DISPLAY_WHEN = {
11
+ I18n.t('concerto_template_scheduling.never') => DISPLAY_NEVER,
12
+ I18n.t('concerto_template_scheduling.always') => DISPLAY_ALWAYS,
13
+ I18n.t('concerto_template_scheduling.as_scheduled') => DISPLAY_AS_SCHEDULED,
14
+ I18n.t('concerto_template_scheduling.content_exists') => DISPLAY_CONTENT_EXISTS
15
+ }
16
+
17
+ belongs_to :screen
18
+ belongs_to :template
19
+
20
+ attr_accessor :config
21
+
22
+ after_initialize :create_config
23
+ after_find :load_config
24
+ before_validation :save_config
25
+
26
+ validates_associated :screen
27
+ validates_presence_of :screen, :message => I18n.t('concerto_template_scheduling.must_be_selected')
28
+ # do not require uniqueness because the same template may be scheduled
29
+ # for different time frames with different occurrence criteria
30
+ # validates_uniqueness_of :template_id, :scope => :screen_id
31
+
32
+ validates_associated :template
33
+ validates_presence_of :template, :message => I18n.t('concerto_template_scheduling.must_be_selected')
34
+
35
+ validate :from_time_must_precede_to_time
36
+ validate :schedule_must_be_defined
37
+
38
+ def from_time_must_precede_to_time
39
+ if Time.zone.parse(self.config['from_time']) > Time.zone.parse(self.config['to_time'])
40
+ errors.add(:base, I18n.t('concerto_template_scheduling.from_time_must_precede_to_time'))
41
+ end
42
+ end
43
+
44
+ def schedule_must_be_defined
45
+ if self.config['display_when'].to_i == DISPLAY_AS_SCHEDULED
46
+ if self.config['scheduling_criteria'].empty?
47
+ errors.add(:base, I18n.t('concerto_template_scheduling.schedule_must_be_defined'))
48
+ end
49
+ end
50
+ end
51
+
52
+ def self.active
53
+ where("start_time < :now AND end_time > :now", {:now => Clock.time})
54
+ end
55
+
56
+ def self.form_attributes
57
+ attributes = [:screen_id, :template_id,
58
+ {:start_time => [:time, :date]}, {:end_time => [:time, :date]},
59
+ {:config => [:display_when, :from_time, :to_time, :feed_id, :scheduling_criteria]}]
60
+ end
61
+
62
+ # Specify the default configuration hash.
63
+ # This will be used if a configuration doesn't exist.
64
+ #
65
+ # @return [Hash{String => String, Number}] configuration hash.
66
+ def default_config
67
+ {
68
+ 'display_when' => DISPLAY_ALWAYS,
69
+ 'from_time' => '12:00am',
70
+ 'to_time' => '11:59pm'
71
+ }
72
+ end
73
+
74
+ # Create a new configuration hash if one does not already exist.
75
+ # Called during `after_initialize`, where a config may or may not exist.
76
+ def create_config
77
+ self.start_time ||= Time.zone.parse("12:00am", Clock.time + ConcertoConfig[:start_date_offset].to_i.days)
78
+ self.end_time ||= Time.zone.parse("11:59pm", Clock.time + ConcertoConfig[:start_date_offset].to_i.days + ConcertoConfig[:default_content_run_time].to_i.days)
79
+
80
+ self.config = {} if !self.config
81
+ self.config = default_config().merge(self.config)
82
+ self.config
83
+ end
84
+
85
+ # Load a configuration hash.
86
+ # Converts the JSON data stored for the schedule into the configuration.
87
+ # Called during `after_find`.
88
+ def load_config
89
+ self.config = JSON.load(self.data)
90
+ end
91
+
92
+ # Prepare the configuration to be saved.
93
+ # Compress the config hash back into JSON to be stored in the database.
94
+ # Called during `before_validation`.
95
+ def save_config
96
+ self.config['scheduling_criteria'] = '' if self.config['scheduling_criteria'] == 'null'
97
+ self.data = JSON.dump(self.config)
98
+ end
99
+
100
+ # TODO: make sure these formats are locale-ized!
101
+
102
+ # Setter for the start time. If a hash is passed, convert that into a DateTime object and then a string.
103
+ # Otherwise, just set it like normal. This is a bit confusing due to the differences in how Ruby handles
104
+ # times between 1.9.x and 1.8.x.
105
+ def start_time=(_start_time)
106
+ if _start_time.kind_of?(Hash)
107
+ #write_attribute(:start_time, Time.parse("#{_start_time[:date]} #{_start_time[:time]}").to_s(:db))
108
+ # convert to time, strip off the timezone offset so it reflects local time
109
+ t = DateTime.strptime("#{_start_time[:date]} #{_start_time[:time]}", "%m/%d/%Y %l:%M %p")
110
+ write_attribute(:start_time, Time.zone.parse(t.utc.iso8601.slice(0, 19)).to_s(:db))
111
+ else
112
+ write_attribute(:start_time, _start_time)
113
+ end
114
+ end
115
+
116
+ # See start_time=.
117
+ def end_time=(_end_time)
118
+ if _end_time.kind_of?(Hash)
119
+ # convert to time, strip off the timezone offset so it reflects local time
120
+ t = DateTime.strptime("#{_end_time[:date]} #{_end_time[:time]}", "%m/%d/%Y %l:%M %p")
121
+ write_attribute(:end_time, Time.zone.parse(t.utc.iso8601.slice(0, 19)).to_s(:db))
122
+ else
123
+ write_attribute(:end_time, _end_time)
124
+ end
125
+ end
126
+
127
+ def schedule_in_words
128
+ if !self.config['scheduling_criteria'].empty?
129
+ s = IceCube::Schedule.new(self.start_time)
130
+ s.add_recurrence_rule(RecurringSelect.dirty_hash_to_rule(self.config['scheduling_criteria']))
131
+ s.to_s
132
+ end
133
+ end
134
+
135
+ def is_effective?
136
+ effective = false
137
+
138
+ # if it is during the valid/active time frame and the template still exists
139
+ if Clock.time >= self.start_time && Clock.time <= self.end_time && !self.template.nil?
140
+ # and it is within the viewing window for the day
141
+ if Clock.time >= Time.parse(self.config['from_time']) && Clock.time <= Time.parse(self.config['to_time'])
142
+ # and it is either marked as always shown
143
+ if self.config['display_when'].to_i == DISPLAY_ALWAYS
144
+ effective = true
145
+ elsif self.config['display_when'].to_i == DISPLAY_CONTENT_EXISTS
146
+ # or if we detect actual content on the specified feed
147
+ if !self.feed.nil? && !self.feed.approved_contents.active.where('kind_id != 4').empty?
148
+ effective = true
149
+ end
150
+ elsif self.config['display_when'].to_i == DISPLAY_AS_SCHEDULED
151
+ if !self.config['scheduling_criteria'].empty?
152
+ s = IceCube::Schedule.new(self.start_time)
153
+ s.add_recurrence_rule(RecurringSelect.dirty_hash_to_rule(self.config['scheduling_criteria']))
154
+ effective = s.occurs_on? Clock.time
155
+ else
156
+ # no schedule was set
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ effective
163
+ end
164
+
165
+ def feed
166
+ if self.config.include?('feed_id')
167
+ f = Feed.find(self.config['feed_id'].to_i)
168
+ end
169
+ f
170
+ end
171
+
172
+ def selectable_feeds
173
+ if !self.screen.nil?
174
+ feeds = Feed.all
175
+ ability = Ability.new(self.screen)
176
+ feeds.reject { |feed| !ability.can?(:read, feed) }
177
+ end
178
+ end
179
+
180
+ end
181
+ end