decidim-meetings 0.28.3 → 0.29.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/meetings/attending_organizations_list_cell.rb +1 -1
  3. data/app/cells/decidim/meetings/cancel_registration_meeting_button_cell.rb +1 -2
  4. data/app/cells/decidim/meetings/highlighted_meetings_for_component_cell.rb +1 -2
  5. data/app/cells/decidim/meetings/join_meeting_button/remaining_slots.erb +1 -0
  6. data/app/cells/decidim/meetings/join_meeting_button_cell.rb +0 -3
  7. data/app/cells/decidim/meetings/meeting_card_metadata_cell.rb +0 -3
  8. data/app/cells/decidim/meetings/meeting_cell.rb +0 -1
  9. data/app/cells/decidim/meetings/meeting_l_cell.rb +4 -2
  10. data/app/cells/decidim/meetings/meeting_url/show.erb +5 -5
  11. data/app/cells/decidim/meetings/meeting_url_cell.rb +0 -1
  12. data/app/cells/decidim/meetings/online_meeting_link_cell.rb +0 -2
  13. data/app/cells/decidim/meetings/question_responses/show.erb +7 -3
  14. data/app/cells/decidim/meetings/question_responses_cell.rb +0 -2
  15. data/app/commands/decidim/meetings/admin/copy_meeting.rb +2 -2
  16. data/app/commands/decidim/meetings/admin/create_agenda.rb +9 -37
  17. data/app/commands/decidim/meetings/admin/create_meeting.rb +28 -61
  18. data/app/commands/decidim/meetings/admin/destroy_meeting.rb +4 -33
  19. data/app/commands/decidim/meetings/admin/update_agenda.rb +7 -35
  20. data/app/commands/decidim/meetings/admin/update_meeting.rb +30 -68
  21. data/app/commands/decidim/meetings/admin/update_question_status.rb +1 -1
  22. data/app/commands/decidim/meetings/admin/update_questionnaire.rb +19 -11
  23. data/app/commands/decidim/meetings/admin/update_registrations.rb +20 -46
  24. data/app/commands/decidim/meetings/create_answer.rb +5 -4
  25. data/app/commands/decidim/meetings/create_meeting.rb +38 -62
  26. data/app/commands/decidim/meetings/join_meeting.rb +18 -19
  27. data/app/commands/decidim/meetings/update_meeting.rb +6 -7
  28. data/app/commands/decidim/meetings/withdraw_meeting.rb +1 -1
  29. data/app/controllers/concerns/decidim/meetings/admin/invites/filterable.rb +33 -0
  30. data/app/controllers/decidim/meetings/admin/agenda_controller.rb +1 -1
  31. data/app/controllers/decidim/meetings/admin/invites_controller.rb +6 -6
  32. data/app/controllers/decidim/meetings/admin/meetings_controller.rb +2 -2
  33. data/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb +12 -0
  34. data/app/controllers/decidim/meetings/application_controller.rb +1 -1
  35. data/app/controllers/decidim/meetings/live_events_controller.rb +1 -1
  36. data/app/controllers/decidim/meetings/meetings_controller.rb +3 -33
  37. data/app/controllers/decidim/meetings/polls/answers_controller.rb +10 -2
  38. data/app/controllers/decidim/meetings/registrations_controller.rb +3 -3
  39. data/app/forms/decidim/meetings/admin/meeting_registrations_form.rb +1 -1
  40. data/app/forms/decidim/meetings/admin/question_form.rb +8 -4
  41. data/app/forms/decidim/meetings/answer_form.rb +4 -4
  42. data/app/forms/decidim/meetings/meeting_form.rb +1 -1
  43. data/app/helpers/decidim/meetings/application_helper.rb +1 -5
  44. data/app/mailers/decidim/meetings/close_meeting_reminder_mailer.rb +1 -1
  45. data/app/models/decidim/meetings/meeting.rb +10 -4
  46. data/app/models/decidim/meetings/poll.rb +8 -0
  47. data/app/models/decidim/meetings/question.rb +1 -0
  48. data/app/models/decidim/meetings/questionnaire.rb +0 -6
  49. data/app/packs/src/decidim/meetings/admin/meetings_form.js +3 -3
  50. data/app/packs/src/decidim/meetings/meetings_form.js +3 -3
  51. data/app/packs/src/decidim/meetings/meetings_polls.js +5 -0
  52. data/app/packs/src/decidim/meetings/poll.component.js +32 -4
  53. data/app/packs/stylesheets/decidim/meetings/_item.scss +109 -1
  54. data/app/packs/stylesheets/decidim/meetings/_live_event.scss +0 -94
  55. data/app/permissions/decidim/meetings/permissions.rb +20 -0
  56. data/app/presenters/decidim/meetings/meeting_presenter.rb +5 -2
  57. data/app/services/decidim/meetings/calendar/component_calendar.rb +1 -1
  58. data/app/services/decidim/meetings/calendar/meeting_to_event.rb +1 -1
  59. data/app/services/decidim/meetings/close_meeting_reminder_generator.rb +1 -1
  60. data/app/services/decidim/meetings/meeting_iframe_embedder.rb +2 -2
  61. data/app/views/decidim/meetings/admin/agenda/_agenda_item.html.erb +3 -3
  62. data/app/views/decidim/meetings/admin/agenda/_agenda_item_child.html.erb +3 -3
  63. data/app/views/decidim/meetings/admin/agenda/_agenda_item_fields.html.erb +3 -3
  64. data/app/views/decidim/meetings/admin/invite_join_meeting_mailer/invite.html.erb +3 -1
  65. data/app/views/decidim/meetings/admin/invites/_form.html.erb +4 -4
  66. data/app/views/decidim/meetings/admin/invites/index.html.erb +4 -34
  67. data/app/views/decidim/meetings/admin/meetings/_form.html.erb +8 -8
  68. data/app/views/decidim/meetings/admin/meetings/_service.html.erb +3 -3
  69. data/app/views/decidim/meetings/admin/meetings/index.html.erb +2 -2
  70. data/app/views/decidim/meetings/admin/poll/_answer_option.html.erb +1 -1
  71. data/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb +1 -1
  72. data/app/views/decidim/meetings/admin/poll/_form.html.erb +26 -25
  73. data/app/views/decidim/meetings/admin/poll/_question.html.erb +18 -16
  74. data/app/views/decidim/meetings/admin/poll/edit.html.erb +4 -6
  75. data/app/views/decidim/meetings/admin/registrations/edit.html.erb +2 -4
  76. data/app/views/decidim/meetings/close_meeting_reminder_mailer/close_meeting_reminder.html.erb +1 -1
  77. data/app/views/decidim/meetings/layouts/live_event.html.erb +1 -15
  78. data/app/views/decidim/meetings/meetings/_datetime.html.erb +4 -4
  79. data/app/views/decidim/meetings/meetings/_form.html.erb +17 -17
  80. data/app/views/decidim/meetings/meetings/_meeting.html.erb +2 -1
  81. data/app/views/decidim/meetings/meetings/_meeting_agenda.html.erb +2 -2
  82. data/app/views/decidim/meetings/meetings/_meeting_aside.html.erb +3 -1
  83. data/app/views/decidim/meetings/meetings/_meeting_minutes.html.erb +1 -1
  84. data/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb +15 -0
  85. data/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb +6 -10
  86. data/app/views/decidim/meetings/polls/answers/_single_option.html.erb +4 -10
  87. data/app/views/decidim/meetings/polls/answers/admin.html.erb +34 -0
  88. data/app/views/decidim/meetings/polls/answers/index.html.erb +36 -0
  89. data/app/views/decidim/meetings/polls/questions/_closed_question.html.erb +8 -2
  90. data/app/views/decidim/meetings/polls/questions/_index_admin.html.erb +27 -24
  91. data/app/views/decidim/meetings/polls/questions/_published_question.html.erb +14 -11
  92. data/app/views/decidim/meetings/polls/questions/_question.html.erb +1 -1
  93. data/app/views/decidim/meetings/polls/questions/index.js.erb +3 -3
  94. data/app/views/decidim/meetings/polls/questions/index_admin.js.erb +3 -3
  95. data/app/views/devise/mailer/join_meeting.html.erb +3 -1
  96. data/app/views/devise/mailer/join_meeting.text.erb +3 -1
  97. data/config/locales/ar.yml +0 -10
  98. data/config/locales/bg.yml +44 -12
  99. data/config/locales/ca.yml +44 -12
  100. data/config/locales/cs.yml +24 -12
  101. data/config/locales/de.yml +44 -12
  102. data/config/locales/el.yml +0 -10
  103. data/config/locales/en.yml +44 -12
  104. data/config/locales/es-MX.yml +44 -12
  105. data/config/locales/es-PY.yml +44 -12
  106. data/config/locales/es.yml +44 -12
  107. data/config/locales/eu.yml +44 -12
  108. data/config/locales/fi-plain.yml +44 -12
  109. data/config/locales/fi.yml +44 -12
  110. data/config/locales/fr-CA.yml +44 -12
  111. data/config/locales/fr.yml +44 -12
  112. data/config/locales/ga-IE.yml +0 -5
  113. data/config/locales/gl.yml +0 -4
  114. data/config/locales/hu.yml +0 -7
  115. data/config/locales/id-ID.yml +0 -3
  116. data/config/locales/is-IS.yml +0 -3
  117. data/config/locales/it.yml +0 -8
  118. data/config/locales/ja.yml +44 -12
  119. data/config/locales/lb.yml +0 -3
  120. data/config/locales/lt.yml +0 -10
  121. data/config/locales/lv.yml +0 -3
  122. data/config/locales/nl.yml +0 -10
  123. data/config/locales/no.yml +0 -9
  124. data/config/locales/pl.yml +44 -12
  125. data/config/locales/pt-BR.yml +17 -12
  126. data/config/locales/pt.yml +0 -8
  127. data/config/locales/ro-RO.yml +0 -10
  128. data/config/locales/ru.yml +0 -3
  129. data/config/locales/sk.yml +0 -3
  130. data/config/locales/sv.yml +3 -14
  131. data/config/locales/tr-TR.yml +0 -4
  132. data/config/locales/uk.yml +0 -3
  133. data/config/locales/zh-CN.yml +0 -4
  134. data/config/locales/zh-TW.yml +0 -10
  135. data/db/migrate/20240130135858_add_withdrawn_fields_on_meetings.rb +23 -0
  136. data/decidim-meetings.gemspec +1 -1
  137. data/lib/decidim/api/meeting_type.rb +3 -0
  138. data/lib/decidim/meetings/component.rb +2 -2
  139. data/lib/decidim/meetings/engine.rb +5 -1
  140. data/lib/decidim/meetings/meeting_serializer.rb +3 -1
  141. data/lib/decidim/meetings/seeds.rb +25 -15
  142. data/lib/decidim/meetings/test/factories.rb +1 -1
  143. data/lib/decidim/meetings/test/notifications_handling.rb +1 -1
  144. data/lib/decidim/meetings/version.rb +1 -1
  145. data/lib/tasks/decidim_meetings.rake +1 -1
  146. metadata +23 -19
  147. data/app/views/decidim/meetings/admin/agenda/show.html.erb +0 -0
@@ -9,11 +9,19 @@ module Decidim
9
9
 
10
10
  helper_method :question
11
11
 
12
+ def admin
13
+ enforce_permission_to(:update, :poll, meeting:)
14
+ end
15
+
16
+ def index
17
+ enforce_permission_to(:reply_poll, :meeting, meeting:)
18
+ end
19
+
12
20
  def create
13
21
  enforce_permission_to(:create, :answer, question:)
14
- @form = form(AnswerForm).from_params(params, question:, current_user:)
22
+ @form = form(AnswerForm).from_params(params.merge(question:, current_user:))
15
23
 
16
- CreateAnswer.call(@form, current_user, questionnaire) do
24
+ CreateAnswer.call(@form, questionnaire) do
17
25
  # Both :ok and :invalid render the same template, because
18
26
  # validation errors are displayed in the template
19
27
  respond_to do |format|
@@ -11,7 +11,7 @@ module Decidim
11
11
 
12
12
  @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token:)
13
13
 
14
- JoinMeeting.call(meeting, current_user, @form) do
14
+ JoinMeeting.call(meeting, @form) do
15
15
  on(:ok) do
16
16
  flash[:notice] = I18n.t("registrations.create.success", scope: "decidim.meetings")
17
17
  redirect_to after_answer_path
@@ -32,9 +32,9 @@ module Decidim
32
32
  def create
33
33
  enforce_permission_to(:register, :meeting, meeting:)
34
34
 
35
- @form = JoinMeetingForm.from_params(params)
35
+ @form = JoinMeetingForm.from_params(params).with_context(current_user:)
36
36
 
37
- JoinMeeting.call(meeting, current_user, @form) do
37
+ JoinMeeting.call(meeting, @form) do
38
38
  on(:ok) do
39
39
  flash[:notice] = I18n.t("registrations.create.success", scope: "decidim.meetings")
40
40
  redirect_after_path
@@ -37,7 +37,7 @@ module Decidim
37
37
 
38
38
  # We need this method to ensure the form object will always have an ID,
39
39
  # and thus its `to_param` method will always return a significant value.
40
- # If we remove this method, get an error onn the `update` action and try
40
+ # If we remove this method, get an error on the `update` action and try
41
41
  # to resubmit the form, the form will not hold an ID, so the `to_param`
42
42
  # method will return an empty string and Rails will treat this as a
43
43
  # `create` action, thus raising an error since this action is not defined
@@ -18,10 +18,10 @@ module Decidim
18
18
  translatable_attribute :description, String
19
19
 
20
20
  validates :position, numericality: { greater_than_or_equal_to: 0 }
21
- validates :question_type, inclusion: { in: Decidim::Meetings::Question::QUESTION_TYPES }
22
- validates :max_choices, numericality: { only_integer: true, greater_than: 1, less_than_or_equal_to: ->(form) { form.number_of_options } }, allow_blank: true
21
+ validates :question_type, inclusion: { in: Decidim::Meetings::Question::QUESTION_TYPES }, if: :editable?
22
+ validates :max_choices, numericality: { only_integer: true, greater_than: 1, less_than_or_equal_to: ->(form) { form.number_of_options } }, allow_blank: true, if: :editable?
23
23
  validates :body, translatable_presence: true, if: :requires_body?
24
- validates :answer_options, presence: true
24
+ validates :answer_options, presence: true, if: :editable?
25
25
 
26
26
  def to_param
27
27
  return id if id.present?
@@ -29,6 +29,10 @@ module Decidim
29
29
  "questionnaire-question-id"
30
30
  end
31
31
 
32
+ def editable?
33
+ @editable ||= id.blank? || Decidim::Meetings::Question.unpublished.unanswered.exists?(id:)
34
+ end
35
+
32
36
  def number_of_options
33
37
  answer_options.size
34
38
  end
@@ -36,7 +40,7 @@ module Decidim
36
40
  private
37
41
 
38
42
  def requires_body?
39
- !deleted
43
+ editable? && !deleted
40
44
  end
41
45
  end
42
46
  end
@@ -24,8 +24,8 @@ module Decidim
24
24
  @answer ||= Decidim::Meetings::Answer.find_by(decidim_user_id: current_user.id, decidim_question_id: question_id) if current_user
25
25
  end
26
26
 
27
- def label(idx)
28
- base = "#{idx + 1}. #{translated_attribute(question.body)}"
27
+ def label
28
+ base = translated_attribute(question.body)
29
29
  base += " (#{max_choices_label})" if question.max_choices
30
30
  base
31
31
  end
@@ -43,13 +43,13 @@ module Decidim
43
43
  end
44
44
 
45
45
  def selected_choices
46
- choices.select(&:body)
46
+ choices.select(&:answer_option_id)
47
47
  end
48
48
 
49
49
  private
50
50
 
51
51
  def max_choices
52
- errors.add(:choices, :too_many) if selected_choices.size > question.max_choices
52
+ errors.add(:choices, :too_many, count: question.max_choices) if selected_choices.size > question.max_choices
53
53
  end
54
54
 
55
55
  def mandatory_label
@@ -56,7 +56,7 @@ module Decidim
56
56
 
57
57
  alias component current_component
58
58
 
59
- # Finds the Scope from the given decidim_scope_id, uses the compoenent scope if missing.
59
+ # Finds the Scope from the given decidim_scope_id, uses the component scope if missing.
60
60
  #
61
61
  # Returns a Decidim::Scope
62
62
  def scope
@@ -59,11 +59,7 @@ module Decidim
59
59
 
60
60
  # If the content is safe, HTML tags are sanitized, otherwise, they are stripped.
61
61
  def render_meeting_body(meeting)
62
- render_meeting_sanitize_field(meeting, :description)
63
- end
64
-
65
- def render_meeting_sanitize_field(meeting, field)
66
- sanitized = render_sanitized_content(meeting, field)
62
+ sanitized = render_sanitized_content(meeting, :description)
67
63
  if safe_content?
68
64
  Decidim::ContentProcessor.render_without_format(sanitized).html_safe
69
65
  else
@@ -27,7 +27,7 @@ module Decidim
27
27
  to: @user.email,
28
28
  subject: I18n.t(
29
29
  "decidim.meetings.close_meeting_reminder_mailer.close_meeting_reminder.subject",
30
- organization_name: @organization.name
30
+ meeting_title: decidim_sanitize_translated(@meeting.title)
31
31
  )
32
32
  )
33
33
  end
@@ -61,13 +61,14 @@ module Decidim
61
61
  scope :past, -> { where(arel_table[:end_time].lteq(Time.current)) }
62
62
  scope :upcoming, -> { where(arel_table[:end_time].gteq(Time.current)) }
63
63
  scope :withdrawn, -> { where(state: "withdrawn") }
64
- scope :except_withdrawn, -> { where.not(state: "withdrawn").or(where(state: nil)) }
64
+ scope :withdrawn, -> { where.not(withdrawn_at: nil) }
65
+ scope :not_withdrawn, -> { where(withdrawn_at: nil) }
65
66
  scope :with_availability, lambda { |state_key|
66
67
  case state_key
67
68
  when "withdrawn"
68
69
  withdrawn
69
70
  else
70
- except_withdrawn
71
+ not_withdrawn
71
72
  end
72
73
  }
73
74
  scope_search_multi :with_any_date, [:upcoming, :past]
@@ -271,7 +272,7 @@ module Decidim
271
272
  #
272
273
  # Returns Boolean.
273
274
  def withdrawn?
274
- state == "withdrawn"
275
+ withdrawn_at.present?
275
276
  end
276
277
 
277
278
  # Checks whether the user can withdraw the given meeting.
@@ -282,6 +283,11 @@ module Decidim
282
283
  user && !withdrawn? && !past? && authored_by?(user)
283
284
  end
284
285
 
286
+ def withdraw!
287
+ self.withdrawn_at = Time.zone.now
288
+ save
289
+ end
290
+
285
291
  # Overwrites method from Paddable to add custom rules in order to know
286
292
  # when to display a pad or not.
287
293
  def pad_is_visible?
@@ -323,7 +329,7 @@ module Decidim
323
329
 
324
330
  # Public: Overrides the `reported_searchable_content_extras` Reportable concern method.
325
331
  def reported_searchable_content_extras
326
- [normalized_author.name]
332
+ [author_name]
327
333
  end
328
334
 
329
335
  def has_contributions?
@@ -14,6 +14,14 @@ module Decidim
14
14
  delegate :organization, to: :meeting
15
15
 
16
16
  QUESTION_TYPES = %w(single_option multiple_option).freeze
17
+
18
+ def has_questions?
19
+ questionnaire&.questions&.exists?
20
+ end
21
+
22
+ def has_open_questions?
23
+ has_questions? && questionnaire.questions.not_closed.exists?
24
+ end
17
25
  end
18
26
  end
19
27
  end
@@ -28,6 +28,7 @@ module Decidim
28
28
  validates :question_type, inclusion: { in: QUESTION_TYPES }
29
29
 
30
30
  scope :visible, -> { where(status: [:published, :closed]) }
31
+ scope :unanswered, -> { where.missing(:answers) }
31
32
 
32
33
  def multiple_choice?
33
34
  %w(single_option multiple_option).include?(question_type)
@@ -11,12 +11,6 @@ module Decidim
11
11
  has_many :questions, -> { order(:position) }, class_name: "Question", foreign_key: "decidim_questionnaire_id", dependent: :destroy
12
12
  has_many :answers, class_name: "Answer", foreign_key: "decidim_questionnaire_id", dependent: :destroy
13
13
 
14
- # Public: returns whether the questionnaire questions can be modified or not.
15
- def questions_editable?
16
- has_component = questionnaire_for.meeting.respond_to? :component
17
- (has_component && !questionnaire_for.meeting.component.published?) || answers.empty?
18
- end
19
-
20
14
  def all_questions_unpublished?
21
15
  questions.all?(&:unpublished?)
22
16
  end
@@ -117,9 +117,9 @@ $(() => {
117
117
  toggleDependsOnSelect($meetingRegistrationType, $meetingRegistrationUrl, "on_different_platform");
118
118
 
119
119
  const $meetingTypeOfMeeting = $form.find("#meeting_type_of_meeting");
120
- const $meetingOnlineFields = $form.find(".field[data-meeting-type='online']");
121
- const $meetingInPersonFields = $form.find(".field[data-meeting-type='in_person']");
122
- const $meetingOnlineAccessLevelFields = $form.find(".field[data-meeting-type='online-access-level']");
120
+ const $meetingOnlineFields = $form.find("[data-meeting-type='online']");
121
+ const $meetingInPersonFields = $form.find("[data-meeting-type='in_person']");
122
+ const $meetingOnlineAccessLevelFields = $form.find("[data-meeting-type='online-access-level']");
123
123
  const $meetingIframeEmbedType = $form.find("#meeting_iframe_embed_type");
124
124
 
125
125
  const toggleTypeDependsOnSelect = ($target, $showDiv, type) => {
@@ -11,9 +11,9 @@ $(() => {
11
11
  const $form = $(".meetings_form");
12
12
  if ($form.length > 0) {
13
13
  const $meetingTypeOfMeeting = $form.find("#meeting_type_of_meeting");
14
- const $meetingOnlineFields = $form.find(".field[data-meeting-type='online']");
15
- const $meetingInPersonFields = $form.find(".field[data-meeting-type='in_person']");
16
- const $meetingOnlineAccessLevelFields = $form.find(".field[data-meeting-type='online-access-level']");
14
+ const $meetingOnlineFields = $form.find("[data-meeting-type='online']");
15
+ const $meetingInPersonFields = $form.find("[data-meeting-type='in_person']");
16
+ const $meetingOnlineAccessLevelFields = $form.find("[data-meeting-type='online-access-level']");
17
17
 
18
18
  const toggleDependsOnSelect = ($target, $showDiv, type) => {
19
19
  const value = $target.val();
@@ -9,6 +9,8 @@ $(() => {
9
9
  if ($container.length) {
10
10
  const poll = new MeetingsPollComponent($container, $container.data("decidim-meetings-poll"), $counter);
11
11
 
12
+ poll.mountComponent();
13
+
12
14
  $(".meeting-polls__action-list").on("click", (event) => {
13
15
  event.preventDefault();
14
16
 
@@ -29,6 +31,9 @@ $(() => {
29
31
 
30
32
  if ($adminContainer.length) {
31
33
  const adminPoll = new MeetingsPollComponent($adminContainer, $adminContainer.data("decidim-admin-meetings-poll"));
34
+
35
+ adminPoll.mountComponent();
36
+
32
37
  $(".meeting-polls__action-administrate").on("click", (event) => {
33
38
  event.preventDefault();
34
39
 
@@ -4,7 +4,7 @@ import createOptionAttachedInputs from "src/decidim/forms/option_attached_inputs
4
4
  import createMaxChoicesAlertComponent from "src/decidim/forms/max_choices_alert.component"
5
5
 
6
6
  /**
7
- * A plain Javascript component that handles questions from polls in meetings:
7
+ * A plain JavaScript component that handles questions from polls in meetings:
8
8
  * - fetches them via Ajax
9
9
  * - enables a polling to automatically update them
10
10
  *
@@ -24,9 +24,11 @@ export default class PollComponent {
24
24
  this.$element = $element;
25
25
  this.$counter = $counter;
26
26
  this.questionsUrl = config.questionsUrl;
27
- this.pollingInterval = config.pollingInterval || 5000;
27
+ this.pollingInterval = config.pollingInterval || 10000;
28
28
  this.mounted = false;
29
29
  this.questions = {};
30
+ this.questionsStatuses = {};
31
+ this.answersStatuses = {};
30
32
  }
31
33
 
32
34
  /**
@@ -66,7 +68,9 @@ export default class PollComponent {
66
68
  * @returns {Void} - Returns nothing
67
69
  */
68
70
  _fetchQuestions() {
69
- // Store current questions state (open / closed) before overwritting them with the Ajax call
71
+ $("#content").addClass("spinner-container")
72
+
73
+ // Store current questions state (open / closed) before overwriting them with the Ajax call
70
74
  // response.
71
75
  this._storeQuestionState(this.$element);
72
76
 
@@ -79,6 +83,8 @@ export default class PollComponent {
79
83
  this._setQuestionsState(this.$element);
80
84
  this._pollQuestions();
81
85
  this._addValidations();
86
+
87
+ $("#content").removeClass("spinner-container")
82
88
  });
83
89
  }
84
90
 
@@ -92,6 +98,12 @@ export default class PollComponent {
92
98
  $("[data-question]", $parent).each((_i, el) => {
93
99
  const $el = $(el);
94
100
  const questionId = $el.data("question");
101
+ const elForm = $el.find("form");
102
+
103
+ this.questionsStatuses[questionId] = $el.data("status");
104
+ if (elForm.length > 0) {
105
+ this.answersStatuses[questionId] = Object.fromEntries(new FormData(elForm[0]));
106
+ }
95
107
  if ($el[0].open === true) {
96
108
  this.questions[questionId] = OPEN;
97
109
  } else {
@@ -124,12 +136,28 @@ export default class PollComponent {
124
136
  const questionId = $el.data("question");
125
137
  // Current question state
126
138
  const state = this.questions[questionId];
139
+ const questionStatus = this.questionsStatuses[questionId];
140
+ const answersStatuses = this.answersStatuses[questionId];
141
+
127
142
  // New questions have a special class
128
143
  if (!state) {
129
144
  $el.addClass("is-new");
130
145
  } else if (state === OPEN) {
131
146
  $el.prop(OPEN, true);
132
147
  }
148
+
149
+ if ($el.data("status") === CLOSED && $el.data("status") !== questionStatus) {
150
+ $el.data("status", `${CLOSED}-new`);
151
+ document.getElementById(`closed-announcement-${questionId}`).hidden = false;
152
+ }
153
+
154
+ if (answersStatuses) {
155
+ for (const [key, value] of Object.entries(answersStatuses)) {
156
+ if (key.includes("[choices]")) {
157
+ $el.find(`[name='${key}'][value='${value}']`).prop("checked", true);
158
+ }
159
+ }
160
+ }
133
161
  }
134
162
 
135
163
  /**
@@ -177,7 +205,7 @@ export default class PollComponent {
177
205
  });
178
206
  });
179
207
 
180
- $.unique($(".js-check-box-collection").parents(".answer")).each((idx, el) => {
208
+ $.unique($(".js-check-box-collection").parents("[data-max-choices]")).each((idx, el) => {
181
209
  const maxChoices = $(el).data("max-choices");
182
210
  if (maxChoices) {
183
211
  createMaxChoicesAlertComponent({
@@ -125,13 +125,17 @@
125
125
  }
126
126
 
127
127
  &::-webkit-progress-bar {
128
- @apply bg-white;
128
+ @apply bg-gray lg:bg-white;
129
129
  }
130
130
 
131
131
  &::-moz-progress-bar {
132
132
  @apply bg-success;
133
133
  }
134
134
  }
135
+
136
+ &-label {
137
+ @apply text-sm font-bold lg:hidden;
138
+ }
135
139
  }
136
140
 
137
141
  &-block {
@@ -175,3 +179,107 @@
175
179
  }
176
180
  }
177
181
  }
182
+
183
+ // layout reset stuff
184
+ .meeting-poll__layout {
185
+ header,
186
+ main h1,
187
+ footer {
188
+ @apply hidden md:block;
189
+ }
190
+
191
+ .layout-1col {
192
+ padding: 0;
193
+ }
194
+ }
195
+
196
+ .meeting-polls {
197
+ @apply m-0 md:mt-10 md:mb-24;
198
+
199
+ counter-reset: question;
200
+
201
+ &__question {
202
+ @apply bg-white;
203
+
204
+ summary {
205
+ @apply p-4 font-normal text-black text-md transition bg-background cursor-pointer marker:text-secondary;
206
+
207
+ transition: background-color 0.2s ease-in-out;
208
+
209
+ & > span:first-child::after {
210
+ counter-increment: question;
211
+ content: "#" counter(question);
212
+ }
213
+
214
+ & > span:last-child:not(:only-child) {
215
+ @apply text-sm font-semibold float-right;
216
+ }
217
+
218
+ & + * {
219
+ @apply mt-4 mb-8 md:mb-16 pr-4 pl-[calc(1rem+14px)] md:px-0 space-y-6;
220
+ }
221
+ }
222
+
223
+ &[open] summary {
224
+ @apply bg-secondary md:bg-background marker:text-white md:marker:text-secondary text-white md:text-black;
225
+ }
226
+
227
+ &.is-new {
228
+ animation: animateHighlight 5s ease-in-out forwards;
229
+ }
230
+
231
+ & + & {
232
+ @apply mt-4;
233
+ }
234
+
235
+ @keyframes animateHighlight {
236
+ 0%,
237
+ 80% {
238
+ background-color: rgba(var(--warning-rgb), 0.1);
239
+ }
240
+ }
241
+ }
242
+
243
+ &__answer {
244
+ label {
245
+ @apply block p-4 ring-4 ring-background rounded transition;
246
+
247
+ &:not(:has([disabled])) {
248
+ @apply hover:ring-tertiary hover:cursor-pointer;
249
+ }
250
+ }
251
+
252
+ label + label,
253
+ & + & {
254
+ @apply mt-4;
255
+ }
256
+
257
+ &--value {
258
+ @apply flex gap-2 justify-between text-gray-2 text-lg;
259
+
260
+ > *:last-child {
261
+ @apply w-1/6 flex-none text-gray text-right;
262
+ }
263
+ }
264
+
265
+ &--bar {
266
+ @apply w-full h-2.5 overflow-hidden rounded bg-background;
267
+
268
+ > * {
269
+ @apply bg-success h-full rounded;
270
+ }
271
+ }
272
+ }
273
+
274
+ &__admin-action {
275
+ @apply py-4 grid grid-cols-2 gap-x-4 gap-y-8 border-t border-t-gray [&>*:nth-child(2)]:ml-auto [&>*:nth-child(3)]:col-span-2;
276
+ }
277
+
278
+ &__topbar {
279
+ @apply my-4 md:my-0 px-4 md:px-0 py-2 md:py-10 flex justify-between gap-4 bg-background md:bg-transparent;
280
+
281
+ &.is-admin {
282
+ @apply mt-0 bg-tertiary md:bg-transparent text-black;
283
+ }
284
+ }
285
+ }
@@ -18,8 +18,6 @@
18
18
  &__aside {
19
19
  @apply flex-none bg-background-2;
20
20
 
21
- counter-reset: question;
22
-
23
21
  &.is-open {
24
22
  @apply w-1/5 [&+div]:w-4/5;
25
23
 
@@ -33,96 +31,4 @@
33
31
  }
34
32
  }
35
33
  }
36
-
37
- &__question {
38
- @apply bg-white;
39
-
40
- summary {
41
- @apply p-4 cursor-pointer font-bold text-secondary text-md transition border-b border-b-background hover:bg-background;
42
-
43
- transition: background-color 0.2s ease-in-out;
44
-
45
- &::after {
46
- counter-increment: question;
47
- content: "#" counter(question);
48
- }
49
-
50
- & ~ * {
51
- @apply p-4;
52
-
53
- // dynamic content
54
- > :first-child {
55
- @apply font-bold;
56
- }
57
-
58
- label {
59
- @apply flex items-baseline cursor-pointer;
60
- }
61
-
62
- label + label {
63
- @apply mt-4;
64
- }
65
- // end dynamic content
66
- }
67
- }
68
-
69
- &[open] .meeting-polls__answer--bar > * {
70
- opacity: 1;
71
- transform: translateX(0);
72
- }
73
-
74
- &.is-new {
75
- animation: animateHighlight 5s ease-in-out forwards;
76
- }
77
-
78
- @keyframes animateHighlight {
79
- 0%,
80
- 80% {
81
- background-color: rgba(var(--warning-rgb), 0.1);
82
- }
83
- }
84
- }
85
-
86
- &__question--admin {
87
- summary ~ * {
88
- @apply border-t border-t-background p-4 bg-background-2;
89
-
90
- a {
91
- @apply my-4 block text-sm underline;
92
- }
93
-
94
- > :first-child {
95
- @apply font-normal;
96
- }
97
- }
98
- }
99
-
100
- &__answer {
101
- @apply flex items-center gap-2;
102
-
103
- &--value {
104
- @apply w-1/5 font-bold;
105
- }
106
-
107
- &--bar {
108
- @apply w-4/5 h-2.5 overflow-hidden;
109
-
110
- > * {
111
- @apply bg-primary h-full opacity-0;
112
- }
113
- }
114
- }
115
-
116
- // vertical flow
117
- label + &__answer + label {
118
- @apply mt-6;
119
- }
120
-
121
- &__admin-label {
122
- @apply p-4 bg-secondary text-white font-bold text-sm;
123
- }
124
-
125
- &__admin-action {
126
- @apply py-2 flex items-center border-t-background [&>*]:flex-none first:[&>*]:w-2/5 last:[&>*]:w-3/5;
127
- }
128
34
  }
@@ -39,6 +39,13 @@ module Decidim
39
39
  toggle_allow(can_close_meeting?)
40
40
  when :register
41
41
  toggle_allow(can_register_invitation_meeting?)
42
+ when :reply_poll
43
+ toggle_allow(can_reply_poll?)
44
+ end
45
+ when :poll
46
+ case permission_action.action
47
+ when :update
48
+ toggle_allow(can_update_poll?)
42
49
  end
43
50
  else
44
51
  return permission_action
@@ -112,6 +119,19 @@ module Decidim
112
119
  authorized?(:register, resource: meeting)
113
120
  end
114
121
 
122
+ def can_reply_poll?
123
+ meeting.present? &&
124
+ meeting.poll.present? &&
125
+ authorized?(:reply_poll, resource: meeting)
126
+ end
127
+
128
+ def can_update_poll?
129
+ user.present? &&
130
+ user.admin? &&
131
+ meeting.present? &&
132
+ meeting.poll.present?
133
+ end
134
+
115
135
  def can_answer_question?
116
136
  question.present? && user.present? && !question.answered_by?(user)
117
137
  end
@@ -64,10 +64,13 @@ module Decidim
64
64
  end
65
65
  end
66
66
 
67
- def closing_report(links: false, extras: false, strip_tags: false, all_locales: false)
67
+ def closing_report(links: false, all_locales: false)
68
68
  return unless meeting
69
69
 
70
- content_handle_locale(meeting.closing_report, all_locales, extras, links, strip_tags)
70
+ handle_locales(meeting.closing_report, all_locales) do |content|
71
+ renderer = Decidim::ContentRenderers::HashtagRenderer.new(sanitized(content))
72
+ renderer.render(links:).html_safe
73
+ end
71
74
  end
72
75
 
73
76
  def registration_email_custom_content(links: false, all_locales: false)
@@ -31,7 +31,7 @@ module Decidim
31
31
  #
32
32
  # Returns a collection of Meetings filtered based on provided params.
33
33
  def filtered_meetings
34
- meetings.not_hidden.published.except_withdrawn.ransack(@filters).result
34
+ meetings.not_hidden.published.not_withdrawn.ransack(@filters).result
35
35
  end
36
36
  end
37
37
  end
@@ -22,7 +22,7 @@ module Decidim
22
22
  class MeetingToEvent
23
23
  include ActionView::Helpers::SanitizeHelper
24
24
 
25
- # Initializes the converteer for the given meeting.
25
+ # Initializes the converter for the given meeting.
26
26
  #
27
27
  # meeting - the Meeting to convert
28
28
  def initialize(meeting)