decidim-core 0.27.1 → 0.27.2

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/newsletter_templates/base_cell.rb +8 -0
  3. data/app/cells/decidim/newsletter_templates/basic_only_text/show.erb +4 -4
  4. data/app/cells/decidim/newsletter_templates/image_text_cta/show.erb +4 -4
  5. data/app/cells/decidim/upload_modal_cell.rb +12 -7
  6. data/app/commands/decidim/unendorse_resource.rb +1 -1
  7. data/app/controllers/decidim/groups_controller.rb +5 -0
  8. data/app/controllers/decidim/links_controller.rb +4 -2
  9. data/app/controllers/decidim/profiles_controller.rb +1 -1
  10. data/app/helpers/decidim/icon_helper.rb +3 -3
  11. data/app/helpers/decidim/newsletters_helper.rb +1 -0
  12. data/app/mailers/decidim/newsletter_mailer.rb +10 -3
  13. data/app/models/decidim/newsletter.rb +28 -0
  14. data/app/models/decidim/user.rb +0 -2
  15. data/app/models/decidim/user_base_entity.rb +2 -0
  16. data/app/models/decidim/user_block.rb +2 -2
  17. data/app/models/decidim/user_group.rb +1 -1
  18. data/app/packs/src/decidim/form_filter.component.test.js +148 -5
  19. data/app/packs/src/decidim/form_filter.js +26 -4
  20. data/app/packs/stylesheets/decidim/email.scss +7 -0
  21. data/app/presenters/decidim/admin_log/user_group_presenter.rb +1 -1
  22. data/app/presenters/decidim/admin_log/user_moderation_presenter.rb +1 -1
  23. data/app/presenters/decidim/push_notification_presenter.rb +1 -1
  24. data/app/uploaders/decidim/application_uploader.rb +1 -1
  25. data/app/uploaders/decidim/avatar_uploader.rb +2 -2
  26. data/app/views/decidim/messaging/conversations/_conversation.html.erb +1 -1
  27. data/app/views/decidim/newsletter_mailer/newsletter.html.erb +3 -3
  28. data/app/views/decidim/newsletters/show.html.erb +1 -1
  29. data/app/views/layouts/decidim/_mailer_logo.html.erb +2 -2
  30. data/app/views/layouts/decidim/newsletter_base.html.erb +2 -2
  31. data/config/locales/ar.yml +5 -4
  32. data/config/locales/bg.yml +5 -4
  33. data/config/locales/ca.yml +17 -13
  34. data/config/locales/cs.yml +6 -3
  35. data/config/locales/de.yml +2 -5
  36. data/config/locales/el.yml +4 -5
  37. data/config/locales/en.yml +6 -2
  38. data/config/locales/es-MX.yml +10 -6
  39. data/config/locales/es-PY.yml +10 -6
  40. data/config/locales/es.yml +15 -11
  41. data/config/locales/eu.yml +24 -22
  42. data/config/locales/fi-plain.yml +6 -2
  43. data/config/locales/fi.yml +7 -3
  44. data/config/locales/fr-CA.yml +6 -5
  45. data/config/locales/fr.yml +6 -5
  46. data/config/locales/gl.yml +2 -4
  47. data/config/locales/hu.yml +4 -5
  48. data/config/locales/id-ID.yml +5 -4
  49. data/config/locales/is-IS.yml +0 -1
  50. data/config/locales/it.yml +1 -5
  51. data/config/locales/ja.yml +20 -16
  52. data/config/locales/ka-GE.yml +1 -0
  53. data/config/locales/lb.yml +0 -4
  54. data/config/locales/lt.yml +0 -4
  55. data/config/locales/lv.yml +5 -4
  56. data/config/locales/nl.yml +0 -4
  57. data/config/locales/no.yml +2 -6
  58. data/config/locales/pl.yml +4 -5
  59. data/config/locales/pt-BR.yml +0 -4
  60. data/config/locales/pt.yml +0 -4
  61. data/config/locales/ro-RO.yml +49 -3
  62. data/config/locales/ru.yml +5 -1
  63. data/config/locales/sk.yml +5 -4
  64. data/config/locales/sv.yml +22 -5
  65. data/config/locales/tr-TR.yml +4 -5
  66. data/config/locales/uk.yml +5 -1
  67. data/config/locales/zh-CN.yml +3 -4
  68. data/lib/decidim/api/types/localized_string_type.rb +9 -0
  69. data/lib/decidim/api/types/translated_field_type.rb +20 -5
  70. data/lib/decidim/asset_router/pipeline.rb +93 -0
  71. data/lib/decidim/asset_router/storage.rb +82 -0
  72. data/lib/decidim/asset_router.rb +3 -75
  73. data/lib/decidim/attribute_object/form.rb +9 -0
  74. data/lib/decidim/core/test/factories.rb +13 -6
  75. data/lib/decidim/core/version.rb +1 -1
  76. data/lib/decidim/dependency_resolver.rb +14 -8
  77. data/lib/decidim/form_builder.rb +1 -1
  78. data/lib/decidim/participatory_space_resourceable.rb +7 -1
  79. metadata +10 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9cfa315481be2ecd36504616728a76d1c445b99a233f36e50db71a7351ac04b
4
- data.tar.gz: d67d9216824b975d528f0e64a8f605eacc4849fb042836d63e5c2fe5bc4da218
3
+ metadata.gz: 391ba34f55c860208f7644dd10b1b3e2b6465ed45b9e9f9fd194ce1036516995
4
+ data.tar.gz: 93636f8d556d73547fcdff45986fcc85ba4a22e9c399fdeca0c550ee6c241363
5
5
  SHA512:
6
- metadata.gz: 776eba5812d48f147afa41d5b0a2eab52d2c46ce2db9616bd784c2f784be2851eaa1e341439ba1ac13f9b8152e1a3fb09acdbe08e93a9e2e6721e7c9afb09982
7
- data.tar.gz: eb00d8fe971338da9056eb5e6268934614f5803d1d102d643a14c7602e8323266195948a282220c1013c1f0e39511f8c5adc95aa9e843244be01904589eae57e
6
+ metadata.gz: 157c005fbea98fe374586f91f15f17bc54450d6e4b6f7136d51582d5cae7af9442d250edbbf4fe2a4308aac902ba0eb28c49681153dcda6b6948f8589414e930
7
+ data.tar.gz: 8ebc9089ad86980956ded3024d490f419bf8015ae0dc4b3fe6e003d415010de8ed28751bd0c168cf526c8a17f5db50467c9223242c2150cf600304990e1eda04
@@ -25,6 +25,14 @@ module Decidim
25
25
  def recipient_user
26
26
  options[:recipient_user]
27
27
  end
28
+
29
+ def custom_url_for_mail_root
30
+ options[:custom_url_for_mail_root]
31
+ end
32
+
33
+ def decidim
34
+ @decidim ||= EngineRouter.new("decidim", {})
35
+ end
28
36
  end
29
37
  end
30
38
  end
@@ -16,7 +16,7 @@
16
16
  <tr>
17
17
  <th>
18
18
  <center>
19
- <%= render partial: "layouts/decidim/mailer_logo.html", locals: { organization: organization } %>
19
+ <%= render partial: "layouts/decidim/mailer_logo.html", locals: { organization: organization, custom_url_for_mail_root: custom_url_for_mail_root } %>
20
20
  </center>
21
21
  </th>
22
22
  </tr>
@@ -27,7 +27,7 @@
27
27
  <tr>
28
28
  <th>
29
29
  <% if organization.official_img_header.attached? %>
30
- <%= link_to organization.official_url do %>
30
+ <%= link_to newsletter.organization_official_url do %>
31
31
  <%= image_tag organization.attached_uploader(:official_img_header).path, alt: "", style: "max-height: 50px", class: "float-right" %>
32
32
  <% end %>
33
33
  <% end %>
@@ -72,8 +72,8 @@
72
72
  <th class="expander"></th>
73
73
  <th class="small-12 first columns cityhall-bar">
74
74
  <div class="decidim-logo" style="float: right; text-align: right; padding-right: 16px">
75
- <% if @custom_url_for_mail_root.present? %>
76
- <%= link_to organization.name.html_safe, @custom_url_for_mail_root %>
75
+ <% if custom_url_for_mail_root.present? %>
76
+ <%= link_to organization.name.html_safe, custom_url_for_mail_root %>
77
77
  <% else %>
78
78
  <%= link_to organization.name.html_safe, decidim.root_url(host: organization.host) %>
79
79
  <% end %>
@@ -24,7 +24,7 @@ table.button table td {
24
24
  <tr>
25
25
  <th>
26
26
  <center>
27
- <%= render partial: "layouts/decidim/mailer_logo.html", locals: { organization: organization } %>
27
+ <%= render partial: "layouts/decidim/mailer_logo.html", locals: { organization: organization, custom_url_for_mail_root: custom_url_for_mail_root } %>
28
28
  </center>
29
29
  </th>
30
30
  </tr>
@@ -35,7 +35,7 @@ table.button table td {
35
35
  <tr>
36
36
  <th>
37
37
  <% if organization.official_img_header.attached? %>
38
- <%= link_to organization.official_url do %>
38
+ <%= link_to newsletter.organization_official_url do %>
39
39
  <%= image_tag organization.attached_uploader(:official_img_header).url(host: organization.host), alt: "", style: "max-height: 50px", class: "float-right" %>
40
40
  <% end %>
41
41
  <% end %>
@@ -111,8 +111,8 @@ table.button table td {
111
111
  <th class="expander"></th>
112
112
  <th class="small-12 first columns cityhall-bar">
113
113
  <div class="decidim-logo" style="float: right; text-align: right; padding-right: 16px">
114
- <% if @custom_url_for_mail_root.present? %>
115
- <%= link_to organization.name.html_safe, @custom_url_for_mail_root %>
114
+ <% if custom_url_for_mail_root.present? %>
115
+ <%= link_to organization.name.html_safe, custom_url_for_mail_root %>
116
116
  <% else %>
117
117
  <%= link_to organization.name.html_safe, decidim.root_url(host: organization.host) %>
118
118
  <% end %>
@@ -87,9 +87,18 @@ module Decidim
87
87
  end
88
88
 
89
89
  def explanation
90
- return I18n.t("explanation", scope: options[:help_i18n_scope], attribute: attribute) if options[:help_i18n_scope].present?
90
+ i18n_options = {
91
+ scope: options[:help_i18n_scope].presence || "decidim.forms.upload_help",
92
+ attribute: attribute_translation
93
+ }
91
94
 
92
- I18n.t("explanation", scope: "decidim.forms.upload_help", attribute: attribute)
95
+ I18n.t("explanation", **i18n_options)
96
+ end
97
+
98
+ def attribute_translation
99
+ I18n.t(attribute, scope: [:activemodel, :attributes, resource_class.constantize.model_name.param_key].join("."))
100
+ rescue NameError
101
+ I18n.t(attribute, scope: "activemodel.attributes")
93
102
  end
94
103
 
95
104
  def add_attribute
@@ -145,11 +154,7 @@ module Decidim
145
154
  end
146
155
 
147
156
  def file_name_for(attachment)
148
- filename = determine_filename(attachment)
149
-
150
- return "(#{filename})" if has_title?
151
-
152
- filename
157
+ determine_filename(attachment)
153
158
  end
154
159
 
155
160
  def determine_filename(attachment)
@@ -31,7 +31,7 @@ module Decidim
31
31
  query = if @current_group.present?
32
32
  @resource.endorsements.where(decidim_user_group_id: @current_group&.id)
33
33
  else
34
- @resource.endorsements.where(author: @current_user)
34
+ @resource.endorsements.where(author: @current_user, decidim_user_group_id: nil)
35
35
  end
36
36
  query.destroy_all
37
37
  end
@@ -7,6 +7,7 @@ module Decidim
7
7
  include UserGroups
8
8
 
9
9
  before_action :enforce_user_groups_enabled
10
+ before_action :ensure_user_group_not_blocked
10
11
 
11
12
  def new
12
13
  enforce_permission_to :create, :user_group, current_user: current_user
@@ -78,6 +79,10 @@ module Decidim
78
79
 
79
80
  private
80
81
 
82
+ def ensure_user_group_not_blocked
83
+ raise ActionController::RoutingError, "Blocked User Group" if user_group&.blocked?
84
+ end
85
+
81
86
  def accepted_user_group
82
87
  @accepted_user_group ||= Decidim::UserGroups::AcceptedUserGroups.for(current_user).find_by(nickname: params[:id])
83
88
  end
@@ -7,9 +7,11 @@ module Decidim
7
7
  skip_before_action :store_current_location
8
8
 
9
9
  helper Decidim::ExternalDomainHelper
10
+ helper_method :external_url
10
11
 
11
12
  before_action :parse_url
12
13
  rescue_from Decidim::InvalidUrlError, with: :invalid_url
14
+ rescue_from URI::InvalidURIError, with: :invalid_url
13
15
 
14
16
  def new
15
17
  headers["X-Robots-Tag"] = "noindex"
@@ -25,7 +27,7 @@ module Decidim
25
27
  def parse_url
26
28
  raise Decidim::InvalidUrlError unless external_url
27
29
 
28
- parts = external_url.match %r{^(([a-z]+):)?//([^/]+)(/.*)?$}
30
+ parts = external_url.match %r{\A(([a-z]+):)?//([^/]+)(/.*)?\z}
29
31
  raise Decidim::InvalidUrlError unless parts
30
32
 
31
33
  @url_parts = {
@@ -36,7 +38,7 @@ module Decidim
36
38
  end
37
39
 
38
40
  def external_url
39
- @external_url ||= params[:external_url]
41
+ @external_url ||= URI.parse(params[:external_url]).to_s
40
42
  end
41
43
  end
42
44
  end
@@ -13,7 +13,7 @@ module Decidim
13
13
  before_action :ensure_profile_holder
14
14
  before_action :ensure_profile_holder_is_a_group, only: [:members]
15
15
  before_action :ensure_profile_holder_is_a_user, only: [:groups, :following]
16
- before_action :ensure_user_not_blocked, only: [:following, :followers, :badges]
16
+ before_action :ensure_user_not_blocked
17
17
 
18
18
  def show
19
19
  return redirect_to profile_timeline_path(nickname: params[:nickname]) if profile_holder == current_user
@@ -24,7 +24,7 @@ module Decidim
24
24
  #
25
25
  # Returns an HTML tag with the icon.
26
26
  def manifest_icon(manifest, options = {})
27
- if manifest.icon
27
+ if manifest.respond_to?(:icon) && manifest.icon.present?
28
28
  external_icon manifest.icon, options
29
29
  else
30
30
  icon "question-mark", options
@@ -42,9 +42,9 @@ module Decidim
42
42
  def resource_icon(resource, options = {})
43
43
  if resource.instance_of?(Decidim::Comments::Comment)
44
44
  icon "comment-square", options
45
- elsif resource.respond_to?(:component)
45
+ elsif resource.respond_to?(:component) && resource.component.present?
46
46
  component_icon(resource.component, options)
47
- elsif resource.respond_to?(:manifest)
47
+ elsif resource.respond_to?(:manifest) && resource.manifest.present?
48
48
  manifest_icon(resource.manifest, options)
49
49
  elsif resource.is_a?(Decidim::User)
50
50
  icon "person", options
@@ -31,6 +31,7 @@ module Decidim
31
31
  # this method is used to generate the root link on mail with the utm_codes
32
32
  # If the newsletter_id is nil, it returns the root_url
33
33
  def custom_url_for_mail_root(organization, newsletter_id = nil)
34
+ decidim = EngineRouter.new("decidim", {})
34
35
  if newsletter_id.present?
35
36
  decidim.root_url(host: organization.host) + utm_codes(organization.host, newsletter_id.to_s)
36
37
  else
@@ -11,14 +11,20 @@ module Decidim
11
11
 
12
12
  helper_method :cell
13
13
 
14
- def newsletter(user, newsletter)
14
+ def newsletter(user, newsletter, preview: false)
15
15
  return if user.email.blank?
16
16
 
17
17
  @organization = user.organization
18
18
  @newsletter = newsletter
19
19
  @user = user
20
-
21
- @custom_url_for_mail_root = custom_url_for_mail_root(@organization, @newsletter.id) if Decidim.config.track_newsletter_links
20
+ @preview = preview
21
+
22
+ @custom_url_for_mail_root =
23
+ if @preview
24
+ "#"
25
+ elsif Decidim.config.track_newsletter_links
26
+ custom_url_for_mail_root(@organization, @newsletter.id)
27
+ end
22
28
  @encrypted_token = Decidim::NewsletterEncryptor.sent_at_encrypted(@user.id, @newsletter.sent_at)
23
29
 
24
30
  with_user(user) do
@@ -40,6 +46,7 @@ module Decidim
40
46
  organization: @organization,
41
47
  newsletter: @newsletter,
42
48
  recipient_user: @user,
49
+ custom_url_for_mail_root: @custom_url_for_mail_root,
43
50
  context: {
44
51
  controller: self
45
52
  }
@@ -56,6 +56,24 @@ module Decidim
56
56
  .find_by(scoped_resource_id: id)
57
57
  end
58
58
 
59
+ def url(**kwargs)
60
+ proxy_url(:newsletter_url, id: id, **kwargs)
61
+ end
62
+
63
+ def notifications_settings_url(**kwargs)
64
+ proxy_url(__method__, **kwargs)
65
+ end
66
+
67
+ def unsubscribe_newsletters_url(**kwargs)
68
+ proxy_url(__method__, **kwargs)
69
+ end
70
+
71
+ def organization_official_url
72
+ return "#" unless sent?
73
+
74
+ organization.official_url || proxy_url(:root_url)
75
+ end
76
+
59
77
  private
60
78
 
61
79
  def author_belongs_to_organization
@@ -63,5 +81,15 @@ module Decidim
63
81
 
64
82
  errors.add(:author, :invalid) unless author.organization == organization
65
83
  end
84
+
85
+ def proxy_url(method, **kwargs)
86
+ return "#" unless sent?
87
+
88
+ router.public_send(method, host: organization.host, **kwargs)
89
+ end
90
+
91
+ def router
92
+ @router ||= EngineRouter.new("decidim", {})
93
+ end
66
94
  end
67
95
  end
@@ -36,8 +36,6 @@ module Decidim
36
36
  has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, dependent: :destroy
37
37
  has_many :reminders, foreign_key: "decidim_user_id", class_name: "Decidim::Reminder", dependent: :destroy
38
38
 
39
- has_one :blocking, class_name: "Decidim::UserBlock", foreign_key: :id, primary_key: :block_id, dependent: :destroy
40
-
41
39
  validates :name, presence: true, unless: -> { deleted? }
42
40
  validates :nickname,
43
41
  presence: true,
@@ -17,6 +17,8 @@ module Decidim
17
17
  has_many :notifications, foreign_key: "decidim_user_id", class_name: "Decidim::Notification", dependent: :destroy
18
18
  has_many :following_follows, foreign_key: "decidim_user_id", class_name: "Decidim::Follow", dependent: :destroy
19
19
 
20
+ has_one :blocking, class_name: "Decidim::UserBlock", foreign_key: :id, primary_key: :block_id, dependent: :destroy
21
+
20
22
  # Regex for name & nickname format validations
21
23
  REGEXP_NAME = /\A(?!.*[<>?%&\^*#@()\[\]=+:;"{}\\|])/
22
24
 
@@ -4,7 +4,7 @@ module Decidim
4
4
  class UserBlock < ApplicationRecord
5
5
  MINIMUM_JUSTIFICATION_LENGTH = 15
6
6
 
7
- belongs_to :user, class_name: "Decidim::User", foreign_key: :decidim_user_id
8
- belongs_to :blocking_user, class_name: "Decidim::User"
7
+ belongs_to :user, class_name: "Decidim::UserBaseEntity", foreign_key: :decidim_user_id
8
+ belongs_to :blocking_user, class_name: "Decidim::UserBaseEntity"
9
9
  end
10
10
  end
@@ -21,7 +21,7 @@ module Decidim
21
21
  foreign_key: :decidim_user_id,
22
22
  source: :user
23
23
 
24
- validates :name, presence: true, uniqueness: { scope: :decidim_organization_id }
24
+ validates :name, presence: true, uniqueness: { scope: :decidim_organization_id }, unless: -> { blocked? }
25
25
 
26
26
  validate :correct_state
27
27
  validate :unique_document_number, if: :has_document_number?
@@ -1,4 +1,4 @@
1
- /* global spyOn */
1
+ /* global spyOn, jest */
2
2
  /* eslint-disable id-length */
3
3
  window.$ = $;
4
4
 
@@ -7,6 +7,20 @@ import DataPicker from "./data_picker"
7
7
 
8
8
  const FormFilterComponent = require("./form_filter.component_for_testing.js");
9
9
 
10
+ const expectedPushState = (state, filters) => {
11
+ const queryString = Object.keys(filters).map((key) => {
12
+ const name = `filter[${key}]`;
13
+ const val = filters[key];
14
+ if (Array.isArray(val)) {
15
+ return val.map((v) => `${encodeURIComponent(`${name}[]`)}=${encodeURIComponent(v)}`).join("&");
16
+ }
17
+
18
+ return `${encodeURIComponent(name)}=${encodeURIComponent(val)}`;
19
+ }).join("&");
20
+
21
+ return [state, null, `/filters?${queryString}`];
22
+ }
23
+
10
24
  describe("FormFilterComponent", () => {
11
25
  const selector = "form#new_filter";
12
26
  let subject = null;
@@ -15,6 +29,10 @@ describe("FormFilterComponent", () => {
15
29
  beforeEach(() => {
16
30
  let form = `
17
31
  <form id="new_filter" action="/filters" method="get">
32
+ <fieldset>
33
+ <input id="filter_search_text_cont" placeholder="Search" data-disable-dynamic-change="true" type="search" name="filter[search_text_cont]">
34
+ </fieldset>
35
+
18
36
  <fieldset>
19
37
  <div id="filter_somerandomid_scope_id" class="data-picker picker-multiple" data-picker-name="filter[scope_id]">
20
38
  <div class="picker-values">
@@ -67,11 +85,25 @@ describe("FormFilterComponent", () => {
67
85
  `;
68
86
  $("body").append(form);
69
87
 
88
+ const $form = $(document).find("form");
89
+
70
90
  window.Decidim = window.Decidim || {};
71
91
 
72
92
  window.theDataPicker = new DataPicker($(".data-picker"));
73
93
  window.theCheckBoxesTree = new CheckBoxesTree();
74
- subject = new FormFilterComponent($(document).find("form"));
94
+ window.Rails = {
95
+ fire: (htmlElement, event) => {
96
+ // Hack to call trigger on the correct instance of the form, as fetching
97
+ // with the selector does not work.
98
+ if (htmlElement === $form[0]) {
99
+ $form.trigger(event);
100
+ }
101
+ }
102
+ };
103
+
104
+ subject = new FormFilterComponent($form);
105
+
106
+ jest.useFakeTimers();
75
107
  });
76
108
 
77
109
  it("exists", () => {
@@ -88,7 +120,19 @@ describe("FormFilterComponent", () => {
88
120
 
89
121
  describe("when mounted", () => {
90
122
  beforeEach(() => {
91
- spyOn(subject.$form, "on");
123
+ // Jest doesn't implement listening on the form submit event so we need
124
+ // to hack it.
125
+ const originalOn = subject.$form.on.bind(subject.$form);
126
+ jest.spyOn(subject.$form, "on").mockImplementation((...args) => {
127
+ if (args[0] === "submit") {
128
+ subject.$form.submitHandler = args[1];
129
+ } else if (args[0] === "change" && typeof args[1] === "string") {
130
+ subject.$form.changeHandler = args[2];
131
+ } else {
132
+ originalOn(...args);
133
+ }
134
+ });
135
+
92
136
  subject.mountComponent();
93
137
  });
94
138
 
@@ -100,8 +144,98 @@ describe("FormFilterComponent", () => {
100
144
  expect(subject.mounted).toBeTruthy();
101
145
  });
102
146
 
103
- it("binds the form change event", () => {
147
+ it("binds the form change and submit events", () => {
104
148
  expect(subject.$form.on).toHaveBeenCalledWith("change", "input:not([data-disable-dynamic-change]), select:not([data-disable-dynamic-change])", subject._onFormChange);
149
+ expect(subject.$form.on).toHaveBeenCalledWith("submit", subject._onFormSubmit);
150
+ });
151
+
152
+ describe("form changes", () => {
153
+ beforeEach(() => {
154
+ spyOn(window.history, "pushState");
155
+
156
+ // This is a hack to be able to trigger the events even somewhat close
157
+ // to an actual situation. In real browser environment the change events
158
+ // would be triggered by the input/select elements but to simplify the
159
+ // test implementation, we trigger them directly on the form.
160
+ const originalTrigger = subject.$form.trigger.bind(subject.$form);
161
+ jest.spyOn(subject.$form, "trigger").mockImplementation((...args) => {
162
+ if (args[0] === "submit") {
163
+ subject.$form.submitHandler(
164
+ $.event.fix(new CustomEvent("submit", { bubbles: true, cancelable: true }))
165
+ );
166
+ } else if (args[0] === "change") {
167
+ subject.$form.changeHandler();
168
+ } else {
169
+ originalTrigger(...args);
170
+ }
171
+
172
+ jest.runAllTimers();
173
+ });
174
+ });
175
+
176
+ it("does not save the state in case there were no changes to previous state", () => {
177
+ subject.$form.trigger("change");
178
+
179
+ expect(window.history.pushState).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it("saves the state after dynamic form changes", () => {
183
+ $("#filter_somerandomid_category_id").val(2);
184
+
185
+ subject.$form.trigger("change");
186
+
187
+ const state = {
188
+ "filter_somerandomid_scope_id": [
189
+ {
190
+ "text": "Scope 1",
191
+ "url": "picker_url_1",
192
+ "value": "3"
193
+ },
194
+ {
195
+ "text": "Scope 2",
196
+ "url": "picker_url_2",
197
+ "value": "4"
198
+ }
199
+ ]
200
+ };
201
+ const filters = {
202
+ "search_text_cont": "",
203
+ "scope_id": [3, 4],
204
+ "category_id": 2,
205
+ "state": [""]
206
+ };
207
+ expect(window.history.pushState).toHaveBeenCalledWith(...expectedPushState(state, filters));
208
+ });
209
+
210
+ it("saves the state after form submission through input element", () => {
211
+ const textInput = document.getElementById("filter_search_text_cont");
212
+ textInput.value = "search";
213
+
214
+ subject.$form.trigger("submit");
215
+
216
+ const state = {
217
+ "filter_somerandomid_scope_id": [
218
+ {
219
+ "text": "Scope 1",
220
+ "url": "picker_url_1",
221
+ "value": "3"
222
+ },
223
+ {
224
+ "text": "Scope 2",
225
+ "url": "picker_url_2",
226
+ "value": "4"
227
+ }
228
+ ]
229
+ }
230
+ const filters = {
231
+ "search_text_cont": "search",
232
+ "scope_id": [3, 4],
233
+ "category_id": 1,
234
+ "state": [""]
235
+ }
236
+
237
+ expect(window.history.pushState).toHaveBeenCalledWith(...expectedPushState(state, filters));
238
+ });
105
239
  });
106
240
 
107
241
  describe("onpopstate event", () => {
@@ -131,6 +265,14 @@ describe("FormFilterComponent", () => {
131
265
  expect(checked.map((input) => input.value)).toEqual(["", "accepted", "evaluating"]);
132
266
  expect(checked.filter((input) => input.indeterminate).map((input) => input.value)).toEqual([""]);
133
267
  });
268
+
269
+ it("does not save the state", () => {
270
+ spyOn(window.history, "pushState");
271
+
272
+ window.onpopstate({ isTrusted: true, state: scopesPickerState});
273
+
274
+ expect(window.history.pushState).not.toHaveBeenCalled();
275
+ });
134
276
  });
135
277
  });
136
278
 
@@ -145,8 +287,9 @@ describe("FormFilterComponent", () => {
145
287
  expect(subject.mounted).toBeFalsy();
146
288
  });
147
289
 
148
- it("unbinds the form change event", () => {
290
+ it("unbinds the form change and submit events", () => {
149
291
  expect(subject.$form.off).toHaveBeenCalledWith("change", "input, select", subject._onFormChange);
292
+ expect(subject.$form.off).toHaveBeenCalledWith("submit", subject._onFormSubmit);
150
293
  });
151
294
  });
152
295
 
@@ -23,6 +23,7 @@ export default class FormFilterComponent {
23
23
 
24
24
  this._updateInitialState();
25
25
  this._onFormChange = delayed(this, this._onFormChange.bind(this));
26
+ this._onFormSubmit = delayed(this, this._onFormSubmit.bind(this));
26
27
  this._onPopState = this._onPopState.bind(this);
27
28
 
28
29
  if (window.Decidim.PopStateHandler) {
@@ -42,6 +43,7 @@ export default class FormFilterComponent {
42
43
  if (this.mounted) {
43
44
  this.mounted = false;
44
45
  this.$form.off("change", "input, select", this._onFormChange);
46
+ this.$form.off("submit", this._onFormSubmit);
45
47
 
46
48
  unregisterCallback(`filters-${this.id}`)
47
49
  }
@@ -62,6 +64,7 @@ export default class FormFilterComponent {
62
64
  contentContainer = this.$form.data("remoteFill");
63
65
  }
64
66
  this.$form.on("change", "input:not([data-disable-dynamic-change]), select:not([data-disable-dynamic-change])", this._onFormChange);
67
+ this.$form.on("submit", this._onFormSubmit);
65
68
 
66
69
  this.currentFormRequest = null;
67
70
  this.$form.on("ajax:beforeSend", (e) => {
@@ -254,14 +257,16 @@ export default class FormFilterComponent {
254
257
 
255
258
  // Only one instance should submit the form on browser history navigation
256
259
  if (this.popStateSubmiter) {
257
- Rails.fire(this.$form[0], "submit");
260
+ Rails.fire(this.$form[0], "submit", { from: "pop" });
258
261
  }
259
262
 
260
263
  this.changeEvents = true;
261
264
  }
262
265
 
263
266
  /**
264
- * Handles the logic to update the current location after a form change event.
267
+ * Handles the logic to decide whether the form should be submitted or not
268
+ * after a form change event. The form is only submitted when changes have
269
+ * occurred.
265
270
  * @private
266
271
  * @returns {Void} - Returns nothing.
267
272
  */
@@ -270,7 +275,7 @@ export default class FormFilterComponent {
270
275
  return;
271
276
  }
272
277
 
273
- const [newPath, newState] = this._currentStateAndPath();
278
+ const [newPath] = this._currentStateAndPath();
274
279
  const path = this._getLocation(false);
275
280
 
276
281
  if (newPath === path) {
@@ -278,6 +283,23 @@ export default class FormFilterComponent {
278
283
  }
279
284
 
280
285
  Rails.fire(this.$form[0], "submit");
286
+ }
287
+
288
+ /**
289
+ * Saves the current state of the search on form submit to update the search
290
+ * parameters to the URL and store the picker states.
291
+ * @private
292
+ * @param {jQuery.Event} ev The event that caused the form to submit.
293
+ * @returns {Void} - Returns nothing.
294
+ */
295
+ _onFormSubmit(ev) {
296
+ const eventDetail = ev.originalEvent.detail;
297
+ if (eventDetail && eventDetail.from === "pop") {
298
+ return;
299
+ }
300
+
301
+ const [newPath, newState] = this._currentStateAndPath();
302
+
281
303
  pushState(newPath, newState);
282
304
  this._saveFilters(newPath);
283
305
  }
@@ -314,7 +336,7 @@ export default class FormFilterComponent {
314
336
  * @returns {String} - Returns a unique identifier
315
337
  */
316
338
  _getUID() {
317
- return `filter-form-${new Date().setUTCMilliseconds()}-${Math.floor(Math.random() * 10000000)}`;
339
+ return `filter-form-${new Date().getUTCMilliseconds()}-${Math.floor(Math.random() * 10000000)}`;
318
340
  }
319
341
 
320
342
  /**
@@ -25,6 +25,13 @@ body{
25
25
  -moz-box-sizing: border-box;
26
26
  -webkit-box-sizing: border-box;
27
27
  box-sizing: border-box;
28
+
29
+ &.preview{
30
+ .see-on-website,
31
+ .unsubscribe{
32
+ cursor: not-allowed;
33
+ }
34
+ }
28
35
  }
29
36
 
30
37
  .ExternalClass{
@@ -20,7 +20,7 @@ module Decidim
20
20
 
21
21
  def action_string
22
22
  case action
23
- when "verify", "verify_via_csv", "reject"
23
+ when "verify", "verify_via_csv", "reject", "block", "unblock"
24
24
  "decidim.admin_log.user_group.#{action}"
25
25
  else
26
26
  super
@@ -52,7 +52,7 @@ module Decidim
52
52
  end
53
53
 
54
54
  def unreported_user
55
- @unreported_user ||= Decidim::User.find_by(id: action_log.extra.dig("extra", "user_id"))
55
+ @unreported_user ||= Decidim::UserBaseEntity.find_by(id: action_log.extra.dig("extra", "user_id"))
56
56
  end
57
57
 
58
58
  def has_diff?