decidim-meetings 0.30.2 → 0.31.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.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/meetings/cancel_registration_meeting_button/cancelation_modal.erb +7 -4
  3. data/app/cells/decidim/meetings/cancel_registration_meeting_button/show.erb +1 -1
  4. data/app/cells/decidim/meetings/cancel_registration_meeting_button_cell.rb +36 -0
  5. data/app/cells/decidim/meetings/dates_and_map_cell.rb +1 -1
  6. data/app/cells/decidim/meetings/join_meeting_button/registration_modal.erb +4 -5
  7. data/app/cells/decidim/meetings/join_meeting_button/show.erb +25 -28
  8. data/app/cells/decidim/meetings/join_meeting_button/waitlist_button.erb +22 -0
  9. data/app/cells/decidim/meetings/join_meeting_button_cell.rb +28 -0
  10. data/app/cells/decidim/meetings/question_responses/show.erb +7 -7
  11. data/app/cells/decidim/meetings/question_responses_cell.rb +28 -28
  12. data/app/commands/decidim/meetings/admin/copy_meeting.rb +5 -2
  13. data/app/commands/decidim/meetings/admin/create_agenda.rb +6 -2
  14. data/app/commands/decidim/meetings/admin/create_meeting.rb +7 -3
  15. data/app/commands/decidim/meetings/admin/mark_as_attendee.rb +44 -0
  16. data/app/commands/decidim/meetings/admin/publish_meeting.rb +2 -1
  17. data/app/commands/decidim/meetings/admin/update_agenda.rb +6 -2
  18. data/app/commands/decidim/meetings/admin/update_meeting.rb +15 -6
  19. data/app/commands/decidim/meetings/admin/update_questionnaire.rb +4 -4
  20. data/app/commands/decidim/meetings/admin/update_registrations.rb +19 -7
  21. data/app/commands/decidim/meetings/create_meeting.rb +2 -3
  22. data/app/commands/decidim/meetings/{create_answer.rb → create_response.rb} +11 -11
  23. data/app/commands/decidim/meetings/join_meeting.rb +4 -6
  24. data/app/commands/decidim/meetings/join_waitlist.rb +53 -0
  25. data/app/commands/decidim/meetings/leave_meeting.rb +14 -5
  26. data/app/commands/decidim/meetings/update_meeting.rb +1 -2
  27. data/app/controllers/concerns/decidim/meetings/admin/filterable.rb +1 -1
  28. data/app/controllers/decidim/meetings/admin/agenda_controller.rb +2 -2
  29. data/app/controllers/decidim/meetings/admin/invites_controller.rb +1 -1
  30. data/app/controllers/decidim/meetings/admin/meeting_closes_controller.rb +1 -1
  31. data/app/controllers/decidim/meetings/admin/meeting_copies_controller.rb +1 -1
  32. data/app/controllers/decidim/meetings/admin/meetings_controller.rb +4 -4
  33. data/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb +10 -10
  34. data/app/controllers/decidim/meetings/admin/registrations_attendees_controller.rb +79 -0
  35. data/app/controllers/decidim/meetings/admin/registrations_controller.rb +1 -22
  36. data/app/controllers/decidim/meetings/meeting_closes_controller.rb +1 -1
  37. data/app/controllers/decidim/meetings/meetings_controller.rb +11 -11
  38. data/app/controllers/decidim/meetings/polls/{answers_controller.rb → responses_controller.rb} +7 -7
  39. data/app/controllers/decidim/meetings/registrations_controller.rb +39 -12
  40. data/app/events/decidim/meetings/registration_marked_as_attendee_event.rb +9 -0
  41. data/app/events/decidim/meetings/upcoming_meeting_event.rb +41 -0
  42. data/app/events/decidim/meetings/update_meeting_event.rb +25 -0
  43. data/app/forms/decidim/meetings/admin/close_meeting_form.rb +2 -1
  44. data/app/forms/decidim/meetings/admin/meeting_agenda_items_form.rb +4 -0
  45. data/app/forms/decidim/meetings/admin/meeting_form.rb +28 -3
  46. data/app/forms/decidim/meetings/admin/question_form.rb +3 -3
  47. data/app/forms/decidim/meetings/admin/{answer_option_form.rb → response_option_form.rb} +3 -3
  48. data/app/forms/decidim/meetings/admin/validate_registration_code_form.rb +1 -1
  49. data/app/forms/decidim/meetings/base_meeting_form.rb +0 -2
  50. data/app/forms/decidim/meetings/close_meeting_form.rb +2 -1
  51. data/app/forms/decidim/meetings/join_meeting_form.rb +0 -1
  52. data/app/forms/decidim/meetings/meeting_form.rb +3 -2
  53. data/app/forms/decidim/meetings/response_choice_form.rb +14 -0
  54. data/app/forms/decidim/meetings/{answer_form.rb → response_form.rb} +7 -7
  55. data/app/helpers/decidim/meetings/application_helper.rb +0 -1
  56. data/app/helpers/decidim/meetings/meetings_helper.rb +14 -5
  57. data/app/jobs/decidim/meetings/promote_from_waitlist_job.rb +63 -0
  58. data/app/jobs/decidim/meetings/upcoming_meeting_notification_job.rb +1 -1
  59. data/app/mailers/decidim/meetings/registration_mailer.rb +13 -0
  60. data/app/models/decidim/meetings/agenda_item.rb +5 -0
  61. data/app/models/decidim/meetings/meeting.rb +15 -12
  62. data/app/models/decidim/meetings/question.rb +12 -12
  63. data/app/models/decidim/meetings/questionnaire.rb +1 -1
  64. data/app/models/decidim/meetings/registration.rb +19 -7
  65. data/app/models/decidim/meetings/{answer.rb → response.rb} +6 -6
  66. data/app/models/decidim/meetings/response_choice.rb +15 -0
  67. data/app/models/decidim/meetings/{answer_option.rb → response_option.rb} +5 -5
  68. data/app/packs/src/decidim/meetings/admin/destroy_meeting_alert.js +1 -1
  69. data/app/packs/src/decidim/meetings/admin/meetings_components_form.js +1 -8
  70. data/app/packs/src/decidim/meetings/admin/meetings_form.js +1 -1
  71. data/app/packs/src/decidim/meetings/admin/registrations_form.js +1 -1
  72. data/app/packs/src/decidim/meetings/admin/registrations_invite_form.js +1 -1
  73. data/app/packs/src/decidim/meetings/meetings_form.js +1 -1
  74. data/app/packs/src/decidim/meetings/meetings_polls.js +1 -1
  75. data/app/packs/src/decidim/meetings/poll.component.js +5 -5
  76. data/app/packs/stylesheets/decidim/meetings/_item.scss +5 -1
  77. data/app/packs/stylesheets/decidim/meetings/meetings.scss +4 -4
  78. data/app/permissions/decidim/meetings/admin/agenda_permissions.rb +34 -0
  79. data/app/permissions/decidim/meetings/admin/meeting_permissions.rb +44 -0
  80. data/app/permissions/decidim/meetings/admin/permissions.rb +5 -66
  81. data/app/permissions/decidim/meetings/admin/questionnaire_permissions.rb +30 -0
  82. data/app/permissions/decidim/meetings/meeting_permissions.rb +90 -0
  83. data/app/permissions/decidim/meetings/permissions.rb +9 -105
  84. data/app/presenters/decidim/meetings/admin_log/value_types/meeting_title_description_presenter.rb +1 -1
  85. data/app/presenters/decidim/meetings/agenda_item_presenter.rb +29 -0
  86. data/app/presenters/decidim/meetings/meeting_presenter.rb +11 -15
  87. data/app/presenters/decidim/meetings/registration_presenter.rb +24 -0
  88. data/app/queries/decidim/meetings/{questionnaire_user_answers.rb → questionnaire_user_responses.rb} +5 -5
  89. data/app/serializers/decidim/meetings/registration_serializer.rb +5 -6
  90. data/app/services/decidim/meetings/diff_renderer.rb +0 -1
  91. data/app/views/decidim/meetings/_calendar_modal.html.erb +1 -0
  92. data/app/views/decidim/meetings/admin/agenda/_agenda_item_fields.html.erb +1 -1
  93. data/app/views/decidim/meetings/admin/invites/_form.html.erb +1 -1
  94. data/app/views/decidim/meetings/admin/meeting_closes/_form.html.erb +1 -1
  95. data/app/views/decidim/meetings/admin/meetings/_form.html.erb +3 -2
  96. data/app/views/decidim/meetings/admin/meetings/_linked_spaces.html.erb +1 -1
  97. data/app/views/decidim/meetings/admin/meetings/_meeting-tr.html.erb +11 -8
  98. data/app/views/decidim/meetings/admin/meetings/_meeting_actions.html.erb +200 -69
  99. data/app/views/decidim/meetings/admin/meetings/_reminders.html.erb +19 -0
  100. data/app/views/decidim/meetings/admin/meetings/_services.html.erb +1 -1
  101. data/app/views/decidim/meetings/admin/meetings/index.html.erb +7 -5
  102. data/app/views/decidim/meetings/admin/meetings/manage_trash.html.erb +2 -1
  103. data/app/views/decidim/meetings/admin/poll/_form.html.erb +6 -6
  104. data/app/views/decidim/meetings/admin/poll/_question.html.erb +13 -13
  105. data/app/views/decidim/meetings/admin/poll/_response_option.html.erb +35 -0
  106. data/app/views/decidim/meetings/admin/poll/_response_option_template.html.erb +7 -0
  107. data/app/views/decidim/meetings/admin/poll/edit.html.erb +3 -3
  108. data/app/views/decidim/meetings/admin/registration_form/edit_questions.html.erb +5 -5
  109. data/app/views/decidim/meetings/admin/registrations/edit.html.erb +19 -39
  110. data/app/views/decidim/meetings/admin/registrations_attendees/index.html.erb +126 -0
  111. data/app/views/decidim/meetings/layouts/live_event.html.erb +1 -1
  112. data/app/views/decidim/meetings/meeting_closes/_form.html.erb +2 -2
  113. data/app/views/decidim/meetings/meetings/_form.html.erb +2 -11
  114. data/app/views/decidim/meetings/meetings/_meeting.html.erb +2 -2
  115. data/app/views/decidim/meetings/meetings/_meeting_actions.html.erb +3 -3
  116. data/app/views/decidim/meetings/meetings/_meeting_agenda.html.erb +2 -2
  117. data/app/views/decidim/meetings/meetings/_meeting_aside.html.erb +11 -10
  118. data/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb +3 -3
  119. data/app/views/decidim/meetings/meetings/_registration_code_modal.html.erb +16 -0
  120. data/app/views/decidim/meetings/polls/questions/_index_admin.html.erb +1 -1
  121. data/app/views/decidim/meetings/polls/questions/_published_question.html.erb +5 -5
  122. data/app/views/decidim/meetings/polls/responses/_multiple_option.html.erb +13 -0
  123. data/app/views/decidim/meetings/polls/responses/_single_option.html.erb +13 -0
  124. data/app/views/decidim/meetings/polls/{answers → responses}/admin.html.erb +4 -4
  125. data/app/views/decidim/meetings/polls/{answers → responses}/index.html.erb +4 -4
  126. data/app/views/decidim/meetings/registration_mailer/confirmation.html.erb +6 -1
  127. data/config/assets.rb +2 -2
  128. data/config/locales/ar.yml +1 -26
  129. data/config/locales/bg.yml +2 -32
  130. data/config/locales/ca-IT.yml +86 -40
  131. data/config/locales/ca.yml +86 -40
  132. data/config/locales/cs.yml +71 -43
  133. data/config/locales/de.yml +87 -41
  134. data/config/locales/el.yml +1 -25
  135. data/config/locales/en.yml +89 -43
  136. data/config/locales/es-MX.yml +87 -41
  137. data/config/locales/es-PY.yml +87 -41
  138. data/config/locales/es.yml +87 -41
  139. data/config/locales/eu.yml +86 -40
  140. data/config/locales/fi-plain.yml +86 -40
  141. data/config/locales/fi.yml +85 -39
  142. data/config/locales/fr-CA.yml +79 -38
  143. data/config/locales/fr.yml +79 -38
  144. data/config/locales/ga-IE.yml +1 -7
  145. data/config/locales/gl.yml +1 -19
  146. data/config/locales/hu.yml +1 -22
  147. data/config/locales/id-ID.yml +0 -16
  148. data/config/locales/is-IS.yml +0 -10
  149. data/config/locales/it.yml +1 -29
  150. data/config/locales/ja.yml +88 -42
  151. data/config/locales/lb.yml +1 -15
  152. data/config/locales/lt.yml +1 -28
  153. data/config/locales/lv.yml +0 -16
  154. data/config/locales/nl.yml +1 -26
  155. data/config/locales/no.yml +1 -28
  156. data/config/locales/pl.yml +2 -32
  157. data/config/locales/pt-BR.yml +1 -28
  158. data/config/locales/pt.yml +1 -28
  159. data/config/locales/ro-RO.yml +56 -29
  160. data/config/locales/ru.yml +0 -16
  161. data/config/locales/sk.yml +0 -16
  162. data/config/locales/sl.yml +0 -4
  163. data/config/locales/sv.yml +85 -39
  164. data/config/locales/tr-TR.yml +0 -20
  165. data/config/locales/uk.yml +0 -12
  166. data/config/locales/zh-CN.yml +0 -19
  167. data/config/locales/zh-TW.yml +1 -26
  168. data/db/migrate/20181107175558_add_questionnaire_to_existing_meetings.rb +8 -2
  169. data/db/migrate/20200827153856_add_commentable_counter_cache_to_meetings.rb +8 -2
  170. data/db/migrate/20201016065302_fix_meetings_registration_terms.rb +8 -2
  171. data/db/migrate/20210310120731_add_followable_counter_cache_to_meetings.rb +8 -2
  172. data/db/migrate/20250317103343_rename_answer_to_response_in_decidim_meetings.rb +18 -0
  173. data/db/migrate/20250403094034_add_reminder_customization_to_decidim_meetings.rb +9 -0
  174. data/db/migrate/20250408071941_add_status_to_registrations_to_decidim_meetings_registrations.rb +8 -0
  175. data/lib/decidim/api/agenda_item_type.rb +6 -2
  176. data/lib/decidim/api/agenda_type.rb +6 -2
  177. data/lib/decidim/api/linked_resources_interface.rb +1 -1
  178. data/lib/decidim/api/meeting_type.rb +20 -10
  179. data/lib/decidim/api/service_type.rb +3 -0
  180. data/lib/decidim/meetings/admin_engine.rb +9 -1
  181. data/lib/decidim/meetings/component.rb +29 -8
  182. data/lib/decidim/meetings/engine.rb +6 -21
  183. data/lib/decidim/meetings/meeting_serializer.rb +1 -2
  184. data/lib/decidim/meetings/schema_org_event_meeting_serializer.rb +0 -10
  185. data/lib/decidim/meetings/seeds.rb +4 -13
  186. data/lib/decidim/meetings/test/factories.rb +10 -16
  187. data/lib/decidim/meetings/user_responses_serializer.rb +47 -0
  188. data/lib/decidim/meetings/version.rb +1 -1
  189. data/lib/decidim/meetings.rb +7 -9
  190. metadata +49 -35
  191. data/app/cells/decidim/meetings/attending_organizations_list_cell.rb +0 -32
  192. data/app/forms/decidim/meetings/answer_choice_form.rb +0 -14
  193. data/app/models/decidim/meetings/answer_choice.rb +0 -15
  194. data/app/queries/decidim/meetings/metrics/meeting_followers_metric_measure.rb +0 -31
  195. data/app/queries/decidim/meetings/metrics/meetings_metric_manage.rb +0 -48
  196. data/app/views/decidim/meetings/admin/poll/_answer_option.html.erb +0 -35
  197. data/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb +0 -7
  198. data/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb +0 -13
  199. data/app/views/decidim/meetings/polls/answers/_single_option.html.erb +0 -13
  200. data/lib/decidim/meetings/download_your_data_user_answers_serializer.rb +0 -39
  201. data/lib/decidim/meetings/user_answers_serializer.rb +0 -47
  202. /data/app/views/decidim/meetings/polls/{answers → responses}/create.js.erb +0 -0
@@ -3,7 +3,7 @@
3
3
  module Decidim
4
4
  module Meetings
5
5
  module Polls
6
- class AnswersController < Decidim::Meetings::ApplicationController
6
+ class ResponsesController < Decidim::Meetings::ApplicationController
7
7
  include Decidim::Meetings::PollsResources
8
8
  include FormFactory
9
9
 
@@ -18,10 +18,10 @@ module Decidim
18
18
  end
19
19
 
20
20
  def create
21
- enforce_permission_to(:create, :answer, question:)
22
- @form = form(AnswerForm).from_params(params.merge(question:, current_user:))
21
+ enforce_permission_to(:create, :response, question:)
22
+ @form = form(ResponseForm).from_params(params.merge(question:, current_user:))
23
23
 
24
- CreateAnswer.call(@form, questionnaire) do
24
+ CreateResponse.call(@form, questionnaire) do
25
25
  # Both :ok and :invalid render the same template, because
26
26
  # validation errors are displayed in the template
27
27
  respond_to do |format|
@@ -33,11 +33,11 @@ module Decidim
33
33
  private
34
34
 
35
35
  def question
36
- @question ||= questionnaire.questions.find(answer_params[:question_id]) if questionnaire
36
+ @question ||= questionnaire.questions.find(response_params[:question_id]) if questionnaire
37
37
  end
38
38
 
39
- def answer_params
40
- params.require(:answer).permit(:question_id, choices: [:body, :answer_option_id])
39
+ def response_params
40
+ params.require(:response).permit(:question_id, choices: [:body, :response_option_id])
41
41
  end
42
42
  end
43
43
  end
@@ -6,25 +6,28 @@ module Decidim
6
6
  class RegistrationsController < Decidim::Meetings::ApplicationController
7
7
  include Decidim::Forms::Concerns::HasQuestionnaire
8
8
 
9
- def answer
9
+ def respond
10
10
  enforce_permission_to(:join, :meeting, meeting:)
11
11
 
12
12
  @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token:)
13
13
 
14
- JoinMeeting.call(meeting, @form) do
14
+ command = should_join_waitlist? ? JoinWaitlist : JoinMeeting
15
+ joining_waitlist = should_join_waitlist?
16
+
17
+ command.call(meeting, @form) do
15
18
  on(:ok) do
16
- flash[:notice] = I18n.t("registrations.create.success", scope: "decidim.meetings")
17
- redirect_to after_answer_path
19
+ flash[:notice] = I18n.t(joining_waitlist ? "registrations.waitlist.success" : "registrations.create.success", scope: "decidim.meetings")
20
+ redirect_to after_response_path
18
21
  end
19
22
 
20
23
  on(:invalid) do
21
- flash.now[:alert] = I18n.t("registrations.create.invalid", scope: "decidim.meetings")
22
- render template: "decidim/forms/questionnaires/show"
24
+ flash.now[:alert] = I18n.t(joining_waitlist ? "registrations.waitlist.invalid" : "registrations.create.invalid", scope: "decidim.meetings")
25
+ render template: "decidim/forms/questionnaires/show", status: :unprocessable_entity
23
26
  end
24
27
 
25
28
  on(:invalid_form) do
26
- flash.now[:alert] = I18n.t("answer.invalid", scope: i18n_flashes_scope)
27
- render template: "decidim/forms/questionnaires/show"
29
+ flash.now[:alert] = I18n.t("response.invalid", scope: i18n_flashes_scope)
30
+ render template: "decidim/forms/questionnaires/show", status: :unprocessable_entity
28
31
  end
29
32
  end
30
33
  end
@@ -47,6 +50,24 @@ module Decidim
47
50
  end
48
51
  end
49
52
 
53
+ def join_waitlist
54
+ enforce_permission_to(:join_waitlist, :meeting, meeting:)
55
+
56
+ @form = JoinMeetingForm.from_params(params).with_context(current_user:)
57
+
58
+ JoinWaitlist.call(meeting, @form) do
59
+ on(:ok) do
60
+ flash[:notice] = I18n.t("registrations.waitlist.success", scope: "decidim.meetings")
61
+ redirect_after_path
62
+ end
63
+
64
+ on(:invalid) do
65
+ flash.now[:alert] = I18n.t("registrations.waitlist.invalid", scope: "decidim.meetings")
66
+ redirect_after_path
67
+ end
68
+ end
69
+ end
70
+
50
71
  def destroy
51
72
  enforce_permission_to(:leave, :meeting, meeting:)
52
73
 
@@ -79,18 +100,24 @@ module Decidim
79
100
  end
80
101
  end
81
102
 
82
- def allow_answers?
83
- meeting.registrations_enabled? && meeting.registration_form_enabled? && meeting.has_available_slots?
103
+ def should_join_waitlist?
104
+ meeting.waitlist_enabled? && !meeting.has_available_slots? && !meeting.has_registration_for?(current_user)
105
+ end
106
+
107
+ def allow_responses?
108
+ return false unless meeting.registrations_enabled? && meeting.registration_form_enabled?
109
+
110
+ meeting.has_available_slots? || should_join_waitlist?
84
111
  end
85
112
 
86
- def after_answer_path
113
+ def after_response_path
87
114
  meeting_path(meeting)
88
115
  end
89
116
 
90
117
  # You can implement this method in your controller to change the URL
91
118
  # where the questionnaire will be submitted.
92
119
  def update_url
93
- answer_meeting_registration_path(meeting_id: meeting.id)
120
+ respond_meeting_registration_path(meeting_id: meeting.id)
94
121
  end
95
122
 
96
123
  def questionnaire_for
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ class RegistrationMarkedAsAttendeeEvent < Decidim::Events::SimpleEvent
6
+ include Decidim::Meetings::MeetingEvent
7
+ end
8
+ end
9
+ end
@@ -4,6 +4,47 @@ module Decidim
4
4
  module Meetings
5
5
  class UpcomingMeetingEvent < Decidim::Events::SimpleEvent
6
6
  include Decidim::Meetings::MeetingEvent
7
+
8
+ i18n_attributes :reminders_before_hours
9
+
10
+ def email_intro
11
+ (custom_message.presence || default_email_intro).to_s.html_safe
12
+ end
13
+
14
+ def i18n_options
15
+ {
16
+ resource_title:,
17
+ resource_path:,
18
+ resource_url:,
19
+ participatory_space_url:,
20
+ participatory_space_title:,
21
+ reminders_before_hours: resource.send_reminders_before_hours,
22
+ scope: event_name
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def reminder_message
29
+ translated_attribute(resource.reminder_message_custom_content)
30
+ end
31
+
32
+ def default_email_intro
33
+ I18n.t("email_intro", **i18n_options)
34
+ end
35
+
36
+ def custom_message
37
+ template = translated_attribute(resource.reminder_message_custom_content)
38
+ interpolate_custom_message(template).html_safe
39
+ end
40
+
41
+ def interpolate_custom_message(template)
42
+ title = translated_attribute(resource.title).to_s
43
+ hours = resource.send_reminders_before_hours.to_s
44
+ template
45
+ .gsub("{{meeting_title}}", title)
46
+ .gsub("{{before_hours}}", hours)
47
+ end
7
48
  end
8
49
  end
9
50
  end
@@ -4,6 +4,31 @@ module Decidim
4
4
  module Meetings
5
5
  class UpdateMeetingEvent < Decidim::Events::SimpleEvent
6
6
  include Decidim::Meetings::MeetingEvent
7
+
8
+ i18n_attributes :changed_fields
9
+
10
+ def notification_title
11
+ I18n.t(
12
+ "notification_title",
13
+ scope: i18n_scope,
14
+ changed_fields: changed_fields,
15
+ resource_title: translated_attribute(resource.title),
16
+ resource_path: resource_path
17
+ ).html_safe
18
+ end
19
+
20
+ private
21
+
22
+ def changed_field_keys
23
+ extra[:changed_fields] || []
24
+ end
25
+
26
+ def changed_fields
27
+ keys = changed_field_keys
28
+ return "" if keys.empty?
29
+
30
+ keys.map { |key| I18n.t("field_names.#{key}", scope: i18n_scope) }.to_sentence
31
+ end
7
32
  end
8
33
  end
9
34
  end
@@ -11,7 +11,7 @@ module Decidim
11
11
  attribute :video_url, String
12
12
  attribute :audio_url, String
13
13
  attribute :closing_visible, Boolean, default: true
14
- attribute :attendees_count, Integer, default: 0
14
+ attribute :attendees_count, Integer
15
15
  attribute :contributions_count, Integer, default: 0
16
16
  attribute :attending_organizations, String
17
17
  attribute :proposal_ids, Array[Integer]
@@ -27,6 +27,7 @@ module Decidim
27
27
  # Returns nothing.
28
28
  def map_model(model)
29
29
  self.proposal_ids = model.linked_resources(:proposals, "proposals_from_meeting").pluck(:id)
30
+ self.attendees_count = model.attendees_count || model.registrations.where.not(validated_at: nil).count
30
31
  end
31
32
 
32
33
  def proposals
@@ -31,6 +31,10 @@ module Decidim
31
31
 
32
32
  "meeting-agenda-item-child-id"
33
33
  end
34
+
35
+ def map_model(model)
36
+ self.description = model.presenter.editor_description(all_locales: true)
37
+ end
34
38
  end
35
39
  end
36
40
  end
@@ -20,23 +20,28 @@ module Decidim
20
20
  attribute :comments_start_time, Decidim::Attributes::TimeWithZone
21
21
  attribute :comments_end_time, Decidim::Attributes::TimeWithZone
22
22
  attribute :iframe_access_level, String
23
+ attribute :reminder_enabled, Boolean, default: true
24
+ attribute :send_reminders_before_hours, Integer, default: Decidim::Meetings.upcoming_meeting_notification.in_hours.to_i
23
25
 
24
26
  translatable_attribute :title, String
25
27
  translatable_attribute :description, Decidim::Attributes::RichText
26
28
  translatable_attribute :location, String
27
29
  translatable_attribute :location_hints, String
30
+ translatable_attribute :reminder_message_custom_content, String
28
31
 
29
32
  validates :iframe_embed_type, inclusion: { in: Decidim::Meetings::Meeting.iframe_embed_types }
30
- validates :title, :description, translatable_presence: true
31
- validates :title, :description, translated_etiquette: true
33
+ validates :title, :description, translatable_presence: true, translated_etiquette: true
32
34
  validates :registration_type, presence: true
33
35
  validates :registration_url, presence: true, url: true, if: ->(form) { form.on_different_platform? }
34
36
  validates :type_of_meeting, presence: true
35
- validates :location, translatable_presence: true, if: ->(form) { form.in_person_meeting? || form.hybrid_meeting? }
37
+ validates :address, presence: true, if: ->(form) { form.needs_address? && form.location.values.any?(&:present?) && form.address.blank? }
38
+ validates :location, translatable_presence: true, if: ->(form) { form.needs_address? && form.address.present? }
39
+ validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? && form.needs_address? }
36
40
  validates :online_meeting_url, url: true, if: ->(form) { form.online_meeting? || form.hybrid_meeting? }
37
41
  validates :comments_start_time, date: { before: :comments_end_time, allow_blank: true, if: proc { |obj| obj.comments_end_time.present? } }
38
42
  validates :comments_end_time, date: { after: :comments_start_time, allow_blank: true, if: proc { |obj| obj.comments_start_time.present? } }
39
43
  validates :clean_type_of_meeting, presence: true
44
+ validates :send_reminders_before_hours, numericality: { only_integer: true, greater_than: 0 }, if: :reminder_enabled
40
45
  validates(
41
46
  :iframe_access_level,
42
47
  inclusion: { in: Decidim::Meetings::Meeting.iframe_access_levels },
@@ -83,6 +88,26 @@ module Decidim
83
88
  type_of_meeting.presence
84
89
  end
85
90
 
91
+ def reminder_message_custom_content
92
+ return unless reminder_enabled
93
+
94
+ return super if super.respond_to?(:values) && super.values.any?(&:present?)
95
+
96
+ @default_reminder_message ||= if current_organization
97
+ current_organization.available_locales.index_with do |locale|
98
+ I18n.t("decidim.events.meetings.upcoming_meeting.default_body", locale:)
99
+ end
100
+ else
101
+ {}
102
+ end
103
+ end
104
+
105
+ def send_reminders_before_hours
106
+ return nil unless reminder_enabled
107
+
108
+ super.presence&.to_i
109
+ end
110
+
86
111
  def iframe_access_level_select
87
112
  Decidim::Meetings::Meeting.iframe_access_levels.map do |level, _value|
88
113
  [
@@ -10,7 +10,7 @@ module Decidim
10
10
  attribute :position, Integer
11
11
  attribute :mandatory, Boolean, default: false
12
12
  attribute :question_type, String
13
- attribute :answer_options, Array[AnswerOptionForm]
13
+ attribute :response_options, Array[ResponseOptionForm]
14
14
  attribute :max_choices, Integer
15
15
  attribute :deleted, Boolean, default: false
16
16
 
@@ -21,7 +21,7 @@ module Decidim
21
21
  validates :question_type, inclusion: { in: Decidim::Meetings::Question::QUESTION_TYPES }, if: :editable?
22
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, if: :editable?
24
+ validates :response_options, presence: true, if: :editable?
25
25
 
26
26
  def to_param
27
27
  return id if id.present?
@@ -34,7 +34,7 @@ module Decidim
34
34
  end
35
35
 
36
36
  def number_of_options
37
- answer_options.size
37
+ response_options.size
38
38
  end
39
39
 
40
40
  private
@@ -3,8 +3,8 @@
3
3
  module Decidim
4
4
  module Meetings
5
5
  module Admin
6
- # This class holds a Form to update answer options
7
- class AnswerOptionForm < Decidim::Form
6
+ # This class holds a Form to update response options
7
+ class ResponseOptionForm < Decidim::Form
8
8
  include TranslatableAttributes
9
9
 
10
10
  attribute :deleted, Boolean, default: false
@@ -16,7 +16,7 @@ module Decidim
16
16
  def to_param
17
17
  return id if id.present?
18
18
 
19
- "questionnaire-question-answer-option-id"
19
+ "questionnaire-question-response-option-id"
20
20
  end
21
21
  end
22
22
  end
@@ -25,7 +25,7 @@ module Decidim
25
25
 
26
26
  errors.add(
27
27
  :code,
28
- I18n.t("registrations.validate_registration_code.invalid", scope: "decidim.meetings.admin")
28
+ I18n.t("registrations_attendees.validate_registration_code.invalid", scope: "decidim.meetings.admin")
29
29
  )
30
30
  end
31
31
  end
@@ -15,8 +15,6 @@ module Decidim
15
15
 
16
16
  validates :current_component, presence: true
17
17
 
18
- validates :address, presence: true, if: ->(form) { form.needs_address? }
19
- validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? && form.needs_address? }
20
18
  validates :start_time, presence: true, date: { before: :end_time }
21
19
  validates :end_time, presence: true, date: { after: :start_time }
22
20
 
@@ -8,7 +8,7 @@ module Decidim
8
8
  attribute :proposal_ids, Array[Integer]
9
9
  attribute :proposals
10
10
  attribute :closed_at, Decidim::Attributes::TimeWithZone, default: -> { Time.current }
11
- attribute :attendees_count, Integer, default: 0
11
+ attribute :attendees_count, Integer
12
12
 
13
13
  validates :closing_report, presence: true
14
14
  validates :attendees_count,
@@ -22,6 +22,7 @@ module Decidim
22
22
  self.proposal_ids = model.linked_resources(:proposals, "proposals_from_meeting").pluck(:id)
23
23
  presenter = MeetingEditionPresenter.new(model)
24
24
  self.closing_report = presenter.closing_report(all_locales: false)
25
+ self.attendees_count = model.attendees_count || model.registrations.where.not(validated_at: nil).count
25
26
  end
26
27
 
27
28
  def proposals
@@ -3,7 +3,6 @@
3
3
  module Decidim
4
4
  module Meetings
5
5
  class JoinMeetingForm < Decidim::Form
6
- attribute :user_group_id, Integer
7
6
  attribute :public_participation, Boolean, default: false
8
7
  end
9
8
  end
@@ -2,14 +2,13 @@
2
2
 
3
3
  module Decidim
4
4
  module Meetings
5
- # This class holds a Form to create/update meetings for Participants and UserGroups.
5
+ # This class holds a Form to create/update meetings for Participants
6
6
  class MeetingForm < ::Decidim::Meetings::BaseMeetingForm
7
7
  attribute :title, String
8
8
  attribute :description, String
9
9
  attribute :location, String
10
10
  attribute :location_hints, String
11
11
 
12
- attribute :user_group_id, Integer
13
12
  attribute :registration_type, String
14
13
  attribute :registrations_enabled, Boolean, default: false
15
14
  attribute :registration_url, String
@@ -18,6 +17,8 @@ module Decidim
18
17
  attribute :iframe_embed_type, String, default: "none"
19
18
  attribute :iframe_access_level, String
20
19
 
20
+ validates :address, presence: true, if: ->(form) { form.needs_address? }
21
+ validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? && form.needs_address? }
21
22
  validates :iframe_embed_type, inclusion: { in: Decidim::Meetings::Meeting.participants_iframe_embed_types }
22
23
  validates :title, presence: true, etiquette: true
23
24
  validates :description, presence: true, etiquette: true
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ # This class holds a Form to save the chosen option for an response
6
+ class ResponseChoiceForm < Decidim::Form
7
+ attribute :body, String
8
+ attribute :position, Integer
9
+ attribute :response_option_id, Integer
10
+
11
+ validates :response_option_id, presence: true
12
+ end
13
+ end
14
+ end
@@ -2,13 +2,13 @@
2
2
 
3
3
  module Decidim
4
4
  module Meetings
5
- # This class holds a Form to save the questionnaire answers from Decidim's public page
6
- class AnswerForm < Decidim::Form
5
+ # This class holds a Form to save the questionnaire responses from Decidim's public page
6
+ class ResponseForm < Decidim::Form
7
7
  include Decidim::TranslationsHelper
8
8
 
9
9
  attribute :question_id, String
10
10
  attribute :body, String
11
- attribute :choices, Array[AnswerChoiceForm]
11
+ attribute :choices, Array[ResponseChoiceForm]
12
12
  attribute :current_user, Decidim::User
13
13
 
14
14
  validates :selected_choices, presence: true
@@ -20,8 +20,8 @@ module Decidim
20
20
  @question ||= Decidim::Meetings::Question.find(question_id)
21
21
  end
22
22
 
23
- def answer
24
- @answer ||= Decidim::Meetings::Answer.find_by(decidim_user_id: current_user.id, decidim_question_id: question_id) if current_user
23
+ def response
24
+ @response ||= Decidim::Meetings::Response.find_by(decidim_user_id: current_user.id, decidim_question_id: question_id) if current_user
25
25
  end
26
26
 
27
27
  def label
@@ -38,12 +38,12 @@ module Decidim
38
38
  self.question = model.question
39
39
 
40
40
  self.choices = model.choices.map do |choice|
41
- AnswerChoiceForm.from_model(choice)
41
+ ResponseChoiceForm.from_model(choice)
42
42
  end
43
43
  end
44
44
 
45
45
  def selected_choices
46
- choices.select(&:answer_option_id)
46
+ choices.select(&:response_option_id)
47
47
  end
48
48
 
49
49
  private
@@ -15,7 +15,6 @@ module Decidim
15
15
 
16
16
  def filter_origin_values
17
17
  origin_keys = %w(official participants)
18
- origin_keys << "user_group" if current_organization.user_groups_enabled?
19
18
 
20
19
  origin_values = flat_filter_values(*origin_keys, scope: "decidim.meetings.meetings.filters.origin_values")
21
20
  origin_values.prepend(["", t("all", scope: "decidim.meetings.meetings.filters.origin_values")])
@@ -8,7 +8,7 @@ module Decidim
8
8
  include Decidim::Meetings::ApplicationHelper
9
9
  include Decidim::TranslationsHelper
10
10
  include Decidim::ResourceHelper
11
- include Decidim::EndorsableHelper
11
+ include Decidim::LikeableHelper
12
12
 
13
13
  # Public: truncates the meeting description
14
14
  #
@@ -23,6 +23,19 @@ module Decidim
23
23
  CGI.unescapeHTML html_truncate(description, max_length:, tail:)
24
24
  end
25
25
 
26
+ def waitlist_status_block(registration)
27
+ return unless registration.waiting_list?
28
+
29
+ render layout: "decidim/meetings/layouts/aside_block", locals: { emoji: "ticket-line" } do
30
+ content_tag(:div) do
31
+ safe_join([
32
+ content_tag(:h3, t("waitlist.status", scope: "decidim.meetings.meetings.show"), class: "meeting__aside-block__title"),
33
+ content_tag(:p, t("waitlist.description", scope: "decidim.meetings.meetings.show"), class: "text-sm")
34
+ ])
35
+ end
36
+ end
37
+ end
38
+
26
39
  # Public: The css class applied based on the meeting type to
27
40
  # the css class.
28
41
  #
@@ -118,10 +131,6 @@ module Decidim
118
131
  end
119
132
  end
120
133
 
121
- def current_user_groups?
122
- current_organization.user_groups_enabled? && Decidim::UserGroups::ManageableUserGroups.for(current_user).verified.any?
123
- end
124
-
125
134
  # Public: URL to create an event in Google Calendars based on meeting
126
135
  # data.
127
136
  #
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ # Background job that promotes users from the waitlist to registered status,
6
+ # depending on available slots in the meeting.
7
+ class PromoteFromWaitlistJob < ApplicationJob
8
+ queue_as :default
9
+
10
+ def perform(meeting_id)
11
+ @meeting = Decidim::Meetings::Meeting.find_by(id: meeting_id)
12
+ return unless @meeting
13
+
14
+ promote_users_from_waitlist
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :meeting
20
+
21
+ def promote_users_from_waitlist
22
+ meeting.with_lock do
23
+ loop do
24
+ break unless meeting.remaining_slots.positive?
25
+
26
+ next_in_waitlist = meeting.registrations.on_waiting_list.first
27
+ break unless next_in_waitlist
28
+
29
+ promote(next_in_waitlist)
30
+ end
31
+ end
32
+ end
33
+
34
+ def promote(registration)
35
+ return unless registration.waiting_list?
36
+
37
+ registration.update!(status: :registered)
38
+ notify_user(registration)
39
+ end
40
+
41
+ def notify_user(registration)
42
+ send_email_confirmation(registration)
43
+ send_internal_notification(registration)
44
+ end
45
+
46
+ def send_email_confirmation(registration)
47
+ Decidim::Meetings::RegistrationMailer.confirmation(registration.user, meeting, registration).deliver_later
48
+ end
49
+
50
+ def send_internal_notification(registration)
51
+ Decidim::EventsManager.publish(
52
+ event: "decidim.events.meetings.meeting_registration_confirmed",
53
+ event_class: Decidim::Meetings::MeetingRegistrationNotificationEvent,
54
+ resource: meeting,
55
+ affected_users: [registration.user],
56
+ extra: {
57
+ registration_code: registration.code
58
+ }
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
@@ -14,7 +14,7 @@ module Decidim
14
14
  end
15
15
 
16
16
  def self.generate_checksum(meeting)
17
- Digest::MD5.hexdigest("#{meeting.id}-#{meeting.start_time}")
17
+ Digest::SHA256.hexdigest("#{meeting.id}-#{meeting.start_time}")
18
18
  end
19
19
 
20
20
  private
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rqrcode"
4
+
3
5
  module Decidim
4
6
  module Meetings
5
7
  # A custom mailer for sending notifications to users when
@@ -20,8 +22,11 @@ module Decidim
20
22
  @registration = registration
21
23
  @organization = @meeting.organization
22
24
  @locator = Decidim::ResourceLocatorPresenter.new(@meeting)
25
+ @registration_code_enabled = meeting.component.settings.registration_code_enabled
26
+ @qr_filename = "qr-#{@registration.code.parameterize}.png"
23
27
 
24
28
  add_calendar_attachment
29
+ add_qr_code_attachment if @registration_code_enabled
25
30
 
26
31
  subject = I18n.t("confirmation.subject", scope: "decidim.meetings.mailer.registration_mailer")
27
32
  mail(to: user.email, subject:)
@@ -30,12 +35,20 @@ module Decidim
30
35
 
31
36
  private
32
37
 
38
+ def qr_code
39
+ @qr_code ||= RQRCode::QRCode.new(@registration.validation_code_short_link.short_url)
40
+ end
41
+
33
42
  def add_calendar_attachment
34
43
  calendar = Icalendar::Calendar.new
35
44
  calendar.add_event(Calendar::MeetingToEvent.new(@meeting).event)
36
45
 
37
46
  attachments["meeting-calendar-info.ics"] = calendar.to_ical
38
47
  end
48
+
49
+ def add_qr_code_attachment
50
+ attachments[@qr_filename] = qr_code.as_png(size: 500).to_blob
51
+ end
39
52
  end
40
53
  end
41
54
  end
@@ -33,6 +33,11 @@ module Decidim
33
33
  def self.log_presenter_class_for(_log)
34
34
  Decidim::Meetings::AdminLog::AgendaItemPresenter
35
35
  end
36
+
37
+ # Returns the presenter for this model
38
+ def presenter
39
+ Decidim::Meetings::AgendaItemPresenter.new(self)
40
+ end
36
41
  end
37
42
  end
38
43
  end