decidim-direct_verifications 0.22 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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