diff --git a/.gitignore b/.gitignore
index c3262449f0..61d568b580 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@
/imports
.rspec-failures
+Capfile
+config/deploy*
\ No newline at end of file
diff --git a/.rubocop.yml b/.rubocop.yml
index 66eabf9d2d..dca2cdbcb4 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -89,7 +89,7 @@ Style/Alias:
- prefer_alias_method
# Align the elements of a hash literal if they span more than one line.
-Layout/AlignHash:
+Layout/HashAlignment:
# Alignment of entries using hash rocket as separator. Valid values are:
#
# key - left alignment of keys
@@ -152,7 +152,7 @@ Layout/AlignHash:
- ignore_implicit
- ignore_explicit
-Layout/AlignParameters:
+Layout/ParameterAlignment:
# Alignment of parameters in multi-line method calls.
#
# The `with_first_parameter` style aligns the following lines along the same
@@ -263,20 +263,6 @@ Style/BlockDelimiters:
- proc
- it
-Style/BracesAroundHashParameters:
- EnforcedStyle: no_braces
- SupportedStyles:
- # The `braces` style enforces braces around all method parameters that are
- # hashes.
- - braces
- # The `no_braces` style checks that the last parameter doesn't have braces
- # around it.
- - no_braces
- # The `context_dependent` style checks that the last parameter doesn't have
- # braces around it, but requires braces if the second to last parameter is
- # also a hash literal.
- - context_dependent
-
# Indentation of `when`.
Layout/CaseIndentation:
EnforcedStyle: case
@@ -469,7 +455,7 @@ Naming/FileName:
# files with a shebang in the first line).
IgnoreExecutableScripts: true
-Layout/IndentFirstArgument:
+Layout/FirstArgumentIndentation:
EnforcedStyle: special_for_inner_method_call_in_parentheses
SupportedStyles:
# The first parameter should always be indented one step more than the
@@ -557,7 +543,7 @@ Layout/IndentationWidth:
Width: 2
# Checks the indentation of the first element in an array literal.
-Layout/IndentFirstArrayElement:
+Layout/FirstArrayElementIndentation:
# The value `special_inside_parentheses` means that array literals with
# brackets that have their opening bracket on the same line as a surrounding
# opening round parenthesis, shall have their first element indented relative
@@ -579,13 +565,13 @@ Layout/IndentFirstArrayElement:
IndentationWidth: ~
# Checks the indentation of assignment RHS, when on a different line from LHS
-Layout/IndentAssignment:
+Layout/AssignmentIndentation:
# By default, the indentation width from Style/IndentationWidth is used
# But it can be overridden by setting this parameter
IndentationWidth: ~
# Checks the indentation of the first key in a hash literal.
-Layout/IndentFirstHashElement:
+Layout/FirstHashElementIndentation:
# The value `special_inside_parentheses` means that hash literals with braces
# that have their opening brace on the same line as a surrounding opening
# round parenthesis, shall have their first key indented relative to the
@@ -792,12 +778,12 @@ Naming/PredicateName:
- has_
- have_
# Predicate name prefixes that should be removed.
- NamePrefixBlacklist:
+ ForbiddenPrefixes:
- is_
- have_
# Predicate names which, despite having a blacklisted prefix, or no ?,
# should still be accepted
- NameWhitelist:
+ AllowedMethods:
- is_a?
# Exclude Rspec specs because there is a strong convetion to write spec
# helpers in the form of `have_something` or `be_something`.
@@ -978,7 +964,7 @@ Style/TernaryParentheses:
- require_no_parentheses
AllowSafeAssignment: true
-Layout/TrailingBlankLines:
+Layout/TrailingEmptyLines:
EnforcedStyle: final_newline
SupportedStyles:
- final_newline
@@ -1028,7 +1014,7 @@ Style/TrivialAccessors:
# Commonly used in DSLs
AllowDSLWriters: false
IgnoreClassMethods: false
- Whitelist:
+ AllowedMethods:
- to_ary
- to_a
- to_c
diff --git a/Gemfile b/Gemfile
index babe32ea39..7a25ff3717 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,15 +1,19 @@
source "https://rubygems.org"
-DECIDIM_VERSION = { git: "https://github.com/decidim/decidim", branch: "release/0.24-stable" }
+DECIDIM_MAIN_BRANCH = "feature/bcn-budget-v0.24"
+
+DECIDIM_VERSION = { git: "https://github.com/AjuntamentdeBarcelona/decidim", branch: DECIDIM_MAIN_BRANCH }.freeze
ruby '2.7.2'
gem "decidim", DECIDIM_VERSION
+gem "decidim-census_sms", path: "decidim-census_sms"
gem "decidim-dataviz", path: "decidim-dataviz"
gem "decidim-initiatives", DECIDIM_VERSION
gem "decidim-sortitions", DECIDIM_VERSION
gem "decidim-stats", path: "decidim-stats"
gem "decidim-valid_auth", path: "decidim-valid_auth"
+gem "decidim-ephemeral_participation", path: "decidim-ephemeral_participation"
gem "decidim-navigation_maps", "~> 1.2.0"
# Change term_customizer dependency to ruby-gems' when term-customizer is compatible with DECIDIM_VERSION
@@ -49,6 +53,8 @@ group :development do
end
group :production do
+ # can be removed after
+ gem "letter_opener_web"
gem "sidekiq"
gem "rails_12factor"
gem "fog-aws"
diff --git a/Gemfile.lock b/Gemfile.lock
index 7fbb8509da..1af85f82d6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,15 +1,7 @@
GIT
- remote: https://github.com/CodiTramuntana/decidim-module-term_customizer
- revision: c4ed0ffa87bd9977c6b470aa815d5dc2ed9f88a5
- specs:
- decidim-term_customizer (0.18.0)
- decidim-admin (>= 0.18.0)
- decidim-core (>= 0.18.0)
-
-GIT
- remote: https://github.com/decidim/decidim
- revision: 2f1e3c8ad256dfc2616530a5049107e6335eceea
- branch: release/0.24-stable
+ remote: https://github.com/AjuntamentdeBarcelona/decidim
+ revision: e471864fcb4011b096ed31ea312814d6299b9f3d
+ branch: feature/bcn-budget-v0.24
specs:
decidim (0.24.2)
decidim-accountability (= 0.24.2)
@@ -225,12 +217,32 @@ GIT
decidim-verifications (0.24.2)
decidim-core (= 0.24.2)
+GIT
+ remote: https://github.com/CodiTramuntana/decidim-module-term_customizer
+ revision: c4ed0ffa87bd9977c6b470aa815d5dc2ed9f88a5
+ specs:
+ decidim-term_customizer (0.18.0)
+ decidim-admin (>= 0.18.0)
+ decidim-core (>= 0.18.0)
+
+PATH
+ remote: decidim-census_sms
+ specs:
+ decidim-census_sms (0.0.1)
+ decidim-core
+
PATH
remote: decidim-dataviz
specs:
decidim-dataviz (0.0.1)
decidim-core
+PATH
+ remote: decidim-ephemeral_participation
+ specs:
+ decidim-ephemeral_participation (0.0.1)
+ decidim-verifications
+
PATH
remote: decidim-stats
specs:
@@ -352,7 +364,7 @@ GEM
actionpack (>= 3.0)
cells (>= 4.1.6, < 5.0.0)
charlock_holmes (0.7.7)
- chef-utils (17.0.242)
+ chef-utils (17.1.35)
concurrent-ruby
childprocess (3.0.0)
coercible (1.0.0)
@@ -410,7 +422,7 @@ GEM
doc2text (0.4.3)
nokogiri (~> 1.11.1)
rubyzip (~> 2.3.0)
- docile (1.3.5)
+ docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.5.1)
@@ -503,7 +515,7 @@ GEM
graphiql-rails (1.4.11)
railties
sprockets-rails
- graphql (1.12.9)
+ graphql (1.12.10)
hashdiff (1.0.1)
hashie (4.1.0)
highline (2.0.3)
@@ -653,7 +665,7 @@ GEM
activerecord (>= 5.2)
activesupport (>= 5.2)
polyglot (0.3.5)
- premailer (1.14.3)
+ premailer (1.15.0)
addressable
css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
@@ -839,7 +851,7 @@ GEM
smart_properties (1.15.0)
social-share-button (1.2.4)
coffee-rails
- spreadsheet (1.2.8)
+ spreadsheet (1.2.9)
ruby-ole
spring (2.1.1)
spring-watcher-listen (2.0.1)
@@ -891,7 +903,7 @@ GEM
virtus (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
- webmock (3.12.2)
+ webmock (3.13.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -917,8 +929,10 @@ DEPENDENCIES
dalli
database_cleaner
decidim!
+ decidim-census_sms!
decidim-dataviz!
decidim-dev!
+ decidim-ephemeral_participation!
decidim-initiatives!
decidim-navigation_maps (~> 1.2.0)
decidim-sortitions!
diff --git a/app/assets/stylesheets/_barcelona.scss b/app/assets/stylesheets/_barcelona.scss
index 831cb7d6be..5ab323f409 100644
--- a/app/assets/stylesheets/_barcelona.scss
+++ b/app/assets/stylesheets/_barcelona.scss
@@ -7,3 +7,4 @@
@import "theme-barcelona/hero-custom";
@import "theme-barcelona/event-days";
@import "theme-barcelona/special-process";
+@import "theme-barcelona/budgets";
diff --git a/app/assets/stylesheets/theme-barcelona/_budgets.scss b/app/assets/stylesheets/theme-barcelona/_budgets.scss
new file mode 100644
index 0000000000..25b8f8c9c2
--- /dev/null
+++ b/app/assets/stylesheets/theme-barcelona/_budgets.scss
@@ -0,0 +1,11 @@
+.budget-progress {
+ .progress-meter {
+ &:not(&--minimum) {
+ background-color: $red-light;
+ }
+ }
+}
+
+#project .card.extra .button_to {
+ display: none;
+}
diff --git a/app/services/census_authorization_handler.rb b/app/services/census_authorization_handler.rb
index f71c0b5344..c8e938c9c6 100644
--- a/app/services/census_authorization_handler.rb
+++ b/app/services/census_authorization_handler.rb
@@ -56,7 +56,7 @@ def census_document_types
def unique_id
Digest::MD5.hexdigest(
- "#{document_number&.upcase}-#{Rails.application.secrets.secret_key_base}"
+ "#{sanitized_document_number}-#{Rails.application.secrets.secret_key_base}"
)
end
@@ -83,6 +83,10 @@ def document_type_valid
errors.add(:document_number, I18n.t("census_authorization_handler.invalid_document")) unless response.xpath("//codiRetorn").text == "01"
end
+ def sanitized_document_number
+ document_number&.gsub(/[^A-Za-z0-9]/, "")&.upcase
+ end
+
def response
return nil if document_number.blank? ||
document_type.blank? ||
@@ -109,7 +113,7 @@ def request_body
PAM
#{sanitized_document_type}
- #{sanitize document_number&.upcase}
+ #{sanitized_document_number}
#{sanitize postal_code}
#{sanitized_date_of_birth}
diff --git a/config/initializers/decidim.rb b/config/initializers/decidim.rb
index 5e2613c017..c3730ec0df 100644
--- a/config/initializers/decidim.rb
+++ b/config/initializers/decidim.rb
@@ -23,6 +23,22 @@
if Rails.application.secrets.sms.values.all?(&:present?)
config.sms_gateway_service = "SmsGateway"
+
+ Decidim::Verifications.register_workflow(:census_sms_authorization_handler) do |auth|
+ auth.engine = Decidim::CensusSms::Verification::Engine
+ auth.action_authorizer = "Decidim::CensusSms::Verification::ActionAuthorizer"
+ auth.renewable = true
+ auth.time_between_renewals = 1.day
+ auth.ephemerable = true
+
+ auth.options do |options|
+ parent_scope = Decidim::Scope.where("name->>'ca' = 'Ciutat'").first
+
+ Decidim::Scope.where(parent: parent_scope).pluck(:code).each do |code|
+ options.attribute :"scope_code_#{code}", type: :boolean, required: false
+ end
+ end
+ end
end
config.timestamp_service = "TimestampService"
@@ -38,6 +54,7 @@
auth.renewable = true
auth.time_between_renewals = 1.day
auth.metadata_cell = "census_authorization_metadata"
+ auth.ephemerable = true
end
Decidim::Verifications.register_workflow(:census16_authorization_handler) do |auth|
@@ -45,4 +62,5 @@
auth.renewable = true
auth.time_between_renewals = 1.day
auth.metadata_cell = "census16_authorization_metadata"
+ auth.ephemerable = true
end
diff --git a/config/initializers/decidim_budgets.rb b/config/initializers/decidim_budgets.rb
index f01984a31d..f2afcdfdda 100644
--- a/config/initializers/decidim_budgets.rb
+++ b/config/initializers/decidim_budgets.rb
@@ -1,4 +1,6 @@
# frozen_string_literal: true
require "budgets_workflow_pam2020"
+require "budgets_workflow_pam2021"
Decidim::Budgets.workflows[:pam2020] = BudgetsWorkflowPam2020
+Decidim::Budgets.workflows[:pam2021] = BudgetsWorkflowPam2021
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index f661cfd228..eac57d8ed9 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -141,6 +141,9 @@ ca:
first_login:
actions:
census_authorization_handler: Verifica't amb el padró
+ census16_authorization_handler: Verifica't amb el padró (16+)
+ census_sms_authorization_handler: Verifica't amb el padró i el teu mòbil
+ valid_auth: Valid auth
initiatives:
initiatives:
supports:
@@ -154,4 +157,4 @@ ca:
home:
extended:
debates_explanation: Espais per informar-te i decidir sobre les propostes
- de cada procés.
+ de cada procés.
\ No newline at end of file
diff --git a/config/locales/decidim_budgets_workflows_ca.yml b/config/locales/decidim_budgets_workflows_ca.yml
new file mode 100644
index 0000000000..45ef09a819
--- /dev/null
+++ b/config/locales/decidim_budgets_workflows_ca.yml
@@ -0,0 +1,11 @@
+ca:
+ decidim:
+ components:
+ budgets:
+ settings:
+ global:
+ ephemerous_census_data_verification: Ephemerous Census Data Verification
+ workflow_choices:
+ pam2020: "PAM 2020: allows to Vote in the participant's district and in another of free choice."
+ pam2021: "PAM 2021: allows to Vote in the participant's district and in another of free choice."
+
diff --git a/config/locales/decidim_budgets_workflows_en.yml b/config/locales/decidim_budgets_workflows_en.yml
index 465fa2bdf7..5d18909d7d 100644
--- a/config/locales/decidim_budgets_workflows_en.yml
+++ b/config/locales/decidim_budgets_workflows_en.yml
@@ -4,5 +4,7 @@ en:
budgets:
settings:
global:
+ ephemerous_census_data_verification: Ephemerous Census Data Verification
workflow_choices:
pam2020: "PAM 2020: allows to Vote in the participant's district and in another of free choice."
+ pam2021: "PAM 2021: allows to Vote in the participant's district and in another of free choice."
diff --git a/config/locales/decidim_budgets_workflows_es.yml b/config/locales/decidim_budgets_workflows_es.yml
new file mode 100644
index 0000000000..349f22458f
--- /dev/null
+++ b/config/locales/decidim_budgets_workflows_es.yml
@@ -0,0 +1,11 @@
+es:
+ decidim:
+ components:
+ budgets:
+ settings:
+ global:
+ ephemerous_census_data_verification: Ephemerous Census Data Verification
+ workflow_choices:
+ pam2020: "PAM 2020: allows to Vote in the participant's district and in another of free choice."
+ pam2021: "PAM 2021: allows to Vote in the participant's district and in another of free choice."
+
diff --git a/config/routes.rb b/config/routes.rb
index 0cc29c5da3..6d31ba014f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -55,4 +55,6 @@
mount Decidim::Core::Engine => "/"
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
+
+ mount Decidim::EphemeralParticipation::Engine, at: "/", as: "decidim_ephemeral_participation"
end
diff --git a/config/secrets.yml b/config/secrets.yml
index b878e0c20f..3334e4e6b0 100644
--- a/config/secrets.yml
+++ b/config/secrets.yml
@@ -60,6 +60,13 @@ test:
secret_key_base: 4a6fd36ca5634dbf42f63331b1a236a041976561ec314414d1750f33ef691dd3705ff2828a607d98bee0ba492e11281a33e736c71e959ab04c76679e74ecb564
geocoder:
here_api_key: 1234123412341234
+ sms:
+ service_url: http://example.org/sms
+ proxy_url: http://example.org/proxy
+ username: username
+ password: password
+ service_id: "1234"
+ sub_service_id: "1234"
# Do not keep production secrets in the repository,
# instead read values from the environment.
diff --git a/db/migrate/20210518204806_update_organizations_available_authorizations.decidim_ephemeral_participation.rb b/db/migrate/20210518204806_update_organizations_available_authorizations.decidim_ephemeral_participation.rb
new file mode 100644
index 0000000000..3b0434893c
--- /dev/null
+++ b/db/migrate/20210518204806_update_organizations_available_authorizations.decidim_ephemeral_participation.rb
@@ -0,0 +1,41 @@
+# This migration comes from decidim_ephemeral_participation (originally 20210518192857)
+class UpdateOrganizationsAvailableAuthorizations < ActiveRecord::Migration[5.2]
+ class Organization < ApplicationRecord
+ self.table_name = :decidim_organizations
+ end
+
+ def up
+ workflows = {}
+
+ Organization.find_each do |organization|
+ workflows[organization.id] =
+ organization.available_authorizations.to_a.each_with_object({}) do |workflow, hash|
+ hash[workflow] = { allow_ephemeral_participation: false }
+ end
+ end
+
+ remove_column :decidim_organizations, :available_authorizations
+ add_column :decidim_organizations, :available_authorizations, :jsonb, default: {}
+ Organization.reset_column_information
+
+ Organization.find_each do |organization|
+ organization.update!(available_authorizations: workflows[organization.id])
+ end
+ end
+
+ def down
+ workflows = {}
+
+ Organization.find_each do |organization|
+ workflows[organization.id] = organization.available_authorizations.keys.presence
+ end
+
+ remove_column :decidim_organizations, :available_authorizations
+ add_column :decidim_organizations, :available_authorizations, :string, array: true, default: []
+ Organization.reset_column_information
+
+ Organization.find_each do |organization|
+ organization.update!(available_authorizations: workflows[organization.id])
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 851b95f9b1..55883734b6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_04_22_132738) do
+ActiveRecord::Schema.define(version: 2021_05_18_204806) do
# These are extensions that must be enabled in order to support this database
enable_extension "ltree"
@@ -973,7 +973,6 @@
t.string "github_handler"
t.string "reference_prefix", null: false
t.string "secondary_hosts", default: [], array: true
- t.string "available_authorizations", default: [], array: true
t.text "header_snippets"
t.jsonb "cta_button_text"
t.string "cta_button_path"
@@ -1008,6 +1007,7 @@
t.integer "comments_max_length", default: 1000
t.jsonb "file_upload_settings"
t.string "machine_translation_display_priority", default: "original", null: false
+ t.jsonb "available_authorizations", default: {}
t.index ["host"], name: "index_decidim_organizations_on_host", unique: true
t.index ["name"], name: "index_decidim_organizations_on_name", unique: true
end
diff --git a/decidim-census_sms/README.md b/decidim-census_sms/README.md
new file mode 100644
index 0000000000..9fe18965e1
--- /dev/null
+++ b/decidim-census_sms/README.md
@@ -0,0 +1,13 @@
+# Decidim::CensusSms::Verification
+The CensusSms module adds a verification workflow that checks users against the Census database and sends a code to their mobile phone number to confirm their identity.
+
+## Usage
+Activate workflow in the system panel.
+
+## Contributing
+See [Decidim
+Barcelona](https://github.com/AjuntamentdeBarcelona/decidim-barcelona).
+
+## License
+See [Decidim
+Barcelona](https://github.com/AjuntamentdeBarcelona/decidim-barcelona).
diff --git a/decidim-census_sms/Rakefile b/decidim-census_sms/Rakefile
new file mode 100644
index 0000000000..447b5c1bf2
--- /dev/null
+++ b/decidim-census_sms/Rakefile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require "decidim/dev/common_rake"
diff --git a/decidim-census_sms/app/assets/config/decidim_census_sms_manifest.css b/decidim-census_sms/app/assets/config/decidim_census_sms_manifest.css
new file mode 100644
index 0000000000..830b3d1983
--- /dev/null
+++ b/decidim-census_sms/app/assets/config/decidim_census_sms_manifest.css
@@ -0,0 +1,3 @@
+/*
+ *= link decidim/census_sms/verification.scss
+ */
diff --git a/decidim-census_sms/app/assets/stylesheets/decidim/census_sms/verification.scss b/decidim-census_sms/app/assets/stylesheets/decidim/census_sms/verification.scss
new file mode 100644
index 0000000000..4463b25a22
--- /dev/null
+++ b/decidim-census_sms/app/assets/stylesheets/decidim/census_sms/verification.scss
@@ -0,0 +1,24 @@
+.new_authorization {
+ .field.date {
+ label {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 5px;
+ }
+
+ select {
+ grid-row: 2;
+ }
+
+ .label-required {
+ width: 0.5rem;
+ grid-column: 3;
+ justify-self: flex-end;
+ }
+
+ .form-error {
+ grid-row: 3;
+ grid-column: span 3;
+ }
+ }
+}
diff --git a/decidim-census_sms/app/commands/decidim/census_sms/verification/reset_code.rb b/decidim-census_sms/app/commands/decidim/census_sms/verification/reset_code.rb
new file mode 100644
index 0000000000..d6fdd3e414
--- /dev/null
+++ b/decidim-census_sms/app/commands/decidim/census_sms/verification/reset_code.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Decidim
+ module CensusSms
+ module Verification
+ class ResetCode < Rectify::Command
+ # Public: Initializes the command.
+ #
+ # form - A form object with the params.
+ # authorization - The authorization object to update.
+ def initialize(form, authorization)
+ @form = form
+ @authorization = authorization
+ end
+
+ # Executes the command
+ # Broadcasts these events:
+ #
+ # - :ok when everything is valid.
+ # - :invalid if the handler wasn't valid and we couldn't proceed.
+ #
+ # Returns nothing.
+ def call
+ return broadcast(:ok) if update_authorization
+
+ broadcast(:invalid)
+ end
+
+ private
+
+ def update_authorization
+ metadata = @authorization.metadata
+ metadata[:mobile_phone_number] = @form.mobile_phone_number_hash
+
+ @authorization.update(metadata: metadata, verification_metadata: verification_metadata)
+ end
+
+ def verification_metadata
+ {
+ verification_code: @form.generated_code,
+ code_sent_at: Time.current
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-census_sms/app/controllers/decidim/census_sms/verification/authorizations_controller.rb b/decidim-census_sms/app/controllers/decidim/census_sms/verification/authorizations_controller.rb
new file mode 100644
index 0000000000..ee1b41aa09
--- /dev/null
+++ b/decidim-census_sms/app/controllers/decidim/census_sms/verification/authorizations_controller.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+module Decidim
+ module CensusSms
+ module Verification
+ class AuthorizationsController < Decidim::ApplicationController
+ include Decidim::Verifications::Renewable
+
+ helper_method :authorization, :tos_path
+
+ def new
+ enforce_permission_to :create, :authorization, authorization: authorization
+
+ @form = AuthorizationForm.new
+ end
+
+ def create
+ enforce_permission_to :create, :authorization, authorization: authorization
+
+ @form = AuthorizationForm.from_params(create_params)
+
+ Decidim::Verifications::PerformAuthorizationStep.call(authorization, @form) do
+ on(:ok) do
+ flash[:notice] = t("authorizations.create.success", scope: "decidim.census_sms.verification")
+ authorization_method = Decidim::Verifications::Adapter.from_element(authorization.name)
+ redirect_to authorization_method.resume_authorization_path(redirect_url: redirect_url)
+ end
+
+ on(:invalid) do
+ flash.now[:alert] = t("authorizations.create.error", scope: "decidim.census_sms.verification")
+ render :new
+ end
+ end
+ end
+
+ def edit
+ enforce_permission_to :update, :authorization, authorization: authorization
+
+ @form = Decidim::Verifications::Sms::ConfirmationForm.from_params(params)
+ end
+
+ def update
+ enforce_permission_to :update, :authorization, authorization: authorization
+
+ @form = Decidim::Verifications::Sms::ConfirmationForm.from_params(params)
+
+ Decidim::Verifications::ConfirmUserAuthorization.call(authorization, @form, session) do
+ on(:ok) do
+ flash[:notice] = t("authorizations.update.success", scope: "decidim.census_sms.verification")
+ redirect_to decidim_verifications.authorizations_path
+ end
+
+ on(:invalid) do
+ flash.now[:alert] = t("authorizations.update.error", scope: "decidim.census_sms.verification")
+ render :edit
+ end
+ end
+ end
+
+ def reset
+ enforce_permission_to :update, :authorization, authorization: authorization
+
+ @form = ResetForm.from_params(params)
+
+ return unless request.post?
+
+ ResetCode.call(@form, authorization) do
+ on(:ok) do
+ flash[:notice] = t("authorizations.reset.success", scope: "decidim.census_sms.verification")
+ authorization_method = Decidim::Verifications::Adapter.from_element(authorization.name)
+ redirect_to authorization_method.resume_authorization_path(redirect_url: redirect_url)
+ end
+
+ on(:invalid) do
+ flash.now[:alert] = t("authorizations.reset.error", scope: "decidim.census_sms.verification")
+ render :reset
+ end
+ end
+ end
+
+ def destroy
+ enforce_permission_to :destroy, :authorization, authorization: authorization
+
+ authorization.destroy!
+ flash[:notice] = t("authorizations.destroy.success", scope: "decidim.census_sms.verification")
+
+ redirect_to action: :new
+ end
+
+ private
+
+ def create_params
+ params[:authorization].merge(user: current_user, date_of_birth: date_of_birth)
+ end
+
+ def date_of_birth
+ year, month, day = params[:authorization].select { |k, _v| k.include?("date_of_birth") }.values.reverse.map(&:to_i)
+
+ Date.new(year, month, day)
+ end
+
+ def authorization
+ @authorization ||= Decidim::Authorization.find_or_initialize_by(
+ user: current_user,
+ name: "census_sms_authorization_handler"
+ )
+ end
+
+ def tos_path
+ @terms_and_conditions_page_path ||= decidim.page_path(Decidim::StaticPage.find_by(slug: "terms-and-conditions", organization: current_organization))
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-census_sms/app/forms/decidim/census_sms/verification/authorization_form.rb b/decidim-census_sms/app/forms/decidim/census_sms/verification/authorization_form.rb
new file mode 100644
index 0000000000..dfc0d4ba43
--- /dev/null
+++ b/decidim-census_sms/app/forms/decidim/census_sms/verification/authorization_form.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+# Checks the authorization against the census for Barcelona.
+require "digest/md5"
+
+# This class performs a check against the official census database in order
+# to verify the citizen's residence and sends an SMS to confirm identity.
+module Decidim
+ module CensusSms
+ module Verification
+ class AuthorizationForm < CensusAuthorizationHandler
+ attribute :mobile_phone_number, String
+ attribute :tos_acceptance, Boolean
+
+ validates :mobile_phone_number, :verification_code, :sms_gateway, presence: true
+ validates :tos_acceptance, acceptance: true
+
+ # If you need to store any of the defined attributes in the authorization you
+ # can do it here.
+ #
+ # You must return a Hash that will be serialized to the authorization when
+ # it's created, and available though authorization.metadata
+ def metadata
+ super.merge(
+ tos_accepted_at: Time.now,
+ mobile_phone_number: mobile_phone_number_hash,
+ scope_code: scope.code
+ )
+ end
+
+ # The verification metadata to validate in the next step.
+ def verification_metadata
+ {
+ verification_code: verification_code,
+ code_sent_at: Time.current
+ }
+ end
+
+ # When there's a phone number, sanitize it allowing only numbers and +.
+ def mobile_phone_number
+ return unless super
+
+ super.gsub(/[^+0-9]/, "")
+ end
+
+ private
+
+ def sms_gateway
+ Decidim.sms_gateway_service.to_s.safe_constantize
+ end
+
+ def verification_code
+ return unless sms_gateway
+ return @verification_code if defined?(@verification_code)
+
+ # DEBUG #TODO UNCOMMENT
+ # return unless sms_gateway.new(mobile_phone_number, generated_code).deliver_code
+
+ @verification_code = generated_code
+ end
+
+ def generated_code
+ @generated_code ||= SecureRandom.random_number(1_000_000).to_s
+ end
+
+ # A mobile phone can only be verified once but it should be private.
+ def mobile_phone_number_hash
+ Digest::MD5.hexdigest(
+ "#{mobile_phone_number}-#{Rails.application.secrets.secret_key_base}"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-census_sms/app/forms/decidim/census_sms/verification/reset_form.rb b/decidim-census_sms/app/forms/decidim/census_sms/verification/reset_form.rb
new file mode 100644
index 0000000000..24bd45e40a
--- /dev/null
+++ b/decidim-census_sms/app/forms/decidim/census_sms/verification/reset_form.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Decidim
+ module CensusSms
+ module Verification
+ # A form object to reset verification code and send it again.
+ class ResetForm < Form
+ attribute :mobile_phone_number, String
+
+ validates :mobile_phone_number, :verification_code, :sms_gateway, presence: true
+
+ # When there's a phone number, sanitize it allowing only numbers and +.
+ def mobile_phone_number
+ return unless super
+
+ super.gsub(/[^+0-9]/, "")
+ end
+
+ # A mobile phone can only be verified once but it should be private.
+ def mobile_phone_number_hash
+ Digest::MD5.hexdigest(
+ "#{mobile_phone_number}-#{Rails.application.secrets.secret_key_base}"
+ )
+ end
+
+ def generated_code
+ @generated_code ||= SecureRandom.random_number(1_000_000).to_s
+ end
+
+ private
+
+ def verification_code
+ return unless sms_gateway
+ return @verification_code if defined?(@verification_code)
+
+ return unless sms_gateway.new(mobile_phone_number, generated_code).deliver_code
+
+ @verification_code = generated_code
+ end
+
+ def sms_gateway
+ Decidim.sms_gateway_service.to_s.safe_constantize
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-census_sms/app/services/decidim/census_sms/verification/action_authorizer.rb b/decidim-census_sms/app/services/decidim/census_sms/verification/action_authorizer.rb
new file mode 100644
index 0000000000..8859a1b0aa
--- /dev/null
+++ b/decidim-census_sms/app/services/decidim/census_sms/verification/action_authorizer.rb
@@ -0,0 +1,35 @@
+module Decidim
+ module CensusSms
+ module Verification
+ class ActionAuthorizer < Decidim::Verifications::DefaultActionAuthorizer
+ BASE_OPTION_KEY = "scope_code".freeze
+
+ protected
+
+ def missing_fields
+ authorized_code.present? ? [] : [BASE_OPTION_KEY]
+ end
+
+ def unmatched_fields
+ scope_valid? ? [] : [BASE_OPTION_KEY]
+ end
+
+ private
+
+ def scope_valid?
+ scope_codes = options.keys.map { |k| k.gsub("#{BASE_OPTION_KEY}_", "") if k.match?(BASE_OPTION_KEY) }.compact
+
+ return unless scope_codes.any?
+
+ authorized_code_key = "#{BASE_OPTION_KEY}_#{authorized_code}"
+
+ authorized_code.present? && options[authorized_code_key] == "1"
+ end
+
+ def authorized_code
+ authorization.metadata[BASE_OPTION_KEY]
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/edit.html.erb b/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/edit.html.erb
new file mode 100644
index 0000000000..5fd88782fa
--- /dev/null
+++ b/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/edit.html.erb
@@ -0,0 +1,29 @@
+
+
+
+
<%= t(".title") %>
+
+
+
+
+ <%= decidim_form_for(@form, url: authorization_path(redirect_url: redirect_url), method: :put) do |form| %>
+ <%= form_required_explanation %>
+
+
+ <%= form.text_field :verification_code %>
+
+
+
+ <%= form.submit t(".send"), class: "button expanded", "data-disable-with" => "#{t('.send')}..." %>
+
+ <% end %>
+
+
+
+
+ <%= t(".instructions") %>
+ <%= link_to t(".reset"), reset_authorization_path(id: authorization.id) %>
+
+
+
+
diff --git a/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/new.html.erb b/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/new.html.erb
new file mode 100644
index 0000000000..9ad8b079c7
--- /dev/null
+++ b/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/new.html.erb
@@ -0,0 +1,58 @@
+
+
+
+
<%= t(".title") %>
+
+
+
+
+ <%= decidim_form_for(@form, url: authorization_path) do |form| %>
+ <%= form_required_explanation %>
+
+
+ <%= form.select :document_type, form.object.census_document_types, prompt: true %>
+
+
+
+ <%= form.text_field :document_number %>
+
+
+
+ <%= form.date_select :date_of_birth, start_year: 1900, end_year: 14.years.ago.year, default: 35.years.ago, prompt: { day: t(".date_select.day"), month: t(".date_select.month"), year: t(".date_select.year") } %>
+
+
+
+ <%= form.text_field :postal_code %>
+
+ <%== t(".postal_code_help") %>
+
+
+
+
+ <% parent_scope = Decidim::Scope.where("name->>'ca' = 'Ciutat'").first %>
+ <%= form.collection_select :scope_id, current_organization.scopes.where(parent: parent_scope), :id, ->(scope){ translated_attribute(scope.name) }, prompt: t(".scope_prompt") %>
+
+
+
+ <%= form.phone_field :mobile_phone_number %>
+
+
+
+ <%= form.label :tos_acceptance do %>
+ <%= form.check_box :tos_acceptance, label: false %>
+ <%== t("activemodel.attributes.authorization.tos_acceptance_label", tos_path: tos_path) %>
+ <% end %>
+
+
+ <%= form.hidden_field :handler_name %>
+
+
+ <%= form.submit t(".send"), class: "button expanded", "data-disable-with" => "#{t('.send')}..." %>
+
+ <% end %>
+
+
+
+
+
+<%= stylesheet_link_tag "decidim/census_sms/verification" %>
diff --git a/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/reset.html.erb b/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/reset.html.erb
new file mode 100644
index 0000000000..c638e99df5
--- /dev/null
+++ b/decidim-census_sms/app/views/decidim/census_sms/verification/authorizations/reset.html.erb
@@ -0,0 +1,23 @@
+
+
+
+
<%= t(".title") %>
+
+
+
+
+ <%= decidim_form_for(@form, url: reset_authorization_path(id: authorization.id), method: :post) do |form| %>
+ <%= form_required_explanation %>
+
+
+ <%= form.text_field :mobile_phone_number, label: t("mobile_phone_number", scope: "activemodel.attributes.authorization") %>
+
+
+
+ <%= form.submit t(".send"), class: "button expanded", "data-disable-with" => "#{t('.send')}..." %>
+
+ <% end %>
+
+
+
+
diff --git a/decidim-census_sms/config/i18n-tasks.yml b/decidim-census_sms/config/i18n-tasks.yml
new file mode 100644
index 0000000000..7c55c3e957
--- /dev/null
+++ b/decidim-census_sms/config/i18n-tasks.yml
@@ -0,0 +1,2 @@
+base_locale: ca
+locales: [ca,es]
\ No newline at end of file
diff --git a/decidim-census_sms/config/locales/ca.yml b/decidim-census_sms/config/locales/ca.yml
new file mode 100644
index 0000000000..a4cc568cc2
--- /dev/null
+++ b/decidim-census_sms/config/locales/ca.yml
@@ -0,0 +1,58 @@
+---
+ca:
+ activemodel:
+ attributes:
+ authorization:
+ date_of_birth: Data de naixement
+ document_number: Número de document
+ document_type: Tipus de document
+ mobile_phone_number: Número de telèfon mòbil
+ postal_code: Codi postal
+ scope_id: Districte
+ tos_acceptance: Accepto els Termes i Condicions
+ tos_acceptance_label: En registrar-te acceptes els Termes i Condicions
+ decidim:
+ authorization_handlers:
+ census_sms_authorization_handler:
+ explanation: El padró + SMS #TODO
+ name: El padró + SMS #TODO
+ fields:
+ scope_code_1: Districte 1 #TODO
+ scope_code_2: Districte 2 #TODO
+ scope_code_3: Districte 3 #TODO
+ scope_code_4: Districte 4 #TODO
+ scope_code_5: Districte 5 #TODO
+ scope_code_6: Districte 6 #TODO
+ scope_code_7: Districte 7 #TODO
+ scope_code_8: Districte 8 #TODO
+ scope_code_9: Districte 9 #TODO
+ scope_code_10: Districte 10 #TODO
+ scope_code: Districte
+ census_sms:
+ verification:
+ authorizations:
+ create:
+ error: S'ha produit un error en crear l'autorització
+ success: Has completat el primer pas per obtenir l'autorització
+ edit:
+ instructions: No has rebut el codi de verificació?
+ reset: Restableix el codi de verificació
+ send: Verifica't
+ title: Introdueix el codi que has rebut per SMS
+ new:
+ date_select:
+ day: Dia
+ month: Mes
+ year: Any
+ postal_code_help: No saps quin codi postal correspon a la teva adreça del Padró? Pots consultar-ho clicant aquí .
+ scope_prompt: Selecciona el teu districte
+ send: Verifica't
+ title: Verifica't amb el Padró
+ reset:
+ mobile_phone_number: Número de telèfon mòbil
+ send: Envia'm un nou codi
+ title: Restableix el codi de verificació
+ success: T'hem enviat un nou codi de verificació
+ update:
+ error: El codi de verificació que has introduït no coincideix amb el nostre. Si us plau, revisa l'SMS que t'hem enviat.
+ success: Felicitats. T'has verificat correctament.
diff --git a/decidim-census_sms/config/locales/es.yml b/decidim-census_sms/config/locales/es.yml
new file mode 100644
index 0000000000..5f56cbc36f
--- /dev/null
+++ b/decidim-census_sms/config/locales/es.yml
@@ -0,0 +1,5 @@
+---
+es:
+ decidim:
+ census_sms:
+ verification:
\ No newline at end of file
diff --git a/decidim-census_sms/decidim-census_sms.gemspec b/decidim-census_sms/decidim-census_sms.gemspec
new file mode 100644
index 0000000000..b1a7202833
--- /dev/null
+++ b/decidim-census_sms/decidim-census_sms.gemspec
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+$LOAD_PATH.push File.expand_path("lib", __dir__)
+
+Gem::Specification.new do |s|
+ s.name = "decidim-census_sms"
+ s.summary = "A verification workflow for Decidim Barcelona."
+ s.description = s.summary
+ s.version = "0.0.1"
+ s.authors = ["Vera Rojman"]
+ s.email = ["vera@platoniq.net"]
+
+ s.files = Dir["{app,config,lib}/**/*", "Rakefile", "README.md"]
+
+ s.add_dependency "decidim-core"
+
+ s.add_development_dependency "decidim-dev"
+end
diff --git a/decidim-census_sms/lib/decidim/census_sms.rb b/decidim-census_sms/lib/decidim/census_sms.rb
new file mode 100644
index 0000000000..9710f1a689
--- /dev/null
+++ b/decidim-census_sms/lib/decidim/census_sms.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "decidim/census_sms/verification"
+
+module Decidim
+ # Base module for this engine.
+ module CensusSms
+ end
+end
diff --git a/decidim-census_sms/lib/decidim/census_sms/verification.rb b/decidim-census_sms/lib/decidim/census_sms/verification.rb
new file mode 100644
index 0000000000..4f2f5b3949
--- /dev/null
+++ b/decidim-census_sms/lib/decidim/census_sms/verification.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "decidim/census_sms/verification/engine"
+
+module Decidim
+ module CensusSms
+ module Verification
+ end
+ end
+end
diff --git a/decidim-census_sms/lib/decidim/census_sms/verification/engine.rb b/decidim-census_sms/lib/decidim/census_sms/verification/engine.rb
new file mode 100644
index 0000000000..27e137ba4f
--- /dev/null
+++ b/decidim-census_sms/lib/decidim/census_sms/verification/engine.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Decidim
+ module CensusSms
+ module Verification
+ # This is an engine that performs user authorization.
+ class Engine < ::Rails::Engine
+ isolate_namespace Decidim::CensusSms::Verification
+
+ paths["db/migrate"] = nil
+ paths["lib/tasks"] = nil
+
+ routes do
+ resource :authorizations, only: [:new, :create, :edit, :update, :destroy], as: :authorization do
+ get :reset, on: :member
+ post :reset, on: :member
+ get :renew, on: :collection
+ end
+ root to: "authorizations#new"
+ end
+
+ initializer "decidim_census_sms.assets" do |app|
+ app.config.assets.precompile += %w(decidim_census_sms_manifest.css
+ decidim/census_sms/verification.scss)
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/README.md b/decidim-ephemeral_participation/README.md
new file mode 100644
index 0000000000..785ab21109
--- /dev/null
+++ b/decidim-ephemeral_participation/README.md
@@ -0,0 +1,44 @@
+# Decidim::EphemeralParticipation
+
+This module adds a specific integration with local Barcelona services so citizens can participate without registration.
+
+> NOTE: this module might be provisional if the main changes introduces by it are ported to `decidim-core`
+> (which is the intention of the team behind it)
+
+## Install
+
+Available_authorizations can now hold some configurable data, this migrations takes car of updating the organization field:
+
+```
+rails decidim_ephemeral_participation:install:migrations
+```
+
+In order to allow an Authorization handler to be used for ephemeral participation, it needs to be configured in the initializer:
+
+```ruby
+# config/initializers/decidim.rb
+
+Decidim::Verifications.register_workflow(:census) do |workflow|
+ workflow.form = "myAuthorizationHandlerClass"
+ workflow.renewable = true
+ workflow.time_between_renewals = 1.day
+ workflow.metadata_cell = "decidim/verifications/authorization_metadata"
+ # set the next varialble to true to allows the system admin use this as a method for direct participation
+ workflow.ephemerable = true
+end
+```
+
+**IMPORANT**
+The following assumptions are made:
+- The verification workflow is responsible for making users accept the TOS.
+- The verification workflow is redirecting to `authorizations_path` or `redirect_url` after creating the authorization.
+
+## Contributing
+
+See [Decidim
+Barcelona](https://github.com/AjuntamentdeBarcelona/decidim-barcelona).
+
+## License
+
+See [Decidim
+Barcelona](https://github.com/AjuntamentdeBarcelona/decidim-barcelona).
diff --git a/decidim-ephemeral_participation/app/cells/decidim/ephemeral_participation/project_list_item_cell_override.rb b/decidim-ephemeral_participation/app/cells/decidim/ephemeral_participation/project_list_item_cell_override.rb
new file mode 100644
index 0000000000..8c58734004
--- /dev/null
+++ b/decidim-ephemeral_participation/app/cells/decidim/ephemeral_participation/project_list_item_cell_override.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module ProjectListItemCellOverride
+ def vote_button_disabled?
+ return unless current_user
+ return if current_user.ephemeral_participant?
+
+ !can_have_order?
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/create_ephemeral_participant.rb b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/create_ephemeral_participant.rb
new file mode 100644
index 0000000000..094ba78a63
--- /dev/null
+++ b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/create_ephemeral_participant.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class CreateEphemeralParticipant < Rectify::Command
+ include ::Devise::Controllers::Helpers
+
+ def initialize(request, current_user)
+ @request = request
+ @current_user = current_user
+ end
+
+ def call
+ return broadcast(:invalid) unless valid_params?
+
+ sign_in(new_ephemeral_participant) unless @current_user
+
+ broadcast(:ok, authorization_path)
+ end
+
+ private
+
+ def valid_params?
+ @request.is_a?(ActionDispatch::Request) && component_id.present? && ephemeral_participation_path.present?
+ end
+
+ def component_id
+ @request.params[:component_id]
+ end
+
+ def ephemeral_participation_path
+ @request.params[:ephemeral_participation_path]
+ end
+
+ def new_ephemeral_participant
+ Decidim::User.new(
+ organization: component.organization,
+ managed: true,
+ tos_agreement: true,
+ extended_data: {
+ ephemeral_participation: {
+ authorization_name: authorization_name,
+ component_id: component.id,
+ permissions: component.ephemeral_participation_permissions,
+ request_path: ephemeral_participation_path
+ }
+ }
+ ).tap do |user|
+ user.nickname = nicknamize(user)
+ user.save!
+ end
+ end
+
+ # nickname is needed to ensure some links are not broken
+ def nicknamize(user)
+ Decidim::UserBaseEntity.nicknamize(user.name, organization: user.organization)
+ end
+
+ def authorization_path
+ adapter.root_path(redirect_url: ephemeral_participation_path)
+ end
+
+ def adapter
+ @adapter ||= Decidim::Verifications::Adapter.from_element(authorization_name)
+ end
+
+ def authorization_name
+ component.organization.ephemeral_participation_authorization
+ end
+
+ def component
+ @component ||= Decidim::Component.find(component_id)
+ end
+
+ # Needed for Devise::Controllers::Helpers#sign_in
+ def session
+ @request.session
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/destroy_ephemeral_participant.rb b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/destroy_ephemeral_participant.rb
new file mode 100644
index 0000000000..63e06f9cc7
--- /dev/null
+++ b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/destroy_ephemeral_participant.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class DestroyEphemeralParticipant < Rectify::Command
+ include ::Devise::Controllers::Helpers
+
+ def initialize(request, user)
+ @request = request
+ @user = user
+ end
+
+ def call
+ return broadcast(:invalid) unless valid_params?
+
+ @user.invalidate_all_sessions!
+ @user.destroy! if destroy_user?
+ sign_out(@user)
+
+ broadcast(:ok)
+ end
+
+ private
+
+ def valid_params?
+ @request.is_a?(ActionDispatch::Request) && @user.is_a?(Decidim::User)
+ end
+
+ def destroy_user?
+ return false if @user.verified_ephemeral_participant?
+ return false if verification_conflicts.any?
+
+ true
+ end
+
+ def verification_conflicts
+ Decidim::Verifications::Conflict.where(current_user: @user)
+ end
+
+ # Needed for Devise::Controllers::Helpers#sign_out
+ def session
+ @request.session
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/transfer_ephemeral_participant.rb b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/transfer_ephemeral_participant.rb
new file mode 100644
index 0000000000..66f27b8e77
--- /dev/null
+++ b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/transfer_ephemeral_participant.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class TransferEphemeralParticipant < Rectify::Command
+ def initialize(verified_user, unverifiable_user, form)
+ @verified_user = verified_user
+ @unverifiable_user = unverifiable_user
+ @form = form
+ end
+
+ def call
+ return broadcast(:invalid) unless valid_params?
+
+ update_verified_user
+ update_unverifiable_user
+
+ broadcast(:ok)
+ end
+
+ private
+
+ def valid_params?
+ @verified_user.verified_ephemeral_participant?
+ end
+
+
+ def update_verified_user
+ @verified_user.session_token = SecureRandom.hex
+ @verified_user.managed = false
+
+ @verified_user.email = @form.email
+
+ @verified_user.skip_reconfirmation!
+ @verified_user.save!
+ @verified_user.send_reset_password_instructions
+ end
+
+ def update_unverifiable_user
+ Decidim::DestroyAccount.call(
+ @unverifiable_user,
+ Decidim::DeleteAccountForm.from_params(reason: @form.reason),
+ )
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/transfer_user_override.rb b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/transfer_user_override.rb
new file mode 100644
index 0000000000..ab9080ac57
--- /dev/null
+++ b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/transfer_user_override.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module TransferUserOverride
+ extend ActiveSupport::Concern
+
+ included do
+ alias :update_regular_managed_user :update_managed_user
+
+ private
+
+ def update_managed_user
+ if managed_user.verified_ephemeral_participant?
+ Decidim::EphemeralParticipation::TransferEphemeralParticipant.call(managed_user, new_user, form)
+ else
+ update_regular_managed_user
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/update_ephemeral_participant.rb b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/update_ephemeral_participant.rb
new file mode 100644
index 0000000000..1544fa9487
--- /dev/null
+++ b/decidim-ephemeral_participation/app/commands/decidim/ephemeral_participation/update_ephemeral_participant.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class UpdateEphemeralParticipant < Rectify::Command
+ include ::Devise::Controllers::Helpers
+
+ def initialize(request, user, form)
+ @request = request
+ @user = user
+ @form = form
+ end
+
+ def call
+ return broadcast(:invalid) unless valid_params?
+
+ update_user
+ bypass_sign_in(@user)
+
+ broadcast(:ok)
+ end
+
+ private
+
+ def valid_params?
+ @request.is_a?(ActionDispatch::Request) && @user.is_a?(Decidim::User) && @form.valid?
+ end
+
+ def update_user
+ @user.managed = false
+ @user.accepted_tos_version = @user.organization.tos_version
+
+ @user.name = @form.name
+ @user.nickname = @form.nickname
+ @user.email = @form.email
+ @user.password = @form.password
+ @user.password_confirmation = @form.password_confirmation
+ @user.password_confirmation = @form.password_confirmation
+
+ @user.skip_reconfirmation!
+ @user.save!
+ @user.send(:after_confirmation)
+ end
+
+ # Needed for Devise::Controllers::Helpers#bypass_sign_in
+ def session
+ @request.session
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/controllers/concerns/decidim/ephemeral_participation/ephemeral_participable.rb b/decidim-ephemeral_participation/app/controllers/concerns/decidim/ephemeral_participation/ephemeral_participable.rb
new file mode 100644
index 0000000000..960fdd5c2c
--- /dev/null
+++ b/decidim-ephemeral_participation/app/controllers/concerns/decidim/ephemeral_participation/ephemeral_participable.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module EphemeralParticipable
+ extend ActiveSupport::Concern
+
+ included do
+
+ before_action :destroy_ephemeral_participant, if: :ephemeral_participant_session?
+ before_action :redirect_ephemeral_participant, if: :ephemeral_participant_session?
+ before_action :inform_ephemeral_participant, if: :ephemeral_participant_session?
+
+ private
+
+ def ephemeral_participant_session?
+ current_user && current_user.ephemeral_participant?
+ end
+
+ def destroy_ephemeral_participant
+ return end_unverifiable_ephemeral_participant_session if end_unverifiable_ephemeral_participant_session?
+ return end_expired_ephemeral_participant_session if end_expired_ephemeral_participant_session?
+ end
+
+ def end_unverifiable_ephemeral_participant_session
+ Decidim::EphemeralParticipation::DestroyEphemeralParticipant.call(request, current_user) do
+ on(:ok) do
+ flash[:alert] = I18n.t("unverifiable", scope: "decidim.ephemeral_participation.ephemeral_participants")
+
+ redirect_to(Decidim::Core::Engine.routes.url_helpers.root_path)
+ end
+ end
+ end
+
+ def end_unverifiable_ephemeral_participant_session?
+ current_user.unverifiable_ephemeral_participant?
+ end
+
+ def end_expired_ephemeral_participant_session
+ Decidim::EphemeralParticipation::DestroyEphemeralParticipant.call(request, current_user) do
+ on(:ok) do
+ flash[:notice] = I18n.t("destroy", scope: "decidim.ephemeral_participation.ephemeral_participants")
+
+ redirect_to(Decidim::Core::Engine.routes.url_helpers.root_path)
+ end
+ end
+ end
+
+ def end_expired_ephemeral_participant_session?
+ Decidim::EphemeralParticipation::SessionPresenter.new(current_user, helpers).ephemeral_participant_session_expired?
+ end
+
+ def redirect_ephemeral_participant
+ return redirect_to(ephemeral_participation_path) if redirect_to_ephemeral_participation_path?
+ return redirect_to(edit_ephemeral_participant_path(current_user)) if redirect_to_edit_ephemeral_participant_path?
+ end
+
+ def ephemeral_participation_path
+ current_user.ephemeral_participation_data["request_path"]
+ end
+
+ def redirect_to_ephemeral_participation_path?
+ Decidim::EphemeralParticipation::RedirectionRecognizer.new(request, current_user).redirect_to_ephemeral_participation_path?
+ end
+
+ def edit_ephemeral_participant_path(current_user)
+ Decidim::EphemeralParticipation::Engine.routes.url_helpers.edit_ephemeral_participant_path(current_user)
+ end
+
+ def redirect_to_edit_ephemeral_participant_path?
+ Decidim::EphemeralParticipation::RedirectionRecognizer.new(request, current_user).redirect_to_edit_ephemeral_participant_path?
+ end
+
+ def inform_ephemeral_participant
+ return (flash.now[:warning] = unverified_ephemeral_participant_message) if inform_unverified_ephemeral_participant?
+ return (flash.now[:warning] = verified_ephemeral_participant_message) if inform_verified_ephemeral_participant?
+ end
+
+ def unverified_ephemeral_participant_message
+ Decidim::EphemeralParticipation::FlashMessagesPresenter.new(current_user, helpers).unverified_ephemeral_participant_message
+ end
+
+ def inform_unverified_ephemeral_participant?
+ Decidim::EphemeralParticipation::InformingRecognizer.new(request, current_user).inform_unverified_ephemeral_participant?
+ end
+
+ def verified_ephemeral_participant_message
+ Decidim::EphemeralParticipation::FlashMessagesPresenter.new(current_user, helpers).verified_ephemeral_participant_message
+ end
+
+ def inform_verified_ephemeral_participant?
+ Decidim::EphemeralParticipation::InformingRecognizer.new(request, current_user).inform_verified_ephemeral_participant?
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/controllers/concerns/decidim/ephemeral_participation/needs_permission_override.rb b/decidim-ephemeral_participation/app/controllers/concerns/decidim/ephemeral_participation/needs_permission_override.rb
new file mode 100644
index 0000000000..1e9499abba
--- /dev/null
+++ b/decidim-ephemeral_participation/app/controllers/concerns/decidim/ephemeral_participation/needs_permission_override.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module NeedsPermissionOverride
+ extend ActiveSupport::Concern
+
+ included do
+ alias :old_user_has_no_permission :user_has_no_permission
+ alias :old_permissions_context :permissions_context
+
+ def user_has_no_permission
+ if request.xhr?
+ new_user_has_no_permission
+ else
+ old_user_has_no_permission
+ end
+ end
+
+ def permissions_context
+ old_permissions_context.merge(request: request)
+ end
+
+ private
+
+ def new_user_has_no_permission
+ render(js: unauthorized_error_flash_message_js)
+ end
+
+ def unauthorized_error_flash_message_js
+ <<~JAVASCRIPT
+ $alertBoxParsedHtml = $.parseHTML('#{unauthorized_error_flash_message_html}')[0].outerHTML;
+ alertBoxNotFound = $('#content').html().indexOf($alertBoxParsedHtml) == -1;
+
+ if (alertBoxNotFound) $('#content').prepend($alertBoxParsedHtml);
+
+ $(window).scrollTop(0);
+ JAVASCRIPT
+ end
+
+ def unauthorized_error_flash_message_html
+ flash.clear
+
+ flash.now[:alert] = unauthorized_message
+
+ helpers.display_flash_messages
+ end
+
+ def unauthorized_message
+ if (current_user && current_user.ephemeral_participant?)
+ unauthorized_ephemeral_participant_message
+ else
+ I18n.t("actions.unauthorized", scope: "decidim.core")
+ end
+ end
+
+ def unauthorized_ephemeral_participant_message
+ presenter = Decidim::EphemeralParticipation::FlashMessagesPresenter.new(current_user, helpers)
+
+ if current_user.verified_ephemeral_participant?
+ presenter.unverified_ephemeral_participant_message
+ else
+ presenter.unauthorized_ephemeral_participant_message
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/application_controller_override.rb b/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/application_controller_override.rb
new file mode 100644
index 0000000000..d70eb6addf
--- /dev/null
+++ b/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/application_controller_override.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module ApplicationControllerOverride
+ extend ActiveSupport::Concern
+
+ included do
+ include Decidim::EphemeralParticipation::EphemeralParticipable
+ include Decidim::EphemeralParticipation::NeedsPermissionOverride
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/conflicts_controller_override.rb b/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/conflicts_controller_override.rb
new file mode 100644
index 0000000000..4717e865ec
--- /dev/null
+++ b/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/conflicts_controller_override.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module ConflictsControllerOverride
+ extend ActiveSupport::Concern
+
+ included do
+ # TEMPORARY OVERRIDE TO RENDER FORM ON ERROR (BUG IN DECIDIM)
+ # https://github.com/decidim/decidim/blob/00bad01ccfa95473fd2d7b2f2cb1919623295ba3/decidim-admin/app/controllers/decidim/admin/conflicts_controller.rb#L40
+ def update
+ conflict = Decidim::Verifications::Conflict.find(params[:id])
+
+ @form = form(Decidim::Admin::TransferUserForm).from_params(
+ current_user: current_user,
+ conflict: conflict,
+ reason: params[:transfer_user][:reason],
+ email: params[:transfer_user][:email]
+ )
+
+ Decidim::Admin::TransferUser.call(@form) do
+ on(:ok) do
+ flash[:notice] = I18n.t("success", scope: "decidim.admin.conflicts.transfer")
+ redirect_to conflicts_path
+ end
+
+ on(:invalid) do
+ flash.now[:alert] = I18n.t("error", scope: "decidim.admin.conflicts.transfer")
+ render :edit
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/ephemeral_participants_controller.rb b/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/ephemeral_participants_controller.rb
new file mode 100644
index 0000000000..4300081950
--- /dev/null
+++ b/decidim-ephemeral_participation/app/controllers/decidim/ephemeral_participation/ephemeral_participants_controller.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class EphemeralParticipantsController < Decidim::ApplicationController
+ include FormFactory
+
+ def create
+ enforce_permission_to(:create, :ephemeral_participant)
+
+ Decidim::EphemeralParticipation::CreateEphemeralParticipant.call(request, current_user) do
+ on(:ok) do |authorization_path|
+ flash[:notice] = I18n.t("create", scope: "decidim.ephemeral_participation.ephemeral_participants")
+
+ redirect_to(authorization_path)
+ end
+
+ on(:invalid) do
+ render template: "decidim/errors/not_found", locals: { root_path: decidim_root_path }
+ end
+ end
+ end
+
+ def edit
+ enforce_permission_to(:update, :ephemeral_participant, current_user: current_user)
+
+ @form = form(EphemeralParticipantForm).from_model(current_user)
+
+ render(layout: "layouts/decidim/ephemeral_participation/user_profile")
+ end
+
+ def update
+ enforce_permission_to(:update, :ephemeral_participant, current_user: current_user)
+
+ @form = form(EphemeralParticipantForm).from_params(params)
+
+ Decidim::EphemeralParticipation::UpdateEphemeralParticipant.call(request, current_user, @form) do
+ on(:ok) do
+ flash[:notice] = I18n.t("update.success", scope: "decidim.ephemeral_participation.ephemeral_participants")
+
+ redirect_to(decidim.account_path)
+ end
+
+ on(:invalid) do
+ flash[:alert] = I18n.t("update.error", scope: "decidim.ephemeral_participation.ephemeral_participants")
+
+ render(action: :edit)
+ end
+ end
+ end
+
+ def destroy
+ enforce_permission_to(:destroy, :ephemeral_participant, current_user: current_user)
+
+ Decidim::EphemeralParticipation::DestroyEphemeralParticipant.call(request, current_user) do
+ on(:ok) do
+ flash[:notice] = I18n.t("destroy", scope: "decidim.ephemeral_participation.ephemeral_participants")
+
+ redirect_to(decidim_root_path)
+ end
+ end
+ end
+
+ private
+
+ def decidim_root_path
+ Decidim::Core::Engine.routes.url_helpers.root_path
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/component_form_override.rb b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/component_form_override.rb
new file mode 100644
index 0000000000..bd44357c6a
--- /dev/null
+++ b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/component_form_override.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module ComponentFormOverride
+ extend ActiveSupport::Concern
+
+ included do
+ validate :validate_ephemeral_participation_enabled
+
+ private
+
+ def validate_ephemeral_participation_enabled
+ return unless settings.try(:ephemeral_participation_enabled) == true
+ return if participatory_space.organization.ephemeral_participation_authorization
+
+ settings.errors.add(:ephemeral_participation_enabled, :missing_ephemeral_participation_authorization)
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/ephemeral_participant_form.rb b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/ephemeral_participant_form.rb
new file mode 100644
index 0000000000..72eecf0469
--- /dev/null
+++ b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/ephemeral_participant_form.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class EphemeralParticipantForm < Decidim::Form
+ mimic :ephemeral_participant
+
+ attribute :name
+ attribute :nickname
+ attribute :email
+ attribute :password
+ attribute :password_confirmation
+
+ validates :name, presence: true
+ validates :email, presence: true, 'valid_email_2/email': { disposable: true }
+ validates :nickname, presence: true, format: Decidim::User::REGEXP_NICKNAME
+ validates :nickname, length: { maximum: Decidim::User.nickname_max_length, allow_blank: true }
+ validates :password, presence: true, confirmation: true
+ validates :password, password: { name: :name, email: :email, username: :nickname }
+ validates :password_confirmation, presence: true
+
+ validate :unique_email
+ validate :unique_nickname
+
+ alias organization current_organization
+
+ private
+
+ def unique_email
+ return if duplicates(email: email).none?
+
+ errors.add(:email, :taken)
+ end
+
+ def unique_nickname
+ return if duplicates(nickname: nickname).none?
+
+ errors.add(:nickname, :taken)
+ end
+
+ def duplicates(where_clause)
+ Decidim::EphemeralParticipation::DuplicatedUsers.new(
+ organization: current_organization,
+ excluding: current_user,
+ where_clause: where_clause,
+ ).query
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/permissions_form_override.rb b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/permissions_form_override.rb
new file mode 100644
index 0000000000..1d813359b0
--- /dev/null
+++ b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/permissions_form_override.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module PermissionsFormOverride
+ extend ActiveSupport::Concern
+
+ included do
+ attribute :component_id, String
+ attribute :resource_id, String
+
+ validate :validate_ephemeral_participation_permissions_settings
+
+ private
+
+ def validate_ephemeral_participation_permissions_settings
+ return if resource_permissions?
+ return unless ephemeral_participation_enabled?
+
+ permissions.values.each do |permission_form|
+ next if valid_permission_form?(permission_form)
+
+ permission_form.errors.add(:base, :invalid_ephemeral_participation_permissions, i18n_options)
+ end
+ end
+
+ def resource_permissions?
+ resource_id.present?
+ end
+
+ def ephemeral_participation_enabled?
+ component.ephemeral_participation_enabled?
+ end
+
+ def valid_permission_form?(permission_form)
+ handler_names = permission_form.authorization_handlers.keys & component.organization.available_authorizations
+
+ handler_names.exclude?(component.organization.ephemeral_participation_authorization) || handler_names.size == 1
+ end
+
+ def component
+ @component ||= Decidim::Component.find(component_id)
+ end
+
+ def i18n_options
+ {
+ ephemeral_participation_authorization: I18n.t("decidim.authorization_handlers.#{component.organization.ephemeral_participation_authorization}.name"),
+ ephemeral_participation_enabled: I18n.t("decidim.components.#{component.manifest_name}.settings.global.ephemeral_participation_enabled"),
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/transfer_user_form_override.rb b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/transfer_user_form_override.rb
new file mode 100644
index 0000000000..e3c36c236c
--- /dev/null
+++ b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/transfer_user_form_override.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module TransferUserFormOverride
+ extend ActiveSupport::Concern
+
+ included do
+ validate :unique_email
+
+ private
+
+ def unique_email
+ return if duplicates(email: email).none?
+
+ errors.add(:email, :taken)
+ end
+
+ def duplicates(where_clause)
+ Decidim::EphemeralParticipation::DuplicatedUsers.new(
+ organization: current_user.organization,
+ where_clause: where_clause,
+ ).query
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/update_organization_form_override.rb b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/update_organization_form_override.rb
new file mode 100644
index 0000000000..d836c03281
--- /dev/null
+++ b/decidim-ephemeral_participation/app/forms/decidim/ephemeral_participation/update_organization_form_override.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module UpdateOrganizationFormOverride
+ extend ActiveSupport::Concern
+
+ included do
+ alias :old_map_model :map_model
+
+ attribute :available_authorizations, Object
+
+ validate :validate_available_authorizations
+
+ def map_model(model)
+ old_map_model(model)
+ new_map_model(model)
+ end
+
+ def new_map_model(model)
+ self.available_authorizations = model.read_attribute(:available_authorizations)
+ self.available_authorizations = self.available_authorizations.map { |a| [a, {}] }.to_h if self.available_authorizations.is_a?(Array)
+ end
+
+ def clean_available_authorizations
+ available_authorizations
+ end
+
+ def before_validation
+ available_authorizations.transform_values! { |string| JSON.parse(string).presence }.compact!
+ end
+
+ private
+
+ def validate_available_authorizations
+ return unless available_authorizations.values.count { |hash| hash["allow_ephemeral_participation"] == true } > 1
+
+ errors.add(:available_authorizations, :invalid)
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/component_override.rb b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/component_override.rb
new file mode 100644
index 0000000000..1321c5cc50
--- /dev/null
+++ b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/component_override.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module ComponentOverride
+ extend ActiveSupport::Concern
+
+ included do
+ def ephemeral_participation_enabled?
+ settings.try(:ephemeral_participation_enabled) == true
+ end
+
+ # Given organization.ephemeral_participation_authorization == "valid_auth"
+ # permissions => {"vote"=>{"authorization_handlers"=>{"valid_auth"=>{}}}}
+ # ephemeral_participation_permissions => ["vote"]
+ def ephemeral_participation_permissions
+ @ephemeral_participation_permissions ||= begin
+ return [] unless ephemeral_participation_enabled?
+ return [] unless permissions.present?
+
+ permissions.map do |action, authorization_handlers|
+ handler_names = authorization_handlers.values.flat_map(&:keys)
+
+ action if handler_names.include?(organization.ephemeral_participation_authorization)
+ end.compact
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/organization_override.rb b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/organization_override.rb
new file mode 100644
index 0000000000..520ef4fbf5
--- /dev/null
+++ b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/organization_override.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module OrganizationOverride
+ extend ActiveSupport::Concern
+
+ included do
+ # altough this might introduce some confusion it maintains compatibility accross the application
+ # for any code expecting to obtain an array
+ def available_authorizations
+ authorizations = read_attribute(:available_authorizations)
+ authorizations.is_a?(Array) ? authorizations : authorizations.keys
+ end
+
+ def ephemeral_participation_authorization
+ read_attribute(:available_authorizations).key("allow_ephemeral_participation" => true)
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/permission_action_override.rb b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/permission_action_override.rb
new file mode 100644
index 0000000000..c4fba8aead
--- /dev/null
+++ b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/permission_action_override.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module PermissionActionOverride
+ extend ActiveSupport::Concern
+
+ included do
+ def disallowed?
+ @state == :disallowed
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/user_override.rb b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/user_override.rb
new file mode 100644
index 0000000000..12280e4f93
--- /dev/null
+++ b/decidim-ephemeral_participation/app/models/decidim/ephemeral_participation/user_override.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module UserOverride
+ extend ActiveSupport::Concern
+
+ included do
+ scope :ephemeral_participant, -> { managed.where("extended_data ? 'ephemeral_participation'") }
+
+ def ephemeral_participant?
+ managed? && ephemeral_participation_data.present?
+ end
+
+ def ephemeral_participation_data
+ extended_data.fetch("ephemeral_participation", {})
+ end
+
+ def verified_ephemeral_participant?
+ return false unless ephemeral_participant?
+
+ Decidim::Authorization.exists?(
+ user: self,
+ name: ephemeral_participation_data["authorization_name"]
+ )
+ end
+
+ def verifiable_ephemeral_participant?
+ return false unless ephemeral_participant?
+
+ Decidim::EphemeralParticipation::VerificationConflicts.for(self).none?
+ end
+
+ def unverifiable_ephemeral_participant?
+ return false unless ephemeral_participant?
+
+ Decidim::EphemeralParticipation::VerificationConflicts.for(self).any?
+ end
+
+ def ephemeral_participation_verification_adapter
+ return nil unless ephemeral_participant?
+
+ Decidim::Verifications::Adapter.from_element(ephemeral_participation_data["authorization_name"])
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/ephemeral_action_permissions_dictionary.rb b/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/ephemeral_action_permissions_dictionary.rb
new file mode 100644
index 0000000000..4e327300a6
--- /dev/null
+++ b/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/ephemeral_action_permissions_dictionary.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class EphemeralActionPermissionsDictionary
+ COMPONENT_PERMISSIONS_DICTIONARY = {
+ "budgets" => {
+ "vote" => [
+ { action: :vote, scope: :public, subject: :project },
+ { action: :create, scope: :public, subject: :order },
+ ]
+ }
+ }
+
+ def self.for(component)
+ new(component).fetch
+ end
+
+ def initialize(component)
+ @component = component
+ end
+
+ def fetch
+ return {} unless @component && @component.ephemeral_participation_permissions.any?
+
+ COMPONENT_PERMISSIONS_DICTIONARY.fetch(@component.manifest_name).select do |action, _|
+ @component.ephemeral_participation_permissions.include?(action)
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/ephemeral_participation_permissions.rb b/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/ephemeral_participation_permissions.rb
new file mode 100644
index 0000000000..3175cc8cfc
--- /dev/null
+++ b/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/ephemeral_participation_permissions.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require_relative "ephemeral_action_permissions_dictionary"
+
+module Decidim
+ module EphemeralParticipation
+ class EphemeralParticipationPermissions < DefaultPermissions
+ def permissions
+ return permission_action if regular_user?
+ return permission_action if permission_action.disallowed?
+
+ if create_ephemeral_participant?
+ allow! if allowed_to_create_ephemeral_participant?
+ elsif update_ephemeral_participant?
+ allow! if allowed_to_update_ephemeral_participant?
+ elsif destroy_ephemeral_participant?
+ allow! if allowed_to_destroy_ephemeral_participant?
+ elsif update_profile?
+ disallow! unless allowed_to_update_profile?
+ else
+ disallow! unless allowed_ephemeral_participation?
+ end
+
+ permission_action
+ end
+
+ private
+
+ def regular_user?
+ user && (not user.ephemeral_participant?)
+ end
+
+ def create_ephemeral_participant?
+ permission_action.action == :create &&
+ permission_action.scope == :public &&
+ permission_action.subject == :ephemeral_participant
+ end
+
+ def allowed_to_create_ephemeral_participant?
+ return true if user.nil?
+ return true if (not user.verified_ephemeral_participant?)
+
+ false
+ end
+
+ def destroy_ephemeral_participant?
+ permission_action.action == :destroy &&
+ permission_action.scope == :public &&
+ permission_action.subject == :ephemeral_participant
+ end
+
+ def allowed_to_destroy_ephemeral_participant?
+ user && user == context[:current_user]
+ end
+
+ def update_ephemeral_participant?
+ permission_action.action == :update &&
+ permission_action.scope == :public &&
+ permission_action.subject == :ephemeral_participant
+ end
+
+ def allowed_to_update_ephemeral_participant?
+ user && user == context[:current_user] && user.verifiable_ephemeral_participant?
+ end
+
+ def update_profile?
+ permission_action.action == :update_profile &&
+ permission_action.scope == :public &&
+ permission_action.subject == :user
+ end
+
+ def allowed_to_update_profile?
+ verify_ephemeral_participant_path? && (not user.verified_ephemeral_participant?)
+ end
+
+ def verify_ephemeral_participant_path?
+ Decidim::EphemeralParticipation::InformingRecognizer.new(context[:request], user).verify_ephemeral_participant_path?
+ end
+
+ def decidim_verifiations
+ Decidim::Verifications::Engine.routes.url_helpers
+ end
+
+ def allowed_ephemeral_participation?
+ return true if browsing_public_pages?
+ return true if changing_locales?
+ return true if user && user.verified_ephemeral_participant? && ephemeral_participation_permission_action?
+
+ false
+ end
+
+ def browsing_public_pages?
+ permission_action.scope == :public && [:read, :list].include?(permission_action.action)
+ end
+
+ def changing_locales?
+ permission_action.action == :create &&
+ permission_action.scope == :public &&
+ permission_action.subject == :locales
+ end
+
+ def ephemeral_participation_permission_action?
+ Decidim::EphemeralParticipation::EphemeralActionPermissionsDictionary.for(component)
+ .any? do |_, permission_action_attributes|
+ permission_action_attributes.any? do |action:, scope:, subject:|
+ permission_action.matches?(scope, action, subject)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/permissions_override.rb b/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/permissions_override.rb
new file mode 100644
index 0000000000..b0d7377c9c
--- /dev/null
+++ b/decidim-ephemeral_participation/app/permissions/decidim/ephemeral_participation/permissions_override.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module PermissionsOverride
+ extend ActiveSupport::Concern
+
+ included do
+ alias :old_permissions :permissions
+
+ def permissions
+ old_permissions
+ new_permissions
+ end
+
+ private
+
+ def new_permissions
+ Decidim::EphemeralParticipation::EphemeralParticipationPermissions.new(user, permission_action, context).permissions
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/presenter/decidim/ephemeral_participation/flash_messages_presenter.rb b/decidim-ephemeral_participation/app/presenter/decidim/ephemeral_participation/flash_messages_presenter.rb
new file mode 100644
index 0000000000..428b7ebdfa
--- /dev/null
+++ b/decidim-ephemeral_participation/app/presenter/decidim/ephemeral_participation/flash_messages_presenter.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class FlashMessagesPresenter
+ def initialize(user, view_helpers)
+ @user = user
+ @view_helpers = view_helpers
+ end
+
+ def unauthorized_ephemeral_participant_message
+ I18n.t(
+ "decidim.ephemeral_participation.actions.unauthorized",
+ link: (
+ @view_helpers.link_to(
+ I18n.t("decidim.ephemeral_participation.actions.unauthorized_link"),
+ decidim_ephemeral_participation.edit_ephemeral_participant_path(@user),
+ )
+ )
+ ).html_safe
+ end
+
+ def verified_ephemeral_participant_message
+ I18n.t(
+ "decidim.ephemeral_participation.actions.verified",
+ link: @view_helpers.link_to(
+ I18n.t("decidim.ephemeral_participation.actions.verified_link"),
+ decidim_ephemeral_participation.edit_ephemeral_participant_path(@user),
+ )
+ ).html_safe
+ end
+
+ def edit_ephemeral_participant_path
+ Decidim::EphemeralParticipation::Engine.routes.url_helpers.edit_ephemeral_participant_path(@user)
+ end
+
+ def unverified_ephemeral_participant_message
+ I18n.t(
+ "decidim.ephemeral_participation.actions.unverified",
+ link: @view_helpers.link_to(
+ I18n.t("decidim.ephemeral_participation.actions.unverified_link"),
+ verify_ephemeral_participant_path,
+ )
+ ).html_safe
+ end
+
+ def verify_ephemeral_participant_path
+ @user
+ .ephemeral_participation_verification_adapter
+ .root_path(redirect_url: @user.ephemeral_participation_data["request_path"])
+ end
+
+ def decidim_ephemeral_participation
+ Decidim::EphemeralParticipation::Engine.routes.url_helpers
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/presenter/decidim/ephemeral_participation/session_presenter.rb b/decidim-ephemeral_participation/app/presenter/decidim/ephemeral_participation/session_presenter.rb
new file mode 100644
index 0000000000..d817907e48
--- /dev/null
+++ b/decidim-ephemeral_participation/app/presenter/decidim/ephemeral_participation/session_presenter.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class SessionPresenter
+ EPHEMERAL_PARTICIPANT_SESSION_DURATION = 30.minutes
+
+ def initialize(user, view_helpers)
+ @user = user
+ @view_helpers = view_helpers
+ end
+
+ def ephemeral_participant_session_remaining_time_in_minutes
+ (ephemeral_participant_session_remaining_time / 1.minute).round
+ end
+
+ def ephemeral_participant_session_expired?
+ ephemeral_participant_session_remaining_time.negative?
+ end
+
+ private
+
+ def ephemeral_participant_session_remaining_time
+ (@user.created_at + EPHEMERAL_PARTICIPANT_SESSION_DURATION) - Time.current
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/queries/decidim/ephemeral_participation/duplicated_users.rb b/decidim-ephemeral_participation/app/queries/decidim/ephemeral_participation/duplicated_users.rb
new file mode 100644
index 0000000000..64dc2f757f
--- /dev/null
+++ b/decidim-ephemeral_participation/app/queries/decidim/ephemeral_participation/duplicated_users.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class DuplicatedUsers < Rectify::Query
+ def initialize(organization:, excluding: nil, where_clause:)
+ @organization = organization
+ @excluding = Array.wrap(excluding).compact.map(&:id)
+ @where_clause = where_clause
+ end
+
+ def query
+ Decidim::User
+ .where(organization: @organization)
+ .where.not(id: @excluding)
+ .where(**@where_clause)
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/queries/decidim/ephemeral_participation/verification_conflicts.rb b/decidim-ephemeral_participation/app/queries/decidim/ephemeral_participation/verification_conflicts.rb
new file mode 100644
index 0000000000..fc6645564a
--- /dev/null
+++ b/decidim-ephemeral_participation/app/queries/decidim/ephemeral_participation/verification_conflicts.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class VerificationConflicts < Rectify::Query
+ def self.for(user)
+ new(user).query
+ end
+
+ def initialize(user)
+ @user = user
+ end
+
+ def query
+ Decidim::Verifications::Conflict.where(current_user: @user)
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/services/decidim/ephemeral_participation/informing_recognizer.rb b/decidim-ephemeral_participation/app/services/decidim/ephemeral_participation/informing_recognizer.rb
new file mode 100644
index 0000000000..3d896da887
--- /dev/null
+++ b/decidim-ephemeral_participation/app/services/decidim/ephemeral_participation/informing_recognizer.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class InformingRecognizer
+ def initialize(request, user)
+ @request = request
+ @user = user
+ end
+
+ def inform_unverified_ephemeral_participant?
+ informable_ephemeral_participant? && (not @user.verified_ephemeral_participant?)
+ end
+
+
+ def inform_verified_ephemeral_participant?
+ informable_ephemeral_participant? && @user.verified_ephemeral_participant?
+ end
+
+ def informable_ephemeral_participant?
+ return false if verify_ephemeral_participant_path?
+ return false if edit_ephemeral_participant_path?
+ return false if @request.flash.any?
+
+ true
+ end
+
+ def verify_ephemeral_participant_path?
+ adapter = @user.ephemeral_participation_verification_adapter
+ engine = (adapter.type == "direct") ? Decidim::Verifications::Engine : adapter.engine
+
+ engine.routes.recognize_path_with_request(@request, @request.path, method: @request.method)
+ rescue ActionController::RoutingError
+ false
+ end
+
+ private
+
+ def edit_ephemeral_participant_path?
+ @request.path == edit_ephemeral_participant_path
+ end
+
+ def edit_ephemeral_participant_path
+ Decidim::EphemeralParticipation::FlashMessagesPresenter
+ .new(@user, nil)
+ .edit_ephemeral_participant_path
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/services/decidim/ephemeral_participation/redirection_recognizer.rb b/decidim-ephemeral_participation/app/services/decidim/ephemeral_participation/redirection_recognizer.rb
new file mode 100644
index 0000000000..dd5fdfa7ed
--- /dev/null
+++ b/decidim-ephemeral_participation/app/services/decidim/ephemeral_participation/redirection_recognizer.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class RedirectionRecognizer
+ def initialize(request, user)
+ @request = request
+ @user = user
+ end
+
+ # Handles verification workflows redirecting to authorizations#index after creating authorization.
+ def redirect_to_ephemeral_participation_path?
+ @user.verified_ephemeral_participant? &&
+ @request.method == "GET" &&
+ @request.path == decidim_verifications.authorizations_path
+ end
+
+ def redirect_to_edit_ephemeral_participant_path?
+ return true if path?(decidim.account_path)
+ return true if path?(decidim.notifications_settings_path)
+ return true if path?(decidim.data_portability_path)
+ return true if path?(decidim.own_user_groups_path)
+ return true if path?(decidim.user_interests_path)
+ return true if path?(decidim.profile_path(@user.nickname))
+ return true if path?(decidim.notifications_path)
+ return true if path?(decidim.conversations_path)
+
+ false
+ end
+
+ private
+
+ def path?(path)
+ @request.path.include?(path)
+ end
+
+ def decidim_verifications
+ Decidim::Verifications::Engine.routes.url_helpers
+ end
+
+ def decidim
+ Decidim::Core::Engine.routes.url_helpers
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/app/views/decidim/budgets/projects/_project_budget_button.html.erb b/decidim-ephemeral_participation/app/views/decidim/budgets/projects/_project_budget_button.html.erb
new file mode 100644
index 0000000000..10fef00ebb
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/budgets/projects/_project_budget_button.html.erb
@@ -0,0 +1,46 @@
+
+ <% if voted_for?(project) %>
+ <%= action_authorized_button_to(
+ "vote",
+ t(".added"),
+ budget_order_line_item_path(budget, project_id: project),
+ method: :delete,
+ remote: true,
+ data: {
+ disable: true,
+ budget: project.budget_amount,
+ "redirect-url": budget_project_path(budget, project)
+ },
+ disabled: !can_have_order? || current_order_checked_out?,
+ class: "button expanded button--sc success",
+ "aria-label": t(".added_descriptive", resource_name: translated_attribute(project.title))
+ ) %>
+ <% elsif current_user.present? && (!current_user.ephemeral_participant? || current_user.verified_ephemeral_participant?) %>
+ <%= action_authorized_button_to(
+ "vote",
+ t(".add"),
+ budget_order_line_item_path(budget, project_id: project),
+ method: :post,
+ remote: true,
+ data: {
+ disable: true,
+ budget: project.budget_amount,
+ add: true,
+ "redirect-url": budget_project_path(budget, project)
+ },
+ disabled: !can_have_order? || current_order_checked_out?,
+ class: "button expanded button--sc",
+ "aria-label": t(".add_descriptive", resource_name: translated_attribute(project.title))
+ ) %>
+ <% elsif current_user.present? && (current_user.ephemeral_participant? && !current_user.verified_ephemeral_participant?) %>
+ <%= link_to t(".add"),
+ Decidim::EphemeralParticipation::FlashMessagesPresenter.new(current_user, self).verify_ephemeral_participant_path,
+ class: "button expanded button--sc",
+ "aria-label": t(".add_descriptive", resource_name: translated_attribute(project.title))
+ %>
+ <% else %>
+
+ <%= t(".add") %>
+
+ <% end %>
+
diff --git a/decidim-ephemeral_participation/app/views/decidim/ephemeral_participation/ephemeral_participants/edit.html.erb b/decidim-ephemeral_participation/app/views/decidim/ephemeral_participation/ephemeral_participants/edit.html.erb
new file mode 100644
index 0000000000..b97f17a37c
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/ephemeral_participation/ephemeral_participants/edit.html.erb
@@ -0,0 +1,16 @@
+
+
+ <%= decidim_form_for(@form, url: ephemeral_participant_path, method: :put, html: { autocomplete: "off" }) do |form| %>
+
+
+ <%= form.text_field :name %>
+ <%= form.text_field :nickname %>
+ <%= form.email_field :email %>
+
+ <%= form.password_field :password, value: form.object.password, autocomplete: "off", help_text: I18n.t("devise.passwords.edit.password_help", minimun_characters: NOBSPW.configuration.min_password_length) %>
+ <%= form.password_field :password_confirmation, value: form.object.password_confirmation, autocomplete: "off" %>
+
+ <%= form.submit I18n.t("submit", scope: "decidim.ephemeral_participation.ephemeral_participants") %>
+
+ <% end %>
+
diff --git a/decidim-ephemeral_participation/app/views/decidim/ephemeral_participation/shared/_login_modal.erb b/decidim-ephemeral_participation/app/views/decidim/ephemeral_participation/shared/_login_modal.erb
new file mode 100644
index 0000000000..03b4c712c0
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/ephemeral_participation/shared/_login_modal.erb
@@ -0,0 +1,30 @@
+<%
+ if(
+ controller.respond_to?(:current_component) &&
+ current_component.ephemeral_participation_enabled? &&
+ current_component.ephemeral_participation_permissions.any?
+ )
+%>
+
+
+
+ <%= I18n.t("or", scope: "decidim.devise.shared.omniauth_buttons") %>
+
+
+
+ <%=
+ button_to(
+ I18n.t("button", scope: "decidim.ephemeral_participation.login_modal"),
+ decidim_ephemeral_participation.ephemeral_participants_path(
+ component_id: current_component.id,
+ ephemeral_participation_path: request.path,
+ ),
+ class: "button expanded"
+ )
+ %>
+ <%= I18n.t("help", scope: "decidim.ephemeral_participation.login_modal") %>
+
+
+
+
+<% end %>
diff --git a/decidim-ephemeral_participation/app/views/decidim/shared/_login_modal.html.erb b/decidim-ephemeral_participation/app/views/decidim/shared/_login_modal.html.erb
new file mode 100644
index 0000000000..4bb53666da
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/shared/_login_modal.html.erb
@@ -0,0 +1,63 @@
+
+
+ <% if current_organization.sign_in_enabled? %>
+
+
+ <%
+ path = if content_for(:redirect_after_login)
+ session_path(:user, redirect_url: content_for(:redirect_after_login))
+ else
+ session_path(:user)
+ end
+ %>
+ <%= decidim_form_for(Decidim::User.new, namespace: "login", as: :user, url: path, html: { class: "register-form new_user" }) do |f| %>
+
+
+ <%= f.email_field :email %>
+
+
+ <%= f.password_field :password, autocomplete: "off" %>
+
+
+
+ <%= f.submit I18n.t("devise.sessions.new.sign_in"), class: "button expanded" %>
+
+ <% end %>
+ <% if current_organization.sign_up_enabled? %>
+
+ <%= link_to I18n.t("sign_up", scope: "decidim.shared.login_modal"), decidim.new_user_registration_path, class: "sign-up-link" %>
+
+ <% end %>
+
+ <%= link_to I18n.t("devise.shared.links.forgot_your_password"), new_password_path(:user) %>
+
+
+
+ <% cache current_organization do %>
+ <%= render "decidim/devise/shared/omniauth_buttons_mini" %>
+ <% end %>
+ <% cache current_organization do %>
+ <%= render "decidim/ephemeral_participation/shared/login_modal" %>
+ <% end %>
+ <% else %>
+
+
+
+ <%= I18n.t("sign_in_disabled", scope: "decidim.devise.sessions.new") %>
+
+
+
+ <% cache current_organization do %>
+ <%= render "decidim/devise/shared/omniauth_buttons" %>
+ <% end %>
+ <% cache current_organization do %>
+ <%= render "decidim/ephemeral_participation/shared/login_modal" %>
+ <% end %>
+ <% end %>
+
diff --git a/decidim-ephemeral_participation/app/views/decidim/system/organizations/_authorizations_settings.erb b/decidim-ephemeral_participation/app/views/decidim/system/organizations/_authorizations_settings.erb
new file mode 100644
index 0000000000..dd61d4ad5f
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/system/organizations/_authorizations_settings.erb
@@ -0,0 +1,61 @@
+
+
+
+
+ <%= f.label :available_authorizations %>
+ <%= f.label :enabled %>
+ <%= f.label :allows_ephemeral_participation %>
+
+
+
+ <%= f.fields_for :available_authorizations, f.object.available_authorizations do |ff| %>
+ <%= f.error_for(:available_authorizations) %>
+ <% Decidim.authorization_workflows.each do |authorization_workflow| %>
+
+
+ <%= ff.label authorization_workflow.description %>
+
+
+ <%= ff.check_box(
+ authorization_workflow.name, # attribute
+ {
+ label: false,
+ id: "organization_available_authorizations_#{authorization_workflow.name}_enabled",
+ checked: f.object.available_authorizations&.key?(authorization_workflow.name)
+ }, # options
+ { allow_ephemeral_participation: false }.to_json, # checked_value
+ {}.to_json # unchecked_value
+ ) %>
+
+
+ <%= ff.radio_button(
+ authorization_workflow.name, # attribute
+ { allow_ephemeral_participation: true }.to_json, # value
+ {
+ label: false,
+ id: "organization_available_authorizations_#{authorization_workflow.name}_allow_ephemeral_participation",
+ checked: f.object.available_authorizations&.dig(authorization_workflow.name, "allow_ephemeral_participation") == true,
+ disabled: !authorization_workflow.ephemerable,
+ class: ("hide" if !authorization_workflow.ephemerable)
+ } # options
+ ) %>
+
+
+ <% end %>
+ <% end %>
+
+
+
+
+
diff --git a/decidim-ephemeral_participation/app/views/decidim/system/organizations/edit.html.erb b/decidim-ephemeral_participation/app/views/decidim/system/organizations/edit.html.erb
new file mode 100644
index 0000000000..0063d25239
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/system/organizations/edit.html.erb
@@ -0,0 +1,36 @@
+<%= decidim_form_for(@form) do |f| %>
+
+ <%= f.text_field :name, autofocus: true %>
+
+
+
+ <%= f.text_field :host %>
+
+
+
+ <%= f.text_area :secondary_hosts %>
+
<%= I18n.t("decidim.system.organizations.edit.secondary_hosts_hint") %>
+
+
+
+ <%= f.label :force_authentication %>
+ <%= f.check_box :force_users_to_authenticate_before_access_organization %>
+
+
+
+ <%= f.label :users_registration_mode %>
+ <%= f.collection_radio_buttons :users_registration_mode,
+ Decidim::Organization.users_registration_modes,
+ :first,
+ ->(mode) { I18n.t("decidim.system.organizations.users_registration_mode.#{mode.first}") } %>
+
+
+ <%= render partial: "authorizations_settings", locals: { f: f } %>
+ <%= render partial: "smtp_settings", locals: { f: f } %>
+ <%= render partial: "omniauth_settings", locals: { f: f } %>
+ <%= render partial: "file_upload_settings", locals: { f: f } %>
+
+
+ <%= f.submit I18n.t("decidim.system.actions.save") %>
+
+<% end %>
diff --git a/decidim-ephemeral_participation/app/views/decidim/system/organizations/new.html.erb b/decidim-ephemeral_participation/app/views/decidim/system/organizations/new.html.erb
new file mode 100644
index 0000000000..dbc2ca2dde
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/decidim/system/organizations/new.html.erb
@@ -0,0 +1,77 @@
+<% provide :title do %>
+ <%= t ".title" %>
+<% end %>
+
+<%= decidim_form_for(@form) do |f| %>
+
+ <%= f.text_field :name, autofocus: true %>
+
+
+
+ <%= f.text_field :reference_prefix %>
+
<%= I18n.t("decidim.system.organizations.new.reference_prefix_hint") %>
+
+
+
+ <%= f.text_field :host %>
+
+
+
+ <%= f.text_area :secondary_hosts %>
+
<%= I18n.t("decidim.system.organizations.new.secondary_hosts_hint") %>
+
+
+
+ <%= f.text_field :organization_admin_name %>
+
+
+
+ <%= f.email_field :organization_admin_email %>
+
+
+ <%= f.fields_for :locales do |fields| %>
+
+ <%= f.label :organization_locales, "", class: @form.respond_to?(:errors) && @form.errors[:default_locale].present? ? "is-invalid-label" : "" %>
+
+
+
+ Locale
+ Enabled <%= f.error_for(:available_locales) %>
+ Default? <%= f.error_for(:default_locale) %>
+
+
+
+ <% localized_locales.each do |locale| %>
+
+ <%= locale.name %>
+ <%= check_box_tag "organization[available_locales][#{locale.id}]", locale.id, @form.available_locales.include?(locale.id) %>
+ <%= radio_button_tag "organization[default_locale]", locale.id, @form.default_locale == locale.id %>
+
+ <% end %>
+
+
+
+ <% end %>
+
+
+ <%= f.label :force_authentication %>
+ <%= f.check_box :force_users_to_authenticate_before_access_organization %>
+
+
+
+ <%= f.label :users_registration_mode %>
+ <%= f.collection_radio_buttons :users_registration_mode,
+ Decidim::Organization.users_registration_modes,
+ :first,
+ ->(mode) { I18n.t("decidim.system.organizations.users_registration_mode.#{mode.first}") } %>
+
+
+ <%= render partial: "authorizations_settings", locals: { f: f } %>
+ <%= render partial: "smtp_settings", locals: { f: f } %>
+ <%= render partial: "omniauth_settings", locals: { f: f } %>
+ <%= render partial: "file_upload_settings", locals: { f: f } %>
+
+
+ <%= f.submit I18n.t("decidim.system.models.organization.actions.save_and_invite") %>
+
+<% end %>
diff --git a/decidim-ephemeral_participation/app/views/layouts/decidim/_user_menu.html.erb b/decidim-ephemeral_participation/app/views/layouts/decidim/_user_menu.html.erb
new file mode 100644
index 0000000000..c3e645e2a2
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/layouts/decidim/_user_menu.html.erb
@@ -0,0 +1,24 @@
+<% if current_user.ephemeral_participant? %>
+ <%= render partial: "layouts/decidim/ephemeral_participation/user_menu" %>
+<% else %>
+
+ <%# The code BELOW raises: Cannot use t(".profile") shortcut because path is not available %>
+ <%#
+ decidim_gem_dir = Gem::Specification.find_by_name("decidim").gem_dir
+ view_path = "decidim-core/app/views/layouts/decidim/_user_menu.html.erb"
+ decidim_core_user_menu_partial = "#{decidim_gem_dir}/#{view_path}"
+ %>
+ <%#= render file: decidim_core_user_menu_partial %>
+ <%# The code ABOVE raises: Cannot use t(".profile") shortcut because path is not available %>
+
+ <%= link_to I18n.t("profile", scope: "layouts.decidim.user_menu"), decidim.account_path, tabindex: "-1" %>
+ <% if current_user.nickname.present? && !current_user.managed? %>
+ <%= link_to I18n.t("public_profile", scope: "layouts.decidim.user_menu"), decidim.profile_path(current_user.nickname), tabindex: "-1" %>
+ <% end %>
+ <%= link_to I18n.t("notifications", scope: "layouts.decidim.user_menu"), decidim.notifications_path, tabindex: "-1" %>
+ <%= link_to I18n.t("conversations", scope: "layouts.decidim.user_menu"), decidim.conversations_path, tabindex: "-1" %>
+ <% if allowed_to? :read, :admin_dashboard %>
+ <%= link_to I18n.t("admin_dashboard", scope: "layouts.decidim.user_menu"), decidim_admin.root_path, tabindex: "-1" %>
+ <% end %>
+ <%= link_to I18n.t("sign_out", scope: "layouts.decidim.user_menu"), decidim.destroy_user_session_path, method: :delete, class: "sign-out-link", tabindex: "-1" %>
+<% end %>
diff --git a/decidim-ephemeral_participation/app/views/layouts/decidim/ephemeral_participation/_user_menu.html.erb b/decidim-ephemeral_participation/app/views/layouts/decidim/ephemeral_participation/_user_menu.html.erb
new file mode 100644
index 0000000000..43c7fe2852
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/layouts/decidim/ephemeral_participation/_user_menu.html.erb
@@ -0,0 +1,40 @@
+
+ <%=
+ t(
+ "remaining",
+ scope: "decidim.ephemeral_participation.user_menu",
+ remaining: (
+ Decidim::EphemeralParticipation::SessionPresenter
+ .new(current_user, self)
+ .ephemeral_participant_session_remaining_time_in_minutes
+ )
+ )
+ %>
+
+<% if current_user.verifiable_ephemeral_participant? %>
+
+ <%=
+ link_to(
+ I18n.t("complete_registration", scope: "decidim.ephemeral_participation.user_menu"),
+ decidim_ephemeral_participation.edit_ephemeral_participant_path(current_user),
+ tabindex: "-1",
+ )
+ %>
+
+<% end %>
+
+ <%=
+ link_to(
+ I18n.t("sign_out", scope: "decidim.ephemeral_participation.user_menu"),
+ decidim_ephemeral_participation.ephemeral_participant_path(current_user, redirect_url: request.path),
+ method: :delete,
+ class: "sign-out-link",
+ tabindex: "-1"
+ )
+ %>
+
+
+<%#
+ Maybe use JS to style dropdown and draw attention to it.
+ Also: would be nice to have a live countdown for the session.
+%>
diff --git a/decidim-ephemeral_participation/app/views/layouts/decidim/ephemeral_participation/user_profile.html.erb b/decidim-ephemeral_participation/app/views/layouts/decidim/ephemeral_participation/user_profile.html.erb
new file mode 100644
index 0000000000..a3065c15e4
--- /dev/null
+++ b/decidim-ephemeral_participation/app/views/layouts/decidim/ephemeral_participation/user_profile.html.erb
@@ -0,0 +1,31 @@
+<%= render "layouts/decidim/application" do %>
+
+
+
+
+
+
+
+
+ <%#= user_menu.render %>
+
+
+
+
+
+
+
+
+<% end %>
diff --git a/decidim-ephemeral_participation/config/locales/ca.yml b/decidim-ephemeral_participation/config/locales/ca.yml
new file mode 100644
index 0000000000..4e7ce351dc
--- /dev/null
+++ b/decidim-ephemeral_participation/config/locales/ca.yml
@@ -0,0 +1,51 @@
+---
+ca:
+ activemodel:
+ attributes:
+ organization:
+ enabled: Actiu
+ allows_ephemeral_participation: Permet participació sense registre
+ errors:
+ models:
+ organization:
+ attributes:
+ available_authorizations:
+ invalid: Only one authorization method can be used to allow ephemeral participation
+ permission:
+ attributes:
+ base:
+ invalid_ephemeral_participation_permissions: Cannot set permissions using multiple authorizations when '%{ephemeral_participation_authorization}' authorization is selected and '%{ephemeral_participation_enabled}' component setting is enabled.
+ settings:
+ attributes:
+ ephemeral_participation_enabled:
+ missing_ephemeral_participation_authorization: Must enable ephemeral participation authorization at system level.
+ decidim:
+ authorization_handlers:
+ ephemerable: Permet participació directe
+ ephemeral_participation:
+ actions:
+ unauthorized: "No tens permís per realitzar aquesta acció: %{link}"
+ unauthorized_link: Completa el teu registre aquí.
+ unverified: "Per poder participar, cal que estiguis verificada: %{link}"
+ unverified_link: Completa el procés de verificació aquí.
+ verified: "Completa el teu registre %{link}"
+ verified_link: aquí.
+ login_modal:
+ button: Vull participar sense registrar-me
+ help: Fes servir aquesta opció per una participació puntual
+ user_menu:
+ remaining: "%{remaining} min. per la desconexió automàtica"
+ sign_out: Cancel·la i desconnecta
+ complete_registration: Completa el teu registre
+ ephemeral_participants:
+ create: Per poder participar sense registre, has de completar el procés de verificació
+ destroy: S'ha cancel·lat la participació sense registre
+ submit: Envia
+ title: Completa el teu perfil d'usuari per simplificar la teva participació en el futur
+ unverifiable: No ha estat possible completar la verificació. Pots tornar a intentar-ho en un altre moment.
+ components:
+ budgets:
+ settings:
+ global:
+ ephemeral_participation_enabled: Ephemeral participation enabled
+ ephemeral_participation_enabled_help: Allows users to participate without registration. Requires configuring permissions.
diff --git a/decidim-ephemeral_participation/config/locales/en.yml b/decidim-ephemeral_participation/config/locales/en.yml
new file mode 100644
index 0000000000..6b195b5f7f
--- /dev/null
+++ b/decidim-ephemeral_participation/config/locales/en.yml
@@ -0,0 +1,51 @@
+---
+en:
+ activemodel:
+ attributes:
+ organization:
+ enabled: Enabled
+ allows_ephemeral_participation: Allows participation without registering
+ errors:
+ models:
+ permission:
+ attributes:
+ base:
+ invalid_ephemeral_participation_permissions: Cannot set permissions using multiple authorizations when '%{ephemeral_participation_authorization}' authorization is selected and '%{ephemeral_participation_enabled}' component setting is enabled.
+ organization:
+ attributes:
+ available_authorizations:
+ invalid: Only one authorization method can be used to allow ephemeral participation
+ settings:
+ attributes:
+ ephemeral_participation_enabled:
+ missing_ephemeral_participation_authorization: Must enable ephemeral participation authorization at system level.
+ decidim:
+ authorization_handlers:
+ ephemerable: Allows direct participation
+ ephemeral_participation:
+ actions:
+ unauthorized: "You are not authorized to perform this action: %{link}"
+ unauthorized_link: Finish your registration here.
+ unverified: "You need to be verified in order tor participate: %{link}"
+ unverified_link: Complete the verification process here.
+ verified: "Finish your registration %{link}"
+ verified_link: here.
+ login_modal:
+ button: I want to participate without registering
+ help: Use this option for a one-time participation
+ user_menu:
+ remaining: "%{remaining} min. before automatic sign out"
+ sign_out: Cancel and sign out
+ complete_registration: Finish your registration
+ ephemeral_participants:
+ create: en.decidim.ephemeral_participation.ephemeral_participants.create
+ destroy: en.decidim.ephemeral_participation.ephemeral_participants.destroy
+ submit: Send
+ title: Complete your user profile for easily future participation
+ unverifiable: The verification process has been unsuccessful. You can try it again later.
+ components:
+ budgets:
+ settings:
+ global:
+ ephemeral_participation_enabled: Ephemeral participation enabled
+ ephemeral_participation_enabled_help: Allows users to participate without registration. Requires configuring permissions.
diff --git a/decidim-ephemeral_participation/config/locales/es.yml b/decidim-ephemeral_participation/config/locales/es.yml
new file mode 100644
index 0000000000..f8bb50be2f
--- /dev/null
+++ b/decidim-ephemeral_participation/config/locales/es.yml
@@ -0,0 +1,51 @@
+---
+es:
+ activemodel:
+ attributes:
+ organization:
+ enabled: Activo
+ allows_ephemeral_participation: Permite la participación sin registro
+ errors:
+ models:
+ permission:
+ attributes:
+ base:
+ invalid_ephemeral_participation_permissions: Cannot set permissions using multiple authorizations when '%{ephemeral_participation_authorization}' authorization is selected and '%{ephemeral_participation_enabled}' component setting is enabled.
+ organization:
+ attributes:
+ available_authorizations:
+ invalid: Only one authorization method can be used to allow ephemeral participation
+ settings:
+ attributes:
+ ephemeral_participation_enabled:
+ missing_ephemeral_participation_authorization: Must enable ephemeral participation authorization at system level.
+ decidim:
+ authorization_handlers:
+ ephemerable: Permite participación directa
+ ephemeral_participation:
+ actions:
+ unauthorized: "No tienes permisos para realizar esta acción: %{link}"
+ unauthorized_link: Completa tu registro aquí.
+ unverified: "Para poder participar, es necesario que estés verificada: %{link}"
+ unverified_link: Completa el proceos de verificación aquí.
+ verified: "Completa tu registro %{link}"
+ verified_link: aquí.
+ login_modal:
+ button: Quiero participar sin registrarme
+ help: Utiliza esta opción para una participación puntual
+ user_menu:
+ remaining: "%{remaining} min. para desconexión automática"
+ sign_out: Cancela y desconecta
+ complete_registration: Completa tu registro
+ ephemeral_participants:
+ create: es.decidim.ephemeral_participation.ephemeral_participants.create
+ destroy: Se ha cancelado la participación sin registro
+ submit: Enviar
+ title: Completa tu perfil de usuario para simplificar tu participación en el futuro
+ unverifiable: unverifiable es
+ components:
+ budgets:
+ settings:
+ global:
+ ephemeral_participation_enabled: Ephemeral participation enabled
+ ephemeral_participation_enabled_help: Allows users to participate without registration. Requires configuring permissions.
diff --git a/decidim-ephemeral_participation/db/migrate/20210518192857_update_organizations_available_authorizations.rb b/decidim-ephemeral_participation/db/migrate/20210518192857_update_organizations_available_authorizations.rb
new file mode 100644
index 0000000000..fc7801089a
--- /dev/null
+++ b/decidim-ephemeral_participation/db/migrate/20210518192857_update_organizations_available_authorizations.rb
@@ -0,0 +1,38 @@
+class UpdateOrganizationsAvailableAuthorizations < ActiveRecord::Migration[5.2]
+ class Organization < ApplicationRecord
+ self.table_name = :decidim_organizations
+ end
+
+ def up
+ workflows = {}
+
+ Organization.find_each do |organization|
+ workflows[organization.id] =
+ organization.available_authorizations.each_with_object({}) do |workflow, hash|
+ hash[workflow] = { allow_ephemeral_participation: false }
+ end
+ end
+
+ remove_column :decidim_organizations, :available_authorizations
+ add_column :decidim_organizations, :available_authorizations, :jsonb, default: {}
+
+ Organization.find_each do |organization|
+ organization.update!(available_authorizations: workflows[organization.id])
+ end
+ end
+
+ def down
+ workflows = {}
+
+ Organization.find_each do |organization|
+ workflows[organization.id] = organization.available_authorizations.keys
+ end
+
+ remove_column :decidim_organizations, :available_authorizations
+ add_column :decidim_organizations, :available_authorizations, :string, array: true, default: []
+
+ Organization.find_each do |organization|
+ organization.update!(available_authorizations: workflows[organization.id])
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/decidim-ephemeral_participation.gemspec b/decidim-ephemeral_participation/decidim-ephemeral_participation.gemspec
new file mode 100644
index 0000000000..86f578dd48
--- /dev/null
+++ b/decidim-ephemeral_participation/decidim-ephemeral_participation.gemspec
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+$LOAD_PATH.push File.expand_path("../lib", __FILE__)
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "decidim-ephemeral_participation"
+ s.summary = "A decidim module that allows users to participate without registration."
+ s.description = s.summary
+ s.version = "0.0.1"
+ s.authors = ["Ivan Vergés"]
+ s.email = ["ivan@platoniq.net"]
+
+ s.files = Dir["{app,config,db,lib}/**/*", "Rakefile", "README.md"]
+
+ s.add_dependency "decidim-verifications"
+
+ s.add_development_dependency "decidim-dev"
+end
diff --git a/decidim-ephemeral_participation/lib/decidim/ephemeral_participation.rb b/decidim-ephemeral_participation/lib/decidim/ephemeral_participation.rb
new file mode 100644
index 0000000000..4a0bb4b2a9
--- /dev/null
+++ b/decidim-ephemeral_participation/lib/decidim/ephemeral_participation.rb
@@ -0,0 +1,7 @@
+require "decidim/ephemeral_participation/engine"
+require "decidim/ephemeral_participation/verifications_workflow_manifest_override"
+
+module Decidim
+ module EphemeralParticipation
+ end
+end
diff --git a/decidim-ephemeral_participation/lib/decidim/ephemeral_participation/engine.rb b/decidim-ephemeral_participation/lib/decidim/ephemeral_participation/engine.rb
new file mode 100644
index 0000000000..c26599c6f5
--- /dev/null
+++ b/decidim-ephemeral_participation/lib/decidim/ephemeral_participation/engine.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ class Engine < ::Rails::Engine
+ isolate_namespace Decidim::EphemeralParticipation
+
+ config.to_prepare do
+ # commands
+ Decidim::Admin::TransferUser.include(Decidim::EphemeralParticipation::TransferUserOverride)
+ # controllers
+ Decidim::ApplicationController.include(Decidim::EphemeralParticipation::ApplicationControllerOverride)
+ Decidim::Admin::ConflictsController.include(Decidim::EphemeralParticipation::ConflictsControllerOverride)
+ # forms
+ Decidim::Admin::ComponentForm.include(Decidim::EphemeralParticipation::ComponentFormOverride)
+ Decidim::Admin::PermissionsForm.include(Decidim::EphemeralParticipation::PermissionsFormOverride)
+ Decidim::Admin::TransferUserForm.include(Decidim::EphemeralParticipation::TransferUserFormOverride)
+ Decidim::System::UpdateOrganizationForm.include(Decidim::EphemeralParticipation::UpdateOrganizationFormOverride)
+ # models
+ Decidim::Component.include(Decidim::EphemeralParticipation::ComponentOverride)
+ Decidim::Organization.include(Decidim::EphemeralParticipation::OrganizationOverride)
+ Decidim::PermissionAction.include(Decidim::EphemeralParticipation::PermissionActionOverride)
+ Decidim::User.include(Decidim::EphemeralParticipation::UserOverride)
+ # permissions
+ Decidim::Permissions.include(Decidim::EphemeralParticipation::PermissionsOverride)
+ # budgets states
+ Decidim::Budgets::ProjectListItemCell.include(Decidim::EphemeralParticipation::ProjectListItemCellOverride)
+
+ end
+
+ initializer "ephemeral_participation.component_override" do
+ Decidim.component_registry.find(:budgets).tap do |component|
+ component.settings(:global) do |settings|
+ settings.attribute(:ephemeral_participation_enabled, type: :boolean, default: false)
+ end
+ end
+ end
+
+ routes do
+ scope :ephemeral_participation do
+ resources :ephemeral_participants, only: [:create, :edit, :update, :destroy]
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-ephemeral_participation/lib/decidim/ephemeral_participation/verifications_workflow_manifest_override.rb b/decidim-ephemeral_participation/lib/decidim/ephemeral_participation/verifications_workflow_manifest_override.rb
new file mode 100644
index 0000000000..dc9b6e2599
--- /dev/null
+++ b/decidim-ephemeral_participation/lib/decidim/ephemeral_participation/verifications_workflow_manifest_override.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Decidim
+ module EphemeralParticipation
+ module VerificationsWorkflowManifestOverride
+ extend ActiveSupport::Concern
+
+ included do
+ # Allows to be configured (in /system) for ephermeral participation (where available)
+ attribute :ephemerable, Virtus::Attribute::Boolean, default: false
+
+ def description
+ ephemerable_text = ", #{I18n.t("ephemerable", scope: "decidim.authorization_handlers")}" if ephemerable
+ "#{fullname} (#{I18n.t(type, scope: "decidim.authorization_handlers")}#{ephemerable_text})"
+ end
+ end
+ end
+ end
+end
+
+# needs to be available in initializers
+Decidim::Verifications::WorkflowManifest.include(Decidim::EphemeralParticipation::VerificationsWorkflowManifestOverride)
diff --git a/decidim-valid_auth/app/controllers/decidim/valid_auth/authorizations_controller.rb b/decidim-valid_auth/app/controllers/decidim/valid_auth/authorizations_controller.rb
index b9be835dea..c0078cd7ae 100644
--- a/decidim-valid_auth/app/controllers/decidim/valid_auth/authorizations_controller.rb
+++ b/decidim-valid_auth/app/controllers/decidim/valid_auth/authorizations_controller.rb
@@ -43,4 +43,4 @@ def load_authorization
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/budgets_workflow_pam2020.rb b/lib/budgets_workflow_pam2020.rb
index 216e880b2d..d9b5f0d25a 100644
--- a/lib/budgets_workflow_pam2020.rb
+++ b/lib/budgets_workflow_pam2020.rb
@@ -52,8 +52,8 @@ def user_authorization
def user_scope_resource
return unless user_authorization_scope
- @user_scope_resource ||= budgets.each do |resource|
- return resource if resource.scope == user_authorization_scope
+ @user_scope_resource ||= budgets.find do |resource|
+ resource.scope == user_authorization_scope
end
end
diff --git a/lib/budgets_workflow_pam2021.rb b/lib/budgets_workflow_pam2021.rb
new file mode 100644
index 0000000000..209c6ae28c
--- /dev/null
+++ b/lib/budgets_workflow_pam2021.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+# Specific Workflow for Barcelona's 2021 PAM
+class BudgetsWorkflowPam2021 < Decidim::Budgets::Workflows::Base
+ PAM2021AUTHORIZATIONHANDLER = 'census_sms_authorization_handler'
+
+ # The budget resource in the user's scope is highlighted.
+ def highlighted?(resource)
+ return unless user_scope_resource && !voted?(user_scope_resource)
+
+ resource == user_scope_resource
+ end
+
+ # Can vote in the budget resource in the user's scope
+ # and in an extra budget resource out of its scope
+ def vote_allowed?(resource, consider_progress = true)
+ return true if resource == user_scope_resource
+
+ resources_with_order = voted
+ resources_with_order += progress if consider_progress
+
+ (resources_with_order - [user_scope_resource, resource]).empty?
+ end
+
+ # The user can change of mind and change the vote on these budget resources
+ #
+ # Returns Array.
+ def discardable
+ (voted + progress) - [user_scope_resource]
+ end
+
+ # The user can vote on maximum 2 budget resources
+ #
+ # Returns Boolean.
+ def limit_reached?
+ (voted + progress).count < 3
+ end
+
+ private
+
+ # Returns Object (Authorization).
+ def user_authorization
+ @user_authorization ||= Decidim::Authorization.find_by(
+ name: PAM2021AUTHORIZATIONHANDLER,
+ user: user
+ )
+ end
+
+ # The budget resources the user can and should vote on
+ #
+ # Returns Object (Decidim::Budgets:Budget).
+ def user_scope_resource
+ return unless user_authorization_scope
+
+ @user_scope_resource ||= budgets.find do |resource|
+ resource.scope == user_authorization_scope
+ end
+ end
+
+ # The user's scope from the verifcation
+ #
+ # Returns Object (Scope).
+ def user_authorization_scope
+ return unless user_authorization
+
+ @user_authorization_scope ||= Decidim::Scope.find_by(
+ "name->>'ca' = '#{user_authorization.metadata['scope']}'"
+ )
+ end
+end
diff --git a/spec/system/census16_authorization_spec.rb b/spec/system/census16_authorization_spec.rb
index e0641ebca5..99359acf8f 100644
--- a/spec/system/census16_authorization_spec.rb
+++ b/spec/system/census16_authorization_spec.rb
@@ -13,7 +13,7 @@
)
end
- let(:authorizations) { ["census16_authorization_handler"] }
+ let(:authorizations) { {"census16_authorization_handler" => {"allow_ephemeral_participation" => true}} }
let!(:scope) { create :scope, organization: organization, code: "1" }
let(:response) do
diff --git a/spec/system/census_authorization_spec.rb b/spec/system/census_authorization_spec.rb
index e102102e19..329e2d441f 100644
--- a/spec/system/census_authorization_spec.rb
+++ b/spec/system/census_authorization_spec.rb
@@ -13,7 +13,7 @@
)
end
- let(:authorizations) { ["census_authorization_handler"] }
+ let(:authorizations) { {"census_authorization_handler" => {"allow_ephemeral_participation" => true}} }
let!(:scope) { create :scope, organization: organization, code: "1" }
let(:response) do
diff --git a/spec/system/census_sms_authorization_spec.rb b/spec/system/census_sms_authorization_spec.rb
new file mode 100644
index 0000000000..2e316a9f05
--- /dev/null
+++ b/spec/system/census_sms_authorization_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe "Census + SMS authorization", type: :system, perform_enqueued: true, with_authorization_workflows: ["census_sms_authorization_handler"] do
+ let(:organization) do
+ create(
+ :organization,
+ name: "Ajuntament",
+ default_locale: :ca,
+ available_locales: [:es, :ca],
+ available_authorizations: authorizations
+ )
+ end
+
+ let(:authorization_name) { "El padró + SMS" }
+ let(:authorizations) { {"census_sms_authorization_handler" => {"allow_ephemeral_participation" => true}} }
+ let(:code) { user_authorization.verification_metadata["verification_code"] }
+ let(:user_authorization) { Decidim::Authorization.find_by(user: user, name: "census_sms_authorization_handler") }
+
+ let!(:scope) { create :scope, organization: organization, code: "1" }
+
+ let(:response) do
+ Nokogiri::XML("01 ").remove_namespaces!
+ end
+
+ # Selects a birth date that will not cause errors in the form: January 12, 1979.
+ def fill_in_authorization_form
+ select "DNI", from: "authorization_document_type"
+ fill_in "authorization_document_number", with: "12345678A"
+ select "12", from: "authorization_date_of_birth_3i"
+ select "Gener", from: "authorization_date_of_birth_2i"
+ select "1979", from: "authorization_date_of_birth_1i"
+ fill_in "authorization_postal_code", with: "08001"
+ fill_in "authorization_mobile_phone_number", with: "(+34) 654 321 987"
+ check "authorization_tos_acceptance"
+ select translated(scope.name), from: "authorization_scope_id"
+ end
+
+ before do
+ allow_any_instance_of(Decidim::CensusSms::Verification::AuthorizationForm).to receive(:response).and_return(response)
+ switch_to_host(organization.host)
+ end
+
+ context "when visiting authorizations" do
+ let(:user) { create(:user, :confirmed, organization: organization) }
+
+ before do
+ login_as user, scope: :user
+ visit decidim.root_path
+ end
+
+ it "allows the user to authorize against available authorizations" do
+ within_user_menu do
+ click_link "El meu compte"
+ end
+
+ click_link "Autoritzacions"
+ click_link authorization_name
+
+ fill_in_authorization_form
+ click_button "Verifica't"
+
+ expect(page).to have_content("Has completat el primer pas")
+
+ fill_in "confirmation_verification_code", with: code
+ click_button "Verifica't"
+
+ expect(page).to have_content("T'has verificat correctament")
+
+ visit decidim_verifications.authorizations_path
+
+ within ".authorizations-list" do
+ expect(page).to have_content(authorization_name)
+ expect(page).not_to have_link(authorization_name)
+ end
+ end
+
+ it "allows the user to reset the verification code" do
+ within_user_menu do
+ click_link "El meu compte"
+ end
+
+ click_link "Autoritzacions"
+ click_link authorization_name
+
+ fill_in_authorization_form
+ click_button "Verifica't"
+
+ click_link "Restableix el codi de verificació"
+
+ fill_in "reset[mobile_phone_number]", with: "(+34) 654 321 987"
+ click_button "Envia'm un nou codi"
+
+ expect(page).to have_content("T'hem enviat un nou codi de verificació")
+
+ fill_in "confirmation_verification_code", with: code
+ click_button "Verifica't"
+
+ expect(page).to have_content("T'has verificat correctament")
+
+ visit decidim_verifications.authorizations_path
+
+ within ".authorizations-list" do
+ expect(page).to have_content(authorization_name)
+ expect(page).not_to have_link(authorization_name)
+ end
+ end
+
+ context "when the user has completed the first authorization step" do
+ let!(:code) { "012345" }
+ let!(:authorization) { create(:authorization, :pending, name: "census_sms_authorization_handler", user: user, verification_metadata: { verification_code: code, code_sent_at: Time.current }) }
+
+ it "can resume the authorization" do
+ visit decidim_verifications.authorizations_path
+
+ click_link authorization_name
+
+ expect(page).to have_content("Introdueix el codi")
+
+ fill_in "confirmation_verification_code", with: code
+ click_button "Verifica't"
+
+ expect(page).to have_content("T'has verificat correctament")
+ end
+ end
+
+ context "when the user has already been authorised" do
+ let!(:authorization) { create(:authorization, name: "census_sms_authorization_handler", user: user) }
+
+ it "shows the authorization at their account" do
+ visit decidim_verifications.authorizations_path
+
+ within ".authorizations-list" do
+ expect(page).to have_content(authorization_name)
+ expect(page).to have_content(I18n.localize(authorization.granted_at, format: :long, locale: :ca))
+ end
+ end
+ end
+ end
+end