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
@@ -9,6 +9,8 @@ module Decidim
9
9
  belongs_to :parent, class_name: "Decidim::Category", foreign_key: "parent_id", inverse_of: :subcategories, optional: true
10
10
  has_many :categorizations, foreign_key: "decidim_category_id", class_name: "Decidim::Categorization", dependent: :destroy
11
11
 
12
+ default_scope { order(arel_table[:parent_id].asc, arel_table[:weight].asc) }
13
+
12
14
  validate :forbid_deep_nesting
13
15
  before_validation :subcategories_have_same_participatory_space
14
16
 
@@ -80,6 +80,8 @@ module Decidim
80
80
  participatory_space.can_participate?(user)
81
81
  end
82
82
 
83
+ delegate :serializes_specific_data?, to: :manifest
84
+
83
85
  private
84
86
 
85
87
  def participatory_space_name
@@ -9,6 +9,7 @@ module Decidim
9
9
  class User < UserBaseEntity
10
10
  include Decidim::DataPortability
11
11
  include Decidim::Searchable
12
+ include Decidim::ActsAsAuthor
12
13
 
13
14
  OMNIAUTH_PROVIDERS = [:facebook, :twitter, :google_oauth2, (:developer if Rails.env.development?)].compact
14
15
 
@@ -19,8 +20,8 @@ module Decidim
19
20
  end
20
21
 
21
22
  devise :invitable, :database_authenticatable, :registerable, :confirmable, :timeoutable,
22
- :recoverable, :rememberable, :trackable, :decidim_validatable,
23
- :decidim_newsletterable,
23
+ :recoverable, :rememberable, :trackable, :lockable,
24
+ :decidim_validatable, :decidim_newsletterable,
24
25
  :omniauthable, omniauth_providers: OMNIAUTH_PROVIDERS,
25
26
  request_keys: [:env], reset_password_keys: [:decidim_organization_id, :email],
26
27
  confirmation_keys: [:decidim_organization_id, :email]
@@ -81,6 +82,12 @@ module Decidim
81
82
  # Returns a String.
82
83
  attr_accessor :invitation_instructions
83
84
 
85
+ # Returns the presenter for this author, to be used in the views.
86
+ # Required by ActsAsAuthor.
87
+ def presenter
88
+ Decidim::UserPresenter.new(self)
89
+ end
90
+
84
91
  def self.log_presenter_class_for(_log)
85
92
  Decidim::AdminLog::UserPresenter
86
93
  end
@@ -8,6 +8,7 @@ module Decidim
8
8
  class UserGroup < UserBaseEntity
9
9
  include Decidim::Traceable
10
10
  include Decidim::DataPortability
11
+ include Decidim::ActsAsAuthor
11
12
 
12
13
  has_many :memberships, class_name: "Decidim::UserGroupMembership", foreign_key: :decidim_user_group_id, dependent: :destroy
13
14
  has_many :users, through: :memberships, class_name: "Decidim::User", foreign_key: :decidim_user_id
@@ -35,6 +36,12 @@ module Decidim
35
36
  .where("extended_data->>'document_number' = ?", number)
36
37
  end
37
38
 
39
+ # Returns the presenter for this author, to be used in the views.
40
+ # Required by ActsAsAuthor.
41
+ def presenter
42
+ Decidim::UserGroupPresenter.new(self)
43
+ end
44
+
38
45
  def self.log_presenter_class_for(_log)
39
46
  Decidim::AdminLog::UserGroupPresenter
40
47
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ #
5
+ # Decorator for attachments
6
+ #
7
+ class AttachmentPresenter < SimpleDelegator
8
+ include Rails.application.routes.mounted_helpers
9
+ include ActionView::Helpers::UrlHelper
10
+
11
+ delegate :url, to: :file, prefix: true
12
+
13
+ def attachment_file_url
14
+ URI.join(decidim.root_url(host: attachment.attached_to.organization.host), attachment.file_url).to_s
15
+ end
16
+
17
+ def attachment
18
+ __getobj__
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Core
5
+ # A GraphQL resolver to handle `user` queries
6
+ class UserResolver
7
+ #
8
+ # - organization: Decidim::Organization scoping
9
+ # - filters: hash of attr - value to filter results
10
+ #
11
+ def initialize(organization, filters = {})
12
+ @organization = organization
13
+ if filters.include? :wildcard
14
+ filters.delete(:name)
15
+ filters.delete(:nickname)
16
+ end
17
+ @filters = filters
18
+ end
19
+
20
+ def users
21
+ resolve
22
+ end
23
+
24
+ private
25
+
26
+ def resolve
27
+ return @records if @records
28
+
29
+ scope
30
+ global_filter
31
+ filter
32
+ @records.limit(50)
33
+ end
34
+
35
+ def scope
36
+ @records = Decidim::User
37
+ .where(organization: organization)
38
+ .confirmed
39
+ end
40
+
41
+ # Only key name attributes in Decidim::User will be applied
42
+ def filter
43
+ @filters.each do |key, value|
44
+ next unless Decidim::User.column_names.include? key.to_s
45
+
46
+ @records = @records.where("#{key} ilike ?", "%#{value}%")
47
+ end
48
+ end
49
+
50
+ # Special search key ":wildcard"
51
+ def global_filter
52
+ return unless @filters.include? :wildcard
53
+
54
+ term = "%#{@filters[:wildcard]}%"
55
+ @records = @records.where("name ilike ? or nickname ilike ?", term, term)
56
+ end
57
+
58
+ attr_reader :organization, :filters
59
+ end
60
+ end
61
+ end
@@ -21,11 +21,11 @@ module Decidim
21
21
  private
22
22
 
23
23
  def custom_allowed_attributes
24
- Loofah::HTML5::WhiteList::ALLOWED_ATTRIBUTES + %w(frameborder allowfullscreen)
24
+ Loofah::HTML5::SafeList::ALLOWED_ATTRIBUTES + %w(frameborder allowfullscreen)
25
25
  end
26
26
 
27
27
  def custom_allowed_tags
28
- Loofah::HTML5::WhiteList::ALLOWED_ELEMENTS_WITH_LIBXML2 + %w(iframe)
28
+ Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2 + %w(iframe)
29
29
  end
30
30
  end
31
31
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Exporters
5
+ # This class serializes all components in a ParticipatorySpace so can be
6
+ # exported to CSV, JSON or other formats.
7
+ class ParticipatorySpaceComponentsSerializer < Decidim::Exporters::Serializer
8
+ include Decidim::ApplicationHelper
9
+ include Decidim::ResourceHelper
10
+ include Decidim::TranslationsHelper
11
+
12
+ # Public: Initializes the serializer with a participatory_space.
13
+ def initialize(participatory_space)
14
+ @participatory_space = participatory_space
15
+ end
16
+
17
+ # Public: Exports a hash with the serialized data for this participatory_space.
18
+ def serialize
19
+ participatory_space.components.collect do |component|
20
+ serialized = {
21
+ manifest_name: component.manifest_name,
22
+ id: component.id,
23
+ name: component.name,
24
+ participatory_space_id: component.participatory_space_id,
25
+ participatory_space_type: component.participatory_space_type,
26
+ settings: component.settings.as_json,
27
+ weight: component.weight,
28
+ permissions: component.permissions,
29
+ published_at: component.published_at
30
+ }
31
+ serialized[:specific_data] = serialize_component_specific_data(component) if component.serializes_specific_data?
32
+ serialized
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :participatory_space
39
+
40
+ def serialize_component_specific_data(component)
41
+ specific_serializer = component.manifest.specific_data_serializer_class.new(component)
42
+ specific_serializer.serialize
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Importers
5
+ # This class is an abstraction that defines a common and flexible interface
6
+ # in how importers should be called.
7
+ class Importer
8
+ # Imports the contents of the `serialized` argument.
9
+ #
10
+ # Importers that import JSON will normally accept a JSON valid value for
11
+ # the `serialized` argument.
12
+ # This values may be either: object, array, string, number, true, false
13
+ # or null.
14
+ #
15
+ # Returns: What has been imported.
16
+ #
17
+ # +serialized+: The serialized version of the resource to import.
18
+ # +user+: The Decidim::User that is importing.
19
+ # +opts+: Extra options that specific subclasses may require.
20
+ def import(_serialized, _user, _opts = {})
21
+ raise NotImplementedError, "Decidim::Importers::Importer should be subclassed."
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Importers
5
+ # This class parses and imports all components in a ParticipatorySpace.
6
+ # It currently supports JSON format.
7
+ class ParticipatorySpaceComponentsImporter < Decidim::Importers::Importer
8
+ # +participatory_space+: The ParticipatorySpace to which all components
9
+ # will belong to.
10
+ def initialize(participatory_space)
11
+ @participatory_space = participatory_space
12
+ end
13
+
14
+ # Parses an exported list of components and imports them into the
15
+ # platform.
16
+ #
17
+ # +participatory_space+: The ParticipatorySpace to which all components
18
+ # will belong to.
19
+ # +json_text+: A json document as a String.
20
+ # +user+: The Decidim::User that is importing.
21
+ def from_json(json_text, user)
22
+ json = JSON.parse(json_text)
23
+ import(json, user)
24
+ end
25
+
26
+ # For each component configuration in the json,
27
+ # creates a new Decidim::Component with that configuration.
28
+ #
29
+ # Returns: An Array with all components created.
30
+ #
31
+ # +json+: An array of json compatible Hashes with the configuration of Decidim::Components.
32
+ # +user+: The Decidim::User that is importing.
33
+ def import(json_ary, user)
34
+ ActiveRecord::Base.transaction do
35
+ json_ary.collect do |serialized|
36
+ attributes = serialized.with_indifferent_access
37
+ # we override the parent participatory sapce
38
+ attributes["participatory_space_id"] = @participatory_space.id
39
+ attributes["participatory_space_type"] = @participatory_space.class.name
40
+ import_component_from_attributes(attributes, user)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Returns a persisted Component instance build from the +attributes+ argument.
48
+ def import_component_from_attributes(attributes, user)
49
+ component = Decidim.traceability.perform_action!(:create,
50
+ Decidim::Component,
51
+ user) do
52
+ c = Decidim::Component.new(attributes.except(:id, :settings, :specific_data))
53
+ c[:settings] = attributes[:settings]
54
+ c.save!
55
+ c
56
+ end
57
+ import_component_specific_data(component, attributes, user) if component.serializes_specific_data?
58
+ component
59
+ end
60
+
61
+ def import_component_specific_data(component, serialized, user)
62
+ specific_importer = component.manifest.specific_data_importer_class.new(component)
63
+ specific_importer.import(serialized[:specific_data], user)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -37,7 +37,7 @@ module Decidim
37
37
  end
38
38
 
39
39
  def data_for(export_manifest)
40
- collection = components.where(manifest_name: export_manifest.component_manifest.name).find_each.flat_map do |component|
40
+ collection = components.where(manifest_name: export_manifest.manifest.name).find_each.flat_map do |component|
41
41
  export_manifest.collection.call(component)
42
42
  end
43
43
 
@@ -3,6 +3,5 @@
3
3
  module Decidim
4
4
  # This class deals with uploading banner images to ParticipatoryProcesses.
5
5
  class BannerImageUploader < ImageUploader
6
- process resize_to_limit: [1000, 200]
7
6
  end
8
7
  end
@@ -17,8 +17,10 @@
17
17
 
18
18
  <%= f.hidden_field :invitation_token %>
19
19
 
20
- <div class="field">
21
- <%= f.text_field :nickname, help_text: t("devise.invitations.edit.nickname_help", organization: current_organization.name), required: "required" %>
20
+ <div class="user-nickname">
21
+ <div class="field">
22
+ <%= f.text_field :nickname, help_text: t("devise.invitations.edit.nickname_help", organization: current_organization.name), required: "required", prefix: { value: "@", small: 1, large: 1 } %>
23
+ </div>
22
24
  </div>
23
25
 
24
26
  <% if f.object.class.require_password_on_accepting %>
@@ -34,9 +34,9 @@
34
34
  </div>
35
35
  </div>
36
36
 
37
- <div class="user-person">
37
+ <div class="user-nickname">
38
38
  <div class="field">
39
- <%= f.text_field :nickname, help_text: t(".nickname_help", organization: current_organization.name) %>
39
+ <%= f.text_field :nickname, help_text: t(".nickname_help", organization: current_organization.name), prefix: { value: "@", small: 1, large: 1 } %>
40
40
  </div>
41
41
  </div>
42
42
 
@@ -0,0 +1,33 @@
1
+ <% add_decidim_page_title(t("devise.unlocks.new.resend_unlock_instructions")) %>
2
+
3
+ <main class="wrapper">
4
+ <div class="row collapse">
5
+ <div class="row collapse">
6
+ <div class="columns large-8 large-centered text-center page-title">
7
+ <h1><%= t("devise.unlocks.new.resend_unlock_instructions") %></h1>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="row">
12
+ <div class="columns medium-7 large-5 medium-centered">
13
+ <div class="card">
14
+ <div class="card__content">
15
+ <%= decidim_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: "register-form new_user" }) do |f| %>
16
+ <%= devise_error_messages! %>
17
+
18
+ <div class="field">
19
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
20
+ </div>
21
+
22
+ <div class="actions">
23
+ <%= f.submit t("devise.unlocks.new.resend_unlock_instructions"), class: "button expanded" %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <%= render "decidim/devise/shared/links" %>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </main>
@@ -6,25 +6,9 @@
6
6
  <% end %>
7
7
  </p>
8
8
  <% end %>
9
- <div class="card card--secondary">
10
- <div class="filters">
11
- <% @sections.each do |type, results| %>
12
- <div class="filters__section--general<%= " no-results" if results[:count].zero? %>">
13
- <% if results[:count].positive? %>
14
- <%= link_to search_path_by_resource_type(type), class: "flex--sbc" do %>
15
- <strong class="section-title"><%= searchable_resource_human_name(type) %></strong>
16
- <span class="muted">&nbsp;<%= results[:count] %></span>
17
- <% end %>
18
- <% else %>
19
- <div class="flex--sbc">
20
- <strong class="section-title"><%= searchable_resource_human_name(type) %></strong>
21
- <span class="muted">&nbsp;<%= results[:count] %></span>
22
- </div>
23
- <% end %>
24
- </div>
25
- <% end %>
26
- </div>
27
- </div>
9
+ <%= render partial: "resources_filter_block", locals: { sections: @sections, types: Decidim::Searchable.searchable_resources_of_type_participant } %>
10
+ <%= render partial: "resources_filter_block", locals: { sections: @sections, types: Decidim::Searchable.searchable_resources_of_type_participatory_space } %>
11
+ <%= render partial: "resources_filter_block", locals: { sections: @sections, types: Decidim::Searchable.searchable_resources_of_type_component } %>
28
12
  <div class="card card--secondary">
29
13
  <%= filter_form_for filter do |form| %>
30
14
  <%= scopes_picker_filter form, :decidim_scope_id %>
@@ -0,0 +1,20 @@
1
+ <% resources= @sections.select {|type, results| types.include?(type)} %>
2
+ <div class="card card--secondary">
3
+ <div class="filters">
4
+ <% resources.each do |type, results| %>
5
+ <div class="filters__section--general<%= " no-results" if results[:count].zero? %>">
6
+ <% if results[:count].positive? %>
7
+ <%= link_to search_path_by_resource_type(type), class: "flex--sbc" do %>
8
+ <strong class="section-title"><%= searchable_resource_human_name(type) %></strong>
9
+ <span class="muted">&nbsp;<%= results[:count] %></span>
10
+ <% end %>
11
+ <% else %>
12
+ <div class="flex--sbc">
13
+ <strong class="section-title"><%= searchable_resource_human_name(type) %></strong>
14
+ <span class="muted">&nbsp;<%= results[:count] %></span>
15
+ </div>
16
+ <% end %>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+ </div>
@@ -0,0 +1,7 @@
1
+ <p><%= t(".greeting", recipient: @resource.email) %></p>
2
+
3
+ <p><%= t(".message") %></p>
4
+
5
+ <p><%= t(".instruction") %></p>
6
+
7
+ <p><%= link_to t(".action"), unlock_url(@resource, unlock_token: @token, host: @resource.organization.host) %></p>