-
Notifications
You must be signed in to change notification settings - Fork 8
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
Conversation
- 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
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| |
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
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?
decidim-censuses/README.md
Outdated
|
||
> Add census authorization to Decidim platform | ||
|
||
Allows to upload a census CSV file to perform authorizations agains real user age. |
There was a problem hiding this comment.
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.
decidim-censuses/README.md
Outdated
Add this line to your application's Gemfile: | ||
|
||
```ruby | ||
gem 'decidim-census' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's decidim-censuses
There was a problem hiding this comment.
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 👏
decidim-censuses/README.md
Outdated
Create a dummy app: | ||
|
||
```bash | ||
$ bin/rails decidim:generate_test_app |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 😅)
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ;-)
@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. |
- 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' |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Fixes #7