decidim-accountability 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +46 -0
  3. data/Rakefile +3 -0
  4. data/app/assets/config/decidim_accountability_admin_manifest.js +1 -0
  5. data/app/assets/config/decidim_accountability_manifest.js +1 -0
  6. data/app/assets/images/decidim/accountability/icon.svg +1 -0
  7. data/app/assets/javascripts/decidim/accountability/accountability.js.es6 +10 -0
  8. data/app/assets/javascripts/decidim/accountability/admin/accountability_admin.js +8 -0
  9. data/app/assets/stylesheets/decidim/accountability/_accountability.scss +1 -0
  10. data/app/assets/stylesheets/decidim/accountability/accountability/_cards.scss +13 -0
  11. data/app/assets/stylesheets/decidim/accountability/accountability/_categories.scss +109 -0
  12. data/app/assets/stylesheets/decidim/accountability/accountability/_lines_breadcrumb.scss +29 -0
  13. data/app/assets/stylesheets/decidim/accountability/accountability/_results.scss +144 -0
  14. data/app/commands/decidim/accountability/admin/create_result.rb +72 -0
  15. data/app/commands/decidim/accountability/admin/create_status.rb +42 -0
  16. data/app/commands/decidim/accountability/admin/create_timeline_entry.rb +40 -0
  17. data/app/commands/decidim/accountability/admin/update_result.rb +80 -0
  18. data/app/commands/decidim/accountability/admin/update_status.rb +46 -0
  19. data/app/commands/decidim/accountability/admin/update_template_texts.rb +47 -0
  20. data/app/commands/decidim/accountability/admin/update_timeline_entry.rb +44 -0
  21. data/app/controllers/decidim/accountability/admin/application_controller.rb +15 -0
  22. data/app/controllers/decidim/accountability/admin/imports_controller.rb +30 -0
  23. data/app/controllers/decidim/accountability/admin/results_controller.rb +84 -0
  24. data/app/controllers/decidim/accountability/admin/statuses_controller.rb +70 -0
  25. data/app/controllers/decidim/accountability/admin/template_texts_controller.rb +38 -0
  26. data/app/controllers/decidim/accountability/admin/timeline_entries_controller.rb +75 -0
  27. data/app/controllers/decidim/accountability/application_controller.rb +13 -0
  28. data/app/controllers/decidim/accountability/result_widgets_controller.rb +19 -0
  29. data/app/controllers/decidim/accountability/results_controller.rb +77 -0
  30. data/app/forms/decidim/accountability/admin/result_form.rb +74 -0
  31. data/app/forms/decidim/accountability/admin/status_form.rb +21 -0
  32. data/app/forms/decidim/accountability/admin/template_texts_form.rb +19 -0
  33. data/app/forms/decidim/accountability/admin/timeline_entry_form.rb +20 -0
  34. data/app/helpers/decidim/accountability/application_helper.rb +50 -0
  35. data/app/models/decidim/accountability/application_record.rb +10 -0
  36. data/app/models/decidim/accountability/result.rb +66 -0
  37. data/app/models/decidim/accountability/status.rb +18 -0
  38. data/app/models/decidim/accountability/template_texts.rb +17 -0
  39. data/app/models/decidim/accountability/timeline_entry.rb +11 -0
  40. data/app/services/decidim/accountability/csv_exporter.rb +77 -0
  41. data/app/services/decidim/accountability/csv_importer.rb +106 -0
  42. data/app/services/decidim/accountability/result_search.rb +40 -0
  43. data/app/services/decidim/accountability/result_stats_calculator.rb +52 -0
  44. data/app/services/decidim/accountability/results_calculator.rb +34 -0
  45. data/app/views/decidim/accountability/admin/imports/new.html.erb +47 -0
  46. data/app/views/decidim/accountability/admin/results/_form.html.erb +70 -0
  47. data/app/views/decidim/accountability/admin/results/edit.html.erb +9 -0
  48. data/app/views/decidim/accountability/admin/results/index.html.erb +52 -0
  49. data/app/views/decidim/accountability/admin/results/new.html.erb +9 -0
  50. data/app/views/decidim/accountability/admin/shared/_subnav.html.erb +3 -0
  51. data/app/views/decidim/accountability/admin/statuses/_form.html.erb +23 -0
  52. data/app/views/decidim/accountability/admin/statuses/edit.html.erb +8 -0
  53. data/app/views/decidim/accountability/admin/statuses/index.html.erb +45 -0
  54. data/app/views/decidim/accountability/admin/statuses/new.html.erb +8 -0
  55. data/app/views/decidim/accountability/admin/template_texts/_form.html.erb +30 -0
  56. data/app/views/decidim/accountability/admin/template_texts/edit.html.erb +8 -0
  57. data/app/views/decidim/accountability/admin/timeline_entries/_form.html.erb +15 -0
  58. data/app/views/decidim/accountability/admin/timeline_entries/edit.html.erb +8 -0
  59. data/app/views/decidim/accountability/admin/timeline_entries/index.html.erb +41 -0
  60. data/app/views/decidim/accountability/admin/timeline_entries/new.html.erb +8 -0
  61. data/app/views/decidim/accountability/result_widgets/show.html.erb +2 -0
  62. data/app/views/decidim/accountability/results/_home_categories.html.erb +73 -0
  63. data/app/views/decidim/accountability/results/_home_header.html.erb +24 -0
  64. data/app/views/decidim/accountability/results/_linked_results.html.erb +12 -0
  65. data/app/views/decidim/accountability/results/_nav_breadcrumb.html.erb +29 -0
  66. data/app/views/decidim/accountability/results/_results_leaf.html.erb +45 -0
  67. data/app/views/decidim/accountability/results/_results_parent.html.erb +32 -0
  68. data/app/views/decidim/accountability/results/_scope_filters.html.erb +14 -0
  69. data/app/views/decidim/accountability/results/_search.html.erb +12 -0
  70. data/app/views/decidim/accountability/results/_show_leaf.html.erb +79 -0
  71. data/app/views/decidim/accountability/results/_show_parent.html.erb +25 -0
  72. data/app/views/decidim/accountability/results/_stats.html.erb +11 -0
  73. data/app/views/decidim/accountability/results/_stats_box.html.erb +36 -0
  74. data/app/views/decidim/accountability/results/_tags.html.erb +10 -0
  75. data/app/views/decidim/accountability/results/_timeline.html.erb +24 -0
  76. data/app/views/decidim/accountability/results/home.html.erb +9 -0
  77. data/app/views/decidim/accountability/results/index.html.erb +19 -0
  78. data/app/views/decidim/accountability/results/index.js.erb +2 -0
  79. data/app/views/decidim/accountability/results/show.html.erb +14 -0
  80. data/config/i18n-tasks.yml +10 -0
  81. data/config/locales/ca.yml +190 -0
  82. data/config/locales/en.yml +190 -0
  83. data/config/locales/es.yml +190 -0
  84. data/config/locales/eu.yml +79 -0
  85. data/config/locales/fi.yml +77 -0
  86. data/db/migrate/20170425154712_create_accountability_statuses.rb +13 -0
  87. data/db/migrate/20170426104125_create_accountability_results.rb +22 -0
  88. data/db/migrate/20170508104902_add_description_and_progress_to_statuses.rb +6 -0
  89. data/db/migrate/20170508161109_create_template_texts.rb +14 -0
  90. data/db/migrate/20170606102902_add_index_to_accountability_results_on_external_id.rb +5 -0
  91. data/db/migrate/20170620154712_create_accountability_timeline_entries.rb +13 -0
  92. data/db/migrate/20170623094200_migrate_accountability_results_category.rb +13 -0
  93. data/db/migrate/20170623144902_add_children_counter_cache_to_results.rb +5 -0
  94. data/lib/decidim/accountability.rb +12 -0
  95. data/lib/decidim/accountability/admin.rb +10 -0
  96. data/lib/decidim/accountability/admin_engine.rb +32 -0
  97. data/lib/decidim/accountability/feature.rb +86 -0
  98. data/lib/decidim/accountability/list_engine.rb +27 -0
  99. data/lib/decidim/accountability/test/factories.rb +45 -0
  100. metadata +238 -0
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # The data store for a Status in the Decidim::Accountability component. It stores a
6
+ # key, a localized name, a localized description and and associated progress number.
7
+ class Status < Accountability::ApplicationRecord
8
+ include Decidim::HasFeature
9
+
10
+ feature_manifest_name "accountability"
11
+
12
+ has_many :results, foreign_key: "decidim_accountability_status_id", class_name: "Decidim::Accountability::Result", inverse_of: :status
13
+
14
+ validates :key, presence: true, uniqueness: { scope: :decidim_feature_id }
15
+ validates :name, presence: true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # The data store for a Result in the Decidim::Accountability component. It stores a
6
+ # title, description and any other useful information to render a custom result.
7
+ class TemplateTexts < Accountability::ApplicationRecord
8
+ include Decidim::HasFeature
9
+
10
+ feature_manifest_name "accountability"
11
+
12
+ def self.for(current_feature)
13
+ self.where(feature: current_feature).first || self.create!(feature: current_feature)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # The data store for a TimelineEntry for a Result in the Decidim::Accountability component.
6
+ # It stores a date, and localized description.
7
+ class TimelineEntry < Accountability::ApplicationRecord
8
+ belongs_to :result, foreign_key: "decidim_accountability_result_id", class_name: "Decidim::Accountability::Result", inverse_of: :timeline_entries
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require "csv"
3
+
4
+ module Decidim
5
+ module Accountability
6
+ # This class handles exporting results to a CSV file.
7
+ # Needs a `current_feature` param with a `Decidim::Feature`
8
+ class CSVExporter
9
+
10
+ # Public: Initializes the service.
11
+ # feature - A Decidim::Feature to import the results into.
12
+ def initialize(feature)
13
+ @feature = feature
14
+ end
15
+
16
+ def export
17
+ results = Decidim::Accountability::Result.where(feature: @feature).order(:id)
18
+
19
+ generated_csv = CSV.generate(headers: true) do |csv|
20
+ headers = [
21
+ "result_id",
22
+ "decidim_category_id",
23
+ "decidim_scope_id",
24
+ "parent_id",
25
+ "external_id",
26
+ "start_date",
27
+ "end_date",
28
+ "decidim_accountability_status_id",
29
+ "progress",
30
+ "proposal_ids"
31
+ ]
32
+
33
+ available_locales = @feature.participatory_space.organization.available_locales
34
+ available_locales.each do |locale|
35
+ headers << "title_#{locale}"
36
+ headers << "description_#{locale}"
37
+ end
38
+
39
+ csv << headers
40
+
41
+ results.find_each do |result|
42
+ row = Rails.cache.fetch("#{result.cache_key}/csv") do
43
+ row_for_result(result, available_locales)
44
+ end
45
+
46
+ csv << row
47
+ end
48
+ end
49
+
50
+ generated_csv
51
+ end
52
+
53
+ private
54
+
55
+ def row_for_result(result, available_locales)
56
+ row = [
57
+ result.id,
58
+ result.category.try(:id),
59
+ result.decidim_scope_id,
60
+ result.parent_id,
61
+ result.external_id,
62
+ result.start_date,
63
+ result.end_date,
64
+ result.decidim_accountability_status_id,
65
+ result.progress,
66
+ result.linked_resources(:proposals, "included_proposals").pluck(:id).sort.join(";"),
67
+ ]
68
+ available_locales.each do |locale|
69
+ row << result.title[locale]
70
+ row << result.description[locale]
71
+ end
72
+
73
+ row
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+ require "csv"
3
+
4
+ module Decidim
5
+ module Accountability
6
+ # This class handles importing results from a CSV file.
7
+ # Needs a `current_feature` param with a `Decidim::Feature`
8
+ # in order to import the results in that feature.
9
+ class CSVImporter
10
+ include Decidim::FormFactory
11
+
12
+ # Public: Initializes the service.
13
+ # feature - A Decidim::Feature to import the results into.
14
+ # csv_file_path - The path to the csv file.
15
+ def initialize(feature, csv_file_path)
16
+ @feature = feature
17
+ @csv_file_path = csv_file_path
18
+ @extra_context = { current_feature: feature, current_organization: feature.organization}
19
+ end
20
+
21
+ def import!
22
+ errors = []
23
+
24
+ ActiveRecord::Base.transaction do
25
+ i = 1
26
+ CSV.foreach(@csv_file_path, headers: true) do |row|
27
+ i += 1
28
+ next if row.empty?
29
+
30
+ params = {}
31
+ params["result"] = row.to_hash
32
+
33
+ if row["result_id"].present?
34
+ existing_result = Decidim::Accountability::Result.find_by(id: row['result_id'].to_i)
35
+ unless existing_result.present?
36
+ errors << [i, [I18n.t("imports.create.not_found", scope: "decidim.accountability.admin", result_id: row["result_id"])]]
37
+ next
38
+ end
39
+ elsif row["external_id"].present?
40
+ existing_result = Decidim::Accountability::Result.find_by(external_id: row["external_id"])
41
+ params["result"]["id"] = existing_result.id if existing_result
42
+ end
43
+
44
+ if row["parent_id"].blank? && row["parent_external_id"].present?
45
+ if parent = Decidim::Accountability::Result.find_by(external_id: row["parent_external_id"])
46
+ params["result"]["parent_id"] = parent.id
47
+ end
48
+ end
49
+
50
+ if row["decidim_accountability_status_id"].present? && status = Decidim::Accountability::Status.find_by(id: row["decidim_accountability_status_id"])
51
+ params["result"]["progress"] = status.progress if status.progress.present?
52
+ end
53
+
54
+ default_locale = @feature.participatory_space.organization.default_locale
55
+ available_locales = @feature.participatory_space.organization.available_locales
56
+
57
+ available_locales.each do |locale|
58
+ params["result"]["title_#{locale}"] = params["result"]["title_#{default_locale}"] if params["result"]["title_#{locale}"].blank?
59
+ params["result"]["description_#{locale}"] = params["result"]["description_#{default_locale}"] if params["result"]["description_#{locale}"].blank?
60
+ end
61
+
62
+ if params["result"]["proposal_ids"].presence
63
+ proposal_ids = params["result"]["proposal_ids"].split(";")
64
+ params["result"]["proposal_ids"] = proposal_ids
65
+ end
66
+
67
+ @form = form(Decidim::Accountability::Admin::ResultForm).from_params(params, @extra_context)
68
+
69
+ begin
70
+ start_date = Date.parse(row["start_date"]) if row["start_date"].present?
71
+ rescue ArgumentError
72
+ @form.errors.add(:start_date, :invalid_date)
73
+ end
74
+
75
+ begin
76
+ end_date = Date.parse(row["end_date"]) if row["end_date"].present?
77
+ rescue ArgumentError
78
+ @form.errors.add(:end_date, :invalid_date)
79
+ end
80
+
81
+ # add form errors now because when calling valid on the form in UpdateResult/CreateResult will clear the errors
82
+ errors << [i, @form.errors.full_messages] if @form.errors.any?
83
+
84
+ if existing_result #update existing result
85
+ Decidim::Accountability::Admin::UpdateResult.call(@form, existing_result) do
86
+ on(:invalid) do
87
+ errors << [i, @form.errors.full_messages]
88
+ end
89
+ end
90
+ else #create new result
91
+ Decidim::Accountability::Admin::CreateResult.call(@form) do
92
+ on(:invalid) do
93
+ errors << [i, @form.errors.full_messages]
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ raise ActiveRecord::Rollback if errors.any?
100
+ end
101
+
102
+ errors
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # This class handles search and filtering of results. Needs a
6
+ # `current_feature` param with a `Decidim::Feature` in order to
7
+ # find the results.
8
+ class ResultSearch < ResourceSearch
9
+ # Public: Initializes the service.
10
+ # feature - A Decidim::Feature to get the results from.
11
+ # page - The page number to paginate the results.
12
+ # per_page - The number of proposals to return per page.
13
+ def initialize(options = {})
14
+ super(Result.all, options)
15
+ end
16
+
17
+ # Handle the search_text filter
18
+ def search_search_text
19
+ query
20
+ .where(localized_search_text_in(:title), text: "%#{search_text}%")
21
+ .or(query.where(localized_search_text_in(:description), text: "%#{search_text}%"))
22
+ end
23
+
24
+ private
25
+
26
+ # Internal: builds the needed query to search for a text in the organization's
27
+ # available locales. Note that it is intended to be used as follows:
28
+ #
29
+ # Example:
30
+ # Resource.where(localized_search_text_for(:title, text: "my_query"))
31
+ #
32
+ # The Hash with the `:text` key is required or it won't work.
33
+ def localized_search_text_in(field)
34
+ options[:organization].available_locales.map do |l|
35
+ "#{field} ->> '#{l}' ILIKE :text"
36
+ end.join(" OR ")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # This class handles statistics of results. Needs a `result` in
6
+ # order to find the stats.
7
+ class ResultStatsCalculator
8
+ # Public: Initializes the service.
9
+ # result - The result from which to calculate the stats.
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ def proposals_count
15
+ proposals.count
16
+ end
17
+
18
+ def votes_count
19
+ return 0 unless proposals
20
+ proposals.sum { |proposal| proposal.votes.size }
21
+ end
22
+
23
+ def comments_count
24
+ Decidim::Comments::Comment.where(commentable: proposals).count
25
+ end
26
+
27
+ def attendees_count
28
+ meetings.where("attendees_count > 0").sum(:attendees_count)
29
+ end
30
+
31
+ def contributions_count
32
+ meetings.where("contributions_count > 0").sum(:contributions_count)
33
+ end
34
+
35
+ def meetings_count
36
+ meetings.count
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :result
42
+
43
+ def proposals
44
+ @proposals ||= result.linked_resources(:proposals, "included_proposals")
45
+ end
46
+
47
+ def meetings
48
+ @meetings ||= result.linked_resources(:meetings, "meetings_through_proposals")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Accountability
5
+ # This class handles the calculation of progress for a set of results
6
+ class ResultsCalculator
7
+ # Public: Initializes the service.
8
+ def initialize(feature, scope_id, category_id)
9
+ @feature = feature
10
+ @scope_id = scope_id
11
+ @category_id = category_id
12
+ end
13
+
14
+ def progress
15
+ results.average(:progress)
16
+ end
17
+
18
+ def count
19
+ # if there are children return the total number of children results
20
+ # if not return the count of results (they are leafs)
21
+ children = results.sum(:children_count)
22
+ children > 0 ? children : results.count
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :feature, :scope_id, :category_id
28
+
29
+ def results
30
+ @results ||= ResultSearch.new(feature: feature, scope_id: scope_id, category_id: category_id, parent_id: nil).results
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ <%= form_tag({:action => :create}, 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
+ <%= render partial: "decidim/accountability/admin/shared/subnav" %>
7
+ </h2>
8
+ </div>
9
+
10
+ <% unless @errors.empty? %>
11
+ <div class="card-section">
12
+ <div class="row column">
13
+ <table>
14
+ <thead>
15
+ <tr>
16
+ <th><%= t(".row_number") %></th>
17
+ <th><%= t(".errors") %></th>
18
+ </tr>
19
+ </thead>
20
+ <% @errors.each do |error| %>
21
+ <tr>
22
+ <td><%= error.first %></td>
23
+ <td>
24
+ <ul>
25
+ <% error.last.each do |error_message| %>
26
+ <li><%= error_message %></li>
27
+ <% end %>
28
+ </ul>
29
+ </td>
30
+ </tr>
31
+ <% end %>
32
+ </table>
33
+ </div>
34
+ </div>
35
+ <% end %>
36
+
37
+ <div class="card-section">
38
+ <div class="row column">
39
+ <%= file_field_tag :csv_file %>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="button--double form-general-submit">
45
+ <%= submit_tag t(".import"), class: "button" %>
46
+ </div>
47
+ <% end %>
@@ -0,0 +1,70 @@
1
+ <div class="card">
2
+ <div class="card-divider">
3
+ <h2 class="card-title"><%= title %></h2>
4
+ </div>
5
+
6
+ <div class="card-section">
7
+ <div class="row column">
8
+ <%= form.translated :text_field, :title, autofocus: true %>
9
+ </div>
10
+
11
+ <div class="row column">
12
+ <%= form.translated :editor, :description %>
13
+ </div>
14
+
15
+ <% if @form.parent_id %>
16
+
17
+ <div class="row column">
18
+ <%= form.select :parent_id, parent_results.map{|result| [translated_attribute(result.title), result.id] }, include_blank: true %>
19
+ </div>
20
+
21
+ <% else %>
22
+
23
+ <% if current_participatory_space.has_subscopes? %>
24
+ <div class="row column">
25
+ <%= form.scopes_select :decidim_scope_id, prompt: I18n.t("decidim.scopes.global"), remote_path: decidim.scopes_search_path(root: current_participatory_space.scope) %>
26
+ </div>
27
+ <% end %>
28
+
29
+ <div class="row column">
30
+ <%= form.categories_select :decidim_category_id, current_participatory_process.categories, include_blank: true, disable_parents: false %>
31
+ </div>
32
+
33
+ <% end %>
34
+
35
+ <div class="row">
36
+ <div class="columns xlarge-6">
37
+ <%= form.date_field :start_date %>
38
+ </div>
39
+
40
+ <div class="columns xlarge-6">
41
+ <%= form.date_field :end_date %>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="row">
46
+ <div class="columns xlarge-6">
47
+ <%= form.select :decidim_accountability_status_id, statuses.map{|status| [translated_attribute(status.name), status.id, { 'data-progress' => status.progress }] }, include_blank: true %>
48
+ </div>
49
+
50
+ <div class="columns xlarge-6">
51
+ <%= form.number_field :progress %>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="columns">
56
+ <%= form.text_field :external_id %>
57
+ </div>
58
+
59
+ <div class="row column">
60
+ <% if @form.proposals %>
61
+ <%= form.select :proposal_ids,
62
+ @form.proposals,
63
+ {},
64
+ { multiple: true, class: "chosen-select" }
65
+ %>
66
+ <% end %>
67
+ </div>
68
+
69
+ </div>
70
+ </div>