decidim-accountability 0.20.0 → 0.23.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/accountability/admin/accountability_admin.js.es6 +6 -42
  3. data/app/assets/stylesheets/decidim/accountability/accountability/_categories.scss +8 -1
  4. data/app/assets/stylesheets/decidim/accountability/accountability/_lines_breadcrumb.scss +8 -0
  5. data/app/assets/stylesheets/decidim/accountability/accountability/_results.scss +0 -3
  6. data/app/cells/decidim/accountability/highlighted_results_for_component/show.erb +3 -3
  7. data/app/cells/decidim/accountability/highlighted_results_for_component_cell.rb +1 -1
  8. data/app/commands/decidim/accountability/admin/create_imported_result.rb +106 -0
  9. data/app/commands/decidim/accountability/admin/update_imported_result.rb +110 -0
  10. data/app/controllers/decidim/accountability/admin/import_results_controller.rb +34 -0
  11. data/app/controllers/decidim/accountability/admin/results_controller.rb +2 -20
  12. data/app/controllers/decidim/accountability/results_controller.rb +0 -1
  13. data/app/controllers/decidim/accountability/versions_controller.rb +7 -8
  14. data/app/forms/decidim/accountability/admin/result_form.rb +3 -9
  15. data/app/helpers/decidim/accountability/admin/application_helper.rb +13 -0
  16. data/app/helpers/decidim/accountability/application_helper.rb +1 -1
  17. data/app/jobs/application_job.rb +4 -0
  18. data/app/jobs/decidim/accountability/admin/import_results_csv_job.rb +19 -0
  19. data/app/mailers/decidim/accountability/import_mailer.rb +26 -0
  20. data/app/models/decidim/accountability/result.rb +4 -1
  21. data/app/models/decidim/accountability/status.rb +3 -0
  22. data/app/models/decidim/accountability/timeline_entry.rb +3 -0
  23. data/app/queries/decidim/accountability/metrics/results_metric_manage.rb +2 -8
  24. data/app/services/decidim/accountability/result_search.rb +2 -20
  25. data/app/services/decidim/accountability/result_stats_calculator.rb +1 -1
  26. data/app/services/decidim/accountability/results_csv_importer.rb +139 -0
  27. data/app/types/decidim/accountability/accountability_type.rb +32 -0
  28. data/app/types/decidim/accountability/result_type.rb +35 -0
  29. data/app/types/decidim/accountability/status_type.rb +20 -0
  30. data/app/types/decidim/accountability/timeline_entry_type.rb +18 -0
  31. data/app/views/decidim/accountability/admin/import_results/new.html.erb +22 -0
  32. data/app/views/decidim/accountability/admin/results/_form.html.erb +3 -7
  33. data/app/views/decidim/accountability/admin/results/index.html.erb +3 -0
  34. data/app/views/decidim/accountability/admin/results/proposals_picker.html.erb +1 -0
  35. data/app/views/decidim/accountability/import_mailer/import.html.erb +25 -0
  36. data/app/views/decidim/accountability/results/_linked_results.html.erb +1 -1
  37. data/app/views/decidim/accountability/results/_results_leaf.html.erb +5 -5
  38. data/app/views/decidim/accountability/results/_results_parent.html.erb +2 -2
  39. data/app/views/decidim/accountability/results/_scope_filters.html.erb +17 -4
  40. data/app/views/decidim/accountability/results/_search.html.erb +3 -3
  41. data/app/views/decidim/accountability/results/_stats_box.html.erb +11 -5
  42. data/app/views/decidim/accountability/results/_timeline.html.erb +2 -2
  43. data/app/views/decidim/accountability/versions/index.html.erb +7 -25
  44. data/app/views/decidim/accountability/versions/show.html.erb +9 -31
  45. data/app/views/decidim/participatory_spaces/_result.html.erb +3 -3
  46. data/config/locales/am-ET.yml +1 -0
  47. data/config/locales/ar.yml +1 -24
  48. data/config/locales/bg-BG.yml +220 -0
  49. data/config/locales/bg.yml +226 -0
  50. data/config/locales/ca.yml +22 -22
  51. data/config/locales/cs.yml +48 -48
  52. data/config/locales/da-DK.yml +1 -0
  53. data/config/locales/da.yml +1 -0
  54. data/config/locales/de.yml +22 -22
  55. data/config/locales/el-GR.yml +1 -0
  56. data/config/locales/el.yml +224 -0
  57. data/config/locales/en.yml +22 -22
  58. data/config/locales/eo.yml +1 -0
  59. data/config/locales/es-MX.yml +22 -22
  60. data/config/locales/es-PY.yml +22 -22
  61. data/config/locales/es.yml +22 -22
  62. data/config/locales/et-EE.yml +1 -0
  63. data/config/locales/et.yml +1 -0
  64. data/config/locales/eu.yml +1 -24
  65. data/config/locales/fi-plain.yml +22 -22
  66. data/config/locales/fi.yml +35 -35
  67. data/config/locales/fr-CA.yml +226 -0
  68. data/config/locales/fr.yml +22 -22
  69. data/config/locales/ga-IE.yml +1 -0
  70. data/config/locales/gl.yml +1 -24
  71. data/config/locales/hr-HR.yml +1 -0
  72. data/config/locales/hr.yml +1 -0
  73. data/config/locales/hu.yml +19 -22
  74. data/config/locales/id-ID.yml +1 -24
  75. data/config/locales/is-IS.yml +177 -0
  76. data/config/locales/is.yml +177 -0
  77. data/config/locales/it.yml +40 -40
  78. data/config/locales/ja-JP.yml +222 -0
  79. data/config/locales/ja.yml +224 -0
  80. data/config/locales/ko-KR.yml +1 -0
  81. data/config/locales/ko.yml +1 -0
  82. data/config/locales/lt-LT.yml +1 -0
  83. data/config/locales/lt.yml +1 -0
  84. data/config/locales/lv.yml +218 -0
  85. data/config/locales/mt-MT.yml +1 -0
  86. data/config/locales/mt.yml +1 -0
  87. data/config/locales/nl.yml +21 -22
  88. data/config/locales/no.yml +26 -28
  89. data/config/locales/om-ET.yml +1 -0
  90. data/config/locales/pl.yml +75 -75
  91. data/config/locales/pt-BR.yml +2 -25
  92. data/config/locales/pt.yml +60 -62
  93. data/config/locales/ro-RO.yml +224 -0
  94. data/config/locales/ru.yml +1 -24
  95. data/config/locales/sk-SK.yml +224 -0
  96. data/config/locales/sk.yml +207 -0
  97. data/config/locales/sl.yml +130 -0
  98. data/config/locales/so-SO.yml +1 -0
  99. data/config/locales/sr-CS.yml +205 -0
  100. data/config/locales/sv.yml +22 -24
  101. data/config/locales/ti-ER.yml +1 -0
  102. data/config/locales/tr-TR.yml +1 -24
  103. data/config/locales/uk.yml +1 -21
  104. data/config/locales/vi-VN.yml +1 -0
  105. data/config/locales/vi.yml +1 -0
  106. data/config/locales/zh-CN.yml +223 -0
  107. data/config/locales/zh-TW.yml +1 -0
  108. data/db/migrate/20200320105903_index_foreign_keys_in_decidim_accountability_results.rb +7 -0
  109. data/db/migrate/20200827154103_add_commentable_counter_cache_to_results.rb +9 -0
  110. data/lib/decidim/accountability/admin_engine.rb +4 -3
  111. data/lib/decidim/accountability/component.rb +7 -1
  112. data/lib/decidim/accountability/engine.rb +0 -1
  113. data/lib/decidim/accountability/test/factories.rb +3 -1
  114. data/lib/decidim/accountability/version.rb +1 -1
  115. metadata +85 -28
  116. data/app/controllers/decidim/accountability/result_widgets_controller.rb +0 -19
  117. data/app/views/decidim/accountability/admin/results/_proposals.html.erb +0 -12
  118. data/app/views/decidim/accountability/versions/_version.html.erb +0 -20
@@ -5,20 +5,19 @@ module Decidim
5
5
  # Exposes result versions so users can see how a result
6
6
  # has been updated through time.
7
7
  class VersionsController < Decidim::Accountability::ApplicationController
8
- helper Decidim::TraceabilityHelper
9
8
  helper Decidim::Accountability::BreadcrumbHelper
10
- helper_method :current_version, :result
9
+ helper_method :result
11
10
 
12
- private
11
+ include Decidim::ResourceVersionsConcern
13
12
 
14
- def result
15
- @result ||= Result.includes(:timeline_entries).where(component: current_component).find(params[:result_id])
13
+ def versioned_resource
14
+ result
16
15
  end
17
16
 
18
- def current_version
19
- return nil if params[:id].to_i < 1
17
+ private
20
18
 
21
- @current_version ||= result.versions[params[:id].to_i - 1]
19
+ def result
20
+ @result ||= Result.includes(:timeline_entries).where(component: current_component).find(params[:result_id])
22
21
  end
23
22
  end
24
23
  end
@@ -28,13 +28,13 @@ module Decidim
28
28
  validates :progress, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, if: ->(form) { form.progress.present? }
29
29
 
30
30
  validates :scope, presence: true, if: ->(form) { form.decidim_scope_id.present? }
31
+ validates :decidim_scope_id, scope_belongs_to_component: true, if: ->(form) { form.decidim_scope_id.present? }
32
+
31
33
  validates :category, presence: true, if: ->(form) { form.decidim_category_id.present? }
32
34
 
33
35
  validates :parent, presence: true, if: ->(form) { form.parent_id.present? }
34
36
  validates :status, presence: true, if: ->(form) { form.decidim_accountability_status_id.present? }
35
37
 
36
- validate :scope_belongs_to_participatory_space_scope
37
-
38
38
  delegate :categories, to: :current_component
39
39
 
40
40
  def map_model(model)
@@ -59,7 +59,7 @@ module Decidim
59
59
  #
60
60
  # Returns a Decidim::Scope
61
61
  def scope
62
- @scope ||= @decidim_scope_id ? current_participatory_space.scopes.find_by(id: @decidim_scope_id) : current_participatory_space.scope
62
+ @scope ||= @decidim_scope_id ? current_component.scopes.find_by(id: @decidim_scope_id) : current_component.scope
63
63
  end
64
64
 
65
65
  # Scope identifier
@@ -80,12 +80,6 @@ module Decidim
80
80
  def status
81
81
  @status ||= Decidim::Accountability::Status.find_by(component: current_component, id: decidim_accountability_status_id)
82
82
  end
83
-
84
- private
85
-
86
- def scope_belongs_to_participatory_space_scope
87
- errors.add(:decidim_scope_id, :invalid) if current_participatory_space.out_of_scope?(scope)
88
- end
89
83
  end
90
84
  end
91
85
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ module Admin
6
+ # Custom helpers, scoped to the accountability admin engine.
7
+ #
8
+ module ApplicationHelper
9
+ include Decidim::Admin::ResourceScopeHelper
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Decidim
4
4
  module Accountability
5
- # Custom helpers, scoped to the meetings engine.
5
+ # Custom helpers, scoped to the accountability engine.
6
6
  #
7
7
  module ApplicationHelper
8
8
  include PaginateHelper
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ module Admin
6
+ class ImportResultsCSVJob < ApplicationJob
7
+ queue_as :default
8
+
9
+ def perform(current_user, current_component, csv_file)
10
+ importer = Decidim::Accountability::ResultsCSVImporter.new(current_component, csv_file, current_user)
11
+
12
+ errors = importer.import!
13
+
14
+ Decidim::Accountability::ImportMailer.import(current_user, errors).deliver_now
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # This mailer sends a notification email containing the result of importing a
6
+ # CSV of results.
7
+ class ImportMailer < Decidim::ApplicationMailer
8
+ # Public: Sends a notification email with the result of a CSV import
9
+ # of results.
10
+ #
11
+ # user - The user to be notified.
12
+ # errors - The list of errors generated by the import
13
+ #
14
+ # Returns nothing.
15
+ def import(user, errors)
16
+ @user = user
17
+ @organization = user.organization
18
+ @errors = errors
19
+
20
+ with_user(user) do
21
+ mail(to: "#{user.name} <#{user.email}>", subject: I18n.t("decidim.accountability.import_mailer.import.subject"))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -7,7 +7,7 @@ module Decidim
7
7
  class Result < Accountability::ApplicationRecord
8
8
  include Decidim::Resourceable
9
9
  include Decidim::HasComponent
10
- include Decidim::ScopableComponent
10
+ include Decidim::ScopableResource
11
11
  include Decidim::HasCategory
12
12
  include Decidim::HasReference
13
13
  include Decidim::Comments::Commentable
@@ -16,9 +16,12 @@ module Decidim
16
16
  include Decidim::DataPortability
17
17
  include Decidim::Randomable
18
18
  include Decidim::Searchable
19
+ include Decidim::TranslatableResource
19
20
 
20
21
  component_manifest_name "accountability"
21
22
 
23
+ translatable_fields :title, :description
24
+
22
25
  has_many :children, foreign_key: "parent_id", class_name: "Decidim::Accountability::Result", inverse_of: :parent, dependent: :destroy
23
26
  belongs_to :parent, foreign_key: "parent_id", class_name: "Decidim::Accountability::Result", inverse_of: :children, optional: true, counter_cache: :children_count
24
27
 
@@ -6,9 +6,12 @@ module Decidim
6
6
  # key, a localized name, a localized description and and associated progress number.
7
7
  class Status < Accountability::ApplicationRecord
8
8
  include Decidim::HasComponent
9
+ include Decidim::TranslatableResource
9
10
 
10
11
  component_manifest_name "accountability"
11
12
 
13
+ translatable_fields :name, :description
14
+
12
15
  has_many :results, foreign_key: "decidim_accountability_status_id", class_name: "Decidim::Accountability::Result", inverse_of: :status, dependent: :nullify
13
16
 
14
17
  validates :key, presence: true, uniqueness: { scope: :decidim_component_id }
@@ -5,6 +5,9 @@ module Decidim
5
5
  # The data store for a TimelineEntry for a Result in the Decidim::Accountability component.
6
6
  # It stores a date, and localized description.
7
7
  class TimelineEntry < Accountability::ApplicationRecord
8
+ include Decidim::TranslatableResource
9
+
10
+ translatable_fields :description
8
11
  belongs_to :result, foreign_key: "decidim_accountability_result_id", class_name: "Decidim::Accountability::Result", inverse_of: :timeline_entries
9
12
  end
10
13
  end
@@ -9,9 +9,6 @@ module Decidim
9
9
  end
10
10
 
11
11
  def save
12
- return @registry if @registry
13
-
14
- @registry = []
15
12
  cumulative.each do |key, cumulative_value|
16
13
  next if cumulative_value.zero?
17
14
 
@@ -26,10 +23,8 @@ module Decidim
26
23
  related_object_type: "Decidim::Component",
27
24
  related_object_id: related_object_id)
28
25
  record.assign_attributes(cumulative: cumulative_value, quantity: quantity_value)
29
- @registry << record
26
+ record.save!
30
27
  end
31
- @registry.each(&:save!)
32
- @registry
33
28
  end
34
29
 
35
30
  private
@@ -40,9 +35,8 @@ module Decidim
40
35
  spaces = Decidim.participatory_space_manifests.flat_map do |manifest|
41
36
  manifest.participatory_spaces.call(@organization).public_spaces
42
37
  end
43
- components = Decidim::Component.where(participatory_space: spaces).published
44
38
  @query = Decidim::Accountability::Result.select(:decidim_component_id)
45
- .where(component: components)
39
+ .where(component: visible_component_ids_from_spaces(spaces))
46
40
  .joins(:component)
47
41
  .left_outer_joins(:category)
48
42
  @query = @query.where("decidim_accountability_results.created_at <= ?", end_time)
@@ -6,6 +6,8 @@ module Decidim
6
6
  # `current_component` param with a `Decidim::Component` in order to
7
7
  # find the results.
8
8
  class ResultSearch < ResourceSearch
9
+ text_search_fields :title, :description
10
+
9
11
  # Public: Initializes the service.
10
12
  #
11
13
  # options - A hash of options to modify the search. These options will be
@@ -24,13 +26,6 @@ module Decidim
24
26
  super(Result.all, options)
25
27
  end
26
28
 
27
- # Handle the search_text filter
28
- def search_search_text
29
- query
30
- .where(localized_search_text_in(:title), text: "%#{search_text}%")
31
- .or(query.where(localized_search_text_in(:description), text: "%#{search_text}%"))
32
- end
33
-
34
29
  # Handle parent_id filter
35
30
  def search_parent_id
36
31
  parent_id = options[:parent_id]
@@ -48,19 +43,6 @@ module Decidim
48
43
  def children_ids(parent_id)
49
44
  Result.where(parent_id: parent_id).pluck(:id)
50
45
  end
51
-
52
- # Internal: builds the needed query to search for a text in the organization's
53
- # available locales. Note that it is intended to be used as follows:
54
- #
55
- # Example:
56
- # Resource.where(localized_search_text_for(:title, text: "my_query"))
57
- #
58
- # The Hash with the `:text` key is required or it won't work.
59
- def localized_search_text_in(field)
60
- options[:organization].available_locales.map do |l|
61
- "#{field} ->> '#{l}' ILIKE :text"
62
- end.join(" OR ")
63
- end
64
46
  end
65
47
  end
66
48
  end
@@ -20,7 +20,7 @@ module Decidim
20
20
  end
21
21
 
22
22
  def comments_count
23
- Decidim::Comments::Comment.where(commentable: proposals.to_a).count
23
+ proposals.sum(:comments_count)
24
24
  end
25
25
 
26
26
  def attendees_count
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Decidim
6
+ module Accountability
7
+ # This class handles importing results from a CSV file.
8
+ # Needs a `current_component` param with a `Decidim::component`
9
+ # in order to import the results in that component.
10
+ class ResultsCSVImporter
11
+ include Decidim::FormFactory
12
+
13
+ # Public: Initializes the service.
14
+ # component - A Decidim::component to import the results into.
15
+ # csv_file - The contents of the CSV to read.
16
+ def initialize(component, csv_file, current_user)
17
+ @component = component
18
+ @csv_file = csv_file
19
+
20
+ @extra_context = {
21
+ current_component: component,
22
+ current_organization: component.organization,
23
+ current_user: current_user,
24
+ current_participatory_space: component.participatory_space
25
+ }
26
+ @matches_ids = []
27
+ end
28
+
29
+ def import!
30
+ errors = []
31
+
32
+ ActiveRecord::Base.transaction do
33
+ i = 1
34
+ csv = CSV.new(@csv_file, headers: true, col_sep: ";")
35
+ while (row = csv.shift).present?
36
+ i += 1
37
+ next if row.empty?
38
+
39
+ params = set_params_for_import_result_form(row, @component)
40
+ existing_result = Decidim::Accountability::Result.find_by(id: row["id"], component: @component) if row["id"].present?
41
+ params["result"].merge!(parse_date_params(row, "start_date"))
42
+ params["result"].merge!(parse_date_params(row, "end_date"))
43
+ @form = form(Decidim::Accountability::Admin::ResultForm).from_params(params, @extra_context)
44
+ errors << [i, @form.errors.full_messages] if @form.errors.any?
45
+
46
+ if existing_result.present?
47
+ Decidim::Accountability::Admin::UpdateImportedResult.call(@form, existing_result, params["result"]["parent/id"]) do
48
+ on(:invalid) do
49
+ errors << [i, @form.errors.full_messages]
50
+ end
51
+ end
52
+ else
53
+ Decidim::Accountability::Admin::CreateImportedResult.call(@form, params["result"]["parent/id"]) do
54
+ on(:invalid) do
55
+ errors << [i, @form.errors.full_messages]
56
+ end
57
+ end
58
+ add_match_id(row["id"])
59
+ end
60
+
61
+ end
62
+
63
+ raise ActiveRecord::Rollback if errors.any?
64
+
65
+ update_parents
66
+ remove_invalid_results
67
+
68
+ Rails.logger.info "Processed: #{i}"
69
+ end
70
+
71
+ errors
72
+ end
73
+
74
+ private
75
+
76
+ def set_params_for_import_result_form(row, component)
77
+ params = {}
78
+ params["result"] = row.to_hash
79
+ default_locale = component.participatory_space.organization.default_locale
80
+ available_locales = component.participatory_space.organization.available_locales
81
+ params["result"].merge!(get_locale_attributes(default_locale, available_locales, :title, row))
82
+ params["result"].merge!(get_locale_attributes(default_locale, available_locales, :description, row))
83
+ params["result"]["decidim_category_id"] = row["category/id"] if row["category/id"].present?
84
+ params["result"]["decidim_accountability_status_id"] = row["status/id"] if row["status/id"].present?
85
+ params["result"].merge!(get_proposal_ids(row["proposal_urls"]))
86
+ params
87
+ end
88
+
89
+ def get_locale_attributes(default_locale, available_locales, field, row)
90
+ array_field_localized = available_locales.map do |locale|
91
+ if row["#{field}/#{locale}"].present?
92
+ ["#{field}_#{locale}", row["#{field}/#{locale}"]]
93
+ else
94
+ ["#{field}_#{locale}", row["#{field}/#{default_locale}"]]
95
+ end
96
+ end
97
+
98
+ Hash[*array_field_localized.flatten]
99
+ end
100
+
101
+ def parse_date_params(row, field)
102
+ begin
103
+ return { field => Date.parse(row[field]) } if row[field].present?
104
+ rescue ArgumentError
105
+ @form.errors.add(field.to_sym, :invalid_date)
106
+ end
107
+ {}
108
+ end
109
+
110
+ def get_proposal_ids(proposal_urls)
111
+ if proposal_urls.present?
112
+ proposal_urls = proposal_urls.split(";")
113
+ { "proposal_ids" => proposal_urls.map { |proposal_url| proposal_url.scan(/\d$/).first.to_i } }
114
+ else
115
+ {}
116
+ end
117
+ end
118
+
119
+ def add_match_id(id)
120
+ last_created_result = Decidim::Accountability::Result.last
121
+ @matches_ids << [id, Decidim::Accountability::Result.last.id] if id.present? && last_created_result.present?
122
+ end
123
+
124
+ def update_parents
125
+ @matches_ids.each do |match|
126
+ Decidim::Accountability::Result.where(component: @component, parent_id: match.first).find_each { |result| result.update(parent_id: match.last) }
127
+ end
128
+ end
129
+
130
+ def remove_invalid_results
131
+ Decidim::Accountability::Result.includes(:parent).references(:parent)
132
+ .where(parents_decidim_accountability_results: { id: nil })
133
+ .where.not(parent_id: nil).each do |result|
134
+ DestroyResult.call(result, @extra_context[:current_user])
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ AccountabilityType = GraphQL::ObjectType.define do
6
+ interfaces [-> { Decidim::Core::ComponentInterface }]
7
+
8
+ name "Accountability"
9
+ description "An accountability component of a participatory space."
10
+
11
+ connection :results, ResultType.connection_type do
12
+ resolve ->(component, _args, _ctx) {
13
+ ResultTypeHelper.base_scope(component).includes(:component)
14
+ }
15
+ end
16
+
17
+ field(:result, ResultType) do
18
+ argument :id, !types.ID
19
+
20
+ resolve ->(component, args, _ctx) {
21
+ ResultTypeHelper.base_scope(component).find_by(id: args[:id])
22
+ }
23
+ end
24
+ end
25
+
26
+ module ResultTypeHelper
27
+ def self.base_scope(component)
28
+ Result.where(component: component)
29
+ end
30
+ end
31
+ end
32
+ end