decidim 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile.lock +49 -55
  4. data/README.md +2 -1
  5. data/Rakefile +4 -4
  6. data/decidim-admin/app/assets/stylesheets/decidim/admin/extra/_label-required.scss +3 -0
  7. data/decidim-admin/app/assets/stylesheets/decidim/admin/extra/_title_bar.scss +57 -1
  8. data/decidim-admin/app/commands/decidim/admin/create_participatory_process_admin.rb +28 -7
  9. data/decidim-admin/app/commands/decidim/admin/create_participatory_process_step.rb +2 -1
  10. data/decidim-admin/app/commands/decidim/admin/destroy_participatory_process_step.rb +5 -2
  11. data/decidim-admin/app/commands/decidim/admin/update_feature.rb +1 -0
  12. data/decidim-admin/app/commands/decidim/admin/update_organization.rb +10 -5
  13. data/decidim-admin/app/commands/decidim/admin/update_participatory_process.rb +4 -2
  14. data/decidim-admin/app/controllers/decidim/admin/application_controller.rb +2 -0
  15. data/decidim-admin/app/controllers/decidim/admin/concerns/has_attachments.rb +10 -1
  16. data/decidim-admin/app/controllers/decidim/admin/concerns/participatory_process_admin.rb +0 -5
  17. data/decidim-admin/app/controllers/decidim/admin/features/base_controller.rb +0 -1
  18. data/decidim-admin/app/controllers/decidim/admin/moderations_controller.rb +7 -4
  19. data/decidim-admin/app/controllers/decidim/admin/organization_controller.rb +9 -3
  20. data/decidim-admin/app/controllers/decidim/admin/participatory_process_user_roles_controller.rb +17 -0
  21. data/decidim-admin/app/controllers/decidim/admin/participatory_processes_controller.rb +11 -3
  22. data/decidim-admin/app/forms/decidim/admin/feature_form.rb +6 -0
  23. data/decidim-admin/app/forms/decidim/admin/organization_form.rb +5 -0
  24. data/decidim-admin/app/forms/decidim/admin/participatory_process_copy_form.rb +2 -3
  25. data/decidim-admin/app/forms/decidim/admin/participatory_process_form.rb +2 -0
  26. data/decidim-admin/app/forms/decidim/admin/static_page_form.rb +6 -0
  27. data/decidim-admin/app/helpers/decidim/admin/menu_helper.rb +1 -1
  28. data/decidim-admin/app/jobs/decidim/admin/newsletter_job.rb +9 -5
  29. data/decidim-admin/app/views/decidim/admin/attachments/_form.html.erb +1 -12
  30. data/decidim-admin/app/views/decidim/admin/features/_feature.html.erb +2 -2
  31. data/decidim-admin/app/views/decidim/admin/features/_form.html.erb +18 -0
  32. data/decidim-admin/app/views/decidim/admin/moderations/_report.html.erb +2 -2
  33. data/decidim-admin/app/views/decidim/admin/organization/_form.html.erb +5 -35
  34. data/decidim-admin/app/views/decidim/admin/participatory_process_groups/_form.html.erb +1 -1
  35. data/decidim-admin/app/views/decidim/admin/participatory_process_user_roles/index.html.erb +16 -0
  36. data/decidim-admin/app/views/decidim/admin/participatory_processes/_form.html.erb +2 -14
  37. data/decidim-admin/app/views/layouts/decidim/admin/_title_bar.html.erb +26 -19
  38. data/decidim-admin/app/views/layouts/decidim/admin/participatory_process.html.erb +2 -2
  39. data/decidim-admin/bin/rails +2 -2
  40. data/decidim-admin/config/i18n-tasks.yml +2 -0
  41. data/decidim-admin/config/locales/ca.yml +9 -5
  42. data/decidim-admin/config/locales/en.yml +9 -5
  43. data/decidim-admin/config/locales/es.yml +11 -5
  44. data/decidim-admin/config/locales/eu.yml +0 -5
  45. data/decidim-admin/config/locales/fr.yml +18 -23
  46. data/decidim-admin/config/locales/it.yml +0 -5
  47. data/decidim-admin/config/routes.rb +5 -3
  48. data/decidim-admin/decidim-admin.gemspec +2 -2
  49. data/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb +1 -2
  50. data/decidim-admin/spec/commands/create_participatory_process_admin_spec.rb +42 -26
  51. data/decidim-admin/spec/commands/create_participatory_process_spec.rb +2 -1
  52. data/decidim-admin/spec/commands/create_participatory_process_step_spec.rb +56 -0
  53. data/decidim-admin/spec/commands/destroy_participatory_process_step_spec.rb +2 -2
  54. data/decidim-admin/spec/commands/update_feature_spec.rb +6 -0
  55. data/decidim-admin/spec/controllers/participatory_processes_controller_spec.rb +3 -6
  56. data/decidim-admin/spec/features/admin_copy_participatory_process_spec.rb +9 -9
  57. data/decidim-admin/spec/features/admin_manages_features_spec.rb +32 -0
  58. data/decidim-admin/spec/features/admin_manages_participatory_processes_spec.rb +3 -4
  59. data/decidim-admin/spec/forms/static_page_form_spec.rb +12 -0
  60. data/decidim-admin/spec/jobs/newsletter_job_spec.rb +3 -0
  61. data/decidim-admin/spec/shared/manage_process_admins_examples.rb +22 -0
  62. data/decidim-admin/spec/shared/manage_processes_examples.rb +6 -0
  63. data/decidim-admin/spec/spec_helper.rb +4 -0
  64. data/decidim-api/bin/rails +2 -2
  65. data/decidim-api/decidim-api.gemspec +3 -3
  66. data/decidim-api/spec/spec_helper.rb +4 -0
  67. data/decidim-api/spec/types/query_type_spec.rb +2 -2
  68. data/decidim-budgets/app/models/decidim/budgets/project.rb +5 -0
  69. data/decidim-budgets/bin/rails +2 -2
  70. data/decidim-budgets/config/locales/fr.yml +2 -2
  71. data/decidim-budgets/decidim-budgets.gemspec +2 -2
  72. data/decidim-budgets/lib/decidim/budgets/feature.rb +1 -1
  73. data/decidim-budgets/spec/spec_helper.rb +4 -0
  74. data/decidim-comments/app/assets/javascripts/decidim/comments/bundle.js +0 -0
  75. data/decidim-comments/app/commands/decidim/comments/create_comment.rb +10 -14
  76. data/decidim-comments/app/frontend/application/application.component.tsx +1 -1
  77. data/decidim-comments/app/frontend/comments/add_comment_form.component.test.tsx +33 -26
  78. data/decidim-comments/app/frontend/comments/add_comment_form.component.tsx +109 -83
  79. data/decidim-comments/app/frontend/comments/comment.component.test.tsx +29 -24
  80. data/decidim-comments/app/frontend/comments/comment.component.tsx +12 -5
  81. data/decidim-comments/app/frontend/comments/comment_thread.component.test.tsx +12 -7
  82. data/decidim-comments/app/frontend/comments/comment_thread.component.tsx +7 -2
  83. data/decidim-comments/app/frontend/comments/comments.component.tsx +26 -15
  84. data/decidim-comments/app/frontend/comments/down_vote_button.component.test.tsx +9 -4
  85. data/decidim-comments/app/frontend/comments/down_vote_button.component.tsx +56 -37
  86. data/decidim-comments/app/frontend/comments/up_vote_button.component.test.tsx +9 -4
  87. data/decidim-comments/app/frontend/comments/up_vote_button.component.tsx +31 -16
  88. data/decidim-comments/app/frontend/comments/vote_button.component.tsx +3 -0
  89. data/decidim-comments/app/mailers/decidim/comments/comment_notification_mailer.rb +8 -6
  90. data/decidim-comments/app/models/decidim/comments/comment.rb +13 -1
  91. data/decidim-comments/app/views/decidim/comments/comment_notification_mailer/comment_created.html.erb +1 -1
  92. data/decidim-comments/app/views/decidim/comments/comment_notification_mailer/reply_created.html.erb +1 -1
  93. data/decidim-comments/bin/rails +2 -2
  94. data/decidim-comments/config/locales/ca.yml +2 -0
  95. data/decidim-comments/config/locales/en.yml +2 -0
  96. data/decidim-comments/config/locales/es.yml +2 -0
  97. data/decidim-comments/config/locales/fr.yml +2 -2
  98. data/decidim-comments/decidim-comments.gemspec +3 -3
  99. data/decidim-comments/lib/decidim/comments/commentable.rb +1 -0
  100. data/decidim-comments/spec/commands/create_comment_spec.rb +22 -54
  101. data/decidim-comments/spec/features/notifications_spec.rb +2 -6
  102. data/decidim-comments/spec/mailers/comment_notification_mailer_spec.rb +5 -4
  103. data/decidim-comments/spec/models/comment_spec.rb +32 -2
  104. data/decidim-comments/spec/shared/author_localised_email.rb +2 -3
  105. data/decidim-comments/spec/spec_helper.rb +4 -0
  106. data/decidim-dev/decidim-dev.gemspec +3 -4
  107. data/decidim-dev/lib/decidim/dev.rb +23 -1
  108. data/decidim-dev/lib/decidim/dev/common_rake.rb +3 -0
  109. data/decidim-dev/lib/decidim/dev/railtie.rb +3 -3
  110. data/decidim-dev/lib/decidim/dev/test/base_spec_helper.rb +4 -3
  111. data/decidim-dev/lib/decidim/dev/test/rspec_support/capybara.rb +3 -0
  112. data/decidim-dev/lib/decidim/dev/test/rspec_support/feature.rb +8 -0
  113. data/decidim-dev/lib/decidim/dev/test/rspec_support/feature_context.rb +4 -2
  114. data/decidim-dev/lib/decidim/dev/test/rspec_support/geocoder.rb +7 -2
  115. data/decidim-dev/lib/decidim/dev/test/rspec_support/phantomjs_polyfills/phantomjs-getOwnPropertyNames.js +16 -0
  116. data/decidim-dev/lib/decidim/dev/test/rspec_support/warden.rb +17 -1
  117. data/decidim-meetings/app/helpers/decidim/meetings/application_helper.rb +1 -0
  118. data/decidim-meetings/app/helpers/decidim/meetings/meetings_helper.rb +24 -0
  119. data/decidim-meetings/app/views/decidim/meetings/meetings/_meetings.html.erb +1 -1
  120. data/decidim-meetings/bin/rails +2 -2
  121. data/decidim-meetings/config/locales/ca.yml +1 -0
  122. data/decidim-meetings/config/locales/en.yml +2 -1
  123. data/decidim-meetings/config/locales/es.yml +1 -0
  124. data/decidim-meetings/config/locales/fr.yml +5 -5
  125. data/decidim-meetings/decidim-meetings.gemspec +2 -2
  126. data/decidim-meetings/lib/decidim/meetings/feature.rb +1 -1
  127. data/decidim-meetings/spec/features/admin_manages_meetings_attachments_spec.rb +1 -1
  128. data/decidim-meetings/spec/features/admin_manages_meetings_spec.rb +1 -1
  129. data/decidim-meetings/spec/features/explore_meetings_spec.rb +1 -1
  130. data/decidim-meetings/spec/features/process_admin_manages_meetings_attachments_spec.rb +1 -1
  131. data/decidim-meetings/spec/features/process_admin_manages_meetings_spec.rb +1 -1
  132. data/decidim-meetings/spec/helpers/meetings_helper_spec.rb +16 -0
  133. data/decidim-meetings/spec/spec_helper.rb +4 -0
  134. data/decidim-pages/app/models/decidim/pages/page.rb +5 -0
  135. data/decidim-pages/bin/rails +2 -2
  136. data/decidim-pages/decidim-pages.gemspec +2 -2
  137. data/decidim-pages/lib/decidim/pages/feature.rb +1 -1
  138. data/decidim-pages/spec/commands/destroy_page_spec.rb +1 -1
  139. data/decidim-pages/spec/spec_helper.rb +4 -0
  140. data/decidim-proposals/app/assets/config/decidim_proposals_manifest.js +1 -0
  141. data/decidim-proposals/app/assets/javascripts/decidim/proposals/add_proposal.js.es6 +23 -0
  142. data/decidim-proposals/app/forms/decidim/proposals/proposal_form.rb +7 -1
  143. data/decidim-proposals/app/models/decidim/proposals/proposal.rb +14 -2
  144. data/decidim-proposals/app/views/decidim/proposals/proposals/_proposal.html.erb +1 -0
  145. data/decidim-proposals/app/views/decidim/proposals/proposals/new.html.erb +5 -0
  146. data/decidim-proposals/bin/rails +2 -2
  147. data/decidim-proposals/config/locales/fr.yml +11 -11
  148. data/decidim-proposals/db/migrate/20170307085300_migrate_proposal_reports_data_to_reports.rb +1 -1
  149. data/decidim-proposals/decidim-proposals.gemspec +2 -2
  150. data/decidim-proposals/lib/decidim/proposals/feature.rb +1 -1
  151. data/decidim-proposals/spec/features/proposals_spec.rb +8 -2
  152. data/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb +35 -0
  153. data/decidim-proposals/spec/shared/create_proposal_examples.rb +20 -14
  154. data/decidim-proposals/spec/shared/manage_proposals_examples.rb +13 -16
  155. data/decidim-proposals/spec/shared/proposal_form_examples.rb +22 -16
  156. data/decidim-proposals/spec/spec_helper.rb +4 -0
  157. data/decidim-results/app/models/decidim/results/result.rb +10 -0
  158. data/decidim-results/bin/rails +2 -2
  159. data/decidim-results/config/locales/fr.yml +2 -2
  160. data/decidim-results/decidim-results.gemspec +2 -2
  161. data/decidim-results/lib/decidim/results/feature.rb +1 -1
  162. data/decidim-results/spec/features/comments_spec.rb +10 -0
  163. data/decidim-results/spec/spec_helper.rb +4 -0
  164. data/decidim-surveys/app/models/decidim/surveys/survey_question.rb +2 -0
  165. data/decidim-surveys/bin/rails +2 -2
  166. data/decidim-surveys/decidim-surveys.gemspec +2 -2
  167. data/decidim-surveys/lib/decidim/surveys/feature.rb +3 -2
  168. data/decidim-surveys/spec/forms/decidim/surveys/admin/survey_question_form_spec.rb +1 -1
  169. data/decidim-surveys/spec/models/decidim/surveys/survey_question_spec.rb +7 -1
  170. data/decidim-surveys/spec/spec_helper.rb +5 -0
  171. data/decidim-system/app/controllers/decidim/system/application_controller.rb +1 -1
  172. data/decidim-system/app/controllers/decidim/system/devise/sessions_controller.rb +1 -0
  173. data/decidim-system/app/helpers/decidim/system/menu_helper.rb +1 -1
  174. data/decidim-system/app/views/decidim/system/shared/_notices.html.erb +11 -0
  175. data/decidim-system/app/views/layouts/decidim/system/application.html.erb +1 -1
  176. data/decidim-system/app/views/layouts/decidim/system/login.html.erb +1 -1
  177. data/decidim-system/bin/rails +2 -2
  178. data/decidim-system/config/locales/ca.yml +4 -0
  179. data/decidim-system/config/locales/en.yml +4 -0
  180. data/decidim-system/config/locales/es.yml +4 -0
  181. data/decidim-system/db/migrate/20160919105637_devise_create_decidim_admins.rb +5 -5
  182. data/decidim-system/db/seeds.rb +1 -1
  183. data/decidim-system/decidim-system.gemspec +2 -2
  184. data/decidim-system/spec/factories.rb +2 -2
  185. data/decidim-system/spec/features/manage_admins_spec.rb +3 -3
  186. data/decidim-system/spec/spec_helper.rb +4 -0
  187. data/decidim.gemspec +6 -6
  188. data/docs/how_to_create_a_plugin.md +2 -2
  189. data/lib/generators/decidim/app_generator.rb +1 -1
  190. data/lib/generators/decidim/install_generator.rb +35 -27
  191. data/lib/generators/decidim/templates/Gemfile.erb +1 -0
  192. data/lib/generators/decidim/templates/initializer.rb +7 -1
  193. data/logo.svg +62 -0
  194. data/package.json +37 -35
  195. data/tsconfig.json +3 -0
  196. data/yarn.lock +1017 -486
  197. metadata +32 -25
  198. data/decidim-admin/app/assets/stylesheets/decidim/admin/extra/_language-chooser.scss +0 -4
@@ -29,6 +29,12 @@ RSpec.shared_examples "manage processes examples" do
29
29
  end
30
30
  end
31
31
 
32
+ context "viewing a missing process" do
33
+ it_behaves_like "a 404 page" do
34
+ let(:target_path) { decidim_admin.participatory_process_path(99_999_999) }
35
+ end
36
+ end
37
+
32
38
  it "updates a participatory_process" do
33
39
  click_link translated(participatory_process.title)
34
40
 
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "decidim/dev"
4
+
3
5
  ENV["ENGINE_NAME"] = File.dirname(__dir__).split("/").last
4
6
 
7
+ Decidim::Dev.dummy_app_path = File.expand_path(File.join("..", "spec", "decidim_dummy_app"))
8
+
5
9
  require "decidim/dev/test/base_spec_helper"
6
10
 
7
11
  Dir["#{__dir__}/support/**/*.rb"].each { |f| require f }
@@ -8,8 +8,8 @@ ENGINE_ROOT = File.expand_path("..", __dir__)
8
8
  ENGINE_PATH = File.expand_path("../lib/decidim/api/engine", __dir__)
9
9
 
10
10
  # Set up gems listed in the Gemfile.
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
- require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__)
12
+ require "bundler/setup"
13
13
 
14
14
  require "rails/all"
15
15
  require "rails/engine/commands"
@@ -9,15 +9,15 @@ require_relative "../decidim-core/lib/decidim/core/version"
9
9
  Gem::Specification.new do |s|
10
10
  Decidim.add_default_gemspec_properties(s)
11
11
 
12
- s.name = "decidim-api"
13
- s.summary = "API engine for decidim"
12
+ s.name = "decidim-api"
13
+ s.summary = "API engine for decidim"
14
14
  s.description = "API engine for decidim"
15
15
 
16
16
  s.files = Dir["{app,config,db,lib,vendor}/**/*", "Rakefile", "README.md"]
17
17
 
18
18
  s.add_dependency "rails", *Decidim.rails_version
19
19
  s.add_dependency "graphql", "~> 1.6.0"
20
- s.add_dependency "graphiql-rails", "~> 1.4.1"
20
+ s.add_dependency "graphiql-rails", "~> 1.4.2"
21
21
  s.add_dependency "rack-cors", "~> 0.4.0"
22
22
  s.add_dependency "sprockets-es6", "~> 0.9.2"
23
23
 
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "decidim/dev"
4
+
3
5
  ENV["ENGINE_NAME"] = File.dirname(__dir__).split("/").last
4
6
 
7
+ Decidim::Dev.dummy_app_path = File.expand_path(File.join("..", "spec", "decidim_dummy_app"))
8
+
5
9
  require "decidim/dev/test/base_spec_helper"
6
10
 
7
11
  Dir["#{__dir__}/support/**/*.rb"].each { |f| require f }
@@ -16,8 +16,8 @@ module Decidim
16
16
  let(:query) { %({ processes { id }}) }
17
17
 
18
18
  it "returns all the processes" do
19
- expect(response["processes"]).to include("id" => process1.id.to_s)
20
- expect(response["processes"]).to include("id" => process2.id.to_s)
19
+ expect(response["processes"]).to include("id" => process1.id.to_s)
20
+ expect(response["processes"]).to include("id" => process2.id.to_s)
21
21
  expect(response["processes"]).not_to include("id" => process3.id.to_s)
22
22
  end
23
23
  end
@@ -36,6 +36,11 @@ module Decidim
36
36
  def confirmed_orders_count
37
37
  orders.finished.count
38
38
  end
39
+
40
+ # Public: Overrides the `notifiable?` Notifiable concern method.
41
+ def notifiable?(_context)
42
+ false
43
+ end
39
44
  end
40
45
  end
41
46
  end
@@ -8,8 +8,8 @@ ENGINE_ROOT = File.expand_path("..", __dir__)
8
8
  ENGINE_PATH = File.expand_path("../lib/decidim/budgets/engine", __dir__)
9
9
 
10
10
  # Set up gems listed in the Gemfile.
11
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
- require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__)
12
+ require "bundler/setup"
13
13
 
14
14
  require "rails/all"
15
15
  require "rails/engine/commands"
@@ -4,7 +4,7 @@ fr:
4
4
  project:
5
5
  budget: Budget
6
6
  decidim_category_id: Catégorie
7
- decidim_scope_id: Domaine d'application
7
+ decidim_scope_id: Zone d'application
8
8
  description: Description
9
9
  proposal_ids: Propositions liées
10
10
  title: Titre
@@ -73,7 +73,7 @@ fr:
73
73
  filters:
74
74
  category: Catégorie
75
75
  category_prompt: Sélectionner une catégorie
76
- scopes: Domaines d'application
76
+ scopes: Zones d'application
77
77
  search: Rechercher
78
78
  filters_small_view:
79
79
  close_modal: Fermer la vue
@@ -9,8 +9,8 @@ require_relative "../decidim-core/lib/decidim/core/version"
9
9
  Gem::Specification.new do |s|
10
10
  Decidim.add_default_gemspec_properties(s)
11
11
 
12
- s.name = "decidim-budgets"
13
- s.summary = "A budgets component for decidim's participatory processes."
12
+ s.name = "decidim-budgets"
13
+ s.summary = "A budgets component for decidim's participatory processes."
14
14
  s.description = s.summary
15
15
 
16
16
  s.files = Dir["{app,config,db,lib}/**/*", "Rakefile", "README.md"]
@@ -41,7 +41,7 @@ Decidim.register_feature(:budgets) do |feature|
41
41
  end
42
42
 
43
43
  feature.seeds do
44
- Decidim::ParticipatoryProcess.all.each do |process|
44
+ Decidim::ParticipatoryProcess.find_each do |process|
45
45
  next unless process.steps.any?
46
46
 
47
47
  feature = Decidim::Feature.create!(
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "decidim/dev"
4
+
3
5
  ENV["ENGINE_NAME"] = File.dirname(__dir__).split("/").last
4
6
 
7
+ Decidim::Dev.dummy_app_path = File.expand_path(File.join("..", "spec", "decidim_dummy_app"))
8
+
5
9
  require "decidim/dev/test/base_spec_helper"
@@ -23,7 +23,7 @@ module Decidim
23
23
  return broadcast(:invalid) if form.invalid?
24
24
 
25
25
  create_comment
26
- send_notification_to_author if has_author? && !same_author?
26
+ send_notification if @commentable.notifiable?(author: @author)
27
27
 
28
28
  broadcast(:ok, @comment)
29
29
  end
@@ -41,22 +41,18 @@ module Decidim
41
41
  decidim_user_group_id: form.user_group_id)
42
42
  end
43
43
 
44
- def send_notification_to_author
45
- if @comment.depth.positive? && @commentable.author.replies_notifications?
46
- CommentNotificationMailer.reply_created(@comment, @commentable, @comment.root_commentable).deliver_later
47
- elsif @comment.depth.zero? && @commentable.author.comments_notifications?
48
- CommentNotificationMailer.comment_created(@comment, @commentable).deliver_later
44
+ def send_notification
45
+ if @comment.depth.positive?
46
+ @commentable.users_to_notify.each do |user|
47
+ CommentNotificationMailer.reply_created(user, @comment, @commentable, @comment.root_commentable).deliver_later
48
+ end
49
+ elsif @comment.depth.zero?
50
+ @commentable.users_to_notify.each do |user|
51
+ CommentNotificationMailer.comment_created(user, @comment, @commentable).deliver_later
52
+ end
49
53
  end
50
54
  end
51
55
 
52
- def has_author?
53
- @commentable.respond_to?(:author) && @commentable.author.present?
54
- end
55
-
56
- def same_author?
57
- @author == @commentable.author
58
- end
59
-
60
56
  def root_commentable(commentable)
61
57
  return commentable.root_commentable if commentable.is_a? Decidim::Comments::Comment
62
58
  commentable
@@ -15,7 +15,7 @@ interface ApplicationProps {
15
15
  * @class
16
16
  * @augments Component
17
17
  */
18
- export default class Application extends React.Component<ApplicationProps, undefined> {
18
+ export default class Application extends React.Component<ApplicationProps> {
19
19
  constructor(props: ApplicationProps) {
20
20
  const { locale } = props;
21
21
 
@@ -1,7 +1,7 @@
1
1
  import { mount, ReactWrapper, shallow } from "enzyme";
2
2
  import * as React from "react";
3
3
 
4
- import { AddCommentForm } from "./add_comment_form.component";
4
+ import { AddCommentForm, MAX_LENGTH } from "./add_comment_form.component";
5
5
 
6
6
  import generateUserData from "../support/generate_user_data";
7
7
  import generateUserGroupData from "../support/generate_user_group_data";
@@ -13,6 +13,7 @@ describe("<AddCommentForm />", () => {
13
13
  id: "1",
14
14
  type: "Decidim::DummyResource",
15
15
  };
16
+ const orderBy = "older";
16
17
  const addCommentStub = (): any => {
17
18
  return null;
18
19
  };
@@ -31,49 +32,44 @@ describe("<AddCommentForm />", () => {
31
32
  });
32
33
 
33
34
  it("should render a div with class add-comment", () => {
34
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
35
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
35
36
  expect(wrapper.find("div.add-comment")).toBeDefined();
36
37
  });
37
38
 
38
39
  it("should have a reference to body textarea", () => {
39
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
40
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
40
41
  expect((wrapper.instance() as AddCommentForm).bodyTextArea).toBeDefined();
41
42
  });
42
43
 
43
44
  it("should initialize with a state property disabled as true", () => {
44
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
45
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
45
46
  expect(wrapper.state()).toHaveProperty("disabled", true);
46
47
  });
47
48
 
48
49
  it("should have a default prop showTitle as true", () => {
49
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
50
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
50
51
  expect(wrapper.props()).toHaveProperty("showTitle", true);
51
52
  });
52
53
 
53
54
  it("should not render the title if prop showTitle is false", () => {
54
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} showTitle={false} />);
55
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} showTitle={false} rootCommentable={commentable} orderBy={orderBy} />);
55
56
  expect(wrapper.find("h5.section-heading").exists()).toBeFalsy();
56
57
  });
57
58
 
58
59
  it("should have a default prop submitButtonClassName as 'button button--sc'", () => {
59
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
60
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
60
61
  expect(wrapper.props()).toHaveProperty("submitButtonClassName", "button button--sc");
61
62
  });
62
63
 
63
- it("should have a default prop maxLength of 1000", () => {
64
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
65
- expect(wrapper.props()).toHaveProperty("maxLength", 1000);
66
- });
67
-
68
64
  it("should use prop submitButtonClassName as a className prop for submit button", () => {
69
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} submitButtonClassName="button small hollow" />);
65
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} submitButtonClassName="button small hollow" rootCommentable={commentable} orderBy={orderBy} />);
70
66
  expect(wrapper.find('button[type="submit"]').hasClass("button")).toBeTruthy();
71
67
  expect(wrapper.find('button[type="submit"]').hasClass("small")).toBeTruthy();
72
68
  expect(wrapper.find('button[type="submit"]').hasClass("hollow")).toBeTruthy();
73
69
  });
74
70
 
75
71
  it("should enable the submit button if textarea is not blank", () => {
76
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
72
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
77
73
  wrapper.find("textarea").simulate("change", {
78
74
  target: {
79
75
  value: "This is a comment",
@@ -83,7 +79,7 @@ describe("<AddCommentForm />", () => {
83
79
  });
84
80
 
85
81
  it("should disable the submit button if textarea is blank", () => {
86
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
82
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
87
83
  wrapper.find("textarea").simulate("change", {
88
84
  target: {
89
85
  value: "This will be deleted",
@@ -98,10 +94,21 @@ describe("<AddCommentForm />", () => {
98
94
  });
99
95
 
100
96
  it("should not render a div with class 'opinion-toggle'", () => {
101
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
97
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
102
98
  expect(wrapper.find(".opinion-toggle").exists()).toBeFalsy();
103
99
  });
104
100
 
101
+ it("should render the remaining character count", () => {
102
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
103
+ const commentBody = "This is a new comment!";
104
+ wrapper.find("textarea").simulate("change", {
105
+ target: {
106
+ value: commentBody,
107
+ },
108
+ });
109
+ expect(wrapper.find(".remaining-character-count").text()).toContain(MAX_LENGTH - commentBody.length);
110
+ });
111
+
105
112
  describe("submitting the form", () => {
106
113
  let addComment: jasmine.Spy;
107
114
  let onCommentAdded: jasmine.Spy ;
@@ -111,7 +118,7 @@ describe("<AddCommentForm />", () => {
111
118
  beforeEach(() => {
112
119
  addComment = jasmine.createSpy("addComment");
113
120
  onCommentAdded = jasmine.createSpy("onCommentAdded");
114
- wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} onCommentAdded={onCommentAdded} />);
121
+ wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} onCommentAdded={onCommentAdded} rootCommentable={commentable} orderBy={orderBy} />);
115
122
  message = "This will be submitted";
116
123
  (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
117
124
  });
@@ -139,25 +146,25 @@ describe("<AddCommentForm />", () => {
139
146
  });
140
147
 
141
148
  it("should initialize state with a property alignment and value 0", () => {
142
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} />);
149
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
143
150
  expect(wrapper.state()).toHaveProperty("alignment", 0);
144
151
  });
145
152
 
146
153
  describe("when receiving an optional prop arguable with value true", () => {
147
154
  it("should render a div with class 'opinion-toggle'", () => {
148
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} />);
155
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
149
156
  expect(wrapper.find(".opinion-toggle")).toBeDefined();
150
157
  });
151
158
 
152
159
  it("should set state alignment to 1 if user clicks ok button and change its class", () => {
153
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} />);
160
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
154
161
  wrapper.find(".opinion-toggle--ok").simulate("click");
155
162
  expect(wrapper.find(".opinion-toggle--ok").hasClass("is-active")).toBeTruthy();
156
163
  expect(wrapper.state()).toHaveProperty("alignment", 1);
157
164
  });
158
165
 
159
166
  it("should set state alignment to -11 if user clicks ko button and change its class", () => {
160
- const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} />);
167
+ const wrapper = shallow(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
161
168
  wrapper.find(".opinion-toggle--ko").simulate("click");
162
169
  expect(wrapper.find(".opinion-toggle--ko").hasClass("is-active")).toBeTruthy();
163
170
  expect(wrapper.state()).toHaveProperty("alignment", -1);
@@ -170,7 +177,7 @@ describe("<AddCommentForm />", () => {
170
177
 
171
178
  beforeEach(() => {
172
179
  addComment = jasmine.createSpy("addComment");
173
- wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} arguable={true} />);
180
+ wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} arguable={true} rootCommentable={commentable} orderBy={orderBy} />);
174
181
  message = "This will be submitted";
175
182
  (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
176
183
  });
@@ -198,12 +205,12 @@ describe("<AddCommentForm />", () => {
198
205
  });
199
206
 
200
207
  it("should have a reference to user_group_id select", () => {
201
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
208
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
202
209
  expect((wrapper.instance() as AddCommentForm).userGroupIdSelect).toBeDefined();
203
210
  });
204
211
 
205
212
  it("should render a select with option tags for each verified user group", () => {
206
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
213
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
207
214
  expect(wrapper.find("select").children("option").length).toBe(3);
208
215
  });
209
216
 
@@ -215,7 +222,7 @@ describe("<AddCommentForm />", () => {
215
222
 
216
223
  beforeEach(() => {
217
224
  addComment = jasmine.createSpy("addComment");
218
- wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} />);
225
+ wrapper = mount(<AddCommentForm addComment={addComment} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
219
226
  message = "This will be submitted";
220
227
  userGroupId = session.verifiedUserGroups[1].id;
221
228
  (wrapper.instance() as AddCommentForm).bodyTextArea.value = message;
@@ -246,7 +253,7 @@ describe("<AddCommentForm />", () => {
246
253
  });
247
254
 
248
255
  it("display a message to sign in or sign up", () => {
249
- const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} />);
256
+ const wrapper = mount(<AddCommentForm addComment={addCommentStub} session={session} commentable={commentable} rootCommentable={commentable} orderBy={orderBy} />);
250
257
  expect(wrapper.find("span").text()).toContain("sign up");
251
258
  });
252
259
  });
@@ -21,21 +21,25 @@ interface AddCommentFormProps {
21
21
  user: any;
22
22
  } | null;
23
23
  commentable: AddCommentFormCommentableFragment;
24
+ rootCommentable: AddCommentFormCommentableFragment;
24
25
  showTitle?: boolean;
25
26
  submitButtonClassName?: string;
26
27
  autoFocus?: boolean;
27
- maxLength?: number;
28
28
  arguable?: boolean;
29
29
  addComment?: (data: { body: string, alignment: number, userGroupId?: string }) => void;
30
30
  onCommentAdded?: () => void;
31
+ orderBy: string;
31
32
  }
32
33
 
33
34
  interface AddCommentFormState {
34
35
  disabled: boolean;
35
36
  error: boolean;
36
37
  alignment: number;
38
+ remainingCharacterCount: number;
37
39
  }
38
40
 
41
+ export const MAX_LENGTH = 500;
42
+
39
43
  /**
40
44
  * Renders a form to create new comments.
41
45
  * @class
@@ -47,7 +51,6 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
47
51
  submitButtonClassName: "button button--sc",
48
52
  arguable: false,
49
53
  autoFocus: false,
50
- maxLength: 1000,
51
54
  };
52
55
 
53
56
  public bodyTextArea: HTMLTextAreaElement;
@@ -60,6 +63,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
60
63
  disabled: true,
61
64
  error: false,
62
65
  alignment: 0,
66
+ remainingCharacterCount: MAX_LENGTH,
63
67
  };
64
68
  }
65
69
 
@@ -124,7 +128,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
124
128
  */
125
129
  private _renderForm() {
126
130
  const { session, submitButtonClassName, commentable: { id, type } } = this.props;
127
- const { disabled } = this.state;
131
+ const { disabled, remainingCharacterCount } = this.state;
128
132
 
129
133
  if (session) {
130
134
  return (
@@ -141,6 +145,9 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
141
145
  >
142
146
  {I18n.t("components.add_comment_form.form.submit")}
143
147
  </button>
148
+ <span className="remaining-character-count">
149
+ {I18n.t("components.add_comment_form.remaining_characters", { count: remainingCharacterCount })}
150
+ </span>
144
151
  </div>
145
152
  </form>
146
153
  );
@@ -155,7 +162,7 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
155
162
  * @returns {Void|DOMElement} - The heading or an empty element
156
163
  */
157
164
  private _renderTextArea() {
158
- const { commentable: { id, type }, autoFocus, maxLength } = this.props;
165
+ const { commentable: { id, type }, autoFocus } = this.props;
159
166
  const { error } = this.state;
160
167
  const className = classnames({ "is-invalid-input": error });
161
168
 
@@ -164,9 +171,9 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
164
171
  id: `add-comment-${type}-${id}`,
165
172
  className,
166
173
  rows: "4",
167
- maxLength,
174
+ maxLength: MAX_LENGTH,
168
175
  required: "required",
169
- pattern: `^(.){0,${maxLength}}$`,
176
+ pattern: `^(.){0,${MAX_LENGTH}}$`,
170
177
  placeholder: I18n.t("components.add_comment_form.form.body.placeholder"),
171
178
  onChange: (evt: React.ChangeEvent<HTMLTextAreaElement>) => this._checkCommentBody(evt.target.value),
172
179
  };
@@ -186,13 +193,12 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
186
193
  * @returns {Void|DOMElement} - The error or an empty element
187
194
  */
188
195
  private _renderTextAreaError() {
189
- const { maxLength } = this.props;
190
196
  const { error } = this.state;
191
197
 
192
198
  if (error) {
193
199
  return (
194
200
  <span className="form-error is-visible">
195
- {I18n.t("components.add_comment_form.form.form_error", { length: maxLength })}
201
+ {I18n.t("components.add_comment_form.form.form_error", { length: MAX_LENGTH })}
196
202
  </span>
197
203
  );
198
204
  }
@@ -298,8 +304,10 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
298
304
  * @returns {Void} - Returns nothing
299
305
  */
300
306
  private _checkCommentBody(body: string) {
301
- const { maxLength } = this.props;
302
- this.setState({ disabled: body === "", error: body === "" || (maxLength !== undefined && body.length > maxLength) });
307
+ this.setState({
308
+ disabled: body === "", error: body === "" || body.length > MAX_LENGTH,
309
+ remainingCharacterCount: MAX_LENGTH - body.length,
310
+ });
303
311
  }
304
312
 
305
313
  /**
@@ -334,89 +342,107 @@ export class AddCommentForm extends React.Component<AddCommentFormProps, AddComm
334
342
  }
335
343
 
336
344
  const addCommentMutation = require("../mutations/add_comment.mutation.graphql");
345
+ const getCommentsQuery = require("../queries/comments.query.graphql");
337
346
 
338
- const AddCommentFormWithMutation = graphql(addCommentMutation, {
347
+ const AddCommentFormWithMutation = graphql<AddCommentMutation, AddCommentFormProps>(addCommentMutation, {
339
348
  props: ({ ownProps, mutate }) => ({
340
- addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }) => mutate({
341
- variables: {
342
- commentableId: ownProps.commentable.id,
343
- commentableType: ownProps.commentable.type,
344
- body,
345
- alignment,
346
- userGroupId,
347
- },
348
- optimisticResponse: {
349
- commentable: {
350
- __typename: "CommentableMutation",
351
- addComment: {
352
- __typename: "Comment",
353
- id: uuid(),
354
- sgid: uuid(),
355
- type: "Decidim::Comments::Comment",
356
- createdAt: new Date().toISOString(),
349
+ addComment: ({ body, alignment, userGroupId }: { body: string, alignment: number, userGroupId: string }) => {
350
+ if (mutate) {
351
+ mutate({
352
+ variables: {
353
+ commentableId: ownProps.commentable.id,
354
+ commentableType: ownProps.commentable.type,
357
355
  body,
358
356
  alignment,
359
- author: {
360
- __typename: "User",
361
- name: ownProps.session.user.name,
362
- avatarUrl: ownProps.session.user.avatarUrl,
363
- deleted: false,
357
+ userGroupId,
358
+ },
359
+ optimisticResponse: {
360
+ commentable: {
361
+ __typename: "CommentableMutation",
362
+ addComment: {
363
+ __typename: "Comment",
364
+ id: uuid(),
365
+ sgid: uuid(),
366
+ type: "Decidim::Comments::Comment",
367
+ createdAt: new Date().toISOString(),
368
+ body,
369
+ alignment,
370
+ author: {
371
+ __typename: "User",
372
+ name: ownProps.session && ownProps.session.user.name,
373
+ avatarUrl: ownProps.session && ownProps.session.user.avatarUrl,
374
+ deleted: false,
375
+ isVerified: true,
376
+ isUser: true,
377
+ },
378
+ comments: [],
379
+ hasComments: false,
380
+ acceptsNewComments: false,
381
+ upVotes: 0,
382
+ upVoted: false,
383
+ downVotes: 0,
384
+ downVoted: false,
385
+ alreadyReported: false,
386
+ },
364
387
  },
365
- comments: [],
366
- hasComments: false,
367
- acceptsNewComments: false,
368
- upVotes: 0,
369
- upVoted: false,
370
- downVotes: 0,
371
- downVoted: false,
372
- alreadyReported: false,
373
388
  },
374
- },
375
- },
376
- updateQueries: {
377
- GetComments: (prev: GetCommentsQuery, { mutationResult: { data } }: { mutationResult: { data: AddCommentMutation }}) => {
378
- const { id, type } = ownProps.commentable;
379
- const newComment = data.commentable && data.commentable.addComment;
380
- let comments = [];
381
-
382
- const commentReducer = (comment: CommentFragment): CommentFragment => {
383
- const replies = comment.comments || [];
384
-
385
- if (newComment && comment.id === id) {
389
+ update: (store, { data }: { data: AddCommentMutation }) => {
390
+ const variables = {
391
+ commentableId: ownProps.rootCommentable.id,
392
+ commentableType: ownProps.rootCommentable.type,
393
+ orderBy: ownProps.orderBy,
394
+ };
395
+ const prev = store.readQuery<GetCommentsQuery>({
396
+ query: getCommentsQuery,
397
+ variables,
398
+ });
399
+ const { id, type } = ownProps.commentable;
400
+ const newComment = data.commentable && data.commentable.addComment;
401
+ let comments = [];
402
+
403
+ const commentReducer = (comment: CommentFragment): CommentFragment => {
404
+ const replies = comment.comments || [];
405
+
406
+ if (newComment && comment.id === id) {
407
+ return {
408
+ ...comment,
409
+ hasComments: true,
410
+ comments: [
411
+ ...replies,
412
+ newComment,
413
+ ],
414
+ };
415
+ }
386
416
  return {
387
417
  ...comment,
388
- hasComments: true,
389
- comments: [
390
- ...replies,
391
- newComment,
392
- ],
418
+ comments: replies.map(commentReducer),
393
419
  };
394
- }
395
- return {
396
- ...comment,
397
- comments: replies.map(commentReducer),
398
420
  };
399
- };
400
-
401
- if (type === "Decidim::Comments::Comment") {
402
- comments = prev.commentable.comments.map(commentReducer);
403
- } else {
404
- comments = [
405
- ...prev.commentable.comments,
406
- newComment,
407
- ];
408
- }
409
-
410
- return {
411
- ...prev,
412
- commentable: {
413
- ...prev.commentable,
414
- comments,
415
- },
416
- };
417
- },
418
- },
419
- }),
421
+
422
+ if (type === "Decidim::Comments::Comment") {
423
+ comments = prev.commentable.comments.map(commentReducer);
424
+ } else {
425
+ comments = [
426
+ ...prev.commentable.comments,
427
+ newComment,
428
+ ];
429
+ }
430
+
431
+ store.writeQuery({
432
+ query: getCommentsQuery,
433
+ data: {
434
+ ...prev,
435
+ commentable: {
436
+ ...prev.commentable,
437
+ comments,
438
+ },
439
+ },
440
+ variables,
441
+ });
442
+ },
443
+ });
444
+ }
445
+ },
420
446
  }),
421
447
  })(AddCommentForm);
422
448