decidim-elections 0.22.0

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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +31 -0
  3. data/Rakefile +3 -0
  4. data/app/assets/config/decidim_elections_manifest.js +0 -0
  5. data/app/assets/images/decidim/elections/icon.svg +1 -0
  6. data/app/commands/decidim/elections/admin/create_answer.rb +71 -0
  7. data/app/commands/decidim/elections/admin/create_election.rb +48 -0
  8. data/app/commands/decidim/elections/admin/create_question.rb +52 -0
  9. data/app/commands/decidim/elections/admin/destroy_answer.rb +50 -0
  10. data/app/commands/decidim/elections/admin/destroy_election.rb +46 -0
  11. data/app/commands/decidim/elections/admin/destroy_question.rb +46 -0
  12. data/app/commands/decidim/elections/admin/update_answer.rb +73 -0
  13. data/app/commands/decidim/elections/admin/update_election.rb +48 -0
  14. data/app/commands/decidim/elections/admin/update_question.rb +52 -0
  15. data/app/controllers/decidim/elections/admin/answers_controller.rb +92 -0
  16. data/app/controllers/decidim/elections/admin/application_controller.rb +15 -0
  17. data/app/controllers/decidim/elections/admin/elections_controller.rb +82 -0
  18. data/app/controllers/decidim/elections/admin/questions_controller.rb +86 -0
  19. data/app/controllers/decidim/elections/application_controller.rb +13 -0
  20. data/app/controllers/decidim/elections/elections_controller.rb +9 -0
  21. data/app/forms/decidim/elections/admin/answer_form.rb +53 -0
  22. data/app/forms/decidim/elections/admin/election_form.rb +24 -0
  23. data/app/forms/decidim/elections/admin/question_form.rb +26 -0
  24. data/app/helpers/decidim/elections/application_helper.rb +10 -0
  25. data/app/models/decidim/elections/answer.rb +26 -0
  26. data/app/models/decidim/elections/application_record.rb +10 -0
  27. data/app/models/decidim/elections/election.rb +22 -0
  28. data/app/models/decidim/elections/question.rb +20 -0
  29. data/app/permissions/decidim/elections/admin/permissions.rb +42 -0
  30. data/app/permissions/decidim/elections/permissions.rb +16 -0
  31. data/app/types/decidim/elections/election_answer_type.rb +24 -0
  32. data/app/types/decidim/elections/election_question_type.rb +25 -0
  33. data/app/types/decidim/elections/election_type.rb +26 -0
  34. data/app/types/decidim/elections/elections_type.rb +32 -0
  35. data/app/views/decidim/elections/admin/answers/_form.html.erb +25 -0
  36. data/app/views/decidim/elections/admin/answers/edit.html.erb +7 -0
  37. data/app/views/decidim/elections/admin/answers/index.html.erb +46 -0
  38. data/app/views/decidim/elections/admin/answers/new.html.erb +7 -0
  39. data/app/views/decidim/elections/admin/answers/proposals_picker.html.erb +1 -0
  40. data/app/views/decidim/elections/admin/elections/_form.html.erb +28 -0
  41. data/app/views/decidim/elections/admin/elections/edit.html.erb +7 -0
  42. data/app/views/decidim/elections/admin/elections/index.html.erb +54 -0
  43. data/app/views/decidim/elections/admin/elections/new.html.erb +7 -0
  44. data/app/views/decidim/elections/admin/questions/_form.html.erb +27 -0
  45. data/app/views/decidim/elections/admin/questions/edit.html.erb +7 -0
  46. data/app/views/decidim/elections/admin/questions/index.html.erb +53 -0
  47. data/app/views/decidim/elections/admin/questions/new.html.erb +7 -0
  48. data/app/views/decidim/elections/elections/index.html.erb +1 -0
  49. data/app/views/decidim/elections/elections/show.html.erb +0 -0
  50. data/config/i18n-tasks.yml +10 -0
  51. data/config/locales/ar.yml +1 -0
  52. data/config/locales/bg-BG.yml +7 -0
  53. data/config/locales/ca.yml +132 -0
  54. data/config/locales/cs.yml +138 -0
  55. data/config/locales/da-DK.yml +1 -0
  56. data/config/locales/de.yml +132 -0
  57. data/config/locales/el.yml +132 -0
  58. data/config/locales/en.yml +133 -0
  59. data/config/locales/eo-UY.yml +1 -0
  60. data/config/locales/es-MX.yml +132 -0
  61. data/config/locales/es-PY.yml +132 -0
  62. data/config/locales/es.yml +132 -0
  63. data/config/locales/et-EE.yml +1 -0
  64. data/config/locales/eu.yml +1 -0
  65. data/config/locales/fi-plain.yml +132 -0
  66. data/config/locales/fi.yml +132 -0
  67. data/config/locales/fr-CA.yml +132 -0
  68. data/config/locales/fr.yml +132 -0
  69. data/config/locales/ga-IE.yml +1 -0
  70. data/config/locales/gl.yml +1 -0
  71. data/config/locales/hr-HR.yml +1 -0
  72. data/config/locales/hu.yml +1 -0
  73. data/config/locales/id-ID.yml +1 -0
  74. data/config/locales/is-IS.yml +1 -0
  75. data/config/locales/it.yml +132 -0
  76. data/config/locales/ja-JP.yml +129 -0
  77. data/config/locales/lt-LT.yml +1 -0
  78. data/config/locales/lv-LV.yml +135 -0
  79. data/config/locales/mt-MT.yml +1 -0
  80. data/config/locales/nl.yml +132 -0
  81. data/config/locales/no.yml +9 -0
  82. data/config/locales/pl.yml +138 -0
  83. data/config/locales/pt-BR.yml +1 -0
  84. data/config/locales/pt.yml +132 -0
  85. data/config/locales/ro-RO.yml +135 -0
  86. data/config/locales/ru.yml +1 -0
  87. data/config/locales/sk.yml +1 -0
  88. data/config/locales/sl.yml +5 -0
  89. data/config/locales/sr-CS.yml +1 -0
  90. data/config/locales/sv.yml +132 -0
  91. data/config/locales/tr-TR.yml +1 -0
  92. data/config/locales/uk.yml +1 -0
  93. data/db/migrate/20200430083618_create_decidim_elections_elections.rb +15 -0
  94. data/db/migrate/20200518082327_create_decidim_elections_questions.rb +14 -0
  95. data/db/migrate/20200518084144_create_decidim_elections_answers.rb +12 -0
  96. data/lib/decidim/elections.rb +13 -0
  97. data/lib/decidim/elections/admin.rb +10 -0
  98. data/lib/decidim/elections/admin_engine.rb +29 -0
  99. data/lib/decidim/elections/component.rb +123 -0
  100. data/lib/decidim/elections/engine.rb +23 -0
  101. data/lib/decidim/elections/seeds/city.jpeg +0 -0
  102. data/lib/decidim/elections/test/factories.rb +52 -0
  103. data/lib/decidim/elections/version.rb +10 -0
  104. metadata +201 -0
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # Custom helpers, scoped to the elections engine.
6
+ #
7
+ module ApplicationHelper
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # The data store for an Answer in the Decidim::Elections component. It stores a
6
+ # title, description and related resources and attachments.
7
+ class Answer < ApplicationRecord
8
+ include Decidim::Resourceable
9
+ include Decidim::HasAttachments
10
+ include Decidim::HasAttachmentCollections
11
+ include Traceable
12
+ include Loggable
13
+
14
+ delegate :component, to: :question
15
+ delegate :organization, :participatory_space, to: :component
16
+
17
+ belongs_to :question, foreign_key: "decidim_elections_question_id", class_name: "Decidim::Elections::Question", inverse_of: :answers
18
+
19
+ default_scope { order(weight: :asc, id: :asc) }
20
+
21
+ def proposals
22
+ linked_resources(:proposals, "related_proposals")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # Abstract class from which all models in this engine inherit.
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # The data store for an Election in the Decidim::Elections component. It stores a
6
+ # title, description and any other useful information to perform an election.
7
+ class Election < ApplicationRecord
8
+ include Decidim::Resourceable
9
+ include Decidim::HasComponent
10
+ include Traceable
11
+ include Loggable
12
+
13
+ component_manifest_name "elections"
14
+
15
+ has_many :questions, foreign_key: "decidim_elections_election_id", class_name: "Decidim::Elections::Question", inverse_of: :election, dependent: :destroy
16
+
17
+ def started?
18
+ start_time <= Time.current
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # The data store for a Question in the Decidim::Elections component. It stores a
6
+ # title, description and a maximum number of selection that voters can choose.
7
+ class Question < ApplicationRecord
8
+ include Decidim::Resourceable
9
+ include Traceable
10
+ include Loggable
11
+
12
+ delegate :component, to: :election
13
+
14
+ belongs_to :election, foreign_key: "decidim_elections_election_id", class_name: "Decidim::Elections::Election", inverse_of: :questions
15
+ has_many :answers, foreign_key: "decidim_elections_question_id", class_name: "Decidim::Elections::Answer", inverse_of: :question, dependent: :destroy
16
+
17
+ default_scope { order(weight: :asc, id: :asc) }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ module Admin
6
+ class Permissions < Decidim::DefaultPermissions
7
+ def permissions
8
+ return permission_action if permission_action.scope != :admin
9
+
10
+ case permission_action.subject
11
+ when :question, :answer
12
+ case permission_action.action
13
+ when :create, :update, :delete
14
+ allow_if_not_started
15
+ end
16
+ when :election
17
+ case permission_action.action
18
+ when :create, :read
19
+ allow!
20
+ when :update
21
+ toggle_allow(election)
22
+ when :delete
23
+ allow_if_not_started
24
+ end
25
+ end
26
+
27
+ permission_action
28
+ end
29
+
30
+ private
31
+
32
+ def election
33
+ @election ||= context.fetch(:election, nil)
34
+ end
35
+
36
+ def allow_if_not_started
37
+ toggle_allow(election && !election.started?)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ class Permissions < Decidim::DefaultPermissions
6
+ def permissions
7
+ return permission_action unless user
8
+
9
+ # Delegate the admin permission checks to the admin permissions class
10
+ return Decidim::Elections::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin
11
+
12
+ permission_action
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # This type represents an answer to an election question.
6
+ # The name is different from the model because to enforce consistency with Question type name.
7
+ ElectionAnswerType = GraphQL::ObjectType.define do
8
+ interfaces [
9
+ -> { Decidim::Core::AttachableInterface },
10
+ -> { Decidim::Core::TraceableInterface }
11
+ ]
12
+
13
+ name "ElectionAnswer"
14
+ description "An answer for an election's question"
15
+
16
+ field :id, !types.ID, "The internal ID of this answer"
17
+ field :title, !Decidim::Core::TranslatedFieldType, "The title for this answer"
18
+ field :description, Decidim::Core::TranslatedFieldType, "The description for this answer"
19
+ field :weight, types.Int, "The ordering weight for this answer"
20
+
21
+ field :proposals, types[Decidim::Proposals::ProposalType], "The proposals related to this answer"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # This type represents an election Question.
6
+ # The name is different from the model because the Question type is already defined on the Forms module.
7
+ ElectionQuestionType = GraphQL::ObjectType.define do
8
+ interfaces [
9
+ -> { Decidim::Core::TraceableInterface }
10
+ ]
11
+
12
+ name "ElectionQuestion"
13
+ description "A question for an election"
14
+
15
+ field :id, !types.ID, "The internal ID of this question"
16
+ field :title, !Decidim::Core::TranslatedFieldType, "The title for this question"
17
+ field :description, !Decidim::Core::TranslatedFieldType, "The description for this question"
18
+ field :maxSelections, !types.Int, "The maximum number of posible selections for this question", property: :max_selections
19
+ field :weight, types.Int, "The ordering weight for this question"
20
+ field :randomAnswersOrder, types.Boolean, "Should this question order answers in random order?", property: :random_answers_order
21
+
22
+ field :answers, !types[Decidim::Elections::ElectionAnswerType], "The answers for this question"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # This type represents an Election.
6
+ ElectionType = GraphQL::ObjectType.define do
7
+ interfaces [
8
+ -> { Decidim::Core::TraceableInterface }
9
+ ]
10
+
11
+ name "Election"
12
+ description "An election"
13
+
14
+ field :id, !types.ID, "The internal ID of this election"
15
+ field :title, !Decidim::Core::TranslatedFieldType, "The title for this election"
16
+ field :subtitle, !Decidim::Core::TranslatedFieldType, "The subtitle for this election"
17
+ field :description, !Decidim::Core::TranslatedFieldType, "The description for this election"
18
+ field :startTime, !Decidim::Core::DateTimeType, "The start time for this election", property: :start_time
19
+ field :endTime, !Decidim::Core::DateTimeType, "The end time for this election", property: :end_time
20
+ field :createdAt, Decidim::Core::DateTimeType, "When this election was created", property: :created_at
21
+ field :updatedAt, Decidim::Core::DateTimeType, "When this election was updated", property: :updated_at
22
+
23
+ field :questions, !types[Decidim::Elections::ElectionQuestionType], "The questions for this election"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ ElectionsType = GraphQL::ObjectType.define do
6
+ interfaces [-> { Decidim::Core::ComponentInterface }]
7
+
8
+ name "Elections"
9
+ description "An elections component of a participatory space."
10
+
11
+ connection :elections, ElectionType.connection_type do
12
+ resolve ->(component, _args, _ctx) {
13
+ ElectionsTypeHelper.base_scope(component).includes(:component)
14
+ }
15
+ end
16
+
17
+ field(:election, ElectionType) do
18
+ argument :id, !types.ID
19
+
20
+ resolve ->(component, args, _ctx) {
21
+ ElectionsTypeHelper.base_scope(component).find_by(id: args[:id])
22
+ }
23
+ end
24
+ end
25
+
26
+ module ElectionsTypeHelper
27
+ def self.base_scope(component)
28
+ Election.where(component: component)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
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
+ <div class="row column">
16
+ <%= form.number_field :weight %>
17
+ </div>
18
+
19
+ <%= render partial: "decidim/admin/shared/gallery", locals: { form: form } %>
20
+
21
+ <div class="row column">
22
+ <%= proposals_picker(form, :proposals, proposals_picker_election_question_answers_path(election, question)) %>
23
+ </div>
24
+ </div>
25
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= decidim_form_for([election, question, @form], html: { class: "form edit_answer" }) do |f| %>
2
+ <%= render partial: "form", object: f, locals: { title: t(".title") } %>
3
+
4
+ <div class="button--double form-general-submit">
5
+ <%= f.submit t(".update") %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,46 @@
1
+ <div class="card">
2
+ <div class="card-divider">
3
+ <h2 class="card-title">
4
+ <%= link_to translated_attribute(election.title), elections_path %> &gt;
5
+ <%= link_to translated_attribute(question.title), election_questions_path(election) %> &gt;
6
+ <%= t(".title") %>
7
+
8
+ <%= link_to t("actions.new", scope: "decidim.elections", name: t("models.answer.name", scope: "decidim.elections.admin")), new_election_question_answer_path(election, question), class: "button tiny button--title" if allowed_to? :create, :answer, election: election, question: question %>
9
+ </h2>
10
+ </div>
11
+
12
+ <div class="card-section">
13
+ <div class="table-scroll">
14
+ <table class="table-list">
15
+ <thead>
16
+ <tr>
17
+ <th><%= t("models.answer.fields.title", scope: "decidim.elections") %></th>
18
+ <th><%= t("models.answer.fields.proposals", scope: "decidim.elections") %></th>
19
+ <th class="actions"><%= t("actions.title", scope: "decidim.elections") %></th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% answers.each do |answer| %>
24
+ <tr data-id="<%= question.id %>">
25
+ <td>
26
+ <%= link_to translated_attribute(answer.title), edit_election_question_answer_path(election, question, answer) %>
27
+ </td>
28
+ <td>
29
+ <%= answer.linked_resources(:proposals, "related_proposals").count %>
30
+ </td>
31
+ <td class="table-list__actions">
32
+ <% if allowed_to? :update, :answer, election: election, question: question, answer: answer %>
33
+ <%= icon_link_to "pencil", edit_election_question_answer_path(election, question, answer), t("actions.edit", scope: "decidim.elections"), class: "action-icon--edit" %>
34
+ <% end %>
35
+
36
+ <% if allowed_to? :delete, :answer, election: election, question: question, answer: answer %>
37
+ <%= icon_link_to "circle-x", election_question_answer_path(election, question, answer), t("actions.destroy", scope: "decidim.elections"), method: :delete, class: "action-icon--remove", data: { confirm: t("actions.confirm_destroy", scope: "decidim.elections") } %>
38
+ <% end %>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ </div>
46
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= decidim_form_for([election, question, @form], html: { class: "form new_answer" }) do |f| %>
2
+ <%= render partial: "form", object: f, locals: { title: t(".title") } %>
3
+
4
+ <div class="button--double form-general-submit">
5
+ <%= f.submit t(".create") %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= cell "decidim/proposals/proposals_picker", current_component %>
@@ -0,0 +1,28 @@
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 :text_field, :subtitle %>
13
+ </div>
14
+
15
+ <div class="row column">
16
+ <%= form.translated :editor, :description %>
17
+ </div>
18
+
19
+ <div class="row">
20
+ <div class="column xlarge-6">
21
+ <%= form.datetime_field :start_time %>
22
+ </div>
23
+ <div class="column xlarge-6">
24
+ <%= form.datetime_field :end_time %>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= decidim_form_for(@form, html: { class: "form edit_election" }) do |f| %>
2
+ <%= render partial: "form", object: f, locals: { title: t(".title") } %>
3
+
4
+ <div class="button--double form-general-submit">
5
+ <%= f.submit t(".update") %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,54 @@
1
+ <div class="card">
2
+ <div class="card-divider">
3
+ <h2 class="card-title">
4
+ <%= t(".title") %>
5
+
6
+ <%= link_to t("actions.new", scope: "decidim.elections", name: t("models.election.name", scope: "decidim.elections.admin")), new_election_path, class: "button tiny button--title" if allowed_to? :create, :election %>
7
+ </h2>
8
+ </div>
9
+
10
+ <div class="card-section">
11
+ <div class="table-scroll">
12
+ <table class="table-list">
13
+ <thead>
14
+ <tr>
15
+ <th><%= t("models.election.fields.title", scope: "decidim.elections") %></th>
16
+ <th><%= t("models.election.fields.start_time", scope: "decidim.elections") %></th>
17
+ <th><%= t("models.election.fields.end_time", scope: "decidim.elections") %></th>
18
+ <th class="actions"><%= t("actions.title", scope: "decidim.elections") %></th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ <% elections.each do |election| %>
23
+ <tr data-id="<%= election.id %>">
24
+ <td>
25
+ <%= link_to translated_attribute(election.title), election_questions_path(election) %>
26
+ </td>
27
+ <td>
28
+ <% if election.start_time %>
29
+ <%= l election.start_time, format: :long %>
30
+ <% end %>
31
+ </td>
32
+ <td>
33
+ <% if election.end_time %>
34
+ <%= l election.end_time, format: :long %>
35
+ <% end %>
36
+ </td>
37
+ <td class="table-list__actions">
38
+ <%= icon_link_to "eye", resource_locator(election).path, t("actions.preview", scope: "decidim.elections"), class: "action-icon--preview", target: :blank %>
39
+
40
+ <% if allowed_to? :update, :election, election: election %>
41
+ <%= icon_link_to "pencil", edit_election_path(election), t("actions.edit", scope: "decidim.elections"), class: "action-icon--edit" %>
42
+ <% end %>
43
+
44
+ <% if allowed_to? :delete, :election, election: election %>
45
+ <%= icon_link_to "circle-x", election_path(election), t("actions.destroy", scope: "decidim.elections"), method: :delete, class: "action-icon--remove", data: { confirm: t("actions.confirm_destroy", scope: "decidim.elections") } %>
46
+ <% end %>
47
+ </td>
48
+ </tr>
49
+ <% end %>
50
+ </tbody>
51
+ </table>
52
+ </div>
53
+ </div>
54
+ </div>