Scott Pullenhttp://scottpullen.net2016-01-12T00:00:00-05:00Scott PullenBetter Feedback For Long Running Exports With ActionCablehttp://scottpullen.net/2016/01/12/better-feedback-for-long-running-exports-with-actioncable.html2016-01-12T00:00:00-05:002016-01-12T22:03:38-05:00Scott Pullen<section>
<p>
Since Rails 5 is now in beta and ActionCable has been merged in, I wanted to give it a try. At my current job, we have a few reports that take a while
to build and export (usually to PDF). For this we have a background job that will compile the report together and polling from the front end to see
whether or not the file is ready for download. I'm not a fan of polling as it creates a lot of wasteful requests. Using WebSockets to get broadcasted messages
from the backend seemed like a much more elequent approach. This post will use this simple use case to demonstrate ActionCable
</p>
</section>
<section>
<p>
To demonstrate this use case, we will be searching the PubMed database and returning a list of publications based on a search term. When the user enters
a search term, a background job is created (I'm using Sidekiq to handle this through ActiveJob) and on the front end a modal is displayed with a spinner (although
you could display a progress bar or whatever and update it with information received from the backround job). The background job will query the PubMed database
and add each entry to a CSV. For this example I've capped the number of records to 5000 because the search could take an extremely long time depending on how
general the search term the user has entered. As the search results are retrieved, the background job broadcasts what offset in the search it is currently working
on (as mentioned earlier, this could be used to display and updated). Once the search is complete, a record is created and the file location is attached. A link is
generated for the search result record and a success is broadcasted on the socket. The front end then displays the link so the user can download the file. Let's look
at the code!
</p>
</section>
<section>
<p>
Since Rails 5 is now in beta and ActionCable has been merged in, I wanted to give it a try. At my current job, we have a few reports that take a while
to build and export (usually to PDF). For this we have a background job that will compile the report together and polling from the front end to see
whether or not the file is ready for download. I'm not a fan of polling as it creates a lot of wasteful requests. Using WebSockets to get broadcasted messages
from the backend seemed like a much more elequent approach. This post will use this simple use case to demonstrate ActionCable
</p>
</section>
<section>
<p>
To demonstrate this use case, we will be searching the PubMed database and returning a list of publications based on a search term. When the user enters
a search term, a background job is created (I'm using Sidekiq to handle this through ActiveJob) and on the front end a modal is displayed with a spinner (although
you could display a progress bar or whatever and update it with information received from the backround job). The background job will query the PubMed database
and add each entry to a CSV. For this example I've capped the number of records to 5000 because the search could take an extremely long time depending on how
general the search term the user has entered. As the search results are retrieved, the background job broadcasts what offset in the search it is currently working
on (as mentioned earlier, this could be used to display and updated). Once the search is complete, a record is created and the file location is attached. A link is
generated for the search result record and a success is broadcasted on the socket. The front end then displays the link so the user can download the file. Let's look
at the code!
</p>
</section>
<section>
<h2>Backend Code</h2>
<p>
First let's start on the backend, specifically the channel. For this example, it's actually going to be quite boring. The only thing that needs to be defined
is the stream name.
</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">SearchChannel</span> <span class="o"><</span> <span class="no">ApplicationCable</span><span class="o">::</span><span class="no">Channel</span>
<span class="k">def</span> <span class="nf">subscribed</span>
<span class="n">stream_from</span> <span class="s2">"search_</span><span class="si">#{</span><span class="n">params</span><span class="p">[</span><span class="ss">:search_uuid</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">unsubscribed</span>
<span class="c1"># Any cleanup needed when channel is unsubscribed</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s1">'SearchChannel#unsubscribed'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>
In Rails 5 there is a new directory in the apps directory called channels, where this code will live. You can use the generator to create the template for this
code as well. The generator will also generate the coffeescript to create the subscription to the channel. I decided to just create the channel and javascript
manually since it's pretty basic and I prefer javascript over coffeescript. So let's look at this code, we have a subscribed and a unsubscribed method. The
subscribed method sets up a stream to broadcast over. This is necessary to target a spefic socket. The unsubscribed method is used to do any cleanup that might need
to be done. You can also include other methods that allow for the javascript code to communicate back to the channel. I recommend watching DHH's
<a href="https://www.youtube.com/watch?v=n0WUjGkDFS0" target="_blank">video</a> on how to create a simple chat program, which goes over how to handle this in more detail.
</p>
<p>
Now let's look at the background job, which lives in the app/jobs directory.
</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">PubmedSearchJob</span> <span class="o"><</span> <span class="no">ApplicationJob</span>
<span class="nb">require</span> <span class="s1">'csv'</span>
<span class="n">queue_as</span> <span class="ss">:default</span>
<span class="no">SEARCH_LIMIT</span> <span class="o">=</span> <span class="mi">500</span>
<span class="no">HARD_LIMIT</span> <span class="o">=</span> <span class="mi">5000</span>
<span class="no">STATUS_START</span> <span class="o">=</span> <span class="s1">'start'</span>
<span class="no">STATUS_COMPLETE</span> <span class="o">=</span> <span class="s1">'complete'</span>
<span class="no">STATUS_FAILED</span> <span class="o">=</span> <span class="s1">'failed'</span>
<span class="no">HEADERS</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'pubmed_id'</span><span class="p">,</span> <span class="s1">'pubmed_central_id'</span><span class="p">,</span> <span class="s1">'title'</span><span class="p">,</span> <span class="s1">'author_names'</span><span class="p">,</span> <span class="s1">'publication_date'</span><span class="p">,</span> <span class="s1">'journal'</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">search</span><span class="p">,</span> <span class="n">search_uuid</span><span class="p">)</span>
<span class="n">stream</span> <span class="o">=</span> <span class="s2">"search_</span><span class="si">#{</span><span class="n">search_uuid</span><span class="si">}</span><span class="s2">"</span>
<span class="no">ActionCable</span><span class="p">.</span><span class="nf">server</span><span class="p">.</span><span class="nf">broadcast</span><span class="p">(</span><span class="n">stream</span><span class="p">,</span> <span class="ss">status: </span><span class="no">STATUS_START</span><span class="p">)</span>
<span class="n">offset</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">search_result</span> <span class="o">=</span> <span class="no">Pubmed</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">search</span><span class="p">,</span> <span class="n">offset</span><span class="p">,</span> <span class="no">SEARCH_LIMIT</span><span class="p">)</span>
<span class="n">number_of_articles</span> <span class="o">=</span> <span class="n">search_result</span><span class="p">.</span><span class="nf">count</span>
<span class="no">ActionCable</span><span class="p">.</span><span class="nf">server</span><span class="p">.</span><span class="nf">broadcast</span><span class="p">(</span><span class="n">stream</span><span class="p">,</span> <span class="ss">number_of_articles: </span><span class="n">number_of_articles</span><span class="p">)</span>
<span class="n">search_result_export_filename</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="si">}</span><span class="s2">/tmp/</span><span class="si">#{</span><span class="n">stream</span><span class="si">}</span><span class="s2">.csv"</span>
<span class="no">CSV</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">search_result_export_filename</span><span class="p">,</span> <span class="s1">'wb'</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">csv</span><span class="o">|</span>
<span class="n">csv</span> <span class="o"><<</span> <span class="no">HEADERS</span>
<span class="k">while</span> <span class="n">search_result</span><span class="p">.</span><span class="nf">pubmed_ids</span> <span class="o">&&</span> <span class="p">((</span><span class="n">offset</span> <span class="o">+</span> <span class="no">SEARCH_LIMIT</span><span class="p">)</span> <span class="o"><=</span> <span class="no">HARD_LIMIT</span><span class="p">)</span>
<span class="no">ActionCable</span><span class="p">.</span><span class="nf">server</span><span class="p">.</span><span class="nf">broadcast</span><span class="p">(</span><span class="n">stream</span><span class="p">,</span> <span class="ss">current_offset: </span><span class="n">offset</span><span class="p">)</span>
<span class="n">articles</span> <span class="o">=</span> <span class="no">Pubmed</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">search_result</span><span class="p">.</span><span class="nf">pubmed_ids</span><span class="p">)</span>
<span class="n">articles</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">article</span><span class="o">|</span>
<span class="n">csv</span> <span class="o"><<</span> <span class="p">[</span>
<span class="n">article</span><span class="p">.</span><span class="nf">pubmed_id</span><span class="p">,</span>
<span class="n">article</span><span class="p">.</span><span class="nf">pubmed_central_id</span><span class="p">,</span>
<span class="n">article</span><span class="p">.</span><span class="nf">title</span><span class="p">,</span>
<span class="n">article</span><span class="p">.</span><span class="nf">author_names</span><span class="p">,</span>
<span class="n">article</span><span class="p">.</span><span class="nf">publication_date</span><span class="p">,</span>
<span class="n">article</span><span class="p">.</span><span class="nf">journal</span><span class="p">.</span><span class="nf">title</span>
<span class="p">]</span>
<span class="k">end</span>
<span class="n">offset</span> <span class="o">+=</span> <span class="no">SEARCH_LIMIT</span>
<span class="n">search_result</span> <span class="o">=</span> <span class="no">Pubmed</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">search</span><span class="p">,</span> <span class="n">offset</span><span class="p">,</span> <span class="no">SEARCH_LIMIT</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">search_result_export_file</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">search_result_export_filename</span><span class="p">)</span>
<span class="n">search_result_export</span> <span class="o">=</span> <span class="no">SearchResult</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">search: </span><span class="n">search</span><span class="p">,</span> <span class="ss">search_uuid: </span><span class="n">search_uuid</span><span class="p">,</span> <span class="ss">document: </span><span class="n">search_result_export_file</span><span class="p">)</span>
<span class="k">if</span> <span class="n">search_result_export</span>
<span class="n">download_link</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">url_helpers</span><span class="p">.</span><span class="nf">search_result_path</span><span class="p">(</span><span class="n">search_result_export</span><span class="p">)</span>
<span class="no">ActionCable</span><span class="p">.</span><span class="nf">server</span><span class="p">.</span><span class="nf">broadcast</span><span class="p">(</span><span class="n">stream</span><span class="p">,</span> <span class="ss">status: </span><span class="no">STATUS_COMPLETE</span><span class="p">,</span> <span class="ss">download_link: </span><span class="n">download_link</span><span class="p">)</span>
<span class="k">else</span>
<span class="no">ActionCable</span><span class="p">.</span><span class="nf">server</span><span class="p">.</span><span class="nf">broadcast</span><span class="p">(</span><span class="n">stream</span><span class="p">,</span> <span class="ss">status: </span><span class="no">STATUS_FAILED</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>
I won't go into too much detail since I explained it earlier, but this script will conduct the search and build the results for download. The one thing
I do want to point out is the use of ActionCable.server.broadcast. This is broadcasting the information to any consumers for a specific stream, in this case
it's the search stream. And you can pass back data to the consumers listening. In this example, when the search starts it sends a "started" status, when it fetches
the next offset worth of entries it sends the current offset back, which can be used to provide feedback into the progress of the search, and finally it sends a
"complete" status along with a link or a "failed" status if something something went wrong.
</p>
</section>
<section>
<h2>Frontend</h2>
<p>
First, let's just take a quick look at the html. It contains a form to enter the search term into and some markup for a bootstrap modal.
</p>
<pre class="highlight html"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">></span>
<span class="nt"><h1></span>Pubmed Search<span class="nt"></h1></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"col-md-12"</span><span class="nt">></span>
<span class="nt"><form</span> <span class="na">class=</span><span class="s">"form-inline"</span> <span class="na">id=</span><span class="s">"search-form"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"search"</span> <span class="na">class=</span><span class="s">"control-label"</span><span class="nt">></span>Search:<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">id=</span><span class="s">"search"</span> <span class="na">autocomplete=</span><span class="s">"off"</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"modal fade"</span> <span class="na">id=</span><span class="s">"search-status"</span> <span class="na">tabindex=</span><span class="s">"-1"</span> <span class="na">role=</span><span class="s">"dialog"</span> <span class="na">aria-labelledby=</span><span class="s">"search-status-label"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"modal-dialog"</span> <span class="na">role=</span><span class="s">"document"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"modal-content"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"modal-header"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">"button"</span> <span class="na">class=</span><span class="s">"close"</span> <span class="na">data-dismiss=</span><span class="s">"modal"</span> <span class="na">aria-label=</span><span class="s">"Close"</span><span class="nt">><span</span> <span class="na">aria-hidden=</span><span class="s">"true"</span><span class="nt">></span><span class="ni">&times;</span><span class="nt"></span></button></span>
<span class="nt"><h4</span> <span class="na">class=</span><span class="s">"modal-title"</span> <span class="na">id=</span><span class="s">"search-status-label"</span><span class="nt">></span>Search Results Status<span class="nt"></h4></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"modal-body"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"loading"</span> <span class="na">style=</span><span class="s">"display: none;"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">src=</span><span class="s">"assets/loading_spinner.gif"</span> <span class="na">width=</span><span class="s">"100"</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"search-results"</span> <span class="na">style=</span><span class="s">"display: none;"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"search-results-download"</span> <span class="na">target=</span><span class="s">"_blank"</span><span class="nt">></span>Download Search Results<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"modal-footer"</span><span class="nt">></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">"button"</span> <span class="na">class=</span><span class="s">"btn btn-default"</span> <span class="na">data-dismiss=</span><span class="s">"modal"</span><span class="nt">></span>Close<span class="nt"></button></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre>
<p>
Now for the javascript.
</p>
<pre class="highlight javascript"><code><span class="kd">var</span> <span class="nx">$search</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">'#search'</span><span class="p">);</span>
<span class="nx">$search</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'keypress'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">which</span> <span class="o">===</span> <span class="mi">13</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">searchTerm</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">trim</span><span class="p">(</span><span class="nx">$search</span><span class="p">.</span><span class="nx">val</span><span class="p">());</span>
<span class="k">if</span><span class="p">(</span><span class="nx">searchTerm</span> <span class="o">!==</span> <span class="kc">undefined</span> <span class="o">&&</span> <span class="nx">searchTerm</span> <span class="o">!==</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">$search</span><span class="p">.</span><span class="nx">parent</span><span class="p">().</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'has-error'</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">searchUUID</span> <span class="o">=</span> <span class="nx">guid</span><span class="p">();</span>
<span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="na">url</span><span class="p">:</span> <span class="s1">'/search'</span><span class="p">,</span>
<span class="na">method</span><span class="p">:</span> <span class="s1">'POST'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="p">{</span>
<span class="na">search</span><span class="p">:</span> <span class="nx">searchTerm</span><span class="p">,</span>
<span class="na">search_uuid</span><span class="p">:</span> <span class="nx">searchUUID</span>
<span class="p">}</span>
<span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">$search</span><span class="p">.</span><span class="nx">val</span><span class="p">(</span><span class="s1">''</span><span class="p">);</span>
<span class="nx">displaySearchModal</span><span class="p">(</span><span class="nx">searchUUID</span><span class="p">);</span>
<span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// display error</span>
<span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">$search</span><span class="p">.</span><span class="nx">parent</span><span class="p">().</span><span class="nx">addClass</span><span class="p">(</span><span class="s1">'has-error'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre>
<p>
First, an event handler is set up to listen for keypresses on the search field. If the enter key is pressed and there's some value, a search uuid is
generated and an ajax request is made to the backend. The backend then kicks off the job described earlier.
</p>
<p>
From the previous code, if the background job is successfully created, it calls the displaySearchModal function.
</p>
<pre class="highlight javascript"><code><span class="kd">function</span> <span class="nx">displaySearchModal</span><span class="p">(</span><span class="nx">searchUUID</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">$searchModal</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">'#search-status'</span><span class="p">);</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">modal</span><span class="p">({</span>
<span class="na">keyboard</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">});</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'.loading'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'.search-results'</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'.search-results-download'</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s1">'href'</span><span class="p">,</span> <span class="s1">'#'</span><span class="p">);</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'hide.bs.modal'</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'search model close'</span><span class="p">);</span>
<span class="c1">// handle clean up, potentially cancelling the request</span>
<span class="p">});</span>
<span class="kd">var</span> <span class="nx">search</span> <span class="o">=</span> <span class="nx">App</span><span class="p">.</span><span class="nx">cable</span><span class="p">.</span><span class="nx">subscriptions</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
<span class="na">channel</span><span class="p">:</span> <span class="s2">"SearchChannel"</span><span class="p">,</span>
<span class="na">search_uuid</span><span class="p">:</span> <span class="nx">searchUUID</span>
<span class="p">},</span> <span class="p">{</span>
<span class="na">connected</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'connected'</span><span class="p">);</span>
<span class="p">},</span>
<span class="na">disconnected</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'disconnected'</span><span class="p">);</span>
<span class="nx">search</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">},</span>
<span class="na">received</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
<span class="k">switch</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span><span class="p">)</span> <span class="p">{</span>
<span class="k">case</span> <span class="s1">'complete'</span><span class="p">:</span>
<span class="k">this</span><span class="p">.</span><span class="nx">unsubscribe</span><span class="p">();</span>
<span class="nx">search</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'.search-results-download'</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s1">'href'</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">download_link</span><span class="p">);</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'.loading'</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'.search-results'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="k">break</span><span class="p">;</span>
<span class="k">case</span> <span class="s1">'failed'</span><span class="p">:</span>
<span class="k">this</span><span class="p">.</span><span class="nx">unsubscribe</span><span class="p">();</span>
<span class="nx">search</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="nx">$searchModal</span><span class="p">.</span><span class="nx">modal</span><span class="p">(</span><span class="s1">'hide'</span><span class="p">);</span>
<span class="c1">// display error</span>
<span class="k">break</span><span class="p">;</span>
<span class="nl">default</span><span class="p">:</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre>
<p>
This displays the modal with a spinner and clears out any previous search results. Then a subscription is created to listen to any communications broadcasted
on the stream. The subscription takes in what channel to subscribe to and any parameters to pass to it. If you remember from earlier, the channel definition
defined the stream using params[:search_uuid], this is where it is being set from. Then it has functions to handle when the subscription is connected, disconnected,
and received. The received function is called when data is broadcasted from the backend to the stream. When it receives a status of complete it will hide the spinner
and display a link to the search results export to download. When it receives a status of "failed", it cleans unsubscribes and hides the modal. The subscription
is also where you would define methods to send data back to the channel. Again I'll reference DHH's that I mentioned earlier as it covers how to do this.
</p>
</section>
<section>
<p>
And there you have it. This is obviously a simple use case for ActionCable as we are just listening for results from the channel and not sending data to the
channel. You can view the complete project <a href="https://github.com/spullen/exporter" target="_blank">here</a>.
</p>
</section>Useful Ruby (and Rails) Hash Methodshttp://scottpullen.net/2015/12/28/useful-ruby-hash-methods.html2015-12-28T00:00:00-05:002015-12-28T20:39:38-05:00Scott Pullen<section>
<p>Been pretty busy this past year (so much for trying to make an effort to post more :-/). Thought I make a quick post about some useful hash methods that I've been using lately.</p>
</section>
<section>
<p>Been pretty busy this past year (so much for trying to make an effort to post more :-/). Thought I make a quick post about some useful hash methods that I've been using lately.</p>
</section>
<section>
<p>
For this post, I'll be calling methods against the following hash structure.
</p>
<pre class="highlight ruby"><code><span class="n">my_hash</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">test: </span><span class="s1">'Test'</span><span class="p">,</span> <span class="ss">this: </span><span class="s1">'This'</span><span class="p">,</span> <span class="ss">out: </span><span class="s1">'Out'</span> <span class="p">}</span>
</code></pre>
</section>
<section>
<h2>Hash#fetch</h2>
<p>
I've been using fetch to provide default values for keys that are not provided in hash. It can be really useful for parameters passed in to methods. I mostly provide a default value, but if you don't provide one a KeyError exception will be thrown, which could be useful.
</p>
<pre class="highlight ruby"><code><span class="n">my_hash</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:test</span><span class="p">,</span> <span class="s1">'Default'</span><span class="p">)</span> <span class="c1"># => 'Test'</span>
<span class="n">my_hash</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:other</span><span class="p">,</span> <span class="s1">'Other Default'</span><span class="p">)</span> <span class="c1"># => 'Other Default'</span>
<span class="n">my_hash</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:something</span><span class="err">'</span><span class="p">)</span> <span class="c1"># => throws KeyError</span>
</code></pre>
<p>
This is also useful for values that should be boolean. For instance:
</p>
<pre class="highlight ruby"><code><span class="n">my_hash</span><span class="p">[</span><span class="ss">:conditional</span><span class="p">]</span> <span class="c1"># => nil</span>
<span class="n">my_hash</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:conditional</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span> <span class="c1"># => produces false not nil</span>
<span class="n">my_hash</span><span class="p">[</span><span class="ss">:conditional</span><span class="p">]</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">my_hash</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="ss">:conditional</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span> <span class="c1"># => true</span>
</code></pre>
</section>
<section>
<h2>Hash#values_at</h2>
<p>
values_at is useful to select a subset of the values from the hash given a list of keys.
</p>
<pre class="highlight ruby"><code><span class="n">my_hash</span><span class="p">.</span><span class="nf">values_at</span><span class="p">(</span><span class="ss">:test</span><span class="p">,</span> <span class="ss">:this</span><span class="p">)</span> <span class="c1"># => ['Test', 'This']</span>
</code></pre>
</section>
<section>
<h2>Rails Hash Methods</h2>
<p>I'd also like to point out a few Hash methods that I've been using quite a bit on the Rails end, slice and except.</p>
<h3>slice</h3>
<p>slice returns a new Hash containing only the keys passed in as the arguments.</p>
<pre class="highlight ruby"><code><span class="n">my_hash</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="ss">:test</span><span class="p">)</span> <span class="c1"># => { test: 'Test' }</span>
</code></pre>
<h3>except</h3>
<p>except is the inverse of slice. except returns a new Hash that contains all of the key pairs except for the keys passed in as the argument.</p>
<pre class="highlight ruby"><code><span class="n">my_hash</span><span class="p">.</span><span class="nf">except</span><span class="p">(</span><span class="ss">:test</span><span class="p">)</span> <span class="c1"># => { this: 'This', out: 'Out' }</span>
</code></pre>
<p>I've been utilizing these methods when working on building up a filter set for queries.</p>
</section>
<section>
<p>As I mentioned, these are just some methods that I've been using a lot recently. If anyone has any other methods they find useful feel free to drop a comment.</p>
</section>Token Based Authentication with JWT in Railshttp://scottpullen.net/2015/03/01/token-based-authentication-with-jwt-in-rails.html2015-03-01T00:00:00-05:002015-03-01T22:10:22-05:00Scott Pullen<section>
<p>
This past summer, I worked on a project building out an API for a mobile application. I thought it would be cool to use token based authentication in order to secure the API. To accomplish this authentication scheme I used <a href="http://jwt.io/" target="_blank">JSON Web Tokens</a> and <a href="http://redis.io/" target="_blank">Redis</a>.
</p>
</section>
<section>
<p>
This past summer, I worked on a project building out an API for a mobile application. I thought it would be cool to use token based authentication in order to secure the API. To accomplish this authentication scheme I used <a href="http://jwt.io/" target="_blank">JSON Web Tokens</a> and <a href="http://redis.io/" target="_blank">Redis</a>.
</p>
</section>
<section>
<h2>Gemfile</h2>
<p>
For this project I used the <a href="https://github.com/progrium/ruby-jwt" target="_blank">jwt gem</a> and the <a href="https://github.com/redis/redis-rb" target="_blank">redis gem</a>. The JWT gem provides a nice abstraction for encoding and decoding JWTs.
</p>
<pre class="highlight ruby"><code><span class="n">gem</span> <span class="s1">'jwt'</span>
<span class="n">gem</span> <span class="s1">'redis'</span>
<span class="n">gem</span> <span class="s1">'bcrypt'</span>
</code></pre>
<h2>Models</h2>
<p>
I've also used the bcrypt gem in order to make the actual authentication of the user super easy. With this we just need a User model where the table has an email and password_digest columns. Then in the model itself we just need to call has_secure_password.
</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_secure_password</span>
<span class="c1"># ... everything else.</span>
<span class="k">end</span>
</code></pre>
<p>
Then I created a service object to wrap the authentication call to user, as well as guard against invalid data.
</p>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">Session</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">email</span><span class="p">.</span><span class="nf">blank?</span> <span class="o">||</span> <span class="n">password</span><span class="p">.</span><span class="nf">blank?</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">email</span><span class="p">)</span>
<span class="n">user</span> <span class="o">&&</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="p">?</span> <span class="n">user</span> <span class="p">:</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>Application Controller</h2>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
<span class="n">respond_to</span> <span class="ss">:json</span>
<span class="n">before_action</span> <span class="ss">:authenticate</span>
<span class="kp">protected</span>
<span class="k">def</span> <span class="nf">current_token</span>
<span class="vi">@token</span> <span class="o">||</span> <span class="kp">nil</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">current_user</span>
<span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="vg">$redis</span><span class="p">.</span><span class="nf">hget</span><span class="p">(</span><span class="n">current_token</span><span class="p">,</span> <span class="ss">:user_id</span><span class="p">))</span> <span class="k">if</span> <span class="n">current_token</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">authenticate</span>
<span class="n">authenticate_token</span> <span class="o">||</span> <span class="n">render_unauthorized</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">authenticate_token</span>
<span class="n">authenticate_with_http_token</span> <span class="k">do</span> <span class="o">|</span><span class="n">token</span><span class="p">,</span> <span class="n">options</span><span class="o">|</span>
<span class="vi">@token</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">if</span> <span class="no">AuthToken</span><span class="p">.</span><span class="nf">valid?</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="o">&&</span> <span class="vg">$redis</span><span class="p">.</span><span class="nf">ttl</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="o">></span> <span class="mi">0</span>
<span class="vi">@token</span> <span class="o">=</span> <span class="n">token</span>
<span class="vg">$redis</span><span class="p">.</span><span class="nf">expire</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="mi">20</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">to_i</span><span class="p">)</span> <span class="c1"># set TTL as constant</span>
<span class="k">end</span>
<span class="vi">@token</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">render_unauthorized</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'WWW-Authenticate'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'Token realm="Application"'</span>
<span class="n">render</span> <span class="ss">nothing: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">status: :unauthorized</span><span class="p">,</span> <span class="ss">content_type: </span><span class="s1">'application/json'</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>Session Controller</h2>
<pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">skip_before_action</span> <span class="ss">:authenticate</span><span class="p">,</span> <span class="ss">only: :create</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">Session</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="n">token</span> <span class="o">=</span> <span class="no">AuthToken</span><span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="vg">$redis</span><span class="p">.</span><span class="nf">hset</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="s1">'user_id'</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
<span class="vg">$redis</span><span class="p">.</span><span class="nf">expire</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="mi">20</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">to_i</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">token: </span><span class="n">token</span><span class="p">}</span>
<span class="k">else</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid email or password'</span> <span class="p">},</span> <span class="ss">status: :unauthorized</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="vg">$redis</span><span class="p">.</span><span class="nf">del</span><span class="p">(</span><span class="n">current_token</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">nothing: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">status: :ok</span><span class="p">,</span> <span class="ss">content_type: </span><span class="s1">'application/json'</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>
The final piece is a helper for to issue and validate the tokens using the JWT library. I just made a wrapper method around the JWT calls to simplify things a bit more.
</p>
<pre class="highlight ruby"><code><span class="k">module</span> <span class="nn">AuthToken</span>
<span class="k">def</span> <span class="nc">AuthToken</span><span class="o">.</span><span class="nf">issue</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
<span class="n">payload</span><span class="p">[</span><span class="ss">:created_at</span><span class="p">]</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">utc</span><span class="p">.</span><span class="nf">to_i</span>
<span class="no">JWT</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">secrets</span><span class="p">.</span><span class="nf">secret_key_base</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nc">AuthToken</span><span class="o">.</span><span class="nf">valid?</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
<span class="no">JWT</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">secrets</span><span class="p">.</span><span class="nf">secret_key_base</span><span class="p">)</span> <span class="k">rescue</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
</section>
<section>
<h2>How It Works</h2>
<p>
When a request is made from the client application to the API, the generated token goes in the Authorization header of the HTTP request as such:
</p>
<pre class="highlight plaintext"><code>Authorization: Token token=<the-generated-token>
</code></pre>
<p>
The request will hit the authenticate method in ApplicationController. This then calls the authenticate_token method, which gets the token from the HTTP header using authenticate_with_http_token. Then a check is made to see if the token is valid by seeing if it can be decoded via the JWT library. If the token is valid a request is made via redis to see if the token has expired or not. If it is valid and has not expired the user is then authenticated. The expiration time on the token stored in redis is reset to 20 minutes (this could be any TTL that you want). If any of these checks fail a call to render_unauthorized is made and sent back to the client.
</p>
<p>
While we're going over the ApplicationController, take a look at the current_user method. In order to find the appropriate user for a token, a request is made to redis to get the value of the 'user_id' key. This is what was stored there when the user logged in successfully, which is what we'll talk about next.
</p>
<p>
When a user wants to authenticate with the API, a request is made to sessions#create. The action then checks to see if the user's credentials are valid. If they are a new token is generated for the user. The user id is then stored in Redis nested under the token string. Finally, an expiration time is set for 20 minutes on the token key in Redis.
</p>
<p>
To log the user out, the client will make a request to sessions#destroy and discard the token. The destroy action removes the key from Redis.
</p>
</section>
<section>
<p>
I found this approach to be really straightforward. There isn't too much code involved to get this to work! Some advantages of using JWT for token based authentication is the fact it can store data. If you noticed in the code examples above, the user_id is set as well as when the token was created as the payload of the JWT token. This can potentially be used to pass data around to other services that you use in your application. As long as they have the secret key those services will be able to decode the token and get access to that data. In the same vein, using Redis as a session store could also allow other services that make up your application access to this type of information, as well as provide their own checks to see if the user is logged in or not. Redis also provides the mechanism to expire a key, which is great not to have to worry about. JWT also provides a way set an expiration on the JWT token itself. As of writing this, you can add a reserved key, "exp", to the JWT payload with the time that the token is no longer valid. Now I wouldn't use this expiration by itself, because you would need to issue a new token for every request if you wanted to have a reasonable expiration to the session, which seems kind of annoying to do. But I could see it being used in conjunction with the Redis based expiration to ensure the user re-authenticates at some interval (presumably some large interval).
</p>
<p>
There are some potential drawbacks to using this approach. If a user is logged in and a token is generated, the same token can be copied and potentially used in multiple clients. This could be a problem if you don't use SSL on your application, since it would be open to a man in the middle attack where the token could be taken from (but you sould be using SSL on your application ;)). Similarily, the token could be copied from the client itself and used on aother client. While this wasn't necessarily an issue for the project I was working on since it was a native mobile application (and it would be harder for someone to get access to your phone and to read the value out of memory), this could be an issue on javascript client applications and is something to be aware of. Although, I think the risk is small.
<p>
<p>
I also want to point out, that using Redis as a session store isn't completely necessary in order for this to work. You could have your application set up in a way where the token is valid for as long as the client application has it and it's valid.
</p>
</section>
<section>
<h2>Expansion</h2>
<p>
This strategy seemed to work really well for me and I'll definitely be using it again for future projects (unless someone finds a hole in what's been done). One thing I would like to do is clean up the code slightly (the code in this article is pretty much as it was in the application). While it isn't too bad I definitely think it can be tightened up a bit, especially around the parts with redis.
</p>
</section>
<section>
<h2>Further Reading</h2>
<ul>
<li><a href="http://www.codeschool.com/blog/2014/02/03/token-based-authentication-rails/" target="_blank">Token Based Authentication in Rails</a></li>
</ul>
</section>Redesign Launchhttp://scottpullen.net/2015/02/08/redesign-launch.html2015-02-08T00:00:00-05:002015-02-08T14:29:24-05:00Scott Pullen<section>
<p>
Yesterday, I launched a redesigned version of my site! It was long overdue for a fresh coat of paint. It has also been a long time since I last wrote anything. By redesigning the site I hope it re-invigorates me to write more often. Over the past year or so, I've switched between a few jobs where I've learned a lot of new things and have a lot of new ideas from these experiences, all of which I hope to write about here! Some of the topics I plan on discussing include software architecture, agile methodologies, client-side Javascript development, Java, and of course the world of Ruby and Rails. So stay tuned! To start I'm going to talk about my switch from Wordpress to Middleman as the platform for my site.
</p>
</section>
<section>
<p>
Yesterday, I launched a redesigned version of my site! It was long overdue for a fresh coat of paint. It has also been a long time since I last wrote anything. By redesigning the site I hope it re-invigorates me to write more often. Over the past year or so, I've switched between a few jobs where I've learned a lot of new things and have a lot of new ideas from these experiences, all of which I hope to write about here! Some of the topics I plan on discussing include software architecture, agile methodologies, client-side Javascript development, Java, and of course the world of Ruby and Rails. So stay tuned! To start I'm going to talk about my switch from Wordpress to Middleman as the platform for my site.
</p>
</section>
<section>
<p>
Since I decided to redesign my site, I also wanted to investigate alternatives to Wordpress, which is what I was using prior to the redesign. Wordpress is just a bloated mess. Customizing the how the page is configured just feels painful and unintuitive at times (luckily I only needed to do that once when I was initially setting it up). On top of that, I still had to write some of my posts using html in order to get some posts to look right. Now I'm sure if I spent the time to investigate everything a little more this might not have been an issue... Another thing is the site would occasionally take forever to load (even with caching enabled), which is ridiculous since all I was using it for was blog posts. It wasn't all bad, I really like the admin interface Wordpress comes with. It makes it really easy to manage your articles and content. However, it was still overkill for my needs.
</p>
<p>
A few months ago, I was investigating platforms for my current job to build a redesigned version of our marketing site on. The ones I investigated were <a href="http://refinerycms.com/" target="_blank">Refinery CMS</a>, <a href="http://locomotivecms.com/" target="_blank">Locomotive</a>, <a href="https://github.com/comfy/comfortable-mexican-sofa" target="_blank">Comfortable Mexican Sofa</a>, <a href="http://jekyllrb.com/" target="_blank">Jekyll</a>, and one I hadn't heard of before <a href="https://middlemanapp.com" target="_blank">Middleman</a>. From this list, I narrowed it down to Refinery, Comfy, and Middleman. I pushed for Middleman, but ended up going with Refinery, the more traditional CMS because it was more friendly for non-technical people to use (and at the time I didn't have a better argument to push Middleman further).
</p>
<p>
Middleman is super easy and intuitive to use for both simple content pages, semi-dynamic content pages, and blogging. When you build your middleman application it generates the static html pages for you. Now you might be wondering what I meant by semi-dynamic content means if it generates a static page for you. Well, you can store "dynamic" content in yaml files, and you can use this data on your pages! This to me makes it an extremely nice replacement for bulky CMSes like Wordpress. It is still not as user friendly for non-technical people, but I could totally see an interface that allows it to be (might have to do that if there isn't one already...). Middleman also give you a choice between for page templates, including erb, haml, and markdown. I chose to go with erb since it gave me the greatest control over the page structure, not to mention I was already having to do this with the Wordpress version of my site. The flexibility choose which templating option for any page is nice to have.
</p>
<p>
Middleman also has a pretty good selection of plugins that you can activate for your site. For instance the blogging option is a plugin that you can turn on. When I was still investigating Middleman, I played around with extending it (nothing too crazy), which I found to be really simple and straightforward. I would check out their documentation to see how easy it.
</p>
<p>
The documentation for Middleman is also really well done. It is easy to follow and concise. And if something isn't in the documentation (it most likely is covered) the code for Middleman is also easy to follow and understand. One reason I chose Middleman over Jekyll for my blog was the documentation. Jekyll's documentation made it seem a lot more of a to do. I think it's more of the way it is written and displayed that makes it seem harder to follow.
</p>
</section>Rails Rumble 2013: A Retrospectivehttp://scottpullen.net/2013/10/26/rails-rumble-2013-a-retrospective.html2013-10-26T01:00:00-04:002015-02-07T19:25:08-05:00Scott Pullen<section>
<h2>About the Competition</h2>
<p>
Last weekend I participating in a 48 hour rails development competition called <a href="http://railsrumble.com/" target="_blank">Rails Rumble</a>. The goal of the competition is to develop a rails based application. After the 48 hour period the applications are given feedback by other developers, than by the judges. The judges choose their top 10 favorite applications and the top 10 are declared winners. There’s also a winner for the best public favorite application and one for the best application built by a single person. Judging is base on originality, usefulness, appearance, and completeness.
</p>
</section>
<section>
<h2>The Project</h2>
<p>
The <a href="http://thecrystalship.r13.railsrumble.com/" target="_blank">project</a> that my team, which consisted of my <a href="https://twitter.com/BillPull" target="_blank">brother</a> and I, was to build a platform for collaborative brainstorming. Users create themes around a project, or question to get ideas or more questions on. Members are added to the theme to add their ideas. One goal of this project was to make everything realtime. To accomplish this we used <a href="https://pusher.com/" target="_blank">Pusher</a>, which is a publish/subscribe based system. You can check out the application <a href="http://thecrystalship.r13.railsrumble.com/" target="_blank">here</a>, feel free to leave comments here or on the rails rumble site, any feedback will be appreciated.
</p>
</section>
<section>
<h2>About the Competition</h2>
<p>
Last weekend I participating in a 48 hour rails development competition called <a href="http://railsrumble.com/" target="_blank">Rails Rumble</a>. The goal of the competition is to develop a rails based application. After the 48 hour period the applications are given feedback by other developers, than by the judges. The judges choose their top 10 favorite applications and the top 10 are declared winners. There’s also a winner for the best public favorite application and one for the best application built by a single person. Judging is base on originality, usefulness, appearance, and completeness.
</p>
</section>
<section>
<h2>The Project</h2>
<p>
The <a href="http://thecrystalship.r13.railsrumble.com/" target="_blank">project</a> that my team, which consisted of my <a href="https://twitter.com/BillPull" target="_blank">brother</a> and I, was to build a platform for collaborative brainstorming. Users create themes around a project, or question to get ideas or more questions on. Members are added to the theme to add their ideas. One goal of this project was to make everything realtime. To accomplish this we used <a href="https://pusher.com/" target="_blank">Pusher</a>, which is a publish/subscribe based system. You can check out the application <a href="http://thecrystalship.r13.railsrumble.com/" target="_blank">here</a>, feel free to leave comments here or on the rails rumble site, any feedback will be appreciated.
</p>
</section>
<section>
<h2>The Retrospective</h2>
<p>
This was the first code competition/hackathon that I participated in and I feel like there are a few areas that can be approved upon for future competitions.
</p>
<section>
<h3>Planning</h3>
<p>
This is key, and while we did plan a little for this, I felt like we could have planned a little bit more. I would have liked to have more wire diagrams to show the flow through the application. Along with the wire diagrams, having a design in mind might have been a good idea. Instead we just went with <a href="http://getbootstrap.com/" target="_blank">Twitter Bootstrap</a>, which is great for quickly prototyping the user interface but leaves a lot to be desired in terms of aesthetics.
</p>
<p>
One of the things that we wanted to do was make things snappy, to do this we used <a href="http://knockoutjs.com/" target="_blank">KnockoutJS</a>. It would have been better to design how the front end client javascript at a high level. While I think we did a decent job with this, we did run into a bunch of problems (especially with the pusher integration). If we have thought this out a little better before starting the competition we might have been able to prevent this. UML and sequence diagrams would have come in handy!
</p>
<p>
Divvying up the tasks beforehand would have been helpful, too. In the future I think we need to generate user stories and use a service like <a href="http://www.pivotaltracker.com/" target="_blank">Pivotal Tracker</a>.
</p>
</section>
<section>
<h3>More team members</h3>
<p>
Having more team members would have been helpful, especially if at least one was devoted to coming up with the design. If we might have been able to implement everything we had in mind. We could have also divvied up the work better (given we planned a little better). Another note I would like to make on the team would be to have everyone in the same place. My brother and I did this virtually (since he lives in NYC). While this was definitely doable, it would have been better if we were in the same place, especially when issues came up that we had to find a solution to.
</p>
<p>
With this is mind I think next year’s competition (and other potential competitions) could go more smoothly and produce an even better end result.
</p>
</section>
</section>Rails Application Configurationshttp://scottpullen.net/2013/10/08/rails-application-configurations.html2013-10-08T01:00:00-04:002015-02-09T21:51:17-05:00Scott Pullen<section>
<p>
I find myself repeating my configuration all the time when creating a new rails application. I decided to give rails templates a try after watching this <a href="http://railscasts.com/episodes/148-custom-app-generators-revised" target="_blank">railscast</a> on app generators. In it Ryan Bates goes over having a railsrc file, application templates, and application builders. One problem I did run into while trying out the strategies from the railscast was the application builder, I’m using rails 4 and it seems like they have gotten rid of this option, application templates still exist.
</p>
</section>
<section>
<p>
I find myself repeating my configuration all the time when creating a new rails application. I decided to give rails templates a try after watching this <a href="http://railscasts.com/episodes/148-custom-app-generators-revised" target="_blank">railscast</a> on app generators. In it Ryan Bates goes over having a railsrc file, application templates, and application builders. One problem I did run into while trying out the strategies from the railscast was the application builder, I’m using rails 4 and it seems like they have gotten rid of this option, application templates still exist.
</p>
</section>
<section>
<h2>railsrc</h2>
<p>
The first thing I did was create a .railsrc file. This is pretty straightforward, when you run rails new my_app it will use the options defined in the .railsrc file.
</p>
<pre class="highlight shell"><code>-d mysql --skip-test-unit > ~/.railsrc
</code></pre>
<p>
Here I’m stating that I want to use mysql for my database and to skip the generation of the test unit directory.
</p>
</section>
<section>
<h2>Application Template</h2>
<p>
The application template is a way to define your configuration.
</p>
<p>
To run the template you just have to do:
<pre class="highlight shell"><code>rails new my_app -m /path/to/app_template.rb
</code></pre>
</p>
<p>
Here is an example of my application template.
<pre class="highlight ruby"><code><span class="c1"># change README to markdown</span>
<span class="n">remove_file</span> <span class="s1">'README.rdoc'</span>
<span class="n">create_file</span> <span class="s1">'README.md'</span><span class="p">,</span> <span class="s1">'TODO'</span>
<span class="c1"># set up additional application folders</span>
<span class="n">keep_file</span> <span class="s1">'app/services'</span>
<span class="n">keep_file</span> <span class="s1">'app/presenters'</span>
<span class="n">keep_file</span> <span class="s1">'app/forms'</span>
<span class="c1"># Set up gems</span>
<span class="n">gem</span> <span class="s1">'jquery-ui-rails'</span>
<span class="n">gem</span> <span class="s1">'haml'</span>
<span class="n">gem</span> <span class="s1">'will_paginate'</span>
<span class="n">gem</span> <span class="s1">'has_scope'</span>
<span class="n">gem</span> <span class="s1">'carrierwave'</span>
<span class="n">gem</span> <span class="s1">'cocoon'</span>
<span class="n">gem</span> <span class="s1">'cancan'</span>
<span class="n">gem</span> <span class="s1">'american_date'</span>
<span class="c1"># test and development gems</span>
<span class="n">gem_group</span> <span class="ss">:test</span><span class="p">,</span> <span class="ss">:development</span> <span class="k">do</span>
<span class="n">gem</span> <span class="s1">'debugger'</span>
<span class="n">gem</span> <span class="s1">'rspec-rails'</span>
<span class="n">gem</span> <span class="s1">'capybara'</span>
<span class="n">gem</span> <span class="s1">'poltergeist'</span>
<span class="n">gem</span> <span class="s1">'database_cleaner'</span>
<span class="n">gem</span> <span class="s1">'shoulda'</span>
<span class="n">gem</span> <span class="s1">'factory_girl_rails'</span>
<span class="n">gem</span> <span class="s1">'timecop'</span>
<span class="k">end</span>
<span class="c1"># simple_form configuration</span>
<span class="n">simple_form_bootstrap</span> <span class="o">=</span> <span class="kp">false</span>
<span class="k">if</span> <span class="n">install_simple_form</span> <span class="o">=</span> <span class="n">yes?</span><span class="p">(</span><span class="s1">'Install Simple Form?'</span><span class="p">)</span>
<span class="n">gem</span> <span class="s1">'simple_form'</span>
<span class="n">simple_form_bootstrap</span> <span class="o">=</span> <span class="n">yes?</span><span class="p">(</span><span class="s1">'Use bootstrap configuration for simple form?'</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># devise confirguration</span>
<span class="n">devise_model_name</span> <span class="o">=</span> <span class="s1">'User'</span>
<span class="k">if</span> <span class="n">install_devise</span> <span class="o">=</span> <span class="n">yes?</span><span class="p">(</span><span class="s1">'Install Devise?'</span><span class="p">)</span>
<span class="n">gem</span> <span class="s1">'devise'</span>
<span class="k">if</span> <span class="n">no?</span><span class="p">(</span><span class="s1">'Create default devise User model?'</span><span class="p">)</span>
<span class="n">devise_model_name</span> <span class="o">=</span> <span class="n">ask</span><span class="p">(</span><span class="s1">'Devise model name?'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># run the bundle command</span>
<span class="n">run</span> <span class="s1">'bundle install'</span>
<span class="c1"># install rspec</span>
<span class="n">generate</span> <span class="s1">'rspec:install'</span>
<span class="n">keep_file</span> <span class="s1">'spec/acceptance'</span>
<span class="c1"># install simple form</span>
<span class="k">if</span> <span class="n">install_simple_form</span>
<span class="n">simple_form_command</span> <span class="o">=</span> <span class="s1">'simple_form:install'</span>
<span class="n">simple_form_command</span> <span class="o">+=</span> <span class="s1">' --bootstrap'</span> <span class="k">if</span> <span class="n">simple_form_bootstrap</span>
<span class="n">generate</span> <span class="n">simple_form_command</span>
<span class="k">end</span>
<span class="c1"># install devise</span>
<span class="k">if</span> <span class="n">install_devise</span>
<span class="n">generate</span> <span class="s1">'devise:install'</span>
<span class="n">generate</span> <span class="s1">'devise'</span><span class="p">,</span> <span class="n">devise_model_name</span>
<span class="k">end</span>
<span class="c1"># create the ruby version and gemset files</span>
<span class="n">run</span> <span class="s1">'rvm list'</span>
<span class="n">rvm_ruby_version</span> <span class="o">=</span> <span class="n">ask</span><span class="p">(</span><span class="s1">'Ruby Version?'</span><span class="p">)</span>
<span class="n">run</span> <span class="s1">'rvm gemset list'</span>
<span class="n">rvm_ruby_gemset</span> <span class="o">=</span> <span class="n">ask</span><span class="p">(</span><span class="s1">'Ruby Gemset?'</span><span class="p">)</span>
<span class="n">create_file</span> <span class="s1">'.ruby-version'</span><span class="p">,</span> <span class="n">rvm_ruby_version</span>
<span class="n">create_file</span> <span class="s1">'.ruby-gemset'</span><span class="p">,</span> <span class="n">rvm_ruby_gemset</span>
<span class="c1"># git initialization</span>
<span class="n">git</span> <span class="ss">:init</span>
<span class="n">append_file</span> <span class="s1">'.gitignore'</span><span class="p">,</span> <span class="s1">'config/database.yml'</span>
<span class="n">append_file</span> <span class="s1">'.gitignore'</span><span class="p">,</span> <span class="s1">'public/uploads'</span>
<span class="n">run</span> <span class="s1">'cp config/database.yml config/database.yml.example'</span>
<span class="n">git</span> <span class="ss">add: </span><span class="s1">'.'</span><span class="p">,</span> <span class="ss">commit: </span><span class="s1">'-m "initial commit"'</span>
</code></pre>
</p>
</section>
<section>
<p>
I’ve created a github repo for my rails configurations which you can find <a href="https://github.com/spullen/rails_app_template" target="_blank">here</a>.
</p>
</section>