Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for CSV and multiple columns #2

Merged
merged 17 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ spec/decidim_dummy_app

# default development application
development_app

.byebug_history
.ruby-version
tags
8 changes: 3 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,9 @@ GEM
cells (>= 4.1.6, < 5.0.0)
charlock_holmes (0.7.7)
childprocess (3.0.0)
codecov (0.1.16)
codecov (0.2.11)
json
simplecov
url
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (5.0.0)
Expand Down Expand Up @@ -428,7 +427,7 @@ GEM
thor (>= 0.14, < 2.0)
jquery-tmpl-rails (1.1.0)
rails (>= 3.1.0)
json (2.3.0)
json (2.3.1)
jwt (2.2.1)
kaminari (1.2.0)
activesupport (>= 4.1.0)
Expand Down Expand Up @@ -708,7 +707,6 @@ GEM
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.6.1)
url (0.3.2)
valid_email2 (2.3.1)
activemodel (>= 3.2)
mail (~> 2.5)
Expand Down Expand Up @@ -757,7 +755,7 @@ DEPENDENCIES
web-console (~> 3.5)

RUBY VERSION
ruby 2.6.3p62
ruby 2.6.6p146

BUNDLED WITH
2.1.4
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ With the detected list of emails admin have different options available:
3. Revoke the authorization for the list of users using any verification method available.
4. Check the status of the users in order to know if they are verified or registered.

### Metadata mode

This mode provides extra capabilities over the default processing:

* Reads CSV format with header (copy and paste it from your spreadsheet)
* Stores all columns except the email as authorization metadata

This enables querying the authorization metadata however fits you best.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be worth documenting how to do that with an example and a link to PostgreSQL's docs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A link to PostgreSQL's docs? what do you mean? how to export a csv from a sql table?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought people may not be familiar with PostgreSQL JSON operators and it's worth pointing at the docs and show how it could be queried like in CoopCat-Confederacio-de-Cooperatives/decidim-coopcat#2 (comment).


To enable it create a new initializer called `config/initializers/decidim_direct_verifications.rb` with the following contents

```rb
Rails.application.config.direct_verifications_parser = :metadata
```

## Installation

Add this line to your application's Gemfile:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Decidim
module DirectVerifications
module Verification
# Abstract class all concrete parsers should inherit from. They are expected to implement
# #header, #lines, and #parse_data methods.
class BaseParser
EMAIL_REGEXP = /([A-Z0-9+._-]+@[A-Z0-9._-]+\.[A-Z0-9_-]+)\b/i.freeze

def initialize(txt)
@txt = txt
@emails = {}
end

def to_h
lines.each do |line|
EMAIL_REGEXP.match(line) do |match|
email = normalize(match[0])
emails[email] = parse_data(email, line, header)
end
end

emails
end

private

attr_reader :txt, :emails

def normalize(value)
value.to_s.downcase
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "csv"

module Decidim
module DirectVerifications
module Verification
class MetadataParser < BaseParser
def header
header_row = lines[0].chomp
column_names = tokenize(header_row)
column_names.map(&:to_sym).map(&:downcase)
end

def lines
@lines ||= StringIO.new(txt).readlines
end

def parse_data(email, line, header)
tokens = tokenize(line)

hash = {}
header.each_with_index do |column, index|
value = tokens[index].strip
next if value.include?(email)

hash[column] = value
end
hash
end

private

def tokenize(line)
CSV.parse(line)[0]
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Decidim
module DirectVerifications
module Verification
class NameParser < BaseParser
LINE_DELIMITER = /[\r\n;,]/.freeze
NON_ALPHA_CHARS = /[^[:print:]]|[\"\$\<\>\|\\]/.freeze

def header
nil
end

def lines
txt.split(LINE_DELIMITER)
end

def parse_data(email, line, _header)
name = parse_name(email, line)
name = strip_non_alpha_chars(name)
name.presence || fallback_name(email)
end

private

def strip_non_alpha_chars(str)
(str.presence || "").gsub(NON_ALPHA_CHARS, "").strip
end

def parse_name(email, line)
line.split(email).first
end

def fallback_name(email)
email.split("@").first
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ def create

@userlist = params[:userlist]
@workflows = workflows

processor = UserProcessor.new(current_organization, current_user)
processor.emails = extract_emails_to_hash @userlist
processor.emails = parser_class.new(@userlist).to_h
processor.authorization_handler = authorization_handler(params[:authorization_handler])

stats = UserStats.new(current_organization)
stats.authorization_handler = processor.authorization_handler

if params[:register]
processor.register_users
flash[:warning] = t(".registered", count: processor.emails.count,
Expand Down Expand Up @@ -52,21 +55,18 @@ def create
registered: stats.registered)
render(action: :index) && return
end

redirect_to direct_verifications_path
end

private

def extract_emails_to_hash(txt)
reg = /([A-Z0-9+._-]+@[A-Z0-9._-]+\.[A-Z0-9_-]+)\b/i
emails = {}
txt.split(/[\r\n;,]/).each do |line|
reg.match line do |m|
n = line.split(m[0]).first
emails[m[0]] = (n.presence || "").gsub(/[^[:print:]]|[\"\$\<\>\|\\]/, "").strip
end
def parser_class
if Rails.configuration.direct_verifications_parser == :metadata
MetadataParser
else
NameParser
end
emails
end

def authorization_handler(authorization_handler)
Expand Down
28 changes: 18 additions & 10 deletions lib/decidim/direct_verifications/user_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ def initialize(organization, current_user)
@emails = {}
end

attr_reader :organization, :current_user, :errors, :processed, :emails
attr_accessor :authorization_handler

def emails=(email_list)
@emails = email_list.map { |k, v| [k.to_s.downcase, v.presence || k.split("@").first] }.to_h
end
attr_reader :organization, :current_user, :errors, :processed
attr_accessor :authorization_handler, :emails

def register_users
@emails.each do |email, name|
emails.each do |email, data|
next if find_user(email)

name = if data.is_a?(Hash)
data[:name]
else
data
end

form = register_form(email, name)
begin
InviteUser.call(form) do
Expand All @@ -41,9 +43,11 @@ def register_users
end

def authorize_users
@emails.each do |email, _name|
emails.each do |email, data|
if (u = find_user(email))
auth = authorization(u)
auth.metadata = data

next unless !auth.granted? || auth.expired?

Verification::ConfirmUserAuthorization.call(auth, authorize_form(u)) do
Expand All @@ -61,7 +65,7 @@ def authorize_users
end

def revoke_users
@emails.each do |email, _name|
emails.each do |email, _name|
if (u = find_user(email))
auth = authorization(u)
next unless auth.granted?
Expand All @@ -87,14 +91,18 @@ def find_user(email)
end

def register_form(email, name)
OpenStruct.new(name: name.presence || email.split("@").first,
OpenStruct.new(name: name.presence || fallback_name(email),
email: email.downcase,
organization: organization,
admin: false,
invited_by: current_user,
invitation_instructions: "direct_invite")
end

def fallback_name(email)
email.split("@").first
end

def authorization(user)
Authorization.find_or_initialize_by(
user: user,
Expand Down
2 changes: 2 additions & 0 deletions lib/decidim/direct_verifications/verification/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module Verification
class Engine < ::Rails::Engine
isolate_namespace Decidim::DirectVerifications::Verification

config.direct_verifications_parser = :name

paths["db/migrate"] = nil
paths["lib/tasks"] = nil

Expand Down
Loading