decidim-accountability 0.19.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) 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/versions_controller.rb +7 -8
  13. data/app/jobs/application_job.rb +4 -0
  14. data/app/jobs/decidim/accountability/admin/import_results_csv_job.rb +19 -0
  15. data/app/mailers/decidim/accountability/import_mailer.rb +26 -0
  16. data/app/models/decidim/accountability/result.rb +9 -0
  17. data/app/queries/decidim/accountability/metrics/results_metric_manage.rb +2 -8
  18. data/app/services/decidim/accountability/results_csv_importer.rb +139 -0
  19. data/app/types/decidim/accountability/accountability_type.rb +32 -0
  20. data/app/types/decidim/accountability/result_type.rb +35 -0
  21. data/app/types/decidim/accountability/status_type.rb +20 -0
  22. data/app/types/decidim/accountability/timeline_entry_type.rb +18 -0
  23. data/app/views/decidim/accountability/admin/import_results/new.html.erb +22 -0
  24. data/app/views/decidim/accountability/admin/results/_form.html.erb +1 -5
  25. data/app/views/decidim/accountability/admin/results/index.html.erb +1 -0
  26. data/app/views/decidim/accountability/admin/results/proposals_picker.html.erb +1 -0
  27. data/app/views/decidim/accountability/import_mailer/import.html.erb +25 -0
  28. data/app/views/decidim/accountability/results/_linked_results.html.erb +1 -1
  29. data/app/views/decidim/accountability/results/_results_leaf.html.erb +5 -5
  30. data/app/views/decidim/accountability/results/_results_parent.html.erb +2 -2
  31. data/app/views/decidim/accountability/results/_scope_filters.html.erb +16 -3
  32. data/app/views/decidim/accountability/results/_search.html.erb +3 -3
  33. data/app/views/decidim/accountability/results/_stats_box.html.erb +11 -4
  34. data/app/views/decidim/accountability/results/_timeline.html.erb +2 -2
  35. data/app/views/decidim/accountability/versions/index.html.erb +7 -25
  36. data/app/views/decidim/accountability/versions/show.html.erb +9 -31
  37. data/app/views/decidim/participatory_spaces/_result.html.erb +3 -3
  38. data/config/locales/ar.yml +1 -24
  39. data/config/locales/bg-BG.yml +220 -0
  40. data/config/locales/ca.yml +19 -22
  41. data/config/locales/cs.yml +44 -47
  42. data/config/locales/da-DK.yml +1 -0
  43. data/config/locales/de.yml +19 -22
  44. data/config/locales/el-GR.yml +1 -0
  45. data/config/locales/el.yml +223 -0
  46. data/config/locales/en.yml +19 -22
  47. data/config/locales/es-MX.yml +19 -22
  48. data/config/locales/es-PY.yml +19 -22
  49. data/config/locales/es.yml +19 -22
  50. data/config/locales/et-EE.yml +1 -0
  51. data/config/locales/eu.yml +1 -24
  52. data/config/locales/fi-plain.yml +19 -22
  53. data/config/locales/fi.yml +32 -35
  54. data/config/locales/fr-CA.yml +222 -0
  55. data/config/locales/fr.yml +18 -22
  56. data/config/locales/ga-IE.yml +1 -0
  57. data/config/locales/gl.yml +1 -24
  58. data/config/locales/hr-HR.yml +1 -0
  59. data/config/locales/hu.yml +18 -22
  60. data/config/locales/id-ID.yml +1 -24
  61. data/config/locales/is-IS.yml +177 -0
  62. data/config/locales/it.yml +37 -40
  63. data/config/locales/ja-JP.yml +221 -0
  64. data/config/locales/lt-LT.yml +1 -0
  65. data/config/locales/lv-LV.yml +218 -0
  66. data/config/locales/mt-MT.yml +1 -0
  67. data/config/locales/nl.yml +19 -23
  68. data/config/locales/no.yml +218 -0
  69. data/config/locales/pl.yml +46 -49
  70. data/config/locales/pt-BR.yml +2 -25
  71. data/config/locales/pt.yml +59 -62
  72. data/config/locales/ro-RO.yml +223 -0
  73. data/config/locales/ru.yml +13 -21
  74. data/config/locales/sk-SK.yml +224 -0
  75. data/config/locales/sk.yml +207 -0
  76. data/config/locales/sl.yml +129 -0
  77. data/config/locales/sr-CS.yml +205 -0
  78. data/config/locales/sv.yml +21 -24
  79. data/config/locales/tr-TR.yml +1 -24
  80. data/config/locales/uk.yml +1 -21
  81. data/db/migrate/20200320105903_index_foreign_keys_in_decidim_accountability_results.rb +7 -0
  82. data/lib/decidim/accountability/admin_engine.rb +4 -3
  83. data/lib/decidim/accountability/component.rb +2 -0
  84. data/lib/decidim/accountability/version.rb +1 -1
  85. metadata +62 -25
  86. data/app/views/decidim/accountability/admin/results/_proposals.html.erb +0 -12
  87. data/app/views/decidim/accountability/versions/_version.html.erb +0 -20
@@ -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
@@ -15,6 +15,7 @@ module Decidim
15
15
  include Decidim::Loggable
16
16
  include Decidim::DataPortability
17
17
  include Decidim::Randomable
18
+ include Decidim::Searchable
18
19
 
19
20
  component_manifest_name "accountability"
20
21
 
@@ -28,6 +29,14 @@ module Decidim
28
29
 
29
30
  after_save :update_parent_progress, if: -> { parent_id.present? }
30
31
 
32
+ searchable_fields(
33
+ scope_id: :decidim_scope_id,
34
+ participatory_space: { component: :participatory_space },
35
+ A: :title,
36
+ D: :description,
37
+ datetime: :start_date
38
+ )
39
+
31
40
  def self.log_presenter_class_for(_log)
32
41
  Decidim::Accountability::AdminLog::ResultPresenter
33
42
  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)
@@ -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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ ResultType = GraphQL::ObjectType.define do
6
+ interfaces [
7
+ -> { Decidim::Core::ComponentInterface },
8
+ -> { Decidim::Core::CategorizableInterface },
9
+ -> { Decidim::Comments::CommentableInterface },
10
+ -> { Decidim::Core::ScopableInterface }
11
+ ]
12
+
13
+ name "Result"
14
+ description "A result"
15
+
16
+ field :id, !types.ID, "The internal ID for this result"
17
+ field :title, Decidim::Core::TranslatedFieldType, "The title for this result"
18
+ field :description, Decidim::Core::TranslatedFieldType, "The description for this result"
19
+ field :reference, types.String, "The reference for this result"
20
+ field :startDate, Decidim::Core::DateType, "The start date for this result", property: :start_date
21
+ field :endDate, Decidim::Core::DateType, "The end date for this result", property: :end_date
22
+ field :progress, types.Float, "The progress for this result"
23
+ field :createdAt, Decidim::Core::DateTimeType, "When this result was created", property: :created_at
24
+ field :updatedAt, Decidim::Core::DateTimeType, "When this result was updated", property: :updated_at
25
+ field :childrenCount, types.Int, "The number of children results", property: :children_count
26
+ field :weight, !types.Int, "The order of this result"
27
+ field :externalId, types.String, "The external ID for this result", property: :external_id
28
+
29
+ field :children, types[Decidim::Accountability::ResultType], "The childrens results"
30
+ field :parent, Decidim::Accountability::ResultType, "The parent result"
31
+ field :status, Decidim::Accountability::StatusType, "The status for this result"
32
+ field :timelineEntries, types[Decidim::Accountability::TimelineEntryType], "The timeline entries for this result", property: :timeline_entries
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ StatusType = GraphQL::ObjectType.define do
6
+ name "Status"
7
+ description "A status"
8
+
9
+ field :id, !types.ID, "The internal ID for this status"
10
+ field :key, types.String, "The key for this status"
11
+ field :name, Decidim::Core::TranslatedFieldType, "The name for this status"
12
+ field :createdAt, Decidim::Core::DateType, "When this status was created", property: :created_at
13
+ field :updatedAt, Decidim::Core::DateType, "When this status was updated", property: :updated_at
14
+ field :description, Decidim::Core::TranslatedFieldType, "The description for this status"
15
+ field :progress, types.Int, "The progress for this status"
16
+
17
+ field :results, types[Decidim::Accountability::ResultType], "The results for this status"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ TimelineEntryType = GraphQL::ObjectType.define do
6
+ name "TimelineEntry"
7
+ description "A Timeline Entry"
8
+
9
+ field :id, !types.ID, "The internal ID for this timeline entry"
10
+ field :entryDate, Decidim::Core::DateType, "The entry date for this timeline entry", property: :entry_date
11
+ field :description, Decidim::Core::TranslatedFieldType, "The description for this timeline entry"
12
+ field :createdAt, Decidim::Core::DateTimeType, "When this timeline entry was created", property: :created_at
13
+ field :updatedAt, Decidim::Core::DateTimeType, "When this timeline entry was updated", property: :updated_at
14
+
15
+ field :result, Decidim::Accountability::ResultType, "The result for this timeline entry"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ <%= form_tag(import_results_path, multipart: true, class: "form new_import") do %>
2
+ <div class="card">
3
+ <div class="card-divider">
4
+ <h2 class="card-title">
5
+ <%= t(".title") %>
6
+ </h2>
7
+ </div>
8
+
9
+ <div class="card-section">
10
+ <div class="row column">
11
+ <%= file_field_tag :csv_file %>
12
+ </div>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="button--double form-general-submit">
17
+ <%= submit_tag t(".import"), class: "button" %>
18
+ </div>
19
+ <div class="import-process-info">
20
+ <%= t(".info", link_new_status: new_status_path, link_new_result: new_result_path, link_export_csv: link_to(t(".link"), exports_path(@current_component, id: "results", format: "CSV"), method: :post)).try("html_safe") %>
21
+ </div>
22
+ <% end %>
@@ -53,11 +53,7 @@
53
53
  </div>
54
54
 
55
55
  <div class="row column">
56
- <% if @form.proposals %>
57
- <% picker_options = { id: "decidim_accountability_proposals", class: "picker-multiple", name: "result[proposal_ids]", multiple: true }
58
- prompt_params= { url: proposals_results_path(current_component, format: :html), text: t(".add_proposal") } %>
59
- <%= form.data_picker(:proposals, picker_options, prompt_params) {|item| { url:proposals_results_path(current_component, format: :json), text: "##{item.id}- #{present(item).title}" }} %>
60
- <% end %>
56
+ <%= proposals_picker(form, :proposals, proposals_picker_results_path) %>
61
57
  </div>
62
58
 
63
59
  <div class="row column">
@@ -7,6 +7,7 @@
7
7
  <%= link_to t("actions.new", scope: "decidim.accountability", name: t("models.result.name", scope: "decidim.accountability.admin")), new_result_path(parent_id: parent_result), class: "button tiny button--simple" if allowed_to? :create, :result %>
8
8
  <%= render partial: "decidim/accountability/admin/shared/subnav" unless parent_result %>
9
9
  <%= export_dropdown %>
10
+ <%= link_to t("actions.import_csv", scope: "decidim.accountability"), import_results_path, class: "button tiny button--simple" if allowed_to? :create, :result %>
10
11
  </div>
11
12
  </h2>
12
13
  </div>
@@ -0,0 +1 @@
1
+ <%= cell "decidim/proposals/proposals_picker", current_component %>
@@ -0,0 +1,25 @@
1
+ <% if @errors.empty? %>
2
+ <p><%= t(".success") %></p>
3
+ <% else %>
4
+ <p><%= t(".errors_present") %>:</p>
5
+ <table>
6
+ <thead>
7
+ <tr>
8
+ <th><%= t(".row_number") %></th>
9
+ <th><%= t(".errors") %></th>
10
+ </tr>
11
+ </thead>
12
+ <% @errors.each do |error| %>
13
+ <tr>
14
+ <td><%= error.first %></td>
15
+ <td>
16
+ <ul>
17
+ <% error.last.each do |error_message| %>
18
+ <li><%= error_message %></li>
19
+ <% end %>
20
+ </ul>
21
+ </td>
22
+ </tr>
23
+ <% end %>
24
+ </table>
25
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  <div class="card card--action card--list">
2
2
  <% resources.each do |result| %>
3
3
  <div class="card--list__item">
4
- <%= icon "actions", class: "card--list__icon", remove_icon_class: true %>
4
+ <%= icon "actions", class: "card--list__icon", role: "img", "aria-hidden": true, remove_icon_class: true %>
5
5
  <%= link_to resource_locator(result).path, class: "card--list__text card__link card__link--block" do %>
6
6
  <h5 class="card--list__heading">
7
7
  <%= translated_attribute(result.title) %>
@@ -1,7 +1,7 @@
1
1
  <div class="title-action">
2
- <h2 id="results-count" class="title-action__title section-heading">
2
+ <h3 id="results-count" class="title-action__title section-heading">
3
3
  <%= heading_leaf_level_results(total_count) %>
4
- </h2>
4
+ </h3>
5
5
  </div>
6
6
 
7
7
  <div class="row">
@@ -9,12 +9,12 @@
9
9
  <div class="card card--action card--list">
10
10
  <% results.each do |result| %>
11
11
  <div class="card--list__item">
12
- <%= icon "actions", class: "card--list__icon", remove_icon_class: true %>
12
+ <%= icon "actions", class: "card--list__icon", role: "img", "aria-hidden": true, remove_icon_class: true %>
13
13
 
14
14
  <%= link_to result_path(result), class: "card--list__text card__link card__link--block" do %>
15
- <h5 class="card--list__heading">
15
+ <h4 class="card--list__heading">
16
16
  <%= translated_attribute(result.title) %>
17
- </h5>
17
+ </h4>
18
18
 
19
19
  <div class="text-small card--meta">
20
20
  <% if result.start_date %>