decidim-core 0.19.1 → 0.20.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-core might be problematic. Click here for more details.

Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -0
  3. data/app/assets/javascripts/decidim.js.es6 +2 -1
  4. data/app/assets/javascripts/decidim/input_hashtags.js.es6 +1 -1
  5. data/app/assets/javascripts/decidim/input_mentions.js.es6 +112 -50
  6. data/app/assets/stylesheets/decidim/modules/_signup.scss +57 -0
  7. data/app/cells/decidim/coauthorships_cell.rb +2 -6
  8. data/app/cells/decidim/diff_cell.rb +3 -7
  9. data/app/controllers/decidim/application_controller.rb +0 -8
  10. data/app/controllers/decidim/devise/unlocks_controller.rb +25 -0
  11. data/app/controllers/decidim/searches_controller.rb +0 -1
  12. data/app/jobs/decidim/export_participatory_space_job.rb +20 -0
  13. data/app/models/decidim/category.rb +2 -0
  14. data/app/models/decidim/component.rb +2 -0
  15. data/app/models/decidim/user.rb +9 -2
  16. data/app/models/decidim/user_group.rb +7 -0
  17. data/app/presenters/decidim/attachment_presenter.rb +21 -0
  18. data/app/resolvers/decidim/core/user_resolver.rb +61 -0
  19. data/app/scrubbers/decidim/user_input_scrubber.rb +2 -2
  20. data/app/serializers/decidim/exporters/participatory_space_components_serializer.rb +46 -0
  21. data/{lib → app/serializers}/decidim/exporters/serializer.rb +0 -0
  22. data/app/serializers/decidim/importers/importer.rb +25 -0
  23. data/app/serializers/decidim/importers/participatory_space_components_importer.rb +67 -0
  24. data/app/services/decidim/open_data_exporter.rb +1 -1
  25. data/app/uploaders/decidim/banner_image_uploader.rb +0 -1
  26. data/app/views/decidim/devise/invitations/edit.html.erb +4 -2
  27. data/app/views/decidim/devise/registrations/new.html.erb +2 -2
  28. data/app/views/decidim/devise/unlocks/new.html.erb +33 -0
  29. data/app/views/decidim/searches/_filters.html.erb +3 -19
  30. data/app/views/decidim/searches/_resources_filter_block.html.erb +20 -0
  31. data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
  32. data/app/views/layouts/decidim/_main_footer.html.erb +26 -0
  33. data/app/views/layouts/decidim/_mini_footer.html.erb +21 -0
  34. data/app/views/layouts/decidim/_wrapper.html.erb +7 -50
  35. data/config/initializers/devise.rb +6 -6
  36. data/config/locales/en.yml +0 -10
  37. data/config/routes.rb +1 -0
  38. data/db/migrate/20191028135718_add_lockable_to_users.rb +10 -0
  39. data/db/migrate/20191118120529_add_weight_to_categories.rb +7 -0
  40. data/db/migrate/20191212102051_remove_continuity_badges.rb +13 -0
  41. data/db/seeds.rb +18 -0
  42. data/lib/decidim/acts_as_author.rb +21 -0
  43. data/lib/decidim/component_manifest.rb +45 -4
  44. data/lib/decidim/core.rb +1 -0
  45. data/lib/decidim/core/engine.rb +0 -4
  46. data/lib/decidim/core/test.rb +3 -0
  47. data/lib/decidim/core/test/factories.rb +5 -0
  48. data/lib/decidim/core/test/shared_examples/acts_as_author_examples.rb +12 -0
  49. data/lib/decidim/core/test/shared_examples/comments_examples.rb +41 -2
  50. data/lib/decidim/core/test/shared_examples/searchable_participatory_space_examples.rb +145 -0
  51. data/lib/decidim/core/test/shared_examples/searchable_resources_shared_context.rb +12 -0
  52. data/lib/decidim/core/version.rb +1 -1
  53. data/lib/decidim/exporters.rb +0 -1
  54. data/lib/decidim/exporters/export_manifest.rb +72 -0
  55. data/lib/decidim/faker/localized.rb +10 -0
  56. data/lib/decidim/form_builder.rb +2 -3
  57. data/lib/decidim/participatory_space_manifest.rb +33 -0
  58. data/lib/decidim/participatory_space_resourceable.rb +8 -0
  59. data/lib/decidim/query_extensions.rb +16 -0
  60. data/lib/decidim/resourceable.rb +1 -1
  61. data/lib/decidim/searchable.rb +19 -1
  62. data/vendor/assets/javascripts/tribute.js +1683 -1621
  63. metadata +29 -13
  64. data/app/assets/images/decidim/gamification/badges/continuity.svg +0 -73
  65. data/app/models/decidim/continuity_badge_status.rb +0 -9
  66. data/app/services/decidim/continuity_badge_tracker.rb +0 -64
  67. data/lib/decidim/components/export_manifest.rb +0 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3954eab5e8c5ee4b45ae1e256719d74d26ac2a1aa259a22a9561ec7c6d5a5c96
4
- data.tar.gz: caa3bba2c9079440cd0981ebdd914d6a799398cebe6d055c7fb45ff0a43989a2
3
+ metadata.gz: bd45e8b19642518cb5cdc256a688f47b2befb549b73c3edf9445120aca24e3ff
4
+ data.tar.gz: 883849819fefaf6d0b74d0b33d22698c9c87ec596bfd64a1dcc5a7237b4df914
5
5
  SHA512:
6
- metadata.gz: 51cfa35708bdb0ae9a945920c9179a13cc36ce3e763af13910d6f435e1723aea1aa172e8befc79bbe7ad65686369a2cb43ba9a74acb70fae8de6e756db4f0870
7
- data.tar.gz: 1767af1ec45afb37861d7e15e94710ade93f7e1b99b24ad330dc59f0d21c5949559cbf1fbb0c8a2b3f9a4d2b8f23051ae757712f57b71c1449de9676a71e0580
6
+ metadata.gz: d19e5e70f36cd10e18b26bfde95d1feab778c9b100c5ba79a3fdb35c3259e6868c51b51a91746c3cec8199d6fa66659729fb8887947430c5b0a9434692c4fa72
7
+ data.tar.gz: 5e2cf574f4b32a3e83fb0b92c616dba9b14bd96462f06dbcca3a3a0e5b57aa98c422b98e98d9c887af9692ad420ad2addee87a8d714c55808069312651b8c927
data/README.md CHANGED
@@ -20,6 +20,10 @@ And then execute:
20
20
  bundle
21
21
  ```
22
22
 
23
+ ## Users
24
+
25
+ User authentication is set up with [`Devise`](https://github.com/plataformatec/devise) with its modules, see the [`Decidim::User`](https://github.com/decidim/decidim/blob/master/decidim-core/app/models/decidim/user.rb) model configuration and its setup [initializer](https://github.com/decidim/decidim/blob/master/decidim-core/config/initializers/devise.rb).
26
+
23
27
  ## Amendments
24
28
 
25
29
  Core implements an Amendment feature that can be activated in the components. As of now, it's only implemented in the proposal component.
@@ -47,8 +51,23 @@ This module also includes the following models to Decidim's Global Search:
47
51
  - `Searchable` module: A concern with the features needed when you want a model to be searchable.
48
52
  - `SearchableResource` class: The ActiveRecord that finally includes PgSearch and maps the indexed documents into a model.
49
53
 
54
+ ### Adding an artifact to Global Search
55
+
50
56
  Models that want to be indexed must include `Searchable` and declare `Searchable.searchable_fields`.
51
57
 
58
+ They should be registered as resources. In their manifest, in the `register_resource` section, the artifact should be declared searchable.
59
+ This can be done in an initializer (like user does), in a participatory_space manifest, or in a component manifest. i.e.:
60
+
61
+ ```ruby
62
+ initializer "decidim.core.register_resources" do
63
+ Decidim.register_resource(:user) do |resource|
64
+ resource.model_class_name = "Decidim::User"
65
+ resource.card = "decidim/user_profile"
66
+ resource.searchable = true
67
+ end
68
+ ...
69
+ ```
70
+
52
71
  ## Metrics docs
53
72
 
54
73
  Core adds an implementation to show APP metrics within some pages. You can see specific documentation at [Metrics](https://github.com/decidim/decidim/tree/master/docs/advanced/metrics.md)
@@ -1,5 +1,5 @@
1
1
  // = require decidim/core/bundle.js
2
- // = require jquery
2
+ // = require jquery3
3
3
  // = require rails-ujs
4
4
  // = require decidim/foundation
5
5
  // = require modernizr
@@ -18,6 +18,7 @@
18
18
  // = require decidim/editor
19
19
  // = require decidim/input_tags
20
20
  // = require decidim/input_hashtags
21
+ // = require decidim/input_mentions
21
22
  // = require decidim/ajax_modals
22
23
  // = require decidim/conferences
23
24
  // = require decidim/tooltip_keep_on_hover
@@ -25,7 +25,7 @@ $(() => {
25
25
 
26
26
  /* eslint no-use-before-define: ["error", { "variables": false }]*/
27
27
  let remoteSearch = function(text, cb) {
28
- $.post("/api", {query: `{hashtags(name:${text}) {name}}`}).
28
+ $.post("/api", {query: `{hashtags(name:"${text}") {name}}`}).
29
29
 
30
30
  then((response) => {
31
31
  let data = response.data.hashtags || {};
@@ -3,46 +3,73 @@
3
3
  $(() => {
4
4
  const $mentionContainer = $(".js-mentions");
5
5
  const nodatafound = $mentionContainer.attr("data-noresults");
6
- const sources = [];
7
6
 
8
- // EXAMPLE DATA
9
- // tag & name properties are mandatory
10
- //
11
- // const sources = [{
12
- // "tag": "barrera",
13
- // "name": "Collins Franklin",
14
- // },
15
- // {
16
- // "tag": "woods",
17
- // "name": "Nadine Buck",
18
- // }]
7
+ let noMatchTemplate = null
8
+ if (nodatafound) {
9
+ noMatchTemplate = () => `<li>${nodatafound}</li>`;
10
+ }
19
11
 
20
12
  // Listener for the event triggered by quilljs
21
13
  let cursor = "";
22
- $mentionContainer.on("quill-position", function(event) {
23
- cursor = event.detail.index;
14
+ $mentionContainer.on("quill-position", (event) => {
15
+ if (event.detail !== null) {
16
+ // When replacing the text content after selecting a hashtag, we only need
17
+ // to know the hashtag's start position as that is the point which we want
18
+ // to replace.
19
+ let quill = event.target.__quill;
20
+ if (quill.getText(event.detail.index - 1, 1) === "@") {
21
+ cursor = event.detail.index;
22
+ }
23
+ }
24
24
  });
25
25
 
26
+ /* eslint no-use-before-define: ["error", { "variables": false }]*/
27
+ let remoteSearch = function(text, cb) {
28
+ $.post("/api", {query: `{users(wildcard:"${text}") {nickname,name}}`}).
29
+
30
+ then((response) => {
31
+ let data = response.data.users || {};
32
+ cb(data)
33
+ }).fail(function() {
34
+ cb([])
35
+ }).always(() => {
36
+ // This function runs Tribute every single time you type something
37
+ // So we must evalute DOM properties after each
38
+ const $parent = $(tribute.current.element).parent();
39
+ $parent.addClass("is-active");
40
+
41
+ // We need to move the container to the wrapper selected
42
+ const $tribute = $parent.find(".tribute-container");
43
+ // Remove the inline styles, relative to absolute positioning
44
+ $tribute.removeAttr("style");
45
+ })
46
+ };
47
+
26
48
  // tribute.js docs - http://github.com/zurb/tribute
27
49
  /* global Tribute*/
28
50
  let tribute = new Tribute({
29
- values: sources,
30
- positionMenu: false,
51
+ trigger: "@",
52
+ values: function (text, cb) {
53
+ remoteSearch(text, (users) => cb(users));
54
+ },
55
+ positionMenu: true,
31
56
  menuContainer: null,
32
- fillAttr: "tag",
33
- noMatchTemplate: () => `<li>${nodatafound}</li>`,
34
- lookup: (item) => item.tag + item.name,
57
+ menuItemLimit: 5,
58
+ fillAttr: "nickname",
59
+ noMatchTemplate: noMatchTemplate,
60
+ lookup: (item) => item.nickname + item.name,
35
61
  selectTemplate: function(item) {
36
62
  if (typeof item === "undefined") {
37
63
  return null;
38
64
  }
39
65
  if (this.range.isContentEditable(this.current.element)) {
40
66
  // Check quill.js
41
- if ($(this.current.element).hasClass("editor-container")) {
42
- let quill = this.current.element.__quill;
43
- quill.insertText(cursor - 1, `@${item.original.tag} `, Quill.sources.API);
67
+ if ($(this.current.element).hasClass("ql-editor")) {
68
+ let editorContainer = $(this.current.element).parent().get(0);
69
+ let quill = editorContainer.__quill;
70
+ quill.insertText(cursor - 1, `${item.original.nickname} `, Quill.sources.API);
44
71
  // cursor position + nickname length + "@" sign + space
45
- let position = cursor + item.original.tag.length + 2;
72
+ let position = cursor + item.original.nickname.length + 2
46
73
 
47
74
  let next = 0;
48
75
  if (quill.getLength() > position) {
@@ -57,43 +84,78 @@ $(() => {
57
84
 
58
85
  return ""
59
86
  }
60
- return `<span contenteditable="false">@${item.original.tag}</span>`;
87
+ return `<span contenteditable="false">${item.original.nickname}</span>`;
61
88
  }
62
- return `@${item.original.tag}`;
89
+ return item.original.nickname;
63
90
  },
64
91
  menuItemTemplate: function(item) {
65
- let tpl = `<strong>${item.original.tag}</strong>&nbsp;<small>${item.original.name}</small>`;
92
+ let tpl = `<strong>${item.original.nickname}</strong>&nbsp;<small>${item.original.name}</small>`;
66
93
  return tpl;
67
94
  }
68
95
  });
69
96
 
70
- tribute.attach($mentionContainer);
97
+ let setupEvents = function($element) {
98
+ // DOM manipulation
99
+ $element.on("focusin", (event) => {
100
+ // Set the parent container relative to the current element
101
+ tribute.menuContainer = event.target.parentNode;
102
+ });
103
+ $element.on("focusout", (event) => {
104
+ let $parent = $(event.target).parent();
71
105
 
72
- // DOM manipulation
73
- $mentionContainer.on("focusin", (event) => {
74
- // Set the parent container relative to the current element
75
- tribute.menuContainer = event.target.parentNode;
76
- });
77
- $mentionContainer.on("focusout", (event) => {
78
- let $parent = $(event.target).parent();
106
+ if ($parent.hasClass("is-active")) {
107
+ $parent.removeClass("is-active");
108
+ }
109
+ });
110
+ $element.on("input", (event) => {
111
+ let $parent = $(event.target).parent();
79
112
 
80
- if ($parent.hasClass("is-active")) {
81
- $parent.removeClass("is-active");
113
+ if (tribute.isActive) {
114
+ // We need to move the container to the wrapper selected
115
+ let $tribute = $(".tribute-container");
116
+ $tribute.appendTo($parent);
117
+ // // Remove the inline styles, relative to absolute positioning
118
+ $tribute.removeAttr("style");
119
+ // Parent adaptation
120
+ $parent.addClass("is-active");
121
+ } else {
122
+ $parent.removeClass("is-active");
123
+ }
124
+ });
125
+
126
+ };
127
+
128
+ setupEvents($mentionContainer);
129
+
130
+ // This allows external libraries (like React) to use the component
131
+ // by simply firing and event targeting the element where to attach Tribute
132
+ $(document).on("attach-mentions-element", (event, element) => {
133
+ if (!element) {
134
+ return;
82
135
  }
83
- });
84
- $mentionContainer.on("input", (event) => {
85
- let $parent = $(event.target).parent();
86
-
87
- if (tribute.isActive) {
88
- // We need to move the container to the wrapper selected
89
- let $tribute = $(".tribute-container");
90
- $tribute.appendTo($parent);
91
- // Remove the inline styles, relative to absolute positioning
92
- $tribute.removeAttr("style");
93
- // Parent adaptation
94
- $parent.addClass("is-active");
95
- } else {
96
- $parent.removeClass("is-active");
136
+ tribute.attach(element);
137
+ // Due a bug in Tribute, re-add menu to DOM if it has been removed
138
+ // See https://github.com/zurb/tribute/issues/140
139
+ if (tribute.menu && !document.body.contains(tribute.menu)) {
140
+ tribute.range.getDocument().body.appendChild(tribute.menu);
97
141
  }
142
+ setupEvents($(element));
98
143
  });
144
+
145
+ // tribute.attach($mentionContainer);
146
+ // Tribute needs to be attached to the `.ql-editor` element as said at:
147
+ // https://github.com/quilljs/quill/issues/1816
148
+ //
149
+ // For this reason we need to wait a bit for quill to initialize itself.
150
+ setTimeout(function() {
151
+ $mentionContainer.each((index, item) => {
152
+ let $qlEditor = $(".ql-editor", item);
153
+ if ($qlEditor.length > 0) {
154
+ tribute.attach($qlEditor);
155
+ } else {
156
+ tribute.attach(item);
157
+ }
158
+ });
159
+ }, 1000);
99
160
  });
161
+
@@ -58,3 +58,60 @@
58
58
  text-align: center;
59
59
  }
60
60
  }
61
+
62
+ .user-nickname{
63
+ label .row{
64
+ div:first-of-type{
65
+ // this is the prefix div
66
+ @extend .input-group-label;
67
+
68
+ border-#{$global-right}: 0;
69
+ width: 8%;
70
+ height: 3rem;
71
+ border-top-left-radius: $input-radius;
72
+ border-bottom-left-radius: $input-radius;
73
+
74
+ .prefix{
75
+ display: inline-block;
76
+ height: 2.9rem;
77
+ padding-top: .8rem;
78
+ }
79
+ }
80
+
81
+ div:last-of-type{
82
+ // this is the input div
83
+ @extend .input-group-field;
84
+
85
+ width: 92%;
86
+ height: 3rem;
87
+
88
+ input{
89
+ border-top-left-radius: 0;
90
+ border-bottom-left-radius: 0;
91
+ }
92
+ }
93
+ }
94
+
95
+ label.is-invalid-label .row{
96
+ margin-bottom: $form-spacing;
97
+
98
+ div:first-of-type{
99
+ // this is the prefix div
100
+ background-color: mix($input-background-invalid, $light-gray, 8%);
101
+ border-color: $input-error-color;
102
+ }
103
+
104
+ div:last-of-type{
105
+ // this is the input div
106
+ height: 4.2rem;
107
+
108
+ .form-error.is-visible{
109
+ margin-left: -8%;
110
+ }
111
+ }
112
+ }
113
+
114
+ .help-text{
115
+ margin-top: 1rem;
116
+ }
117
+ }
@@ -53,12 +53,8 @@ module Decidim
53
53
  def presenter_for_author(authorable)
54
54
  if official?
55
55
  "#{model.class.parent}::OfficialAuthorPresenter".constantize.new
56
- elsif authorable.user_group
57
- Decidim::UserGroupPresenter.new(authorable.user_group)
58
- elsif authorable.author.is_a?(Decidim::User)
59
- Decidim::UserPresenter.new(authorable.author)
60
- elsif authorable.author.is_a?(Decidim::Meeting)
61
- Decidim::MeetingPresenter.new(authorable.author)
56
+ else
57
+ authorable.user_group&.presenter || authorable.author.presenter
62
58
  end
63
59
  end
64
60
 
@@ -51,11 +51,9 @@ module Decidim
51
51
  #
52
52
  # Returns an HTML-safe string.
53
53
  def output_unified_diff(data)
54
- return unless data && data[:new_value].present?
55
-
56
54
  Diffy::Diff.new(
57
55
  data[:old_value].to_s,
58
- data[:new_value],
56
+ data[:new_value].to_s,
59
57
  allow_empty_diff: false,
60
58
  include_plus_and_minus_in_html: true
61
59
  ).to_s(:html).html_safe
@@ -67,11 +65,9 @@ module Decidim
67
65
  #
68
66
  # Returns an HTML-safe string.
69
67
  def output_split_diff(data, direction)
70
- return unless direction && data && data[:new_value].present?
71
-
72
68
  Diffy::SplitDiff.new(
73
- data[:old_value],
74
- data[:new_value],
69
+ data[:old_value].to_s,
70
+ data[:new_value].to_s,
75
71
  allow_empty_diff: false,
76
72
  format: :html,
77
73
  include_plus_and_minus_in_html: true
@@ -42,8 +42,6 @@ module Decidim
42
42
 
43
43
  skip_before_action :disable_http_caching, unless: :user_signed_in?
44
44
 
45
- before_action :track_continuity_badge
46
-
47
45
  private
48
46
 
49
47
  # Stores the url where the user will be redirected after login.
@@ -90,11 +88,5 @@ module Decidim
90
88
  def add_vary_header
91
89
  response.headers["Vary"] = "Accept"
92
90
  end
93
-
94
- def track_continuity_badge
95
- return unless current_user
96
-
97
- Decidim::ContinuityBadgeTracker.new(current_user).track!(Time.zone.today)
98
- end
99
91
  end
100
92
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Devise
5
+ # Custom Devise UnlocksController to avoid namespace problems.
6
+ class UnlocksController < ::Devise::UnlocksController
7
+ include Decidim::DeviseControllers
8
+
9
+ private
10
+
11
+ # Since we're using a single Devise installation for multiple
12
+ # organizations, and user emails can be repeated across organizations,
13
+ # we need to identify the user by both the email and the organization.
14
+ # Setting the organization ID here will be used by Devise internally to
15
+ # find the correct user.
16
+ #
17
+ # Note that in order for this to work we need to define the `reset_password_keys`
18
+ # Devise attribute in the `Decidim::User` model to include the
19
+ # `decidim_organization_id` attribute.
20
+ def resource_params
21
+ super.merge(decidim_organization_id: current_organization.id)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -13,7 +13,6 @@ module Decidim
13
13
  def index
14
14
  Search.call(term, current_organization, filters, page_params) do
15
15
  on(:ok) do |results|
16
- # results.page(params[:page]).per(params[:per_page])
17
16
  results_count = results.sum { |results_by_type| results_by_type.last[:count] }
18
17
  expose(sections: results, results_count: results_count)
19
18
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ class ExportParticipatorySpaceJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(user, participatory_space, name, format)
8
+ export_manifest = participatory_space.manifest.export_manifests.find do |manifest|
9
+ manifest.name == name.to_sym
10
+ end
11
+
12
+ collection = export_manifest.collection.call(participatory_space)
13
+ serializer = export_manifest.serializer
14
+
15
+ export_data = Decidim::Exporters.find_exporter(format).new(collection, serializer).export
16
+
17
+ Decidim::ExportMailer.export(user, name, export_data).deliver_now
18
+ end
19
+ end
20
+ end