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

Add proposal filters #474

Merged
merged 15 commits into from
Jan 13, 2017
Merged

Add proposal filters #474

merged 15 commits into from
Jan 13, 2017

Conversation

beagleknight
Copy link
Contributor

@beagleknight beagleknight commented Jan 10, 2017

🎩 What? Why?

I'm adding a filter form for proposals. I factor out the code created in #399 so it can be used for both resources.

I'm exposing the API as a single helper:

<%= filter_form_for filter do |form| %>
  ...
<% end %>

The filter form receives a filter object which is created if the controller uses the following concern:

class MyController < ApplicationController
  include FilterResource

  private
  
  def search_klass
    DummySearch # explained above
  end

  def default_search_params
    {} # Params used for search but are not used in the filter form
  end

  def default_filter_params
    {} # Params used in the filter form
  end
end

The controller needs a DummySearch class that will be used to search and filter resources. This class must inherit ResourceSearch base class which includes the default behavior for all search classes.

📌 Related Issues

📋 Subtasks

  • Create static filters for proposals
  • Factor out similar code from meetings to core
  • Refactor backend code
  • Refactor frontend code
  • Update proposals count through ajax
  • Testing
  • Documentation

📷 Screenshots (optional)

image

👻 GIF

@mention-bot
Copy link

@beagleknight, thanks for your PR! By analyzing the history of the files in this pull request, we identified @josepjaume, @oriolgual and @divins to be potential reviewers.

@codecov-io
Copy link

codecov-io commented Jan 10, 2017

Current coverage is 60.18% (diff: 94.91%)

Merging #474 into master will increase coverage by 0.05%

@@             master       #474   diff @@
==========================================
  Files          2824       2833     +9   
  Lines        121487     121685   +198   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits          73048      73231   +183   
- Misses        48439      48454    +15   
  Partials          0          0          

Powered by Codecov. Last update 04f9cbc...27ce8a2

@random_seed = @search.random_seed
@search = ProposalSearch.new(search_params.merge(context_params))
@proposals = @search.results
@random_seed = @search.random_seed

Choose a reason for hiding this comment

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

Trailing whitespace detected.

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Decidim
class ResourceSearch < Searchlight::Search

Choose a reason for hiding this comment

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

Missing top-level class documentation comment.

@proposals = @search.proposals
@random_seed = @search.random_seed
@proposals = search.results
@random_seed = search.random_seed

Choose a reason for hiding this comment

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

Trailing whitespace detected.

@proposals = @search.proposals
@random_seed = @search.random_seed
@proposals = search.results
@random_seed = search.random_seed

Choose a reason for hiding this comment

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

Trailing whitespace detected.

def filter
@filter ||= Filter.new(params[:filter])
end

Choose a reason for hiding this comment

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

Trailing whitespace detected.

def filter
@filter ||= Filter.new(params[:filter])
end

Choose a reason for hiding this comment

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

Trailing whitespace detected.

end

def respond_to_missing?(method_name, include_private = false)
@filter.present? && @filter.has_key?(method_name) || super

Choose a reason for hiding this comment

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

Use Hash#key? instead of Hash#has_key?.

end

def respond_to_missing?(method_name, include_private = false)
@filter.present? && @filter.has_key?(method_name) || super

Choose a reason for hiding this comment

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

Use Hash#key? instead of Hash#has_key?.

end

def method_missing(method_name, *arguments, &block)
@filter.present? && @filter.has_key?(method_name) ? @filter[method_name] : ""

Choose a reason for hiding this comment

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

Use Hash#key? instead of Hash#has_key?.

end

def method_missing(method_name, *arguments, &block)
@filter.present? && @filter.has_key?(method_name) ? @filter[method_name] : ""

Choose a reason for hiding this comment

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

Use Hash#key? instead of Hash#has_key?.

@filter = filter
end

def method_missing(method_name, *arguments, &block)

Choose a reason for hiding this comment

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

When using method_missing, fall back on super.
Unused method argument - arguments. If it's necessary, use _ or _arguments as an argument name to indicate that it won't be used.
Unused method argument - block. If it's necessary, use _ or _block as an argument name to indicate that it won't be used.

@filter = filter
end

def method_missing(method_name, *arguments, &block)

Choose a reason for hiding this comment

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

When using method_missing, fall back on super.
Unused method argument - arguments. If it's necessary, use _ or _arguments as an argument name to indicate that it won't be used.
Unused method argument - block. If it's necessary, use _ or _block as an argument name to indicate that it won't be used.

@oriolgual oriolgual added the wip label Jan 11, 2017
@mention-bot
Copy link

@beagleknight, thanks for your PR! By analyzing the history of the files in this pull request, we identified @mrcasals, @josepjaume and @oriolgual to be potential reviewers.

end
end
end
end

Choose a reason for hiding this comment

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

Final newline missing.

@template.content_tag(:fieldset) do
@template.content_tag(:legend) do
@template.content_tag(:h6, legend_title, class: 'heading6')
end + block.call

Choose a reason for hiding this comment

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

Use yield instead of block.call.

end
end
end
end

Choose a reason for hiding this comment

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

Final newline missing.

@template.content_tag(:div, '', class: "filters__section") do
@template.content_tag(:fieldset) do
@template.content_tag(:legend) do
@template.content_tag(:h6, legend_title, class: 'heading6')

Choose a reason for hiding this comment

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

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@template.content_tag(:fieldset) do
@template.content_tag(:legend) do
@template.content_tag(:h6, legend_title, class: 'heading6')
end + block.call

Choose a reason for hiding this comment

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

Use yield instead of block.call.


# Private: Renders a custom fieldset and execute the given block.
def fieldset_wrapper(legend_title, &block)
@template.content_tag(:div, '', class: "filters__section") do

Choose a reason for hiding this comment

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

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

@template.content_tag(:div, '', class: "filters__section") do
@template.content_tag(:fieldset) do
@template.content_tag(:legend) do
@template.content_tag(:h6, legend_title, class: 'heading6')

Choose a reason for hiding this comment

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

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
block.call b

Choose a reason for hiding this comment

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

Use yield instead of block.call.


# Private: Renders a custom fieldset and execute the given block.
def fieldset_wrapper(legend_title, &block)
@template.content_tag(:div, '', class: "filters__section") do

Choose a reason for hiding this comment

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

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.


# Wrap the check_boxes collection in a custom fieldset.
# It also renders the inputs inside its labels.
def collection_check_boxes(method, collection, value_method, label_method, options = {}, html_options = {}, &block)

Choose a reason for hiding this comment

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

Avoid parameter lists longer than 5 parameters.

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
block.call b

Choose a reason for hiding this comment

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

Use yield instead of block.call.

end

# Wrap the check_boxes collection in a custom fieldset.
# It also renders the inputs inside its labels.

Choose a reason for hiding this comment

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

Trailing whitespace detected.


# Wrap the check_boxes collection in a custom fieldset.
# It also renders the inputs inside its labels.
def collection_check_boxes(method, collection, value_method, label_method, options = {}, html_options = {}, &block)

Choose a reason for hiding this comment

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

Avoid parameter lists longer than 5 parameters.

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
block.call b

Choose a reason for hiding this comment

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

Use yield instead of block.call.

@beagleknight beagleknight force-pushed the feature/proposals/filter branch from b7b1f03 to 3e3989e Compare January 13, 2017 08:13
Copy link
Contributor

@mrcasals mrcasals left a comment

Choose a reason for hiding this comment

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

Good job!

# Creates the SearchLight base query.
# Check if the option feature was provided.
def base_query
# raise order_start_time.inspect
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn 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.

lol xD Forgot to erase that line of RDD (Raise-driven development)

# Initialize the Searchlight::Search base class with the options provided.
#
# scope - The scope used to create the base query
# options - A hash of options to modify the search. These options will be
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you document the options not used by searchlight? Like the paginate ones and other if there are any

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
yield b
Copy link
Contributor

Choose a reason for hiding this comment

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

OriolCI: Avoid variables with meaningless names

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
yield b
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

allow(helper).to receive(:dummies_path)
end

it "should wrap everything in a div with class 'filters'" do
Copy link
Contributor

Choose a reason for hiding this comment

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

it wraps

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok but can you explain why you don't like 'should'?

end
end

it "should call form_for helper with specific arguments" do
Copy link
Contributor

Choose a reason for hiding this comment

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

it calls

let(:builder) { FilterFormBuilder.new(:resource, resource, helper, {}) }

shared_examples "fieldset_wrapper" do
it "should wrap fields in a fieldset inside a div with class 'filters__section'" do
Copy link
Contributor

Choose a reason for hiding this comment

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

remove should everywhere xD

search_text: "",
origin: "all",
category_id: "",
random_seed: params[:random_seed] || @random_seed
Copy link
Contributor

Choose a reason for hiding this comment

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

??? You're passing @random_seed to search but @random_seed it's a value from search.

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! XD

# Handle the search_text filter
def search_search_text
query
.where("title ILIKE ?", "%#{search_text}%")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add an index for this?

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'm agree. Also... we may need to add a better text search in the future.

const { DecidimMeetings: { MeetingsFormFilterComponent } } = exports;
const meetingsFormFilter = new MeetingsFormFilterComponent('form#new_filter');
const { Decidim: { FormFilterComponent } } = exports;
const formFilter = new FormFilterComponent('form#new_filter');
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I like using global DOM ids here. Would it work if we accept a class instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the form is created using the Rails Form Builder it's a trivial change.

@@ -1,12 +1,15 @@
/* eslint-disable no-div-regex, no-useless-escape, no-param-reassign */

/**
* A component that handles the meetings filter form.
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 clarify: "A plain JavaScript component" in order not to get confused with any other framework's components.

*/
_parseLocationFilterValues() {
// Every location param is constructed like this: filter[key]=value
let regexpResult = decodeURIComponent(this._getLocation()).match(/filter\[([^\]]*)\](?:\[\])?=([^&]*)/g);
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there any other way to parse GET params?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no native way to handle query params. You can find a lot of jQuery plugins or npm packages to do that but I think there is no need of that if you can use plain regular expressions. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hey, I'm JavaScript, I know all about the DOM and I know about CORS and x-frame-options but let's not handle URLs properly.

yield form
end
end
filters_container + javascript_include_tag("decidim/filters")
Copy link
Contributor

Choose a reason for hiding this comment

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

You sure this won't break with turbolinks? Does it work if we navigate back / on again?

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 didn't find any issue so far. We are dealing with turbolinks events to bind and unbind change form events. We also use history API to handle the back and forward buttons and update the form values as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it doesn't break because we're using delegation. Cool.


# Handle the category_id filter
def search_category_id
query.where(decidim_category_id: category_ids)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice although not sure if we should make the assumption that category_id will be available in all resources. Maybe we could separate common searches into Query classes if we want to share them amongst resources.

In this scenario, something like:

def search_category_ids
  CategoryFilterQuery.new(query, categories).query
end

or even, if we want to go for the rectify composition way:

def query
  ResourceSearch::BaseQuery.new(@scope)
end

def search_category_ids
  query | CategoryFilterQuery.new(categories)
end

I feel like this is a good place to use rectify query composition. I would even consider removing Searchlight, as it feels like we have some functionality overlap and we could implement this filter ourselves.

Check out Rectify query composition's documentation.

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 think combining Rectify with Searchlight is very useful for complex queries like the category_id filter. I think Searchlight provides us with a simple mechanism for checking the filter properties and adding conditions to the query.
And related to having resources without category_id is not a big deal since the method search_category_id will not be called if the filter doesn't contain a category_id attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@josepjaume Rectify and Searchlight doesn't play nice together... I will not implement this yet but in the future we can rethink about the approach to drop Searchlight in favor of Rectify 😞

Copy link
Contributor

Choose a reason for hiding this comment

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

@beagleknight I KNEW IT HAHAHAHA 💥

Copy link
Contributor

Choose a reason for hiding this comment

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

If you don't send the category_id param, then this method doesn't get executed and will not break, so there's no need to worry about that I guess 😄

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
yield b
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like yield is never being used (coverage reports).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not used because I'm not using the block in my filters but I thought it is a good practise to handle that edge case (since I'm overriding a form builder method).

fieldset_wrapper options[:legend_title] do
super(method, collection, value_method, label_method, options, html_options) do |b|
if block_given?
yield b
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this isn't tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ditto

if origin == "official"
query.where(decidim_author_id: nil)
elsif origin == "citizenship"
query.where.not(decidim_author_id: nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like citizenship filter isn't being tested (coverage)

@beagleknight beagleknight merged commit 6004cee into master Jan 13, 2017
@oriolgual oriolgual deleted the feature/proposals/filter branch January 16, 2017 09:01
@oriolgual oriolgual mentioned this pull request Feb 1, 2017
2 tasks
aitorlb pushed a commit to CodiTramuntana/decidim that referenced this pull request Aug 8, 2019
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.

7 participants