decidim-elections 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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>