decidim-meetings 0.28.5 → 0.29.0.rc1

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.

Potentially problematic release.


This version of decidim-meetings might be problematic. Click here for more details.

Files changed (157) 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/dates_and_map/show.erb +3 -5
  5. data/app/cells/decidim/meetings/dates_and_map_cell.rb +1 -1
  6. data/app/cells/decidim/meetings/highlighted_meetings_for_component_cell.rb +1 -2
  7. data/app/cells/decidim/meetings/join_meeting_button/remaining_slots.erb +1 -0
  8. data/app/cells/decidim/meetings/join_meeting_button_cell.rb +0 -3
  9. data/app/cells/decidim/meetings/meeting_card_metadata_cell.rb +0 -3
  10. data/app/cells/decidim/meetings/meeting_cell.rb +0 -1
  11. data/app/cells/decidim/meetings/meeting_l_cell.rb +4 -7
  12. data/app/cells/decidim/meetings/meeting_url/show.erb +5 -5
  13. data/app/cells/decidim/meetings/meeting_url_cell.rb +0 -1
  14. data/app/cells/decidim/meetings/online_meeting_link_cell.rb +0 -2
  15. data/app/cells/decidim/meetings/question_responses/show.erb +7 -3
  16. data/app/cells/decidim/meetings/question_responses_cell.rb +0 -2
  17. data/app/commands/decidim/meetings/admin/copy_meeting.rb +2 -2
  18. data/app/commands/decidim/meetings/admin/create_agenda.rb +9 -37
  19. data/app/commands/decidim/meetings/admin/create_meeting.rb +28 -61
  20. data/app/commands/decidim/meetings/admin/destroy_meeting.rb +4 -33
  21. data/app/commands/decidim/meetings/admin/update_agenda.rb +7 -35
  22. data/app/commands/decidim/meetings/admin/update_meeting.rb +30 -68
  23. data/app/commands/decidim/meetings/admin/update_question_status.rb +1 -1
  24. data/app/commands/decidim/meetings/admin/update_questionnaire.rb +19 -11
  25. data/app/commands/decidim/meetings/admin/update_registrations.rb +20 -46
  26. data/app/commands/decidim/meetings/create_answer.rb +5 -4
  27. data/app/commands/decidim/meetings/create_meeting.rb +38 -62
  28. data/app/commands/decidim/meetings/join_meeting.rb +18 -19
  29. data/app/commands/decidim/meetings/update_meeting.rb +6 -7
  30. data/app/commands/decidim/meetings/withdraw_meeting.rb +1 -1
  31. data/app/controllers/concerns/decidim/meetings/admin/invites/filterable.rb +33 -0
  32. data/app/controllers/decidim/meetings/admin/agenda_controller.rb +1 -1
  33. data/app/controllers/decidim/meetings/admin/invites_controller.rb +6 -6
  34. data/app/controllers/decidim/meetings/admin/meetings_controller.rb +2 -2
  35. data/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb +12 -0
  36. data/app/controllers/decidim/meetings/application_controller.rb +1 -1
  37. data/app/controllers/decidim/meetings/live_events_controller.rb +1 -1
  38. data/app/controllers/decidim/meetings/meetings_controller.rb +3 -33
  39. data/app/controllers/decidim/meetings/polls/answers_controller.rb +10 -2
  40. data/app/controllers/decidim/meetings/registrations_controller.rb +3 -3
  41. data/app/forms/decidim/meetings/admin/close_meeting_form.rb +1 -1
  42. data/app/forms/decidim/meetings/admin/meeting_agenda_items_form.rb +1 -1
  43. data/app/forms/decidim/meetings/admin/meeting_form.rb +1 -1
  44. data/app/forms/decidim/meetings/admin/meeting_registrations_form.rb +3 -3
  45. data/app/forms/decidim/meetings/admin/question_form.rb +8 -4
  46. data/app/forms/decidim/meetings/answer_form.rb +4 -4
  47. data/app/forms/decidim/meetings/meeting_form.rb +1 -1
  48. data/app/helpers/decidim/meetings/application_helper.rb +1 -5
  49. data/app/mailers/decidim/meetings/close_meeting_reminder_mailer.rb +1 -1
  50. data/app/models/decidim/meetings/meeting.rb +10 -4
  51. data/app/models/decidim/meetings/poll.rb +8 -0
  52. data/app/models/decidim/meetings/question.rb +1 -0
  53. data/app/models/decidim/meetings/questionnaire.rb +0 -6
  54. data/app/packs/src/decidim/meetings/admin/meetings_form.js +3 -3
  55. data/app/packs/src/decidim/meetings/meetings_form.js +3 -3
  56. data/app/packs/src/decidim/meetings/meetings_polls.js +5 -0
  57. data/app/packs/src/decidim/meetings/poll.component.js +32 -4
  58. data/app/packs/stylesheets/decidim/meetings/_item.scss +126 -6
  59. data/app/packs/stylesheets/decidim/meetings/_live_event.scss +0 -94
  60. data/app/permissions/decidim/meetings/permissions.rb +22 -7
  61. data/app/presenters/decidim/meetings/meeting_presenter.rb +10 -3
  62. data/app/services/decidim/meetings/calendar/component_calendar.rb +1 -1
  63. data/app/services/decidim/meetings/calendar/meeting_to_event.rb +1 -1
  64. data/app/services/decidim/meetings/close_meeting_reminder_generator.rb +1 -1
  65. data/app/services/decidim/meetings/meeting_iframe_embedder.rb +2 -2
  66. data/app/views/decidim/meetings/_calendar_modal.html.erb +1 -1
  67. data/app/views/decidim/meetings/admin/agenda/_agenda_item.html.erb +3 -3
  68. data/app/views/decidim/meetings/admin/agenda/_agenda_item_child.html.erb +3 -3
  69. data/app/views/decidim/meetings/admin/agenda/_agenda_item_fields.html.erb +3 -3
  70. data/app/views/decidim/meetings/admin/invite_join_meeting_mailer/invite.html.erb +3 -1
  71. data/app/views/decidim/meetings/admin/invites/_form.html.erb +4 -4
  72. data/app/views/decidim/meetings/admin/invites/index.html.erb +4 -34
  73. data/app/views/decidim/meetings/admin/meetings/_form.html.erb +8 -8
  74. data/app/views/decidim/meetings/admin/meetings/_service.html.erb +3 -3
  75. data/app/views/decidim/meetings/admin/meetings/index.html.erb +2 -2
  76. data/app/views/decidim/meetings/admin/poll/_answer_option.html.erb +1 -1
  77. data/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb +1 -1
  78. data/app/views/decidim/meetings/admin/poll/_form.html.erb +26 -25
  79. data/app/views/decidim/meetings/admin/poll/_question.html.erb +18 -16
  80. data/app/views/decidim/meetings/admin/poll/edit.html.erb +4 -6
  81. data/app/views/decidim/meetings/admin/registrations/edit.html.erb +2 -4
  82. data/app/views/decidim/meetings/close_meeting_reminder_mailer/close_meeting_reminder.html.erb +1 -1
  83. data/app/views/decidim/meetings/layouts/live_event.html.erb +1 -15
  84. data/app/views/decidim/meetings/meetings/_datetime.html.erb +4 -4
  85. data/app/views/decidim/meetings/meetings/_form.html.erb +17 -17
  86. data/app/views/decidim/meetings/meetings/_meeting.html.erb +2 -1
  87. data/app/views/decidim/meetings/meetings/_meeting_agenda.html.erb +2 -2
  88. data/app/views/decidim/meetings/meetings/_meeting_aside.html.erb +3 -1
  89. data/app/views/decidim/meetings/meetings/_meeting_minutes.html.erb +1 -1
  90. data/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb +15 -0
  91. data/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb +6 -10
  92. data/app/views/decidim/meetings/polls/answers/_single_option.html.erb +4 -10
  93. data/app/views/decidim/meetings/polls/answers/admin.html.erb +34 -0
  94. data/app/views/decidim/meetings/polls/answers/index.html.erb +36 -0
  95. data/app/views/decidim/meetings/polls/questions/_closed_question.html.erb +8 -2
  96. data/app/views/decidim/meetings/polls/questions/_index_admin.html.erb +27 -24
  97. data/app/views/decidim/meetings/polls/questions/_published_question.html.erb +14 -11
  98. data/app/views/decidim/meetings/polls/questions/_question.html.erb +1 -1
  99. data/app/views/decidim/meetings/polls/questions/index.js.erb +3 -3
  100. data/app/views/decidim/meetings/polls/questions/index_admin.js.erb +3 -3
  101. data/app/views/decidim/meetings/registration_mailer/confirmation.html.erb +1 -1
  102. data/app/views/devise/mailer/join_meeting.html.erb +3 -1
  103. data/app/views/devise/mailer/join_meeting.text.erb +3 -1
  104. data/config/locales/ar.yml +1 -13
  105. data/config/locales/bg.yml +45 -12
  106. data/config/locales/ca.yml +45 -13
  107. data/config/locales/cs.yml +28 -16
  108. data/config/locales/de.yml +114 -82
  109. data/config/locales/el.yml +1 -13
  110. data/config/locales/en.yml +44 -12
  111. data/config/locales/es-MX.yml +47 -15
  112. data/config/locales/es-PY.yml +47 -15
  113. data/config/locales/es.yml +48 -16
  114. data/config/locales/eu.yml +79 -47
  115. data/config/locales/fi-plain.yml +46 -14
  116. data/config/locales/fi.yml +56 -24
  117. data/config/locales/fr-CA.yml +48 -16
  118. data/config/locales/fr.yml +48 -16
  119. data/config/locales/ga-IE.yml +0 -8
  120. data/config/locales/gl.yml +3 -11
  121. data/config/locales/hu.yml +3 -12
  122. data/config/locales/id-ID.yml +1 -12
  123. data/config/locales/is-IS.yml +1 -12
  124. data/config/locales/it.yml +3 -16
  125. data/config/locales/ja.yml +47 -15
  126. data/config/locales/lb.yml +1 -7
  127. data/config/locales/lt.yml +3 -12
  128. data/config/locales/lv.yml +1 -12
  129. data/config/locales/nl.yml +1 -13
  130. data/config/locales/no.yml +1 -12
  131. data/config/locales/pl.yml +45 -12
  132. data/config/locales/pt-BR.yml +23 -17
  133. data/config/locales/pt.yml +1 -14
  134. data/config/locales/ro-RO.yml +2 -15
  135. data/config/locales/ru.yml +1 -12
  136. data/config/locales/sk.yml +1 -12
  137. data/config/locales/sv.yml +108 -206
  138. data/config/locales/tr-TR.yml +2 -17
  139. data/config/locales/uk.yml +1 -12
  140. data/config/locales/zh-CN.yml +1 -15
  141. data/config/locales/zh-TW.yml +1 -13
  142. data/db/migrate/20240130135858_add_withdrawn_fields_on_meetings.rb +23 -0
  143. data/decidim-meetings.gemspec +2 -2
  144. data/lib/decidim/api/meeting_type.rb +3 -12
  145. data/lib/decidim/api/meetings_type.rb +3 -1
  146. data/lib/decidim/meetings/component.rb +2 -2
  147. data/lib/decidim/meetings/engine.rb +5 -3
  148. data/lib/decidim/meetings/meeting_serializer.rb +3 -38
  149. data/lib/decidim/meetings/seeds.rb +13 -18
  150. data/lib/decidim/meetings/test/factories.rb +1 -7
  151. data/lib/decidim/meetings/test/notifications_handling.rb +1 -1
  152. data/lib/decidim/meetings/version.rb +1 -1
  153. data/lib/tasks/decidim_meetings.rake +1 -1
  154. metadata +24 -22
  155. data/app/views/decidim/meetings/admin/agenda/show.html.erb +0 -0
  156. data/config/locales/bn-BD.yml +0 -1
  157. data/config/locales/bs-BA.yml +0 -8
@@ -15,9 +15,9 @@ module Decidim
15
15
  helper Decidim::ShortLinkHelper
16
16
  include Decidim::AttachmentsHelper
17
17
 
18
- helper_method :meetings, :meeting, :registration, :search, :nav_paths, :tab_panel_items
18
+ helper_method :meetings, :meeting, :registration, :search, :tab_panel_items
19
19
 
20
- before_action :add_addtional_csp_directives, only: [:show]
20
+ before_action :add_additional_csp_directives, only: [:show]
21
21
 
22
22
  def new
23
23
  enforce_permission_to :create, :meeting
@@ -76,7 +76,7 @@ module Decidim
76
76
 
77
77
  @form = meeting_form.from_params(params)
78
78
 
79
- UpdateMeeting.call(@form, current_user, meeting) do
79
+ UpdateMeeting.call(@form, meeting) do
80
80
  on(:ok) do |meeting|
81
81
  flash[:notice] = I18n.t("meetings.update.success", scope: "decidim.meetings")
82
82
  redirect_to Decidim::ResourceLocatorPresenter.new(meeting).path
@@ -110,36 +110,6 @@ module Decidim
110
110
  @meeting ||= Meeting.not_hidden.where(component: current_component).find_by(id: params[:id])
111
111
  end
112
112
 
113
- def next_meeting
114
- return if search_collection.size < 2
115
-
116
- search_collection.order(:start_time, :id).where(
117
- Decidim::Meetings::Meeting.arel_table[:start_time].gt(meeting.start_time).or(
118
- Decidim::Meetings::Meeting.arel_table[:start_time].eq(meeting.start_time).and(
119
- Decidim::Meetings::Meeting.arel_table[:id].gt(meeting.id)
120
- )
121
- )
122
- ).first
123
- end
124
-
125
- def prev_meeting
126
- return if search_collection.size < 2
127
-
128
- search_collection.order(:start_time, :id).where(
129
- Decidim::Meetings::Meeting.arel_table[:start_time].lt(meeting.start_time).or(
130
- Decidim::Meetings::Meeting.arel_table[:start_time].eq(meeting.start_time).and(
131
- Decidim::Meetings::Meeting.arel_table[:id].lt(meeting.id)
132
- )
133
- )
134
- ).last
135
- end
136
-
137
- def nav_paths
138
- return {} if meeting.blank?
139
-
140
- { prev_path: prev_meeting, next_path: next_meeting }.compact_blank.transform_values { |meeting| meeting_path(meeting) }
141
- end
142
-
143
113
  def meetings
144
114
  is_past_meetings = params.dig("filter", "with_any_date")&.include?("past")
145
115
  @meetings ||= paginate(search.result.order(start_time: is_past_meetings ? :desc : :asc))
@@ -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
@@ -7,7 +7,7 @@ module Decidim
7
7
  class CloseMeetingForm < Decidim::Form
8
8
  include TranslatableAttributes
9
9
 
10
- translatable_attribute :closing_report, Decidim::Attributes::RichText
10
+ translatable_attribute :closing_report, String
11
11
  attribute :video_url, String
12
12
  attribute :audio_url, String
13
13
  attribute :closing_visible, Boolean, default: true
@@ -8,7 +8,7 @@ module Decidim
8
8
  include TranslatableAttributes
9
9
 
10
10
  translatable_attribute :title, String
11
- translatable_attribute :description, Decidim::Attributes::RichText
11
+ translatable_attribute :description, String
12
12
 
13
13
  attribute :duration, Integer, default: 0
14
14
  attribute :parent_id, Integer
@@ -23,7 +23,7 @@ module Decidim
23
23
  attribute :iframe_access_level, String
24
24
 
25
25
  translatable_attribute :title, String
26
- translatable_attribute :description, Decidim::Attributes::RichText
26
+ translatable_attribute :description, String
27
27
  translatable_attribute :location, String
28
28
  translatable_attribute :location_hints, String
29
29
 
@@ -15,8 +15,8 @@ module Decidim
15
15
  attribute :available_slots, Integer
16
16
  attribute :reserved_slots, Integer
17
17
 
18
- translatable_attribute :registration_terms, Decidim::Attributes::RichText
19
- translatable_attribute :registration_email_custom_content, Decidim::Attributes::RichText
18
+ translatable_attribute :registration_terms, String
19
+ translatable_attribute :registration_email_custom_content, String
20
20
 
21
21
  validates :registration_terms, translatable_presence: true, if: ->(form) { form.registrations_enabled? }
22
22
  validates :available_slots, :reserved_slots, presence: true, if: ->(form) { form.registrations_enabled? }
@@ -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({
@@ -6,17 +6,29 @@
6
6
  }
7
7
 
8
8
  &__calendar {
9
- @apply order-first flex flex-col min-w-48 rounded bg-background text-center md:w-14;
9
+ @apply w-14 flex flex-col justify-start rounded bg-background text-center;
10
10
 
11
11
  &:only-child &-month {
12
12
  @apply rounded-t;
13
13
  }
14
14
 
15
15
  &-container {
16
- @apply flex flex-wrap flex-col md:flex-row gap-2 border-4 border-background rounded;
16
+ @apply grid grid-cols-[auto_1fr] md:grid-cols-[auto_1fr_1fr] items-center gap-0 md:gap-x-5 border-4 border-background rounded md:h-[140px] overflow-hidden;
17
17
 
18
- ul {
19
- @apply flex-1 p-2;
18
+ > *:nth-child(2) {
19
+ @apply p-4 md:p-0;
20
+
21
+ &:not(:last-child) {
22
+ @apply col-span-2 md:col-auto order-2 md:order-1;
23
+ }
24
+
25
+ &:last-child {
26
+ @apply md:col-span-2 pr-4;
27
+ }
28
+ }
29
+
30
+ > *:nth-child(3) {
31
+ @apply order-1 md:order-2 h-[135px] md:h-full;
20
32
  }
21
33
  }
22
34
 
@@ -43,7 +55,7 @@
43
55
  }
44
56
 
45
57
  &__lg {
46
- @apply flex min-w-48;
58
+ @apply w-fit justify-center [&>*]:px-2 min-w-24 h-[8.5rem];
47
59
  }
48
60
 
49
61
  &__lg &-month {
@@ -113,13 +125,17 @@
113
125
  }
114
126
 
115
127
  &::-webkit-progress-bar {
116
- @apply bg-white;
128
+ @apply bg-gray lg:bg-white;
117
129
  }
118
130
 
119
131
  &::-moz-progress-bar {
120
132
  @apply bg-success;
121
133
  }
122
134
  }
135
+
136
+ &-label {
137
+ @apply text-sm font-bold lg:hidden;
138
+ }
123
139
  }
124
140
 
125
141
  &-block {
@@ -163,3 +179,107 @@
163
179
  }
164
180
  }
165
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
+ }