decidim-direct_verifications 0.22 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +32 -3
  3. data/app/assets/config/direct_verifications_admin_manifest.css +3 -0
  4. data/app/assets/stylesheets/decidim/direct_verifications/authorizations.scss +17 -0
  5. data/app/commands/decidim/direct_verifications/verification/create_import.rb +50 -0
  6. data/app/controllers/decidim/direct_verifications/verification/admin/authorizations_controller.rb +40 -0
  7. data/app/controllers/decidim/direct_verifications/verification/admin/direct_verifications_controller.rb +22 -20
  8. data/app/controllers/decidim/direct_verifications/verification/admin/imports_controller.rb +55 -0
  9. data/app/forms/decidim/direct_verifications/registration_form.rb +8 -0
  10. data/app/forms/decidim/direct_verifications/verification/create_import_form.rb +43 -0
  11. data/app/jobs/decidim/direct_verifications/authorize_users_job.rb +34 -0
  12. data/app/jobs/decidim/direct_verifications/base_import_job.rb +38 -0
  13. data/app/jobs/decidim/direct_verifications/register_users_job.rb +19 -0
  14. data/app/jobs/decidim/direct_verifications/revoke_users_job.rb +17 -0
  15. data/app/mailers/decidim/direct_verifications/import_mailer.rb +27 -0
  16. data/app/mailers/decidim/direct_verifications/stats.rb +23 -0
  17. data/app/views/decidim/direct_verifications/import_mailer/finished_processing.html.erb +7 -0
  18. data/app/views/decidim/direct_verifications/import_mailer/finished_processing.text.erb +7 -0
  19. data/app/views/decidim/direct_verifications/verification/admin/authorizations/index.html.erb +42 -0
  20. data/app/views/decidim/direct_verifications/verification/admin/direct_verifications/index.html.erb +4 -3
  21. data/app/views/decidim/direct_verifications/verification/admin/imports/new.html.erb +50 -0
  22. data/config/initializers/mail_previews.rb +5 -0
  23. data/config/locales/ca.yml +56 -27
  24. data/config/locales/cs.yml +77 -0
  25. data/config/locales/en.yml +44 -13
  26. data/config/locales/es.yml +56 -28
  27. data/config/locales/fr.yml +80 -0
  28. data/lib/decidim/direct_verifications/authorize_user.rb +64 -0
  29. data/lib/decidim/direct_verifications/instrumenter.rb +58 -0
  30. data/lib/decidim/direct_verifications/parsers/base_parser.rb +37 -0
  31. data/lib/decidim/direct_verifications/parsers/metadata_parser.rb +54 -0
  32. data/lib/decidim/direct_verifications/parsers/name_parser.rb +40 -0
  33. data/lib/decidim/direct_verifications/parsers.rb +11 -0
  34. data/lib/decidim/direct_verifications/register_user.rb +54 -0
  35. data/lib/decidim/direct_verifications/revoke_user.rb +45 -0
  36. data/lib/decidim/direct_verifications/tests/factories.rb +11 -0
  37. data/lib/decidim/direct_verifications/tests/verification_controller_examples.rb +13 -8
  38. data/lib/decidim/direct_verifications/user_processor.rb +17 -101
  39. data/lib/decidim/direct_verifications/user_stats.rb +5 -5
  40. data/lib/decidim/direct_verifications/verification/admin_engine.rb +6 -1
  41. data/lib/decidim/direct_verifications/version.rb +3 -3
  42. data/lib/decidim/direct_verifications.rb +24 -1
  43. metadata +37 -9
  44. data/lib/decidim/direct_verifications/config.rb +0 -23
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module DirectVerifications
5
+ module Parsers
6
+ # Abstract class all concrete parsers should inherit from. They are expected to implement
7
+ # #header, #lines, and #parse_data methods.
8
+ class BaseParser
9
+ EMAIL_REGEXP = /([A-Z0-9+._-]+@[A-Z0-9._-]+\.[A-Z0-9_-]+)\b/i.freeze
10
+
11
+ def initialize(txt)
12
+ @txt = txt
13
+ @emails = {}
14
+ end
15
+
16
+ def to_h
17
+ lines.each do |line|
18
+ EMAIL_REGEXP.match(line) do |match|
19
+ email = normalize(match[0])
20
+ emails[email] = parse_data(email, line, header)
21
+ end
22
+ end
23
+
24
+ emails
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :txt, :emails
30
+
31
+ def normalize(value)
32
+ value.to_s.downcase
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module Decidim
6
+ module DirectVerifications
7
+ module Parsers
8
+ class MetadataParser < BaseParser
9
+ I18N_SCOPE = "decidim.direct_verifications.verification.admin.direct_verifications"
10
+
11
+ def header
12
+ @header ||= begin
13
+ header_row = lines[0].chomp
14
+ header_row = tokenize(header_row)
15
+ normalize_header(header_row)
16
+ end
17
+ end
18
+
19
+ def lines
20
+ @lines ||= StringIO.new(txt).readlines
21
+ end
22
+
23
+ def parse_data(email, line, header)
24
+ tokens = tokenize(line)
25
+
26
+ hash = {}
27
+ header.each_with_index do |column, index|
28
+ value = tokens[index]
29
+ next if value&.include?(email)
30
+
31
+ hash[column] = value
32
+ end
33
+ hash
34
+ end
35
+
36
+ private
37
+
38
+ def tokenize(line)
39
+ CSV.parse_line(line).map do |token|
40
+ token&.strip
41
+ end
42
+ end
43
+
44
+ def normalize_header(line)
45
+ line.map do |field|
46
+ raise InputParserError, I18n.t("#{I18N_SCOPE}.create.missing_header") if field.nil?
47
+
48
+ field.to_sym.downcase
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module DirectVerifications
5
+ module Parsers
6
+ class NameParser < BaseParser
7
+ LINE_DELIMITER = /[\r\n;,]/.freeze
8
+ NON_ALPHA_CHARS = /[^[:print:]]|["$<>|\\]/.freeze
9
+
10
+ def header
11
+ nil
12
+ end
13
+
14
+ def lines
15
+ txt.split(LINE_DELIMITER)
16
+ end
17
+
18
+ def parse_data(email, line, _header)
19
+ name = parse_name(email, line)
20
+ name = strip_non_alpha_chars(name)
21
+ name.presence || fallback_name(email)
22
+ end
23
+
24
+ private
25
+
26
+ def strip_non_alpha_chars(str)
27
+ (str.presence || "").gsub(NON_ALPHA_CHARS, "").strip
28
+ end
29
+
30
+ def parse_name(email, line)
31
+ line.split(email).first
32
+ end
33
+
34
+ def fallback_name(email)
35
+ email.split("@").first
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module DirectVerifications
5
+ module Parsers
6
+ autoload :BaseParser, "decidim/direct_verifications/parsers/base_parser"
7
+ autoload :NameParser, "decidim/direct_verifications/parsers/name_parser"
8
+ autoload :MetadataParser, "decidim/direct_verifications/parsers/metadata_parser"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module DirectVerifications
5
+ class RegisterUser
6
+ def initialize(email, data, organization, current_user, instrumenter)
7
+ @email = email
8
+ @name = data.is_a?(Hash) ? data[:name] : data
9
+ @organization = organization
10
+ @current_user = current_user
11
+ @instrumenter = instrumenter
12
+ end
13
+
14
+ def call
15
+ return if user
16
+
17
+ InviteUser.call(form) do
18
+ on(:ok) do
19
+ instrumenter.track(:registered, email, user)
20
+ end
21
+ on(:invalid) do
22
+ instrumenter.track(:registered, email)
23
+ end
24
+ end
25
+ rescue StandardError => e
26
+ instrumenter.track(:registered, email)
27
+ raise e if Rails.env.test? || Rails.env.development?
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :email, :name, :organization, :current_user, :instrumenter
33
+
34
+ def user
35
+ @user ||= User.find_by(email: email, decidim_organization_id: organization.id)
36
+ end
37
+
38
+ def form
39
+ RegistrationForm.new(
40
+ name: name.presence || fallback_name,
41
+ email: email.downcase,
42
+ organization: organization,
43
+ admin: false,
44
+ invited_by: current_user,
45
+ invitation_instructions: "direct_invite"
46
+ )
47
+ end
48
+
49
+ def fallback_name
50
+ email.split("@").first
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module DirectVerifications
5
+ class RevokeUser
6
+ def initialize(email, organization, instrumenter, authorization_handler)
7
+ @email = email
8
+ @organization = organization
9
+ @instrumenter = instrumenter
10
+ @authorization_handler = authorization_handler
11
+ end
12
+
13
+ def call
14
+ unless user
15
+ instrumenter.add_error :revoked, email
16
+ return
17
+ end
18
+
19
+ return unless valid_authorization?
20
+
21
+ Verification::DestroyUserAuthorization.call(authorization) do
22
+ on(:ok) do
23
+ instrumenter.add_processed :revoked, email
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :email, :organization, :instrumenter, :authorization_handler
31
+
32
+ def user
33
+ @user ||= User.find_by(email: email, decidim_organization_id: organization.id)
34
+ end
35
+
36
+ def authorization
37
+ @authorization ||= Authorization.find_by(user: user, name: authorization_handler)
38
+ end
39
+
40
+ def valid_authorization?
41
+ authorization&.granted?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/core/test/factories"
4
+
5
+ FactoryBot.modify do
6
+ factory :authorization do
7
+ trait :direct_verification do
8
+ name { "direct_verifications" }
9
+ end
10
+ end
11
+ end
@@ -2,24 +2,26 @@
2
2
 
3
3
  shared_examples_for "checking users" do |params|
4
4
  context "when check without mails" do
5
+ before { params[:userslist] = "" }
6
+
5
7
  it "renders the index with info message" do
6
- params[:userlist] = ""
7
8
  perform_enqueued_jobs do
8
9
  post :create, params: params
9
10
  expect(flash[:info]).not_to be_empty
10
- expect(flash[:info]).to include("0 users detected")
11
+ expect(flash[:info]).to include("0 participants detected")
11
12
  expect(subject).to render_template("decidim/direct_verifications/verification/admin/direct_verifications/index")
12
13
  end
13
14
  end
14
15
  end
15
16
 
16
17
  context "when check with mails" do
18
+ before { params[:userslist] = "mail@example.com" }
19
+
17
20
  it "renders the index with info message" do
18
- params[:userlist] = "mail@example.com"
19
21
  perform_enqueued_jobs do
20
22
  post :create, params: params
21
23
  expect(flash[:info]).not_to be_empty
22
- expect(flash[:info]).to include("1 users detected")
24
+ expect(flash[:info]).to include("1 participants detected")
23
25
  expect(subject).to render_template("decidim/direct_verifications/verification/admin/direct_verifications/index")
24
26
  end
25
27
  end
@@ -34,7 +36,7 @@ shared_examples_for "registering users" do |params|
34
36
  expect(flash[:warning]).not_to be_empty
35
37
  expect(flash[:warning]).to include("1 detected")
36
38
  expect(flash[:warning]).to include("0 errors")
37
- expect(flash[:warning]).to include("1 users")
39
+ expect(flash[:warning]).to include("1 participants")
38
40
  expect(flash[:warning]).to include("registered")
39
41
  end
40
42
  end
@@ -49,7 +51,7 @@ shared_examples_for "authorizing users" do |params|
49
51
  expect(flash[:notice]).not_to be_empty
50
52
  expect(flash[:notice]).to include("1 detected")
51
53
  expect(flash[:notice]).to include("0 errors")
52
- expect(flash[:notice]).to include("1 users")
54
+ expect(flash[:notice]).to include("1 participants")
53
55
  expect(flash[:notice]).to include("verified")
54
56
  end
55
57
  end
@@ -58,19 +60,22 @@ end
58
60
 
59
61
  shared_examples_for "revoking users" do |params|
60
62
  context "when there are valid emails" do
61
- it "creates notice message" do
63
+ before do
62
64
  create(
63
65
  :authorization,
64
66
  :granted,
65
67
  name: verification_type,
66
68
  user: authorized_user
67
69
  )
70
+ end
71
+
72
+ it "creates notice message" do
68
73
  perform_enqueued_jobs do
69
74
  post :create, params: params
70
75
  expect(flash[:notice]).not_to be_empty
71
76
  expect(flash[:notice]).to include("1 detected")
72
77
  expect(flash[:notice]).to include("0 errors")
73
- expect(flash[:notice]).to include("1 users")
78
+ expect(flash[:notice]).to include("1 participants")
74
79
  expect(flash[:notice]).to include("revoked")
75
80
  end
76
81
  end
@@ -1,131 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "decidim/direct_verifications/register_user"
4
+ require "decidim/direct_verifications/authorize_user"
5
+ require "decidim/direct_verifications/revoke_user"
6
+ require "decidim/direct_verifications/instrumenter"
7
+
3
8
  module Decidim
4
9
  module DirectVerifications
5
10
  class UserProcessor
6
- def initialize(organization, current_user, session)
11
+ def initialize(organization, current_user, session, instrumenter)
7
12
  @organization = organization
8
13
  @current_user = current_user
9
14
  @authorization_handler = :direct_verifications
10
- @errors = { registered: [], authorized: [], revoked: [] }
11
- @processed = { registered: [], authorized: [], revoked: [] }
15
+
12
16
  @emails = {}
13
17
  @session = session
18
+ @instrumenter = instrumenter
14
19
  end
15
20
 
16
- attr_reader :organization, :current_user, :session, :errors, :processed, :emails
17
- attr_accessor :authorization_handler
18
-
19
- def emails=(email_list)
20
- @emails = email_list.map { |k, v| [k.to_s.downcase, v.presence || k.split("@").first] }.to_h
21
- end
21
+ attr_reader :organization, :current_user, :session, :errors, :processed
22
+ attr_accessor :authorization_handler, :emails
22
23
 
23
24
  def register_users
24
- @emails.each do |email, name|
25
- next if find_user(email)
26
-
27
- form = register_form(email, name)
28
- begin
29
- InviteUser.call(form) do
30
- on(:ok) do
31
- add_processed :registered, email
32
- log_action find_user(email)
33
- end
34
- on(:invalid) do
35
- add_error :registered, email
36
- end
37
- end
38
- rescue StandardError
39
- add_error :registered, email
40
- end
25
+ emails.each do |email, data|
26
+ RegisterUser.new(email, data, organization, current_user, instrumenter).call
41
27
  end
42
28
  end
43
29
 
44
30
  def authorize_users
45
- @emails.each do |email, _name|
46
- if (u = find_user(email))
47
- auth = authorization(u)
48
- next unless !auth.granted? || auth.expired?
49
-
50
- Verification::ConfirmUserAuthorization.call(auth, authorize_form(u), session) do
51
- on(:ok) do
52
- add_processed :authorized, email
53
- end
54
- on(:invalid) do
55
- add_error :authorized, email
56
- end
57
- end
58
- else
59
- add_error :authorized, email
60
- end
31
+ emails.each do |email, data|
32
+ AuthorizeUser.new(email, data, session, organization, instrumenter, authorization_handler).call
61
33
  end
62
34
  end
63
35
 
64
36
  def revoke_users
65
- @emails.each do |email, _name|
66
- if (u = find_user(email))
67
- auth = authorization(u)
68
- next unless auth.granted?
69
-
70
- Verification::DestroyUserAuthorization.call(auth) do
71
- on(:ok) do
72
- add_processed :revoked, email
73
- end
74
- on(:invalid) do
75
- add_error :revoked, email
76
- end
77
- end
78
- else
79
- add_error :revoked, email
80
- end
37
+ emails.each do |email, _name|
38
+ RevokeUser.new(email, organization, instrumenter, authorization_handler).call
81
39
  end
82
40
  end
83
41
 
84
42
  private
85
43
 
86
- def find_user(email)
87
- User.find_by(email: email, decidim_organization_id: @organization.id)
88
- end
89
-
90
- def register_form(email, name)
91
- OpenStruct.new(name: name.presence || email.split("@").first,
92
- email: email.downcase,
93
- organization: organization,
94
- admin: false,
95
- invited_by: current_user,
96
- invitation_instructions: "direct_invite")
97
- end
98
-
99
- def authorization(user)
100
- Authorization.find_or_initialize_by(
101
- user: user,
102
- name: authorization_handler
103
- )
104
- end
105
-
106
- def authorize_form(user)
107
- Verification::DirectVerificationsForm.new(email: user.email, name: user.name)
108
- end
109
-
110
- def add_processed(type, email)
111
- @processed[type] << email unless @processed[type].include? email
112
- end
113
-
114
- def add_error(type, email)
115
- @errors[type] << email unless @errors[type].include? email
116
- end
117
-
118
- def log_action(user)
119
- Decidim.traceability.perform_action!(
120
- "invite",
121
- user,
122
- current_user,
123
- extra: {
124
- invited_user_role: "participant",
125
- invited_user_id: user.id
126
- }
127
- )
128
- end
44
+ attr_reader :instrumenter
129
45
  end
130
46
  end
131
47
  end
@@ -22,7 +22,7 @@ module Decidim
22
22
  end
23
23
 
24
24
  def unconfirmed
25
- registered_users.where("decidim_users.confirmed_at IS NULL").count
25
+ registered_users.where(decidim_users: { confirmed_at: nil }).count
26
26
  end
27
27
 
28
28
  def authorized
@@ -30,7 +30,7 @@ module Decidim
30
30
  end
31
31
 
32
32
  def authorized_unconfirmed
33
- authorized_users.where("decidim_users.confirmed_at IS NULL").count
33
+ authorized_users.where(decidim_users: { confirmed_at: nil }).count
34
34
  end
35
35
 
36
36
  private
@@ -41,10 +41,10 @@ module Decidim
41
41
  filter[:email] = emails unless emails.empty?
42
42
  return User.where(filter).where.not(email: "")
43
43
  end
44
- authorized_users(false)
44
+ authorized_users(strict: false)
45
45
  end
46
46
 
47
- def authorized_users(strict = true)
47
+ def authorized_users(strict: true)
48
48
  q = Decidim::Authorization.joins(:user)
49
49
  unless authorization_handler.empty?
50
50
  q = q.where(name: authorization_handler)
@@ -56,7 +56,7 @@ module Decidim
56
56
  q = q.where("decidim_users.decidim_organization_id=:org and decidim_users.email!=''", org: organization.id)
57
57
  return q if emails.empty?
58
58
 
59
- q.where("decidim_users.email IN (:emails)", emails: emails)
59
+ q.where(decidim_users: { email: emails })
60
60
  end
61
61
 
62
62
  def expires_in
@@ -10,12 +10,17 @@ module Decidim
10
10
  routes do
11
11
  resources :direct_verifications, only: [:index, :create, :stats]
12
12
  resources :stats, only: [:index]
13
+ resources :authorizations, only: [:index, :destroy]
14
+ resources :imports, only: [:new, :create]
13
15
 
14
16
  root to: "direct_verifications#index"
15
17
  end
16
18
 
17
19
  initializer "decidim_direct_verifications.admin_assets" do |app|
18
- app.config.assets.precompile += %w(direct_verifications_admin_manifest.js)
20
+ app.config.assets.precompile += %w(
21
+ direct_verifications_admin_manifest.js
22
+ direct_verifications_admin_manifest.css
23
+ )
19
24
  end
20
25
  end
21
26
  end
@@ -3,8 +3,8 @@
3
3
  module Decidim
4
4
  # This holds the decidim-direct_verifications version.
5
5
  module DirectVerifications
6
- VERSION = "0.22"
7
- DECIDIM_VERSION = "0.22"
8
- MIN_DECIDIM_VERSION = ">= 0.22.0"
6
+ VERSION = "1.0.1"
7
+ DECIDIM_VERSION = "0.24.3"
8
+ MIN_DECIDIM_VERSION = ">= 0.23.0"
9
9
  end
10
10
  end
@@ -1,12 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "direct_verifications/version"
4
- require_relative "direct_verifications/config"
5
4
  require_relative "direct_verifications/user_processor"
6
5
  require_relative "direct_verifications/user_stats"
7
6
  require_relative "direct_verifications/verification"
7
+ require_relative "direct_verifications/parsers"
8
8
 
9
9
  module Decidim
10
10
  module DirectVerifications
11
+ include ActiveSupport::Configurable
12
+
13
+ class InputParserError < StandardError; end
14
+
15
+ # Specify in this variable which authorization methods can be managed by the plugin
16
+ # Be careful to specify only what you really need
17
+ config_accessor :manage_workflows do
18
+ ["direct_verifications"]
19
+ end
20
+
21
+ # The processor for the user uploaded data where to extract emails and other info
22
+ # be default it uses Decidim::DirectVerifications::Parsers::NameParser
23
+ # Currently available are:
24
+ # - :name_parser
25
+ # - :metadata_parser
26
+ # A custom parser can be specified as long it respects the module hierachy
27
+ config_accessor :input_parser do
28
+ :name_parser
29
+ end
30
+
31
+ def self.find_parser_class(manifest)
32
+ "Decidim::DirectVerifications::Parsers::#{manifest.to_s.camelize}".safe_constantize || Decidim::DirectVerifications::Parsers::NameParser
33
+ end
11
34
  end
12
35
  end