decidim-meetings 0.24.1 → 0.25.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/meetings/join_meeting_button/registration_confirm.erb +6 -0
  3. data/app/cells/decidim/meetings/join_meeting_button/show.erb +8 -6
  4. data/app/cells/decidim/meetings/meeting_m_cell.rb +4 -0
  5. data/app/cells/decidim/meetings/meeting_url/show.erb +7 -2
  6. data/app/cells/decidim/meetings/meeting_url_cell.rb +14 -2
  7. data/app/cells/decidim/meetings/meetings_map/show.erb +1 -1
  8. data/app/cells/decidim/meetings/meetings_map_cell.rb +1 -1
  9. data/app/cells/decidim/meetings/online_meeting_cell.rb +36 -0
  10. data/app/cells/decidim/meetings/online_meeting_link/show.erb +44 -0
  11. data/app/cells/decidim/meetings/online_meeting_link_cell.rb +27 -0
  12. data/app/cells/decidim/meetings/public_participants_list/show.erb +16 -0
  13. data/app/cells/decidim/meetings/public_participants_list_cell.rb +31 -0
  14. data/app/cells/decidim/meetings/question_responses/show.erb +7 -0
  15. data/app/cells/decidim/meetings/question_responses_cell.rb +72 -0
  16. data/app/commands/decidim/meetings/admin/close_meeting.rb +3 -0
  17. data/app/commands/decidim/meetings/admin/copy_meeting.rb +0 -9
  18. data/app/commands/decidim/meetings/admin/create_meeting.rb +4 -20
  19. data/app/commands/decidim/meetings/admin/destroy_meeting.rb +2 -0
  20. data/app/commands/decidim/meetings/admin/publish_meeting.rb +72 -0
  21. data/app/commands/decidim/meetings/admin/unpublish_meeting.rb +45 -0
  22. data/app/commands/decidim/meetings/admin/update_meeting.rb +6 -3
  23. data/app/commands/decidim/meetings/admin/update_question_status.rb +57 -0
  24. data/app/commands/decidim/meetings/admin/update_questionnaire.rb +93 -0
  25. data/app/commands/decidim/meetings/close_meeting.rb +2 -0
  26. data/app/commands/decidim/meetings/create_answer.rb +52 -0
  27. data/app/commands/decidim/meetings/create_meeting.rb +3 -1
  28. data/app/commands/decidim/meetings/join_meeting.rb +2 -1
  29. data/app/commands/decidim/meetings/update_meeting.rb +2 -1
  30. data/app/controllers/concerns/decidim/meetings/admin/filterable.rb +55 -0
  31. data/app/controllers/concerns/decidim/meetings/polls_resources.rb +29 -0
  32. data/app/controllers/decidim/meetings/admin/application_controller.rb +6 -2
  33. data/app/controllers/decidim/meetings/admin/invites_controller.rb +0 -2
  34. data/app/controllers/decidim/meetings/admin/meeting_closes_controller.rb +1 -1
  35. data/app/controllers/decidim/meetings/admin/meetings_controller.rb +46 -1
  36. data/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb +107 -0
  37. data/app/controllers/decidim/meetings/calendars_controller.rb +8 -0
  38. data/app/controllers/decidim/meetings/live_events_controller.rb +29 -0
  39. data/app/controllers/decidim/meetings/meeting_closes_controller.rb +1 -1
  40. data/app/controllers/decidim/meetings/polls/answers_controller.rb +37 -0
  41. data/app/controllers/decidim/meetings/polls/questions_controller.rb +45 -0
  42. data/app/events/decidim/meetings/create_meeting_event.rb +16 -0
  43. data/app/forms/decidim/meetings/admin/answer_option_form.rb +24 -0
  44. data/app/forms/decidim/meetings/admin/close_meeting_form.rb +3 -1
  45. data/app/forms/decidim/meetings/admin/meeting_form.rb +12 -1
  46. data/app/forms/decidim/meetings/admin/question_form.rb +44 -0
  47. data/app/forms/decidim/meetings/admin/questionnaire_form.rb +19 -0
  48. data/app/forms/decidim/meetings/answer_choice_form.rb +14 -0
  49. data/app/forms/decidim/meetings/answer_form.rb +65 -0
  50. data/app/forms/decidim/meetings/close_meeting_form.rb +4 -0
  51. data/app/forms/decidim/meetings/join_meeting_form.rb +1 -0
  52. data/app/forms/decidim/meetings/meeting_form.rb +9 -0
  53. data/app/helpers/decidim/meetings/admin/filterable_helper.rb +13 -0
  54. data/app/helpers/decidim/meetings/application_helper.rb +3 -2
  55. data/app/helpers/decidim/meetings/map_helper.rb +1 -1
  56. data/app/helpers/decidim/meetings/meetings_helper.rb +21 -0
  57. data/app/models/decidim/meetings/answer.rb +48 -0
  58. data/app/models/decidim/meetings/answer_choice.rb +15 -0
  59. data/app/models/decidim/meetings/answer_option.rb +24 -0
  60. data/app/models/decidim/meetings/meeting.rb +88 -42
  61. data/app/models/decidim/meetings/poll.rb +19 -0
  62. data/app/models/decidim/meetings/question.rb +54 -0
  63. data/app/models/decidim/meetings/questionnaire.rb +23 -0
  64. data/app/models/decidim/meetings/registration.rb +2 -0
  65. data/app/packs/entrypoints/decidim_meetings.js +6 -0
  66. data/app/packs/entrypoints/decidim_meetings_admin.js +5 -0
  67. data/app/{assets/images/decidim/gamification/badges/attended_meetings.svg → packs/images/decidim/gamification/badges/decidim_gamification_badges_attended_meetings.svg} +0 -0
  68. data/app/{assets/images/decidim/meetings/icon.svg → packs/images/decidim/meetings/decidim_meetings.svg} +0 -0
  69. data/app/packs/src/decidim/meetings/admin/agendas.js +159 -0
  70. data/app/{assets/javascripts/decidim/meetings/admin/destroy_meeting_alert.js.es6 → packs/src/decidim/meetings/admin/destroy_meeting_alert.js} +0 -0
  71. data/app/{assets/javascripts/decidim/meetings/admin/meetings_form.js.es6 → packs/src/decidim/meetings/admin/meetings_form.js} +7 -4
  72. data/app/{assets/javascripts/decidim/meetings/admin/registrations_form.js.es6 → packs/src/decidim/meetings/admin/registrations_form.js} +0 -0
  73. data/app/{assets/javascripts/decidim/meetings/admin/registrations_invite_form.es6 → packs/src/decidim/meetings/admin/registrations_invite_form.js} +3 -3
  74. data/app/packs/src/decidim/meetings/meetings_form.js +54 -0
  75. data/app/packs/src/decidim/meetings/meetings_polls.js +41 -0
  76. data/app/packs/src/decidim/meetings/poll.component.js +166 -0
  77. data/app/packs/stylesheets/decidim/meetings/_header.scss +52 -0
  78. data/app/packs/stylesheets/decidim/meetings/_main.scss +178 -0
  79. data/app/packs/stylesheets/decidim/meetings/_meetings.scss +2 -0
  80. data/app/permissions/decidim/meetings/admin/permissions.rb +16 -8
  81. data/app/permissions/decidim/meetings/permissions.rb +42 -17
  82. data/app/presenters/decidim/meetings/admin_log/meeting_presenter.rb +5 -1
  83. data/app/presenters/decidim/meetings/meeting_presenter.rb +25 -25
  84. data/app/queries/decidim/meetings/filtered_meetings.rb +1 -1
  85. data/app/queries/decidim/meetings/metrics/meetings_metric_manage.rb +1 -1
  86. data/app/queries/decidim/meetings/questionnaire_user_answers.rb +29 -0
  87. data/app/serializers/decidim/meetings/data_portability_invite_serializer.rb +2 -1
  88. data/app/serializers/decidim/meetings/data_portability_registration_serializer.rb +2 -1
  89. data/app/serializers/decidim/meetings/registration_serializer.rb +3 -12
  90. data/app/services/decidim/meetings/calendar/component_calendar.rb +2 -2
  91. data/app/services/decidim/meetings/calendar/meeting_calendar.rb +33 -0
  92. data/app/services/decidim/meetings/calendar_renderer.rb +2 -0
  93. data/app/services/decidim/meetings/meeting_iframe_embedder.rb +86 -0
  94. data/app/services/decidim/meetings/meeting_search.rb +2 -2
  95. data/app/views/decidim/meetings/admin/agenda/_form.html.erb +1 -1
  96. data/app/views/decidim/meetings/admin/invite_join_meeting_mailer/invite.html.erb +4 -4
  97. data/app/views/decidim/meetings/admin/invites/_form.html.erb +1 -1
  98. data/app/views/decidim/meetings/admin/meeting_closes/_form.html.erb +16 -1
  99. data/app/views/decidim/meetings/admin/meeting_copies/_form.html.erb +1 -1
  100. data/app/views/decidim/meetings/admin/meetings/_form.html.erb +16 -1
  101. data/app/views/decidim/meetings/admin/meetings/index.html.erb +39 -11
  102. data/app/views/decidim/meetings/admin/poll/_answer_option.html.erb +34 -0
  103. data/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb +7 -0
  104. data/app/views/decidim/meetings/admin/poll/_form.html.erb +63 -0
  105. data/app/views/decidim/meetings/admin/poll/_question.html.erb +96 -0
  106. data/app/views/decidim/meetings/admin/poll/edit.html.erb +9 -0
  107. data/app/views/decidim/meetings/admin/registrations/_form.html.erb +1 -1
  108. data/app/views/decidim/meetings/directory/meetings/index.html.erb +4 -4
  109. data/app/views/decidim/meetings/directory/meetings/index.js.erb +2 -0
  110. data/app/views/decidim/meetings/layouts/live_event.html.erb +50 -0
  111. data/app/views/decidim/meetings/live_events/show.html.erb +12 -0
  112. data/app/views/decidim/meetings/meeting_closes/_form.html.erb +8 -1
  113. data/app/views/decidim/meetings/meeting_closes/edit.html.erb +1 -1
  114. data/app/views/decidim/meetings/meetings/_calendar_modal.html.erb +26 -0
  115. data/app/views/decidim/meetings/meetings/_filters.html.erb +1 -1
  116. data/app/views/decidim/meetings/meetings/_form.html.erb +6 -1
  117. data/app/views/decidim/meetings/meetings/_linked_meetings.html.erb +1 -1
  118. data/app/views/decidim/meetings/meetings/_meeting_minutes.html.erb +7 -18
  119. data/app/views/decidim/meetings/meetings/edit.html.erb +1 -1
  120. data/app/views/decidim/meetings/meetings/index.html.erb +4 -4
  121. data/app/views/decidim/meetings/meetings/index.js.erb +1 -1
  122. data/app/views/decidim/meetings/meetings/new.html.erb +1 -1
  123. data/app/views/decidim/meetings/meetings/show.html.erb +13 -14
  124. data/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb +13 -0
  125. data/app/views/decidim/meetings/polls/answers/_single_option.html.erb +19 -0
  126. data/app/views/decidim/meetings/polls/answers/create.js.erb +5 -0
  127. data/app/views/decidim/meetings/polls/questions/_closed_question.html.erb +7 -0
  128. data/app/views/decidim/meetings/polls/questions/_index.html.erb +5 -0
  129. data/app/views/decidim/meetings/polls/questions/_index_admin.html.erb +43 -0
  130. data/app/views/decidim/meetings/polls/questions/_published_question.html.erb +29 -0
  131. data/app/views/decidim/meetings/polls/questions/_question.html.erb +7 -0
  132. data/app/views/decidim/meetings/polls/questions/index.js.erb +5 -0
  133. data/app/views/decidim/meetings/polls/questions/index_admin.js.erb +5 -0
  134. data/app/views/decidim/meetings/registration_mailer/confirmation.html.erb +5 -2
  135. data/app/views/devise/mailer/join_meeting.html.erb +6 -6
  136. data/config/assets.rb +11 -0
  137. data/config/locales/ar.yml +3 -24
  138. data/config/locales/ca.yml +36 -27
  139. data/config/locales/cs.yml +74 -28
  140. data/config/locales/de.yml +22 -27
  141. data/config/locales/el.yml +3 -24
  142. data/config/locales/en.yml +112 -26
  143. data/config/locales/es-MX.yml +13 -27
  144. data/config/locales/es-PY.yml +13 -27
  145. data/config/locales/es.yml +13 -27
  146. data/config/locales/eu.yml +3 -24
  147. data/config/locales/fi-plain.yml +70 -26
  148. data/config/locales/fi.yml +72 -28
  149. data/config/locales/fr-CA.yml +19 -26
  150. data/config/locales/fr-LU.yml +534 -0
  151. data/config/locales/fr.yml +19 -26
  152. data/config/locales/gl.yml +46 -24
  153. data/config/locales/hu.yml +5 -24
  154. data/config/locales/id-ID.yml +3 -24
  155. data/config/locales/is-IS.yml +2 -22
  156. data/config/locales/it.yml +117 -23
  157. data/config/locales/ja.yml +148 -58
  158. data/config/locales/lb-LU.yml +1 -0
  159. data/config/locales/lv.yml +3 -24
  160. data/config/locales/nl.yml +70 -25
  161. data/config/locales/no.yml +5 -26
  162. data/config/locales/pl.yml +27 -38
  163. data/config/locales/pt-BR.yml +178 -23
  164. data/config/locales/pt.yml +3 -24
  165. data/config/locales/ro-RO.yml +38 -27
  166. data/config/locales/ru.yml +3 -24
  167. data/config/locales/sk.yml +3 -24
  168. data/config/locales/sv.yml +36 -27
  169. data/config/locales/tr-TR.yml +5 -27
  170. data/config/locales/uk.yml +3 -24
  171. data/config/locales/zh-CN.yml +3 -24
  172. data/db/migrate/20210217124802_add_registration_custom_content_to_meetings.rb +8 -0
  173. data/db/migrate/20210413050756_add_published_at_to_meetings.rb +7 -0
  174. data/db/migrate/20210413050917_update_published_at_to_existing_meetings.rb +20 -0
  175. data/db/migrate/20210430123416_add_public_participation_to_decidim_meetings_registrations.rb +7 -0
  176. data/db/migrate/20210506180226_merge_meetings_minutes_into_meetings_table.rb +48 -0
  177. data/db/migrate/20210512055802_create_decidim_meetings_polls.rb +10 -0
  178. data/db/migrate/20210512100333_drop_decidim_meetings_minutes_table.rb +77 -0
  179. data/db/migrate/20210518133236_merge_minutes_with_closing_report_in_meetings_table.rb +57 -0
  180. data/db/migrate/20210520084247_create_decidim_meetings_questionnaires.rb +11 -0
  181. data/db/migrate/20210520084253_create_decidim_meetings_questions.rb +15 -0
  182. data/db/migrate/20210520084321_create_decidim_meetings_answers.rb +13 -0
  183. data/db/migrate/20210520084330_create_decidim_meetings_answer_options.rb +10 -0
  184. data/db/migrate/20210520084337_create_decidim_meetings_answer_choices.rb +13 -0
  185. data/db/migrate/20210520134834_add_status_to_meetings_questions.rb +7 -0
  186. data/db/migrate/20210602040614_add_setting_embed_iframe_to_meetings.rb +7 -0
  187. data/lib/decidim/api/linked_resources_interface.rb +8 -1
  188. data/lib/decidim/api/meeting_type.rb +19 -4
  189. data/lib/decidim/api/meetings_type.rb +2 -2
  190. data/lib/decidim/meetings/admin_engine.rb +5 -1
  191. data/lib/decidim/meetings/api.rb +0 -1
  192. data/lib/decidim/meetings/component.rb +41 -6
  193. data/lib/decidim/meetings/data_portability_user_answers_serializer.rb +33 -0
  194. data/lib/decidim/meetings/engine.rb +10 -11
  195. data/lib/decidim/meetings/meeting_serializer.rb +1 -0
  196. data/lib/decidim/meetings/polls.rb +10 -0
  197. data/lib/decidim/meetings/test/factories.rb +85 -9
  198. data/lib/decidim/meetings/user_answers_serializer.rb +47 -0
  199. data/lib/decidim/meetings/version.rb +1 -1
  200. data/lib/decidim/meetings.rb +10 -0
  201. metadata +103 -113
  202. data/app/assets/config/decidim_meetings_manifest.js +0 -9
  203. data/app/assets/javascripts/decidim/meetings/admin/agendas.js.es6 +0 -158
  204. data/app/assets/javascripts/decidim/meetings/meetings_form.js.es6 +0 -54
  205. data/app/commands/decidim/meetings/admin/create_minutes.rb +0 -55
  206. data/app/commands/decidim/meetings/admin/update_minutes.rb +0 -63
  207. data/app/controllers/decidim/meetings/admin/minutes_controller.rb +0 -69
  208. data/app/forms/decidim/meetings/admin/minutes_form.rb +0 -20
  209. data/app/models/decidim/meetings/minutes.rb +0 -22
  210. data/app/presenters/decidim/meetings/admin_log/minutes_presenter.rb +0 -42
  211. data/app/views/decidim/meetings/admin/minutes/_form.html.erb +0 -23
  212. data/app/views/decidim/meetings/admin/minutes/edit.html.erb +0 -7
  213. data/app/views/decidim/meetings/admin/minutes/new.html.erb +0 -7
  214. data/app/views/decidim/meetings/meetings/_online_meeting_link.html.erb +0 -11
  215. data/config/locales/ja-JP.yml +0 -494
  216. data/lib/decidim/api/minutes_type.rb +0 -20
@@ -8,6 +8,9 @@ module Decidim
8
8
  include TranslatableAttributes
9
9
 
10
10
  translatable_attribute :closing_report, String
11
+ attribute :video_url, String
12
+ attribute :audio_url, String
13
+ attribute :closing_visible, Boolean, default: true
11
14
  attribute :attendees_count, Integer, default: 0
12
15
  attribute :contributions_count, Integer, default: 0
13
16
  attribute :attending_organizations, String
@@ -18,7 +21,6 @@ module Decidim
18
21
  validates :closing_report, translatable_presence: true
19
22
  validates :attendees_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
20
23
  validates :contributions_count, numericality: true, allow_blank: true
21
- validates :attending_organizations, presence: true
22
24
 
23
25
  # Private: Gets the proposals from the meeting and injects them to the form.
24
26
  #
@@ -22,11 +22,14 @@ module Decidim
22
22
  attribute :registration_type, String
23
23
  attribute :registration_url, String
24
24
  attribute :available_slots, Integer, default: 0
25
+ attribute :customize_registration_email, Boolean
26
+ attribute :show_embedded_iframe, Boolean, default: false
25
27
 
26
28
  translatable_attribute :title, String
27
29
  translatable_attribute :description, String
28
30
  translatable_attribute :location, String
29
31
  translatable_attribute :location_hints, String
32
+ translatable_attribute :registration_email_custom_content, String
30
33
 
31
34
  validates :title, translatable_presence: true
32
35
  validates :description, translatable_presence: true
@@ -38,7 +41,7 @@ module Decidim
38
41
 
39
42
  validates :address, presence: true, if: ->(form) { form.needs_address? }
40
43
  validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? && form.needs_address? }
41
- validates :online_meeting_url, presence: true, url: true, if: ->(form) { form.online_meeting? || form.hybrid_meeting? }
44
+ validates :online_meeting_url, url: true, if: ->(form) { form.online_meeting? || form.hybrid_meeting? }
42
45
  validates :start_time, presence: true, date: { before: :end_time }
43
46
  validates :end_time, presence: true, date: { after: :start_time }
44
47
 
@@ -47,6 +50,7 @@ module Decidim
47
50
  validates :scope, presence: true, if: ->(form) { form.decidim_scope_id.present? }
48
51
  validates :decidim_scope_id, scope_belongs_to_component: true, if: ->(form) { form.decidim_scope_id.present? }
49
52
  validates :clean_type_of_meeting, presence: true
53
+ validate :embeddable_meeting_url
50
54
 
51
55
  delegate :categories, to: :current_component
52
56
 
@@ -150,6 +154,13 @@ module Decidim
150
154
  ]
151
155
  end
152
156
  end
157
+
158
+ def embeddable_meeting_url
159
+ if online_meeting_url.present? && show_embedded_iframe
160
+ embedder_service = Decidim::Meetings::MeetingIframeEmbedder.new(online_meeting_url)
161
+ errors.add(:show_embedded_iframe, :not_embeddable) unless embedder_service.embeddable?
162
+ end
163
+ end
153
164
  end
154
165
  end
155
166
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ module Admin
6
+ # This class holds a Form to update questionnaire questions from Decidim's admin panel.
7
+ class QuestionForm < Decidim::Form
8
+ include TranslatableAttributes
9
+
10
+ attribute :position, Integer
11
+ attribute :mandatory, Boolean, default: false
12
+ attribute :question_type, String
13
+ attribute :answer_options, Array[AnswerOptionForm]
14
+ attribute :max_choices, Integer
15
+ attribute :deleted, Boolean, default: false
16
+
17
+ translatable_attribute :body, String
18
+ translatable_attribute :description, String
19
+
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
23
+ validates :body, translatable_presence: true, if: :requires_body?
24
+ validates :answer_options, presence: true
25
+
26
+ def to_param
27
+ return id if id.present?
28
+
29
+ "questionnaire-question-id"
30
+ end
31
+
32
+ def number_of_options
33
+ answer_options.size
34
+ end
35
+
36
+ private
37
+
38
+ def requires_body?
39
+ !deleted
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ module Admin
6
+ # This class holds a Form to update questionnaires from Decidim's admin panel.
7
+ class QuestionnaireForm < Decidim::Form
8
+ attribute :published_at, Decidim::Attributes::TimeWithZone
9
+ attribute :questions, Array[Decidim::Meetings::Admin::QuestionForm]
10
+
11
+ def map_model(model)
12
+ self.questions = model.questions.map do |question|
13
+ Decidim::Meetings::Admin::QuestionForm.from_model(question)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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 answer
6
+ class AnswerChoiceForm < Decidim::Form
7
+ attribute :body, String
8
+ attribute :position, Integer
9
+ attribute :answer_option_id, Integer
10
+
11
+ validates :answer_option_id, presence: true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ # This class holds a Form to save the questionnaire answers from Decidim's public page
6
+ class AnswerForm < Decidim::Form
7
+ include Decidim::TranslationsHelper
8
+
9
+ attribute :question_id, String
10
+ attribute :body, String
11
+ attribute :choices, Array[AnswerChoiceForm]
12
+ attribute :current_user, Decidim::User
13
+ attribute :answer, Decidim::Meetings::Answer
14
+
15
+ validates :selected_choices, presence: true
16
+ validate :max_choices, if: -> { question.max_choices }
17
+
18
+ attr_writer :question
19
+
20
+ def question
21
+ @question ||= Decidim::Meetings::Question.find(question_id)
22
+ end
23
+
24
+ def answer
25
+ @answer ||= Decidim::Meetings::Answer.find_by(decidim_user_id: current_user.id, decidim_question_id: question_id) if current_user
26
+ end
27
+
28
+ def label(idx)
29
+ base = "#{idx + 1}. #{translated_attribute(question.body)}"
30
+ base += " (#{max_choices_label})" if question.max_choices
31
+ base
32
+ end
33
+
34
+ # Public: Map the correct fields.
35
+ #
36
+ # Returns nothing.
37
+ def map_model(model)
38
+ self.question_id = model.decidim_question_id
39
+ self.question = model.question
40
+
41
+ self.choices = model.choices.map do |choice|
42
+ AnswerChoiceForm.from_model(choice)
43
+ end
44
+ end
45
+
46
+ def selected_choices
47
+ choices.select(&:body)
48
+ end
49
+
50
+ private
51
+
52
+ def max_choices
53
+ errors.add(:choices, :too_many) if selected_choices.size > question.max_choices
54
+ end
55
+
56
+ def mandatory_label
57
+ "*"
58
+ end
59
+
60
+ def max_choices_label
61
+ I18n.t("questionnaires.question.max_choices", scope: "decidim.forms", n: question.max_choices)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -8,8 +8,12 @@ module Decidim
8
8
  attribute :proposal_ids, Array[Integer]
9
9
  attribute :proposals
10
10
  attribute :closed_at, Decidim::Attributes::TimeWithZone, default: ->(_form, _attribute) { Time.current }
11
+ attribute :attendees_count, Integer, default: 0
11
12
 
12
13
  validates :closing_report, presence: true
14
+ validates :attendees_count,
15
+ presence: true,
16
+ numericality: { greater_than_or_equal_to: 0, only_integer: true }
13
17
 
14
18
  # Private: Gets the proposals from the meeting and injects them to the form.
15
19
  #
@@ -4,6 +4,7 @@ module Decidim
4
4
  module Meetings
5
5
  class JoinMeetingForm < Decidim::Form
6
6
  attribute :user_group_id, Integer
7
+ attribute :public_participation, Boolean, default: false
7
8
  end
8
9
  end
9
10
  end
@@ -24,6 +24,7 @@ module Decidim
24
24
  attribute :registration_url, String
25
25
  attribute :available_slots, Integer, default: 0
26
26
  attribute :registration_terms, String
27
+ attribute :show_embedded_iframe, Boolean, default: false
27
28
 
28
29
  validates :title, presence: true
29
30
  validates :description, presence: true
@@ -44,6 +45,7 @@ module Decidim
44
45
  validates :scope, presence: true, if: ->(form) { form.decidim_scope_id.present? }
45
46
  validates :decidim_scope_id, scope_belongs_to_component: true, if: ->(form) { form.decidim_scope_id.present? }
46
47
  validates :clean_type_of_meeting, presence: true
48
+ validate :embeddable_meeting_url
47
49
 
48
50
  delegate :categories, to: :current_component
49
51
 
@@ -141,6 +143,13 @@ module Decidim
141
143
  def registrations_enabled
142
144
  on_this_platform?
143
145
  end
146
+
147
+ def embeddable_meeting_url
148
+ if online_meeting_url.present? && show_embedded_iframe
149
+ embedder_service = Decidim::Meetings::MeetingIframeEmbedder.new(online_meeting_url)
150
+ errors.add(:show_embedded_iframe, :not_embeddable) unless embedder_service.embeddable?
151
+ end
152
+ end
144
153
  end
145
154
  end
146
155
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ module Admin
6
+ module FilterableHelper
7
+ def extra_dropdown_submenu_options_items(_filter, _i18n_scope)
8
+ []
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -17,8 +17,9 @@ module Decidim
17
17
  origin_values = []
18
18
  origin_values << TreePoint.new("official", t("decidim.meetings.meetings.filters.origin_values.official"))
19
19
  origin_values << TreePoint.new("citizens", t("decidim.meetings.meetings.filters.origin_values.citizens")) # todo
20
- # if component_settings enabled enabled
21
- origin_values << TreePoint.new("user_group", t("decidim.meetings.meetings.filters.origin_values.user_groups")) # todo
20
+ if current_organization.user_groups_enabled?
21
+ origin_values << TreePoint.new("user_group", t("decidim.meetings.meetings.filters.origin_values.user_groups")) # todo
22
+ end
22
23
  # if current_organization.user_groups_enabled? and component_settings enabled enabled
23
24
 
24
25
  TreeNode.new(
@@ -9,7 +9,7 @@ module Decidim
9
9
  #
10
10
  # meetings - A collection of meetings
11
11
  def meetings_data_for_map(meetings)
12
- geocoded_meetings = meetings.select(&:geocoded?)
12
+ geocoded_meetings = meetings.select(&:geocoded_and_valid?)
13
13
  geocoded_meetings.map do |meeting|
14
14
  meeting.slice(:latitude, :longitude, :address).merge(title: translated_attribute(meeting.title),
15
15
  description: html_truncate(translated_attribute(meeting.description), length: 200),
@@ -119,6 +119,27 @@ module Decidim
119
119
  def current_user_groups?
120
120
  current_organization.user_groups_enabled? && Decidim::UserGroups::ManageableUserGroups.for(current_user).verified.any?
121
121
  end
122
+
123
+ # Public: URL to create an event in Google Calendars based on meeting
124
+ # data.
125
+ #
126
+ # meeting - a Decidim::Meeting instance.
127
+ #
128
+ # Returns a String.
129
+ def google_calendar_event_url(meeting)
130
+ meeting_url = resource_locator(meeting).url
131
+ meeting = present(meeting)
132
+ params = {
133
+ text: meeting.title,
134
+ dates: meeting.dates_param,
135
+ details: I18n.t(
136
+ "decidim.meetings.meetings.calendar_modal.full_details_html",
137
+ link: link_to(meeting_url, meeting_url)
138
+ )
139
+ }
140
+ base_url = "https://calendar.google.com/calendar/u/0/r/eventedit"
141
+ "#{base_url}?#{params.to_param}"
142
+ end
122
143
  end
123
144
  end
124
145
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ # The data store for an Answer in the Decidim::Meetings
6
+ class Answer < Meetings::ApplicationRecord
7
+ include Decidim::DataPortability
8
+
9
+ belongs_to :user, class_name: "Decidim::User", foreign_key: "decidim_user_id", optional: true
10
+ belongs_to :questionnaire, class_name: "Decidim::Meetings::Questionnaire", foreign_key: "decidim_questionnaire_id"
11
+ belongs_to :question, class_name: "Question", foreign_key: "decidim_question_id"
12
+
13
+ has_many :choices,
14
+ class_name: "AnswerChoice",
15
+ foreign_key: "decidim_answer_id",
16
+ dependent: :destroy,
17
+ inverse_of: :answer
18
+
19
+ validate :user_questionnaire_same_organization
20
+ validate :question_belongs_to_questionnaire
21
+
22
+ def self.user_collection(user)
23
+ where(decidim_user_id: user.id)
24
+ end
25
+
26
+ def self.export_serializer
27
+ Decidim::Meetings::DataPortabilityUserAnswersSerializer
28
+ end
29
+
30
+ def organization
31
+ user.organization if user.present?
32
+ questionnaire&.questionnaire_for.try(:organization)
33
+ end
34
+
35
+ private
36
+
37
+ def user_questionnaire_same_organization
38
+ return if user.nil? || user&.organization == questionnaire.questionnaire_for&.organization
39
+
40
+ errors.add(:user, :invalid)
41
+ end
42
+
43
+ def question_belongs_to_questionnaire
44
+ errors.add(:questionnaire, :invalid) if question&.questionnaire != questionnaire
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ class AnswerChoice < Meetings::ApplicationRecord
6
+ belongs_to :answer,
7
+ class_name: "Decidim::Meetings::Answer",
8
+ foreign_key: "decidim_answer_id"
9
+
10
+ belongs_to :answer_option,
11
+ class_name: "Decidim::Meetings::AnswerOption",
12
+ foreign_key: "decidim_answer_option_id"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Meetings
5
+ class AnswerOption < Meetings::ApplicationRecord
6
+ include Decidim::TranslatableResource
7
+
8
+ default_scope { order(arel_table[:id].asc) }
9
+
10
+ translatable_fields :body
11
+
12
+ belongs_to :question, class_name: "Decidim::Meetings::Question", foreign_key: "decidim_question_id"
13
+ has_many :choices,
14
+ class_name: "AnswerChoice",
15
+ foreign_key: "decidim_answer_option_id",
16
+ dependent: :destroy,
17
+ inverse_of: :answer_option
18
+
19
+ def translated_body
20
+ Decidim::Forms::AnswerOptionPresenter.new(self).translated_body
21
+ end
22
+ end
23
+ end
24
+ end
@@ -13,7 +13,7 @@ module Decidim
13
13
  include Decidim::ScopableResource
14
14
  include Decidim::HasCategory
15
15
  include Decidim::Followable
16
- include Decidim::Comments::Commentable
16
+ include Decidim::Comments::CommentableWithComponent
17
17
  include Decidim::Searchable
18
18
  include Decidim::Traceable
19
19
  include Decidim::Loggable
@@ -23,6 +23,7 @@ module Decidim
23
23
  include Decidim::Reportable
24
24
  include Decidim::Authorable
25
25
  include Decidim::TranslatableResource
26
+ include Decidim::Publicable
26
27
 
27
28
  TYPE_OF_MEETING = %w(in_person online hybrid).freeze
28
29
  REGISTRATION_TYPE = %w(registration_disabled on_this_platform on_different_platform).freeze
@@ -32,8 +33,16 @@ module Decidim
32
33
  has_many :registrations, class_name: "Decidim::Meetings::Registration", foreign_key: "decidim_meeting_id", dependent: :destroy
33
34
  has_many :invites, class_name: "Decidim::Meetings::Invite", foreign_key: "decidim_meeting_id", dependent: :destroy
34
35
  has_many :services, class_name: "Decidim::Meetings::Service", foreign_key: "decidim_meeting_id", dependent: :destroy
35
- has_one :minutes, class_name: "Decidim::Meetings::Minutes", foreign_key: "decidim_meeting_id", dependent: :destroy
36
36
  has_one :agenda, class_name: "Decidim::Meetings::Agenda", foreign_key: "decidim_meeting_id", dependent: :destroy
37
+ has_one :poll, class_name: "Decidim::Meetings::Poll", foreign_key: "decidim_meeting_id", dependent: :destroy
38
+ has_many(
39
+ :public_participants,
40
+ -> { merge(Registration.public_participant) },
41
+ through: :registrations,
42
+ class_name: "Decidim::User",
43
+ foreign_key: :decidim_user_id,
44
+ source: :user
45
+ )
37
46
 
38
47
  component_manifest_name "meetings"
39
48
 
@@ -41,42 +50,54 @@ module Decidim
41
50
 
42
51
  geocoded_by :address
43
52
 
53
+ scope :published, -> { where.not(published_at: nil) }
44
54
  scope :past, -> { where(arel_table[:end_time].lteq(Time.current)) }
45
55
  scope :upcoming, -> { where(arel_table[:end_time].gteq(Time.current)) }
46
56
 
47
57
  scope :visible_meeting_for, lambda { |user|
48
- (all.distinct if user&.admin?) ||
58
+ (all.published.distinct if user&.admin?) ||
49
59
  if user.present?
50
- spaces = %w(assembly participatory_process)
51
- spaces << "conference" if defined?(Decidim::Conference)
52
- user_role_queries = spaces.map do |participatory_space_name|
60
+ spaces = Decidim.participatory_space_registry.manifests.map do |manifest|
61
+ {
62
+ name: manifest.model_class_name.constantize.table_name.singularize,
63
+ class_name: manifest.model_class_name
64
+ }
65
+ end
66
+ user_role_queries = spaces.map do |space|
67
+ roles_table = "#{space[:name]}_user_roles"
68
+ next unless connection.table_exists?(roles_table)
69
+
53
70
  "SELECT decidim_components.id FROM decidim_components
54
71
  WHERE CONCAT(decidim_components.participatory_space_id, '-', decidim_components.participatory_space_type)
55
72
  IN
56
- (SELECT CONCAT(decidim_#{participatory_space_name}_user_roles.decidim_#{participatory_space_name}_id, '-Decidim::#{participatory_space_name.classify}')
57
- FROM decidim_#{participatory_space_name}_user_roles WHERE decidim_#{participatory_space_name}_user_roles.decidim_user_id = ?)
73
+ (SELECT CONCAT(#{roles_table}.#{space[:name]}_id, '-#{space[:class_name]}')
74
+ FROM #{roles_table} WHERE #{roles_table}.decidim_user_id = ?)
58
75
  "
59
76
  end
60
77
 
61
- where("decidim_meetings_meetings.private_meeting = ?
62
- OR decidim_meetings_meetings.transparent = ?
63
- OR decidim_meetings_meetings.id IN
64
- (SELECT decidim_meetings_registrations.decidim_meeting_id FROM decidim_meetings_registrations WHERE decidim_meetings_registrations.decidim_user_id = ?)
65
- OR decidim_meetings_meetings.decidim_component_id IN
66
- (SELECT decidim_components.id FROM decidim_components
78
+ query = "
79
+ decidim_meetings_meetings.private_meeting = ?
80
+ OR decidim_meetings_meetings.transparent = ?
81
+ OR decidim_meetings_meetings.id IN (
82
+ SELECT decidim_meetings_registrations.decidim_meeting_id FROM decidim_meetings_registrations WHERE decidim_meetings_registrations.decidim_user_id = ?
83
+ )
84
+ OR decidim_meetings_meetings.decidim_component_id IN (
85
+ SELECT decidim_components.id FROM decidim_components
67
86
  WHERE CONCAT(decidim_components.participatory_space_id, '-', decidim_components.participatory_space_type)
68
87
  IN
69
88
  (SELECT CONCAT(decidim_participatory_space_private_users.privatable_to_id, '-', decidim_participatory_space_private_users.privatable_to_type)
70
89
  FROM decidim_participatory_space_private_users WHERE decidim_participatory_space_private_users.decidim_user_id = ?)
71
90
  )
72
- OR decidim_meetings_meetings.decidim_component_id IN
73
- (
74
- #{user_role_queries.compact.join(" UNION ")}
75
- )
76
- ", false, true, user.id, user.id, *user_role_queries.compact.map { user.id })
77
- .distinct
91
+ "
92
+ if user_role_queries.any?
93
+ query = "#{query} OR decidim_meetings_meetings.decidim_component_id IN
94
+ (#{user_role_queries.compact.join(" UNION ")})
95
+ "
96
+ end
97
+
98
+ where(query, false, true, user.id, user.id, *user_role_queries.compact.map { user.id }).published.distinct
78
99
  else
79
- visible
100
+ published.visible
80
101
  end
81
102
  }
82
103
 
@@ -84,6 +105,7 @@ module Decidim
84
105
 
85
106
  TYPE_OF_MEETING.each do |type|
86
107
  scope type.to_sym, -> { where(type_of_meeting: type.to_sym) }
108
+ scope "not_#{type}".to_sym, -> { where.not(type_of_meeting: type.to_sym) }
87
109
  end
88
110
 
89
111
  searchable_fields({
@@ -93,8 +115,8 @@ module Decidim
93
115
  D: [:description, :address],
94
116
  datetime: :start_time
95
117
  },
96
- index_on_create: ->(meeting) { meeting.visible? },
97
- index_on_update: ->(meeting) { meeting.visible? })
118
+ index_on_create: ->(meeting) { meeting.visible? && meeting.published? },
119
+ index_on_update: ->(meeting) { meeting.visible? && meeting.published? })
98
120
 
99
121
  # we create a salt for the meeting only on new meetings to prevent changing old IDs for existing (Ether)PADs
100
122
  before_create :set_default_salt
@@ -145,14 +167,8 @@ module Decidim
145
167
  registrations.where(user: user).any?
146
168
  end
147
169
 
148
- # Public: Overrides the `commentable?` Commentable concern method.
149
- def commentable?
150
- component.settings.comments_enabled?
151
- end
152
-
153
- # Public: Overrides the `accepts_new_comments?` Commentable concern method.
154
- def accepts_new_comments?
155
- commentable? && !component.current_settings.comments_blocked
170
+ def maps_enabled?
171
+ component.settings.maps_enabled?
156
172
  end
157
173
 
158
174
  # Public: Overrides the `allow_resource_permissions?` Resourceable concern method.
@@ -175,11 +191,6 @@ module Decidim
175
191
  followers
176
192
  end
177
193
 
178
- # Public: Whether the object can have new comments or not.
179
- def user_allowed_to_comment?(user)
180
- can_participate?(user)
181
- end
182
-
183
194
  def can_participate?(user)
184
195
  can_participate_in_space?(user) && can_participate_in_meeting?(user)
185
196
  end
@@ -216,6 +227,8 @@ module Decidim
216
227
  end
217
228
 
218
229
  def authored_proposals
230
+ return [] unless Decidim::Meetings.enable_proposal_linking
231
+
219
232
  Decidim::Proposals::Proposal
220
233
  .joins(:coauthorships)
221
234
  .where(
@@ -241,12 +254,10 @@ module Decidim
241
254
  [normalized_author.name]
242
255
  end
243
256
 
244
- def hybrid_meeting?
245
- type_of_meeting == "hybrid"
246
- end
247
-
248
- def online_meeting?
249
- type_of_meeting == "online"
257
+ TYPE_OF_MEETING.each do |type|
258
+ define_method("#{type}_meeting?") do
259
+ type_of_meeting == type
260
+ end
250
261
  end
251
262
 
252
263
  def registration_disabled?
@@ -269,6 +280,41 @@ module Decidim
269
280
  !!attendees_count && attendees_count.positive?
270
281
  end
271
282
 
283
+ def self.sort_by_translated_title_asc
284
+ field = Arel::Nodes::InfixOperation.new("->>", arel_table[:title], Arel::Nodes.build_quoted(I18n.locale))
285
+ order(Arel::Nodes::InfixOperation.new("", field, Arel.sql("ASC")))
286
+ end
287
+
288
+ def self.sort_by_translated_title_desc
289
+ field = Arel::Nodes::InfixOperation.new("->>", arel_table[:title], Arel::Nodes.build_quoted(I18n.locale))
290
+ order(Arel::Nodes::InfixOperation.new("", field, Arel.sql("DESC")))
291
+ end
292
+
293
+ ransacker :type do
294
+ Arel.sql(%("decidim_meetings_meetings"."type_of_meeting"))
295
+ end
296
+
297
+ ransacker :title do
298
+ Arel.sql(%{cast("decidim_meetings_meetings"."title" as text)})
299
+ end
300
+
301
+ ransacker :id_string do
302
+ Arel.sql(%{cast("decidim_meetings_meetings"."id" as text)})
303
+ end
304
+
305
+ ransacker :is_upcoming do
306
+ Arel.sql("(start_time > NOW())")
307
+ end
308
+
309
+ ransacker :origin do
310
+ Arel.sql("CASE
311
+ WHEN decidim_author_type = 'Decidim::Organization' THEN 'official'
312
+ WHEN decidim_author_type = 'Decidim::UserBaseEntity' AND decidim_user_group_id IS NOT NULL THEN 'user_group'
313
+ WHEN decidim_author_type = 'Decidim::UserBaseEntity' AND decidim_user_group_id IS NULL THEN 'citizen'
314
+ ELSE 'unknown' END
315
+ ")
316
+ end
317
+
272
318
  private
273
319
 
274
320
  def can_participate_in_meeting?(user)