decidim-direct_verifications 0.17.8 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -2
  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 +71 -46
  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 +18 -17
  21. data/app/views/decidim/direct_verifications/verification/admin/imports/new.html.erb +50 -0
  22. data/app/views/decidim/direct_verifications/verification/admin/stats/index.html.erb +3 -3
  23. data/config/initializers/mail_previews.rb +5 -0
  24. data/config/locales/ca.yml +25 -20
  25. data/config/locales/cs.yml +69 -0
  26. data/config/locales/en.yml +34 -2
  27. data/config/locales/es.yml +25 -20
  28. data/config/locales/fr.yml +69 -0
  29. data/lib/decidim/direct_verifications.rb +24 -1
  30. data/lib/decidim/direct_verifications/authorize_user.rb +64 -0
  31. data/lib/decidim/direct_verifications/instrumenter.rb +58 -0
  32. data/lib/decidim/direct_verifications/parsers.rb +11 -0
  33. data/lib/decidim/direct_verifications/parsers/base_parser.rb +37 -0
  34. data/lib/decidim/direct_verifications/parsers/metadata_parser.rb +54 -0
  35. data/lib/decidim/direct_verifications/parsers/name_parser.rb +40 -0
  36. data/lib/decidim/direct_verifications/register_user.rb +54 -0
  37. data/lib/decidim/direct_verifications/revoke_user.rb +45 -0
  38. data/lib/decidim/direct_verifications/tests/factories.rb +11 -0
  39. data/lib/decidim/direct_verifications/tests/verification_controller_examples.rb +83 -0
  40. data/lib/decidim/direct_verifications/user_processor.rb +18 -98
  41. data/lib/decidim/direct_verifications/user_stats.rb +9 -6
  42. data/lib/decidim/direct_verifications/verification/admin_engine.rb +6 -1
  43. data/lib/decidim/direct_verifications/version.rb +3 -2
  44. metadata +38 -9
  45. data/lib/decidim/direct_verifications/config.rb +0 -23
@@ -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,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,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
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples_for "checking users" do |params|
4
+ context "when check without mails" do
5
+ before { params[:userslist] = "" }
6
+
7
+ it "renders the index with info message" do
8
+ perform_enqueued_jobs do
9
+ post :create, params: params
10
+ expect(flash[:info]).not_to be_empty
11
+ expect(flash[:info]).to include("0 users detected")
12
+ expect(subject).to render_template("decidim/direct_verifications/verification/admin/direct_verifications/index")
13
+ end
14
+ end
15
+ end
16
+
17
+ context "when check with mails" do
18
+ before { params[:userslist] = "mail@example.com" }
19
+
20
+ it "renders the index with info message" do
21
+ perform_enqueued_jobs do
22
+ post :create, params: params
23
+ expect(flash[:info]).not_to be_empty
24
+ expect(flash[:info]).to include("1 users detected")
25
+ expect(subject).to render_template("decidim/direct_verifications/verification/admin/direct_verifications/index")
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ shared_examples_for "registering users" do |params|
32
+ context "when there are valid emails" do
33
+ it "creates warning message" do
34
+ perform_enqueued_jobs do
35
+ post :create, params: params
36
+ expect(flash[:warning]).not_to be_empty
37
+ expect(flash[:warning]).to include("1 detected")
38
+ expect(flash[:warning]).to include("0 errors")
39
+ expect(flash[:warning]).to include("1 users")
40
+ expect(flash[:warning]).to include("registered")
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ shared_examples_for "authorizing users" do |params|
47
+ context "when there are valid emails" do
48
+ it "creates notice message" do
49
+ perform_enqueued_jobs do
50
+ post :create, params: params
51
+ expect(flash[:notice]).not_to be_empty
52
+ expect(flash[:notice]).to include("1 detected")
53
+ expect(flash[:notice]).to include("0 errors")
54
+ expect(flash[:notice]).to include("1 users")
55
+ expect(flash[:notice]).to include("verified")
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ shared_examples_for "revoking users" do |params|
62
+ context "when there are valid emails" do
63
+ before do
64
+ create(
65
+ :authorization,
66
+ :granted,
67
+ name: verification_type,
68
+ user: authorized_user
69
+ )
70
+ end
71
+
72
+ it "creates notice message" do
73
+ perform_enqueued_jobs do
74
+ post :create, params: params
75
+ expect(flash[:notice]).not_to be_empty
76
+ expect(flash[:notice]).to include("1 detected")
77
+ expect(flash[:notice]).to include("0 errors")
78
+ expect(flash[:notice]).to include("1 users")
79
+ expect(flash[:notice]).to include("revoked")
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,127 +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)
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 = {}
17
+ @session = session
18
+ @instrumenter = instrumenter
13
19
  end
14
20
 
15
- attr_reader :organization, :current_user, :errors, :processed, :emails
16
- attr_accessor :authorization_handler
17
-
18
- def emails=(email_list)
19
- @emails = email_list.map { |k, v| [k.to_s.downcase, v.presence || k.split("@").first] }.to_h
20
- end
21
+ attr_reader :organization, :current_user, :session, :errors, :processed
22
+ attr_accessor :authorization_handler, :emails
21
23
 
22
24
  def register_users
23
- @emails.each do |email, name|
24
- next if find_user(email)
25
- form = register_form(email, name)
26
- begin
27
- InviteUser.call(form) do
28
- on(:ok) do
29
- add_processed :registered, email
30
- log_action find_user(email)
31
- end
32
- on(:invalid) do
33
- add_error :registered, email
34
- end
35
- end
36
- rescue StandardError
37
- add_error :registered, email
38
- end
25
+ emails.each do |email, data|
26
+ RegisterUser.new(email, data, organization, current_user, instrumenter).call
39
27
  end
40
28
  end
41
29
 
42
30
  def authorize_users
43
- @emails.each do |email, _name|
44
- if (u = find_user(email))
45
- auth = authorization(u)
46
- next unless !auth.granted? || auth.expired?
47
- Verification::ConfirmUserAuthorization.call(auth, authorize_form(u)) do
48
- on(:ok) do
49
- add_processed :authorized, email
50
- end
51
- on(:invalid) do
52
- add_error :authorized, email
53
- end
54
- end
55
- else
56
- add_error :authorized, email
57
- end
31
+ emails.each do |email, data|
32
+ AuthorizeUser.new(email, data, session, organization, instrumenter, authorization_handler).call
58
33
  end
59
34
  end
60
35
 
61
36
  def revoke_users
62
- @emails.each do |email, _name|
63
- if (u = find_user(email))
64
- auth = authorization(u)
65
- next unless auth.granted?
66
- Verification::DestroyUserAuthorization.call(auth) do
67
- on(:ok) do
68
- add_processed :revoked, email
69
- end
70
- on(:invalid) do
71
- add_error :revoked, email
72
- end
73
- end
74
- else
75
- add_error :revoked, email
76
- end
37
+ emails.each do |email, _name|
38
+ RevokeUser.new(email, organization, instrumenter, authorization_handler).call
77
39
  end
78
40
  end
79
41
 
80
42
  private
81
43
 
82
- def find_user(email)
83
- User.find_by(email: email, decidim_organization_id: @organization.id)
84
- end
85
-
86
- def register_form(email, name)
87
- OpenStruct.new(name: name.presence || email.split("@").first,
88
- email: email.downcase,
89
- organization: organization,
90
- admin: false,
91
- invited_by: current_user,
92
- invitation_instructions: "direct_invite")
93
- end
94
-
95
- def authorization(user)
96
- Authorization.find_or_initialize_by(
97
- user: user,
98
- name: authorization_handler
99
- )
100
- end
101
-
102
- def authorize_form(user)
103
- Verification::DirectVerificationsForm.new(email: user.email, name: user.name)
104
- end
105
-
106
- def add_processed(type, email)
107
- @processed[type] << email unless @processed[type].include? email
108
- end
109
-
110
- def add_error(type, email)
111
- @errors[type] << email unless @errors[type].include? email
112
- end
113
-
114
- def log_action(user)
115
- Decidim.traceability.perform_action!(
116
- "invite",
117
- user,
118
- current_user,
119
- extra: {
120
- invited_user_role: "participant",
121
- invited_user_id: user.id
122
- }
123
- )
124
- end
44
+ attr_reader :instrumenter
125
45
  end
126
46
  end
127
47
  end