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

decidim-census: authorise via census using a CSV file (engine) #15

Merged
merged 49 commits into from
Nov 21, 2017

Conversation

danigb
Copy link
Contributor

@danigb danigb commented Nov 14, 2017

Fixes #7

- Refactor CensusCsvService into CsvData model
- Add a background job to remove duplicates
- It only displays the number of unique census rows
…for the current organization

- Move AuthorizeWithAge to lib/extensions
@danigb danigb changed the title decidim-census Engine WIP decidim-census: authorise via census using a CSV file (engine) Nov 20, 2017
db/schema.rb Outdated

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

create_table "censuses", force: :cascade do |t|
Copy link
Contributor

Choose a reason for hiding this comment

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

@danigb Watch out this. You shouldn't have the decidim_censuses_censuses and the censuses table. I imagine that this censuses table is produced by some legacy code.

db/schema.rb Outdated
@@ -161,6 +174,23 @@
t.index ["decidim_root_commentable_type", "decidim_root_commentable_id"], name: "decidim_comments_comment_root_commentable"
end

create_table "decidim_dummy_resources", force: :cascade do |t|
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that this should be into your database. Is it required?


> Add census authorization to Decidim platform

Allows to upload a census CSV file to perform authorizations agains real user age.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rewrite this as:

Allows to upload a census CSV file to perform authorizations against real users parameterised by their age.

Add this line to your application's Gemfile:

```ruby
gem 'decidim-census'
Copy link
Contributor

Choose a reason for hiding this comment

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

It's decidim-censuses

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Impresionante, no se te escapa una 👏

Create a dummy app:

```bash
$ bin/rails decidim:generate_test_app
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are giving instructions to the user to generate a test app, maybe we don't need to check it out into the source code. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The key here is that the README was written with the idea of publish the engine independently from the app.

I've rewritten this message as follows: "Create a dummy app in your application (if not present):"

authorize! :destroy, Census
Census.delete_all
flash[:notice] = t('.success')
redirect_to census_uploads_path
Copy link
Contributor

Choose a reason for hiding this comment

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

you can use: redirect_to census_uploads_path, notice: t('.success') which is shorter.

.having('count(id_document)>1')
.map(&:id_document)

duplicated.each do |id_doc|
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of each, prefer find_each. Find each is less performant for small groups of objects but is needed if you can retrieve many objects.

queue_as :default

def perform
duplicated = Census.select(:id_document)
Copy link
Contributor

Choose a reason for hiding this comment

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

Every time you are assigning a local variable, think if it's worth it to extract this to a method. It usually improves readability.

def perform
   duplicated_census.each do |id_doc|
   end
end

private

def duplicated_census
Census.select(:id_document).blablabla
end

def change
create_table :decidim_censuses_censuses do |t|
t.string :id_document
t.string :birthdate
Copy link
Contributor

Choose a reason for hiding this comment

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

I have a question. Why we save the birthdate as a string? I see more advantages of using a date object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. It departures from the standard, and thats not a good thing. Also it's not clear if it's really a date, and it's not documented the format. So, rolling back to a date field!

@@ -0,0 +1,10 @@
module Decidim
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are not going to send emails, I would remove this class (of everything keeps working fine, of course 😅)

Copy link
Contributor

@xredo xredo left a comment

Choose a reason for hiding this comment

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

I have added a couple of additional comments worth checking out.

@birthdate ||= Date.strptime(authorization.metadata['birthdate'], '%Y/%m/%d')
end

def current_age
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel that this implementation is too complex.

I'm very bad at understanding low level implementations, so, when I'm starting to reach this kind of solutions I tend to search for simpler solutions. The one that comes to my mind is:

return status(:invalid, fields: [:birthdate]) unless minimum_age_check?

def minimum_age_check?
   (birthdate + minimum_age.years) < Date.current
end

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Desde luego es simple y elegante 👏

now.year - birthdate.year - (extra_year ? 0 : 1)
end

def minimum_age
Copy link
Contributor

Choose a reason for hiding this comment

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

When trying to convert something to an integer in Ruby it's a common practice using the .to_i object method. Every object implements it (even nil!)

It returns the integer for integers or 0 if it's not present. So, this could be an alternative implementation:

def minimum_age
   return DEFAULT_MINIMUM_AGE if permission_options['edad'].to_i.zero?
   permission_options['edad'].to_i
end

However, this also has drawbacks. The number 0 does not represent a blank value, and in order to understand this piece of code you need to understand the internals of Ruby.

It's worth thinking about it. What implementation do you like more?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've decided to follow this way after reading at https://stackoverflow.com/questions/49274/safe-integer-parsing-in-ruby:

you might want to watch for strings that are valid non-decimal numbers, such as those starting with 0x for hex and 0b for binary, and potentially more tricky numbers starting with zero that will be parsed as octal.

Probably too much precaution. I prefer more idiomatic ways, but I'm not 100% convinced about your solution (mostly because of two calls to to_i method, but probably ruby is now smart enough)...

And by the way: :hello.to_i fails ;-)

@xredo
Copy link
Contributor

xredo commented Nov 21, 2017

@danigb I'm reviewing the feature, and I have found the following.

Census needs to be associated with an organization.

We need to add a belongs_to association with an organization. And every action performed against the Census needs to be scoped by this organization:

ej:

Census.where(organization: current_organization).delete_all

This way we can treat censuses independently for every organization.

xredo and others added 11 commits November 21, 2017 09:20
- The authorization form requires a date, and the authorization handler verifies the date correspond to the census in order to validate
- The date is now stored in the database as a date
- Remove unused tables from schema.db
- Improve engine's README
- Minor improvements and fixes
- delete_all is renamed to destroy
- Rename `to_id_document` into `normalize_id_document`
- Rename `to_birthdate` into `parse_date`
@@ -0,0 +1,46 @@
require 'pry'
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to remove this. It's only for debug purposes.

@@ -0,0 +1,50 @@
# frozen_string_literal: true

require 'pry'
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to remove this. It's only for debug purposes

@@ -0,0 +1,39 @@
require 'spec_helper'
require 'pry'
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to remove this. Only for debug purposes.

@errors = []
@values = []

CSV.foreach(@file, headers: true, col_sep: ';') do |row|
Copy link
Contributor

Choose a reason for hiding this comment

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

For me it's surprising that when I am instantiating an object it performs some kind of action. This is not a mistake. It's just because I am too used to using the form:

CsvData.new(file).call

However, I imagine that your point is that the object processing should only be run once (with the same file param). So there is no need to have a method call that can be called several times.

It's just a reflexion. I'm not expecting any change here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes... Probably CsvData.new.call(file) is better...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now I'm looking into this issue:

For me it's surprising that when I am instantiating an object it performs some kind of action.

Yes, I understand... but you only know that if you look at the code inside the class ;-) In fact, you can treat it like a kind of object initialization...

I imagine that your point is that the object processing should only be run once

Yes. Exactly.

Probably CsvData.new(file).call it's more idiomatic, but I'm afraid to have an object instance in an inconsistency state... What if you forget to call call?


def create
authorize! :create, CensusDatum
if params[:file]
Copy link
Contributor

Choose a reason for hiding this comment

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

This implementation is correct. However, I would have used a more Rails like approach to the problem.

def create
   authorize! :create, CensusDatum
   csv_data = CsvData.new(file: params[:file])
   if csv_file.valid?
      csv_data.parse
      CensusDatum.insert_all(current_organization, csv_data.values)
      RemoveDuplicatesJob.perform_later(current_organization)
      flash[:notice] = t('.success', count: data.values.count,
                                   errors: data.errors.count)
   end
   redirect_to censuses_path
end

And then, In CsvData:

class CsvData
  include ActiveModel::Model
  attr_accessor :file

  validates :file, presence: true
end

However, I don't believe that we should change the current code, because adding ActiveModel here is overkill for what we need. I'm making this comment as an example of alternate implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great! I'll think about it

@xredo xredo merged commit 70b6b56 into DiputacioBarcelona:master Nov 21, 2017
@xredo xredo deleted the engine branch November 21, 2017 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants