tax_generator 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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.inch.yml +10 -0
  4. data/.reek +10 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +72 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +154 -0
  9. data/LICENSE +20 -0
  10. data/README.md +131 -0
  11. data/Rakefile +26 -0
  12. data/bin/tax_generator +7 -0
  13. data/data/input/.gitignore +25 -0
  14. data/data/input/destinations.xml +1073 -0
  15. data/data/input/taxonomy.xml +78 -0
  16. data/data/output/.gitignore +4 -0
  17. data/init.rb +1 -0
  18. data/lib/tax_generator/all.rb +26 -0
  19. data/lib/tax_generator/application.rb +125 -0
  20. data/lib/tax_generator/classes/destination.rb +103 -0
  21. data/lib/tax_generator/classes/file_creator.rb +100 -0
  22. data/lib/tax_generator/classes/processor.rb +270 -0
  23. data/lib/tax_generator/classes/taxonomy_tree.rb +97 -0
  24. data/lib/tax_generator/cli.rb +14 -0
  25. data/lib/tax_generator/helpers/application_helper.rb +154 -0
  26. data/lib/tax_generator/version.rb +27 -0
  27. data/lib/tax_generator.rb +1 -0
  28. data/spec/lib/tax_generator/application_spec.rb +0 -0
  29. data/spec/lib/tax_generator/classes/destination_spec.rb +62 -0
  30. data/spec/lib/tax_generator/classes/file_creator_spec.rb +96 -0
  31. data/spec/lib/tax_generator/classes/processor_spec.rb +30 -0
  32. data/spec/lib/tax_generator/classes/taxonomy_tree_spec.rb +0 -0
  33. data/spec/lib/tax_generator/cli_spec.rb +0 -0
  34. data/spec/lib/tax_generator/helpers/application_helper_spec.rb +0 -0
  35. data/spec/spec_helper.rb +60 -0
  36. data/tax_generator.gemspec +40 -0
  37. data/templates/static/all.css +586 -0
  38. data/templates/template.html.erb +91 -0
  39. metadata +452 -0
@@ -0,0 +1,270 @@
1
+ require_relative '../helpers/application_helper'
2
+ module TaxGenerator
3
+ # class used to process xml files and create html files
4
+ #
5
+ # @!attribute options
6
+ # @return [Hash] the options that can determine the input and output files and folders
7
+ #
8
+ # @!attribute worker_supervisor
9
+ # @return [Celluloid::SupervisionGroup] the supervision group that supervises workers
10
+ # @!attribute workers
11
+ # @return [Celluloid::Actor] the actors that will work on the jobs
12
+ # @!attribute taxonomy
13
+ # @return [TaxGenerator::TaxonomyTree] the taxonomy tree that holds the nodes from the taxonomy xml document
14
+ # @!attribute jobs
15
+ # @return [Hash] each key from the job list is the job id, and the value is the job itself
16
+ # @!attribute job_to_worker
17
+ # @return [Hash] each key from the list is the job id, and the value is the worker that will handle the job
18
+ # @!attribute worker_to_job
19
+ # @return [Hash] each key from the list is the workers mailbox address, and the value is the job being handled by the worker
20
+ # @!attribute condition
21
+ # @return [Celluloid::Condition] the supervision group that supervises workers
22
+ class Processor
23
+ include Celluloid
24
+ include Celluloid::Logger
25
+ include Celluloid::Notifications
26
+ include TaxGenerator::ApplicationHelper
27
+
28
+ attr_reader :options, :worker_supervisor, :workers, :taxonomy, :jobs, :job_to_worker, :worker_to_job, :condition
29
+
30
+ trap_exit :worker_died
31
+
32
+ # receives a list of options that are used to determine the input files and output and input folders
33
+ #
34
+ # @param [Hash] options the options that can determine the input and output files and folders
35
+ # @option options [String] :input_dir The input directory
36
+ # @option options [String]:output_dir The output directory
37
+ # @option options [String] :taxonomy_file_name The taxonomy file name
38
+ # @option options [String] :destinations_file_name The destinations file name
39
+ #
40
+ # @see #work
41
+ #
42
+ # @return [void]
43
+ #
44
+ # @api public
45
+ def initialize(options = {})
46
+ Celluloid.boot
47
+ @options = options.is_a?(Hash) ? options.symbolize_keys : {}
48
+ @worker_supervisor = Celluloid::SupervisionGroup.run!
49
+ @workers = @worker_supervisor.pool(TaxGenerator::FileCreator, as: :workers, size: 50)
50
+ Actor.current.link @workers
51
+ @jobs = {}
52
+ @job_to_worker = {}
53
+ @worker_to_job = {}
54
+ end
55
+
56
+ # returns the input folder from the options list
57
+ # otherwise the default path
58
+ #
59
+ # @return [String]
60
+ #
61
+ # @api public
62
+ def input_folder
63
+ @options.fetch(:input_dir, "#{root}/data/input")
64
+ end
65
+
66
+ # returns the taxonomy filename from the option list
67
+ # otherwise the default filename
68
+ #
69
+ # @return [String]
70
+ #
71
+ # @api public
72
+ def taxonomy_file_name
73
+ @options.fetch(:taxonomy_filename, 'taxonomy.xml')
74
+ end
75
+
76
+ # returns the destinations filename from the option list
77
+ # otherwise the default filename
78
+ #
79
+ # @return [String]
80
+ #
81
+ # @api public
82
+ def destinations_file_name
83
+ @options.fetch(:destinations_filename, 'destinations.xml')
84
+ end
85
+
86
+ # returns the output folder path from the option list
87
+ # otherwise the default path
88
+ #
89
+ # @return [String]
90
+ #
91
+ # @api public
92
+ def output_folder
93
+ @options.fetch(:output_dir, "#{root}/data/output")
94
+ end
95
+
96
+ # returns the full path to the taxonomy file
97
+ #
98
+ # @return [String]
99
+ #
100
+ # @api public
101
+ def taxonomy_file_path
102
+ File.join(input_folder, taxonomy_file_name)
103
+ end
104
+
105
+ # returns the full path to the destinations file
106
+ #
107
+ # @return [String]
108
+ #
109
+ # @api public
110
+ def destinations_file_path
111
+ File.join(input_folder, destinations_file_name)
112
+ end
113
+
114
+ # returns the full path to the static folder
115
+ #
116
+ # @return [String]
117
+ #
118
+ # @api public
119
+ def static_output_dir
120
+ File.join(output_folder, 'static')
121
+ end
122
+
123
+ # cleans the output folder and re-creates it and the static folder
124
+ #
125
+ # @return [void]
126
+ #
127
+ # @api public
128
+ def prepare_output_dirs
129
+ FileUtils.rm_rf Dir["#{output_folder}/**/*"]
130
+ create_directories(output_folder, static_output_dir)
131
+ FileUtils.cp_r(Dir["#{File.join(root, 'templates', 'static')}/*"], static_output_dir)
132
+ end
133
+
134
+ # checks if all workers finished and returns true or false
135
+ #
136
+ # @return [Boolean]
137
+ #
138
+ # @api public
139
+ def all_workers_finished?
140
+ @jobs.all? { |_job_id, job| job['status'] == 'finished' }
141
+ end
142
+
143
+ # registers all the jobs so that the managers can have access to them at any time
144
+ #
145
+ # @param [Array] jobs the jobs that will be registered
146
+ #
147
+ # @return [void]
148
+ #
149
+ # @api public
150
+ def register_jobs(*jobs)
151
+ jobs.pmap do |job|
152
+ job = job.stringify_keys
153
+ @jobs[job['atlas_id']] = job
154
+ end
155
+ end
156
+
157
+ # registers all the jobs, and then delegates them to workers
158
+ # @see #register_jobs
159
+ # @see TaxGenerator::FileCreator#work
160
+ #
161
+ # @param [Array] jobs the jobs that will be delegated to the workers
162
+ #
163
+ # @return [void]
164
+ #
165
+ # @api public
166
+ def delegate_job(*jobs)
167
+ # jobs need to be added into the manager before starting task to avoid adding new key while iterating
168
+ register_jobs(*jobs)
169
+ current_actor = Actor.current
170
+ @jobs.pmap do |_job_id, job|
171
+ @workers.async.work(job, current_actor) if @workers.alive?
172
+ end
173
+ end
174
+
175
+ # parses the destinations xml document, gets each destination and adds a new job for that
176
+ # destination in the job list and then returns it
177
+ # @see #nokogiri_xml
178
+ #
179
+ # @return [Array<Hash>]
180
+ #
181
+ # @api public
182
+ def fetch_file_jobs
183
+ jobs = [{ atlas_id: 0, taxonomy: @taxonomy, destination: nil, output_folder: output_folder }]
184
+ nokogiri_xml(destinations_file_path).xpath('//destination').pmap do |destination|
185
+ atlas_id = destination.attributes['atlas_id']
186
+ jobs << { atlas_id: atlas_id.value, taxonomy: @taxonomy, destination: destination, output_folder: output_folder }
187
+ end
188
+ jobs
189
+ end
190
+
191
+ # fetches the jobs for file generation, then delegates the jobs to workers and waits untill workers finish
192
+ # @see #fetch_file_jobs
193
+ # @see #delegate_job
194
+ # @see #wait_jobs_termination
195
+ #
196
+ # @return [void]
197
+ #
198
+ # @api public
199
+ def generate_files
200
+ @condition = Celluloid::Condition.new
201
+ jobs = fetch_file_jobs
202
+ delegate_job(*jobs)
203
+ wait_jobs_termination
204
+ end
205
+
206
+ # retrieves the information about the node from the tree and generates for each destination a new File
207
+ # @see #create_file
208
+ #
209
+ # @param [TaxGenerator::TaxonomyTree] taxonomy the taxonomy tree that will be used for fetching node information
210
+ #
211
+ # @return [void]
212
+ #
213
+ # @api public
214
+ def wait_jobs_termination
215
+ result = @condition.wait
216
+ return unless result.present?
217
+ terminate
218
+ end
219
+
220
+ # registers the worker so that the current actor has access to it at any given time and starts the worker
221
+ # @see TaxGenerator::FileCreator#start_work
222
+ #
223
+ # @param [Hash] job the job that the worker will work
224
+ # @param [TaxGenerator::FileCreator] worker the worker that will create the file
225
+ #
226
+ # @return [void]
227
+ #
228
+ # @api public
229
+ def register_worker_for_job(job, worker)
230
+ @job_to_worker[job['atlas_id']] = worker
231
+ @worker_to_job[worker.mailbox.address] = job
232
+ log_message("worker #{worker.job_id} registed into manager")
233
+ Actor.current.link worker
234
+ worker.async.start_work
235
+ end
236
+
237
+ # generates the taxonomy tree , prints it and generates the files
238
+ # @see TaxGenerator::TaxonomyTree#new
239
+ # @see Tree::TreeNode#print_tree
240
+ # @see #generate_files
241
+ #
242
+ # @return [void]
243
+ #
244
+ # @api public
245
+ def work
246
+ prepare_output_dirs
247
+ if File.directory?(input_folder) && File.file?(taxonomy_file_path) && File.file?(destinations_file_path)
248
+ @taxonomy = TaxGenerator::TaxonomyTree.new(taxonomy_file_path)
249
+ @taxonomy.print_tree
250
+ generate_files
251
+ else
252
+ log_message('Please provide valid options', log_method: 'fatal')
253
+ end
254
+ end
255
+
256
+ # logs the message about working being dead if a worker crashes
257
+ # @param [TaxGenerator::FileCreator] worker the worker that died
258
+ # @param [String] reason the reason for which the worker died
259
+ #
260
+ # @return [void]
261
+ #
262
+ # @api public
263
+ def worker_died(worker, reason)
264
+ mailbox_address = worker.mailbox.address
265
+ job = @worker_to_job.delete(mailbox_address)
266
+ return if reason.blank? || job.blank?
267
+ log_message("worker job #{job['atlas_id']} with mailbox #{mailbox_address.inspect} died for reason: #{log_error(reason)}", log_method: 'fatal')
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,97 @@
1
+ require_relative '../helpers/application_helper'
2
+ module TaxGenerator
3
+ # class used to create the Taxonomy Tree
4
+ #
5
+ # @!attribute root_node
6
+ # @return [Tree::TreeNode] the root node of the tree
7
+ #
8
+ # @!attribute document
9
+ # @return [Nokogiri::XML] the xml document used to build the tree
10
+ class TaxonomyTree
11
+ include TaxGenerator::ApplicationHelper
12
+ attr_reader :root_node, :document
13
+
14
+ # receives a file path that will be parsed and used to build the tree
15
+ # @see Tree::TreeNode#new
16
+ # @see #add_node
17
+ #
18
+ # @param [String] file_path the path to the xml file that will be parsed and used to build the tree
19
+ #
20
+ # @return [void]
21
+ #
22
+ # @api public
23
+ def initialize(file_path)
24
+ @document = nokogiri_xml(file_path)
25
+ taxonomy_root = @document.at_xpath('//taxonomy_name')
26
+ @root_node = Tree::TreeNode.new(taxonomy_root.content, nil)
27
+ @document.xpath('//node').pmap do |taxonomy_node|
28
+ add_node(taxonomy_node, @root_node)
29
+ end
30
+ end
31
+
32
+ # gets the atlas_id from the nokogiri element and then searches first child whose name is 'node_name'
33
+ # and uses this to insert the node
34
+ # @see #insert_node
35
+ #
36
+ # @param [Nokogiri::Element] taxonomy_node the nokogiri element that wants to be added to the tree
37
+ # @param [Tree::TreeNode] node the parent node to which the element needs to be added
38
+ #
39
+ # @return [void]
40
+ #
41
+ # @api public
42
+ def add_taxonomy_node(taxonomy_node, node)
43
+ atlas_node_id = taxonomy_node.attributes['atlas_node_id']
44
+ node_name = taxonomy_node.children.find { |child| child.name == 'node_name' }
45
+ insert_node(atlas_node_id, node_name, node)
46
+ end
47
+
48
+ # inserts a new node in the tree by checking first if atlas_id and node_name are present
49
+ # and then adds the node as child to the node passed as third argument
50
+ # @see Tree::TreeNode#new
51
+ #
52
+ # @param [Nokogiri::Element] atlas_node_id the element that holds the value of the atlas_id attribute
53
+ # @param [Nokogiri::Element] node_name the the element that holds the node name of the element
54
+ # @param [Tree::TreeNode] node the parent node to which the element needs to be added
55
+ #
56
+ # @return [void]
57
+ #
58
+ # @api public
59
+ def insert_node(atlas_node_id, node_name, node)
60
+ return if atlas_node_id.blank? || node_name.blank?
61
+ current_node = Tree::TreeNode.new(atlas_node_id.value, node_name.content)
62
+ node << current_node
63
+ current_node
64
+ end
65
+
66
+ # checks to see if the nokogiri element has any childrens, if it has , will add it to the tree and iterates over the
67
+ # children and adds them as child to the newly added node
68
+ # @see #add_taxonomy_node
69
+ #
70
+ # @param [Nokogiri::Element] taxonomy_node the nokogiri element that wants to be added to the tree
71
+ # @param [Tree::TreeNode] node the parent node to which the element needs to be added
72
+ #
73
+ # @return [void]
74
+ #
75
+ # @api public
76
+ def add_node(taxonomy_node, node)
77
+ return unless taxonomy_node.children.any?
78
+ tax_node = add_taxonomy_node(taxonomy_node, node)
79
+ taxonomy_node.xpath('./node').pmap do |child_node|
80
+ add_taxonomy_node(child_node, tax_node) if tax_node.present?
81
+ end
82
+ end
83
+
84
+ # receives a file path that will be parsed and used to build the tree
85
+ #
86
+ # @param [String] name the name of the method that is invoked against the tree
87
+ # @param [Array] args the arguments to the method
88
+ # @param [Proc] block the block that will be passed to the method
89
+ #
90
+ # @return [void]
91
+ #
92
+ # @api public
93
+ def method_missing(name, *args, &block)
94
+ @root_node.send name, *args, &block
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,14 @@
1
+ require_relative './all'
2
+ module TaxGenerator
3
+ # this is the class that will be invoked from terminal , and willl use the invoke task as the primary function.
4
+ class CLI
5
+ # method used to start
6
+ #
7
+ # @return [void]
8
+ #
9
+ # @api public
10
+ def self.start
11
+ TaxGenerator::Application.new.run
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,154 @@
1
+ module TaxGenerator
2
+ # class that holds the helper methods used in the classes
3
+ module ApplicationHelper
4
+ delegate :app_logger,
5
+ to: :'TaxGenerator::Application'
6
+
7
+ module_function
8
+
9
+ # returns the text from a nokogiri element by rejecting blank elements
10
+ #
11
+ # @param [Nokogiri::Element] element the nokogiri element that will select only children with content and returns their text
12
+ #
13
+ # @return [String]
14
+ #
15
+ # @api public
16
+ def elements_with_content(element)
17
+ if element.present?
18
+ element.select { |elem| elem.content.present? }.join(&:text)
19
+ else
20
+ element
21
+ end
22
+ end
23
+
24
+ # returns the root path of the gem
25
+ #
26
+ # @return [void]
27
+ #
28
+ # @api public
29
+ def root
30
+ File.expand_path(File.dirname(File.dirname(File.dirname(__dir__))))
31
+ end
32
+
33
+ # returns a Nokogiri XML document from a file
34
+ #
35
+ # @param [String] file_path the path to the xml file that will be parsed
36
+ #
37
+ # @return [void]
38
+ #
39
+ # @api public
40
+ def nokogiri_xml(file_path)
41
+ Nokogiri::XML(File.open(file_path), nil, 'UTF-8')
42
+ end
43
+
44
+ # creates directories from a list of arguments
45
+ #
46
+ # @param [Array] args the arguments that will be used as directory names and will create them
47
+ #
48
+ # @return [void]
49
+ #
50
+ # @api public
51
+ def create_directories(*args)
52
+ args.pmap do |argument|
53
+ FileUtils.mkdir_p(argument) unless File.directory?(argument)
54
+ end
55
+ end
56
+
57
+ # sets the exception handler for celluloid actors
58
+ #
59
+ #
60
+ # @return [void]
61
+ #
62
+ # @api public
63
+ def set_celluloid_exception_handling
64
+ Celluloid.logger = app_logger
65
+ Celluloid.task_class = Celluloid::TaskThread
66
+ Celluloid.exception_handler do |ex|
67
+ unless ex.is_a?(Interrupt)
68
+ log_error(ex)
69
+ end
70
+ end
71
+ end
72
+
73
+ # Reads a file and interpretes it as ERB
74
+ #
75
+ # @param [String] file_path the file that will be read and interpreted as ERB
76
+ #
77
+ # @return [String]
78
+ #
79
+ # @api public
80
+ def erb_template(file_path)
81
+ template = ERB.new(File.read(file_path))
82
+ template.filename = file_path
83
+ template
84
+ end
85
+
86
+ # Displays a error with fatal log level
87
+ # @see #format_error
88
+ # @see #log_message
89
+ #
90
+ # @param [Exception] exception the exception that will be formatted and printed on screen
91
+ #
92
+ # @return [String]
93
+ #
94
+ # @api public
95
+ def log_error(exception)
96
+ message = format_error(exception)
97
+ log_message(message, log_method: 'fatal')
98
+ end
99
+
100
+ # formats a exception to be displayed on screen
101
+ #
102
+ # @param [String] message the message that will be printed to the log file
103
+ # @param [Hash] options the options used to determine how to log the message
104
+ # @option options [String] :log_method The log method , by default debug
105
+ #
106
+ # @return [String]
107
+ #
108
+ # @api public
109
+ def log_message(message, options = {})
110
+ app_logger.send(options.fetch(:log_method, 'debug'), message)
111
+ end
112
+
113
+ # formats a exception to be displayed on screen
114
+ #
115
+ # @param [Exception] exception the exception that will be formatted and printed on screen
116
+ #
117
+ # @return [String]
118
+ #
119
+ # @api public
120
+ def format_error(exception)
121
+ message = "#{exception.class} (#{exception.respond_to?(:message) ? exception.message : exception.inspect}):\n"
122
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
123
+ message << ' ' << exception.backtrace.join("\n ") if exception.respond_to?(:backtrace)
124
+ message
125
+ end
126
+
127
+ # wrapper to execute a block and rescue from exception
128
+ # @see #set_celluloid_exception_handling
129
+ # @see #rescue_interrupt
130
+ # @see #log_error
131
+ #
132
+ # @return [void]
133
+ #
134
+ # @api public
135
+ def execute_with_rescue
136
+ set_celluloid_exception_handling
137
+ yield if block_given?
138
+ rescue Interrupt
139
+ rescue_interrupt
140
+ rescue => error
141
+ log_error(error)
142
+ end
143
+
144
+ # rescues from a interrupt error and shows a message
145
+ #
146
+ # @return [void]
147
+ #
148
+ # @api public
149
+ def rescue_interrupt
150
+ `stty icanon echo`
151
+ puts "\n Command was cancelled due to an Interrupt error."
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,27 @@
1
+ # Returns the version of the gem as a <tt>Gem::Version</tt>
2
+ module TaxGenerator
3
+ # it prints the gem version as a string
4
+ #
5
+ # @return [String]
6
+ #
7
+ # @api public
8
+ def self.gem_version
9
+ Gem::Version.new VERSION::STRING
10
+ end
11
+
12
+ # module used to generate the version string
13
+ # provides a easy way of getting the major, minor and tiny
14
+ module VERSION
15
+ # major release version
16
+ MAJOR = 0
17
+ # minor release version
18
+ MINOR = 0
19
+ # tiny release version
20
+ TINY = 1
21
+ # prelease version ( set this only if it is a prelease)
22
+ PRE = nil
23
+
24
+ # generates the version string
25
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ require 'tax_generator/all'
File without changes
@@ -0,0 +1,62 @@
1
+ # encoding:utf-8
2
+ require 'spec_helper'
3
+ describe TaxGenerator::Destination do
4
+ let(:default_processor) {TaxGenerator::Processor.new}
5
+ let(:str) { default_processor.destinations_file_path }
6
+
7
+ let(:destination_xml) { @xml ||= Nokogiri::XML(File.open(str), nil, 'UTF-8') }
8
+ let(:info_base) { './/practical_information/health_and_safety' }
9
+ before(:each) do
10
+ @destination = TaxGenerator::Destination.new(destination_xml)
11
+ end
12
+
13
+ context 'xpaths' do
14
+ it 'fetches the introduction' do
15
+ destination_xml.expects(:xpath).with('.//introductory/introduction/overview').returns(true)
16
+ @destination.introduction
17
+ end
18
+
19
+ it 'fetches the history' do
20
+ destination_xml.expects(:xpath).with('./history/history/history').returns(true)
21
+ @destination.history
22
+ end
23
+
24
+ ['dangers_and_annoyances', 'while_youre_there', 'before_you_go', 'money_and_costs/money'].each do |name|
25
+ it "fetches the practical_information with #{name}" do
26
+ destination_xml.stubs(:xpath).returns([])
27
+ destination_xml.expects(:xpath).with(".//practical_information/health_and_safety/#{name}").returns([])
28
+ @destination.practical_information
29
+ end
30
+ end
31
+
32
+ it 'fetches the transport' do
33
+ destination_xml.expects(:xpath).with('.//transport/getting_around').returns(true)
34
+ @destination.transport
35
+ end
36
+
37
+ it 'fetches the weather' do
38
+ destination_xml.expects(:xpath).with('.//weather').returns(true)
39
+ @destination.weather
40
+ end
41
+
42
+ it 'fetches the work_live_study' do
43
+ destination_xml.expects(:xpath).with('.//work_live_study').returns(true)
44
+ @destination.work_live_study
45
+ end
46
+ end
47
+
48
+ it 'returns the hash' do
49
+ expect(@destination.to_hash).to eq(
50
+ {
51
+ introduction: @destination.introduction,
52
+ history: @destination.history,
53
+ practical_information: @destination.practical_information,
54
+ transport: @destination.transport,
55
+ weather: @destination.weather,
56
+ work_live_study: @destination.work_live_study
57
+ }.each_with_object({}) do |(key, value), hsh|
58
+ hsh[key] = elements_with_content(value)
59
+ hsh
60
+ end)
61
+ end
62
+ end