Context:
I’m working on a simple (at least for now) CRUD Rails application that is designed for teachers and administrators to create and edit sets of flash cards for students to use as study resources. Currently my table structure consists of Users, Decks, and Cards. A Card belongs_to a deck, and a Deck belongs_to a User. Also, the :users
table has an :admin
boolean, which of course denotes if that user has administrative capabilities, and the :decks
table has an :admin_approved
attribute, which means that it has been reviewed and deemed acceptable as adequate study material for students. What I’m implementing in this blog is a search tool that an administrator can use in order to edit decks that don’t belong to them, especially for being able to change a deck’s :admin_approved
attribute to true.
Code Design:
I wanted to come up with the cleanest, most elegant solution to creating this search tool, so I had to sort through a number of different options. Initially, I was inclined to put the search route and view within the :decks
resources, but as I was working on it, I felt like it was cluttering things up a bit more than I preferred, so I decided to look into decoupling it. From there, I played with the idea of creating a full on DeckSearch resource with its own MVC components, which would allow me to make @deck_search
instances and then utilize Rails’ built in tools to pass and manipulate through the MVC structure. Unfortunately, in order to pass these instances along through the MVC structure, they need to be saved to the database, which I didn’t really want to incorporate at this time. Ultimately, I figured out a way to leverage Rails’ MVC design in a way that decouples the search from the :decks
resource while not having to involve the database in the process.
PORO with a Show Controller and View:
For the model, I created a plain old Ruby object (PORO) DeckSearch
class with attr_accessor :admin_approved, :level, :user_id
. Then, I created a DeckSearchesController
with a show
method, and in my /config/routes.rb
file I set up the one route: get '/deck_search' => "deck_searches#show"
. Lastly, I set up a views directory and show file: /views/deck_searches/show.html.erb
- which will automatically be directed to from the controller thanks to Rails magic. That’s the basic MVC file structure needed for this setup, so let’s look into the code itself.
Model: I already mentioned the attributes needed for this model. Now I just need to set up an initialize method for creating a deck_search object. I also set up some empty string default values for these attributes, which will come in handy with the form in our view. So, here is what our model looks like at this point:
class DeckSearch
attr_accessor :admin_approved, :level, :user_id
DEFAULT_ATTRS = {
admin_approved: "",
level: "",
user_id: ""
}
def initialize(attributes=DEFAULT_ATTRS)
attributes.each{ |key, value| self.send("#{key}=", value) }
end
def decks
# we'll add search logic here later
end
end
Controller: Initially, I was having issues with the controller because I was trying to incorporate an update route in order to update a current search. The tricky part about it was that I couldn’t complete the cycle of passing an instance once it got back to the show route. Ultimately, I decided to just stick with only the show route and just make a new deck search object each time. This streamlined everything. Then, it was just a matter of checking to see if the paramaters were filled out or not to determine if the default paramaters should be used:
class DeckSearchesController < ApplicationController
before_action :validate_admin
before_action :initialize_search
def show
end
private
def deck_search_params
params.permit(:admin_approved, :level, :user_id)
end
def search_params?
deck_search_params != {}
end
def initialize_search
@deck_search = search_params? ? DeckSearch.new(deck_search_params) : DeckSearch.new
end
end
Views: At this point, the only tricky part is getting the syntax right for the rails form, including blank and pre-selected fields. Here is my code (I used partials for the form and table), which worked out just fine in rendering an effective search form and output table.
# /views/deck_searches/show.html.erb
<h1>Search Decks: </h1>
<%= render partial: "search_fields" %>
<% if @deck_search.decks.length == 0 %>
<h3>Sorry, no decks match that search.</h3>
<% else %>
<%= render partial: "deck_table" %>
<% end %>
# /views/deck_searches/search_fields.html.erb
<%= form_tag '/deck_search', method: 'get' do %>
<label>Approved: </label>
<%= select_tag 'admin_approved', options_for_select([ ["Admin Approved", true], ["Not Admin Approved", false] ], @deck_search.admin_approved), include_blank: "All" %><br>
<label>Level: </label>
<%= select_tag 'level', options_for_select(["1", "2", "3", "4", "5", "6", "7"], @deck_search.level), include_blank: "All" %><br>
<label>User: </label>
<%= select_tag 'user_id', options_for_select(User.all.collect{ |u| [u.name, u.id] }, @deck_search.user_id), include_blank: "All" %><br>
<%= submit_tag "Search Decks" %>
<% end %>
# /views/deck_searches/show.html.erb
<table>
<thead>
<tr>
<td>Deck Title: </td>
<td>Approved: </td>
<td>Level: </td>
<td>Creator: </td>
</tr>
</thead>
<tbody>
<% @deck_search.decks.map do |deck| %>
<tr>
<td><%= link_to deck.name, deck_path(deck) %></td>
<td><%= deck.admin_approved ? "Yes" : "No" %></td>
<td><%= deck.level %></td>
<td><%= deck.user.name %></td>
</tr>
<% end %>
</tbody>
</table>
Search Filter:
revised => click here to see a better query method for deck search
Now we just need to employ some search logic to filter out the decks according to the search. I chose to keep all the methods within the class, but I’m sure a proper stickler would argue that it belongs in a module or as a class method within the Deck class. Also, I chose to link them together through by passing them as arguments. The method nested deepest is run first and then moves outwards. I left a note in the deck
instance method. Here is what the finished product for the DeckSearch
class looks like:
class DeckSearch
attr_accessor :admin_approved, :level, :user_id
DEFAULT_ATTRS = {
admin_approved: "",
level: "",
user_id: ""
}
def initialize(attributes=DEFAULT_ATTRS)
attributes.each{ |key, value| self.send("#{key}=", value) }
end
def decks
# 1) filter_admin, 2) filter_level, 3) filter_user
decks_array = filter_user(filter_level(filter_admin))
end
def filter_admin
self.admin_approved == "" ? Deck.all : Deck.where("admin_approved = #{self.admin_approved}")
end
def filter_level(decks_array)
self.level == "" ? decks_array : decks_array.where("level = #{self.level}")
end
def filter_user(decks_array)
self.user_id == "" ? decks_array : decks_array.where("user_id = #{self.user_id}")
end
end
And there is my simple search tool with it’s own MVC structure. Thanks for reading!