decidim-elections 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/decidim_elections_manifest.js +2 -0
  3. data/app/assets/javascripts/decidim/elections/vote.js.es6 +170 -0
  4. data/app/assets/stylesheets/decidim/elections/elections.scss +10 -0
  5. data/app/assets/stylesheets/decidim/elections/focus/_evote.scss +279 -0
  6. data/app/assets/stylesheets/decidim/elections/focus/_focus.scss +128 -0
  7. data/app/cells/decidim/elections/election_cell.rb +22 -0
  8. data/app/cells/decidim/elections/election_m/data.erb +19 -0
  9. data/app/cells/decidim/elections/election_m/footer.erb +5 -0
  10. data/app/cells/decidim/elections/election_m_cell.rb +103 -0
  11. data/app/cells/decidim/elections/voting_step_navigation/show.erb +29 -0
  12. data/app/cells/decidim/elections/voting_step_navigation_cell.rb +42 -0
  13. data/app/commands/decidim/elections/admin/create_election.rb +14 -3
  14. data/app/commands/decidim/elections/admin/create_question.rb +2 -1
  15. data/app/commands/decidim/elections/admin/destroy_election.rb +4 -0
  16. data/app/commands/decidim/elections/admin/import_proposals_to_elections.rb +87 -0
  17. data/app/commands/decidim/elections/admin/publish_election.rb +54 -0
  18. data/app/commands/decidim/elections/admin/unpublish_election.rb +43 -0
  19. data/app/commands/decidim/elections/admin/update_election.rb +15 -3
  20. data/app/commands/decidim/elections/admin/update_question.rb +2 -1
  21. data/app/controllers/concerns/decidim/elections/orderable.rb +36 -0
  22. data/app/controllers/decidim/elections/admin/answers_controller.rb +9 -1
  23. data/app/controllers/decidim/elections/admin/elections_controller.rb +26 -0
  24. data/app/controllers/decidim/elections/admin/proposals_imports_controller.rb +53 -0
  25. data/app/controllers/decidim/elections/elections_controller.rb +68 -0
  26. data/app/controllers/decidim/elections/votes_controller.rb +50 -0
  27. data/app/events/decidim/elections/election_published_event.rb +8 -0
  28. data/app/forms/decidim/elections/admin/answer_import_proposals_form.rb +41 -0
  29. data/app/forms/decidim/elections/admin/election_form.rb +15 -2
  30. data/app/forms/decidim/elections/admin/question_form.rb +1 -0
  31. data/app/helpers/decidim/elections/application_helper.rb +12 -0
  32. data/app/helpers/decidim/elections/election_cells_helper.rb +12 -0
  33. data/app/helpers/decidim/elections/votes_helper.rb +21 -0
  34. data/app/models/decidim/elections/answer.rb +5 -1
  35. data/app/models/decidim/elections/election.rb +69 -0
  36. data/app/models/decidim/elections/question.rb +9 -2
  37. data/app/permissions/decidim/elections/admin/permissions.rb +13 -3
  38. data/app/permissions/decidim/elections/permissions.rb +35 -0
  39. data/app/presenters/decidim/elections/admin_log/election_presenter.rb +41 -0
  40. data/app/presenters/decidim/elections/election_presenter.rb +28 -0
  41. data/app/queries/decidim/elections/filtered_elections.rb +37 -0
  42. data/app/services/decidim/elections/election_search.rb +35 -0
  43. data/app/types/decidim/elections/election_question_type.rb +2 -2
  44. data/app/types/decidim/elections/election_type.rb +2 -1
  45. data/app/types/decidim/elections/elections_type.rb +1 -1
  46. data/app/views/decidim/elections/admin/answers/index.html.erb +2 -1
  47. data/app/views/decidim/elections/admin/elections/_form.html.erb +2 -4
  48. data/app/views/decidim/elections/admin/elections/index.html.erb +23 -1
  49. data/app/views/decidim/elections/admin/proposals_imports/new.html.erb +24 -0
  50. data/app/views/decidim/elections/admin/questions/_form.html.erb +4 -0
  51. data/app/views/decidim/elections/admin/questions/index.html.erb +3 -1
  52. data/app/views/decidim/elections/elections/_count.html.erb +1 -0
  53. data/app/views/decidim/elections/elections/_election.html.erb +1 -0
  54. data/app/views/decidim/elections/elections/_elections.html.erb +24 -0
  55. data/app/views/decidim/elections/elections/_filters.html.erb +24 -0
  56. data/app/views/decidim/elections/elections/_filters_small_view.html.erb +18 -0
  57. data/app/views/decidim/elections/elections/index.html.erb +24 -0
  58. data/app/views/decidim/elections/elections/index.js.erb +10 -0
  59. data/app/views/decidim/elections/elections/show.html.erb +62 -0
  60. data/app/views/decidim/elections/votes/_election_votes_confirm.html.erb +53 -0
  61. data/app/views/decidim/elections/votes/_election_votes_confirm_footer.html.erb +24 -0
  62. data/app/views/decidim/elections/votes/_election_votes_confirmed.html.erb +26 -0
  63. data/app/views/decidim/elections/votes/_election_votes_encrypting.html.erb +20 -0
  64. data/app/views/decidim/elections/votes/_election_votes_header.html.erb +8 -0
  65. data/app/views/decidim/elections/votes/_election_votes_modal.html.erb +46 -0
  66. data/app/views/decidim/elections/votes/_election_votes_question.html.erb +49 -0
  67. data/app/views/decidim/elections/votes/_election_votes_steps_header.html.erb +12 -0
  68. data/app/views/decidim/elections/votes/new.html.erb +44 -0
  69. data/app/views/layouts/decidim/_election_votes_header.html.erb +13 -0
  70. data/app/views/layouts/decidim/election_votes.html.erb +24 -0
  71. data/config/locales/am-ET.yml +1 -0
  72. data/config/locales/bg.yml +7 -0
  73. data/config/locales/ca.yml +136 -6
  74. data/config/locales/cs.yml +133 -1
  75. data/config/locales/da.yml +1 -0
  76. data/config/locales/de.yml +93 -1
  77. data/config/locales/el.yml +11 -1
  78. data/config/locales/en.yml +131 -1
  79. data/config/locales/eo.yml +1 -0
  80. data/config/locales/es-MX.yml +131 -1
  81. data/config/locales/es-PY.yml +131 -1
  82. data/config/locales/es.yml +134 -4
  83. data/config/locales/et.yml +1 -0
  84. data/config/locales/fi-plain.yml +131 -1
  85. data/config/locales/fi.yml +131 -1
  86. data/config/locales/fr-CA.yml +131 -1
  87. data/config/locales/fr.yml +131 -1
  88. data/config/locales/hr.yml +1 -0
  89. data/config/locales/hu.yml +18 -0
  90. data/config/locales/is.yml +1 -0
  91. data/config/locales/it.yml +88 -1
  92. data/config/locales/ja-JP.yml +95 -8
  93. data/config/locales/ja.yml +254 -0
  94. data/config/locales/ko-KR.yml +1 -0
  95. data/config/locales/ko.yml +1 -0
  96. data/config/locales/lt.yml +1 -0
  97. data/config/locales/{lv-LV.yml → lv.yml} +11 -1
  98. data/config/locales/mt.yml +1 -0
  99. data/config/locales/nl.yml +88 -1
  100. data/config/locales/no.yml +64 -0
  101. data/config/locales/om-ET.yml +1 -0
  102. data/config/locales/pl.yml +124 -1
  103. data/config/locales/pt.yml +84 -1
  104. data/config/locales/ro-RO.yml +85 -1
  105. data/config/locales/so-SO.yml +1 -0
  106. data/config/locales/sv.yml +121 -1
  107. data/config/locales/ti-ER.yml +1 -0
  108. data/config/locales/vi-VN.yml +1 -0
  109. data/config/locales/vi.yml +1 -0
  110. data/config/locales/zh-CN.yml +254 -0
  111. data/config/locales/zh-TW.yml +1 -0
  112. data/db/migrate/20200601141412_add_published_at_to_elections.rb +7 -0
  113. data/db/migrate/20200807125040_remove_subtitle_from_decidim_elections.rb +7 -0
  114. data/db/migrate/20200910103648_add_min_selections_to_decidim_elections_questions.rb +7 -0
  115. data/lib/decidim/elections.rb +5 -0
  116. data/lib/decidim/elections/admin_engine.rb +7 -0
  117. data/lib/decidim/elections/bulletin_board_client.rb +35 -0
  118. data/lib/decidim/elections/component.rb +17 -12
  119. data/lib/decidim/elections/engine.rb +8 -1
  120. data/lib/decidim/elections/test/factories.rb +59 -6
  121. data/lib/decidim/elections/version.rb +1 -1
  122. data/lib/tasks/decidim_elections.rake +16 -0
  123. metadata +79 -13
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ module Admin
6
+ # A form object to be used when admin users want to import a collection of proposals
7
+ # from another component into answers resource.
8
+ class AnswerImportProposalsForm < Decidim::Form
9
+ mimic :proposals_import
10
+
11
+ attribute :origin_component_id, Integer
12
+ attribute :import_all_accepted_proposals, Boolean
13
+ attribute :weight, Integer, default: 0
14
+
15
+ validates :origin_component_id, :origin_component, :current_component, presence: true
16
+
17
+ def origin_component
18
+ @origin_component ||= origin_components.find_by(id: origin_component_id)
19
+ end
20
+
21
+ def origin_components
22
+ @origin_components ||= current_participatory_space.components.where(manifest_name: :proposals)
23
+ end
24
+
25
+ def origin_components_collection
26
+ origin_components.map do |component|
27
+ [component.name[I18n.locale.to_s], component.id]
28
+ end
29
+ end
30
+
31
+ def election
32
+ @election ||= context[:election]
33
+ end
34
+
35
+ def question
36
+ @question ||= context[:question]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -8,16 +8,29 @@ module Decidim
8
8
  include TranslatableAttributes
9
9
 
10
10
  translatable_attribute :title, String
11
- translatable_attribute :subtitle, String
12
11
  translatable_attribute :description, String
13
12
  attribute :start_time, Decidim::Attributes::TimeWithZone
14
13
  attribute :end_time, Decidim::Attributes::TimeWithZone
14
+ attribute :attachment, AttachmentForm
15
+ attribute :photos, Array[String]
16
+ attribute :add_photos, Array
15
17
 
16
18
  validates :title, translatable_presence: true
17
- validates :subtitle, translatable_presence: true
18
19
  validates :description, translatable_presence: true
19
20
  validates :start_time, presence: true, date: { before: :end_time }
20
21
  validates :end_time, presence: true, date: { after: :start_time }
22
+ validate :notify_missing_attachment_if_errored
23
+
24
+ private
25
+
26
+ # This method will add an error to the `photos` field only if there's
27
+ # any error in any other field. This is needed because when the form has
28
+ # an error, the attachment is lost, so we need a way to inform the user of
29
+ # this problem.
30
+ def notify_missing_attachment_if_errored
31
+ errors.add(:attachment, :needs_to_be_reattached) if errors.any? && attachment.present?
32
+ errors.add(:add_photos, :needs_to_be_reattached) if errors.any? && add_photos.present?
33
+ end
21
34
  end
22
35
  end
23
36
  end
@@ -12,6 +12,7 @@ module Decidim
12
12
  attribute :max_selections, Integer, default: 1
13
13
  attribute :weight, Integer, default: 0
14
14
  attribute :random_answers_order, Boolean, default: true
15
+ attribute :min_selections, Integer, default: 1
15
16
 
16
17
  validates :title, translatable_presence: true
17
18
  validates :description, translatable_presence: true
@@ -5,6 +5,18 @@ module Decidim
5
5
  # Custom helpers, scoped to the elections engine.
6
6
  #
7
7
  module ApplicationHelper
8
+ include Decidim::CheckBoxesTreeHelper
9
+
10
+ def state_filter_values
11
+ TreeNode.new(
12
+ TreePoint.new("", t("elections.elections.filters.all", scope: "decidim")),
13
+ [
14
+ TreePoint.new("active", t("elections.elections.filters.active", scope: "decidim")),
15
+ TreePoint.new("upcoming", t("elections.elections.filters.upcoming", scope: "decidim")),
16
+ TreePoint.new("finished", t("elections.elections.filters.finished", scope: "decidim"))
17
+ ]
18
+ )
19
+ end
8
20
  end
9
21
  end
10
22
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # Custom helpers for election cells.
6
+ #
7
+ module ElectionCellsHelper
8
+ include PaginateHelper
9
+ include Decidim::Comments::CommentsHelper
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # Custom helpers for the voting booth views.
6
+ #
7
+ module VotesHelper
8
+ def ordered_answers(question)
9
+ if question.random_answers_order
10
+ question.answers.shuffle
11
+ else
12
+ question.answers.sort_by { |answer| [answer.weight, answer.id] }
13
+ end
14
+ end
15
+
16
+ def more_information?(answer)
17
+ answer.description || answer.proposals.any? || answer.photos.any?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -11,13 +11,17 @@ module Decidim
11
11
  include Traceable
12
12
  include Loggable
13
13
 
14
- delegate :component, to: :question
15
14
  delegate :organization, :participatory_space, to: :component
16
15
 
17
16
  belongs_to :question, foreign_key: "decidim_elections_question_id", class_name: "Decidim::Elections::Question", inverse_of: :answers
17
+ has_one :election, through: :question, foreign_key: "decidim_elections_election_id", class_name: "Decidim::Elections::Election"
18
+ has_one :component, through: :election, foreign_key: "decidim_component_id", class_name: "Decidim::Component"
18
19
 
19
20
  default_scope { order(weight: :asc, id: :asc) }
20
21
 
22
+ # Public: Get all the proposals related to the answer
23
+ #
24
+ # Returns an ActiveRecord::Relation.
21
25
  def proposals
22
26
  linked_resources(:proposals, "related_proposals")
23
27
  end
@@ -5,18 +5,87 @@ module Decidim
5
5
  # The data store for an Election in the Decidim::Elections component. It stores a
6
6
  # title, description and any other useful information to perform an election.
7
7
  class Election < ApplicationRecord
8
+ include Decidim::HasAttachments
9
+ include Decidim::HasAttachmentCollections
10
+ include Decidim::Publicable
8
11
  include Decidim::Resourceable
9
12
  include Decidim::HasComponent
13
+ include Decidim::TranslatableResource
10
14
  include Traceable
11
15
  include Loggable
12
16
 
17
+ translatable_fields :title, :description
18
+
13
19
  component_manifest_name "elections"
14
20
 
15
21
  has_many :questions, foreign_key: "decidim_elections_election_id", class_name: "Decidim::Elections::Question", inverse_of: :election, dependent: :destroy
16
22
 
23
+ scope :active, lambda {
24
+ where("start_time <= ?", Time.current)
25
+ .where("end_time >= ?", Time.current)
26
+ }
27
+
28
+ scope :upcoming, lambda {
29
+ where("start_time > ?", Time.current)
30
+ .where("end_time > ?", Time.current)
31
+ }
32
+
33
+ scope :finished, lambda {
34
+ where("start_time < ?", Time.current)
35
+ .where("end_time < ?", Time.current)
36
+ }
37
+
38
+ def self.log_presenter_class_for(_log)
39
+ Decidim::Elections::AdminLog::ElectionPresenter
40
+ end
41
+
42
+ # Public: Checks if the election started
43
+ #
44
+ # Returns a boolean.
17
45
  def started?
18
46
  start_time <= Time.current
19
47
  end
48
+
49
+ # Public: Checks if the election finished
50
+ #
51
+ # Returns a boolean.
52
+ def finished?
53
+ end_time < Time.current
54
+ end
55
+
56
+ # Public: Checks if the election ongoing now
57
+ #
58
+ # Returns a boolean.
59
+ def ongoing?
60
+ started? && !finished?
61
+ end
62
+
63
+ # Public: Checks if the election questions are valid
64
+ #
65
+ # Returns a boolean.
66
+ def valid_questions?
67
+ questions.each do |question|
68
+ return false unless question.valid_max_selection?
69
+ end
70
+ end
71
+
72
+ # Public: Gets the voting period status of the election
73
+ #
74
+ # Returns one of these symbols: upcoming, ongoing or finished
75
+ def voting_period_status
76
+ if finished?
77
+ :finished
78
+ elsif started?
79
+ :ongoing
80
+ else
81
+ :upcoming
82
+ end
83
+ end
84
+
85
+ # Public: Overrides the Resourceable concern method to allow setting permissions at resource level
86
+ def allow_resource_permissions?
87
+ true
88
+ end
20
89
  end
21
90
  end
22
91
  end
@@ -9,12 +9,19 @@ module Decidim
9
9
  include Traceable
10
10
  include Loggable
11
11
 
12
- delegate :component, to: :election
13
-
14
12
  belongs_to :election, foreign_key: "decidim_elections_election_id", class_name: "Decidim::Elections::Election", inverse_of: :questions
15
13
  has_many :answers, foreign_key: "decidim_elections_question_id", class_name: "Decidim::Elections::Answer", inverse_of: :question, dependent: :destroy
16
14
 
15
+ has_one :component, through: :election, foreign_key: "decidim_component_id", class_name: "Decidim::Component"
16
+
17
17
  default_scope { order(weight: :asc, id: :asc) }
18
+
19
+ # Public: Checks if enough answers are given for max_selections attribute
20
+ #
21
+ # Returns a boolean.
22
+ def valid_max_selection?
23
+ max_selections <= answers.count
24
+ end
18
25
  end
19
26
  end
20
27
  end
@@ -12,15 +12,17 @@ module Decidim
12
12
  case permission_action.action
13
13
  when :create, :update, :delete
14
14
  allow_if_not_started
15
+ when :import_proposals
16
+ allow_if_not_started
15
17
  end
16
18
  when :election
17
19
  case permission_action.action
18
20
  when :create, :read
19
21
  allow!
20
- when :update
21
- toggle_allow(election)
22
- when :delete
22
+ when :delete, :update, :unpublish
23
23
  allow_if_not_started
24
+ when :publish
25
+ allow_if_valid_and_not_started
24
26
  end
25
27
  end
26
28
 
@@ -33,9 +35,17 @@ module Decidim
33
35
  @election ||= context.fetch(:election, nil)
34
36
  end
35
37
 
38
+ def question
39
+ @question ||= context.fetch(:question, nil)
40
+ end
41
+
36
42
  def allow_if_not_started
37
43
  toggle_allow(election && !election.started?)
38
44
  end
45
+
46
+ def allow_if_valid_and_not_started
47
+ toggle_allow(election && !election.started? && election.valid_questions?)
48
+ end
39
49
  end
40
50
  end
41
51
  end
@@ -4,13 +4,48 @@ module Decidim
4
4
  module Elections
5
5
  class Permissions < Decidim::DefaultPermissions
6
6
  def permissions
7
+ # Anonymous users can only view elections
8
+ toggle_allow(can_view?) if permission_action.scope == :public && permission_action.subject == :election && permission_action.action == :view
9
+
7
10
  return permission_action unless user
8
11
 
9
12
  # Delegate the admin permission checks to the admin permissions class
10
13
  return Decidim::Elections::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin
11
14
 
15
+ return permission_action if permission_action.scope != :public
16
+ return permission_action if permission_action.subject != :election
17
+
18
+ case permission_action.action
19
+ when :vote
20
+ toggle_allow(can_vote?)
21
+ when :preview
22
+ toggle_allow(can_preview?)
23
+ end
24
+
12
25
  permission_action
13
26
  end
27
+
28
+ private
29
+
30
+ def can_view?
31
+ election.published? || user&.admin?
32
+ end
33
+
34
+ def can_vote?
35
+ election.published? && election.ongoing? && authorized_to_vote?
36
+ end
37
+
38
+ def can_preview?
39
+ user.admin? && !can_vote?
40
+ end
41
+
42
+ def authorized_to_vote?
43
+ authorized?(:vote, resource: election)
44
+ end
45
+
46
+ def election
47
+ @election ||= context[:election]
48
+ end
14
49
  end
15
50
  end
16
51
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ module AdminLog
6
+ # This class holds the logic to present a `Decidim::Election`
7
+ # for the `AdminLog` log.
8
+ #
9
+ # Usage should be automatic and you shouldn't need to call this class
10
+ # directly, but here's an example:
11
+ #
12
+ # action_log = Decidim::ActionLog.last
13
+ # view_helpers # => this comes from the views
14
+ # ElectionPresenter.new(action_log, view_helpers).present
15
+ class ElectionPresenter < Decidim::Log::BasePresenter
16
+ private
17
+
18
+ def diff_fields_mapping
19
+ {
20
+ name: :i18n,
21
+ published_at: :date,
22
+ weight: :integer
23
+ }
24
+ end
25
+
26
+ def i18n_labels_scope
27
+ "activemodel.attributes.election"
28
+ end
29
+
30
+ def action_string
31
+ case action
32
+ when "publish", "unpublish"
33
+ "decidim.elections.admin_log.election.#{action}"
34
+ else
35
+ super
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ #
6
+ # Decorator for election
7
+ #
8
+ class ElectionPresenter < SimpleDelegator
9
+ include Decidim::SanitizeHelper
10
+ include Decidim::TranslatableAttributes
11
+
12
+ def election
13
+ __getobj__
14
+ end
15
+
16
+ def title
17
+ content = translated_attribute(election.title)
18
+ decidim_html_escape(content)
19
+ end
20
+
21
+ def description(strip_tags: false)
22
+ content = translated_attribute(election.description)
23
+ content = strip_tags(content) if strip_tags
24
+ content
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Elections
5
+ # A class used to find elections filtered by its state
6
+ class FilteredElections < Rectify::Query
7
+ # Syntactic sugar to initialize the class and return the queried objects.
8
+ #
9
+ # components - An array of Decidim::Component
10
+ # start_at - A date to filter resources created after it
11
+ # end_at - A date to filter resources created before it.
12
+ def self.for(components, start_at = nil, end_at = nil)
13
+ new(components, start_at, end_at).query
14
+ end
15
+
16
+ # Initializes the class.
17
+ #
18
+ # components - An array of Decidim::Component
19
+ # start_at - A date to filter resources created after it
20
+ # end_at - A date to filter resources created before it.
21
+ def initialize(components, start_at = nil, end_at = nil)
22
+ @components = components
23
+ @start_at = start_at
24
+ @end_at = end_at
25
+ end
26
+
27
+ # Finds the Projects scoped to an array of components and filtered
28
+ # by a range of dates.
29
+ def query
30
+ elections = Decidim::Elections::Election.where(component: @components)
31
+ elections = elections.where("created_at >= ?", @start_at) if @start_at.present?
32
+ elections = elections.where("created_at <= ?", @end_at) if @end_at.present?
33
+ elections
34
+ end
35
+ end
36
+ end
37
+ end