Skip to content

Instantly share code, notes, and snippets.

@rayhamel
Last active October 4, 2019 13:41
Show Gist options
  • Select an option

  • Save rayhamel/a900b50fcc7aeb6a22b5 to your computer and use it in GitHub Desktop.

Select an option

Save rayhamel/a900b50fcc7aeb6a22b5 to your computer and use it in GitHub Desktop.

Revisions

  1. rayhamel revised this gist Apr 22, 2015. 1 changed file with 7 additions and 7 deletions.
    14 changes: 7 additions & 7 deletions ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -60,9 +60,9 @@ services:

    And you're ready for action!

    Let's define the logic behind how upvotes and downvotes work and how we interface with Redis. We're going to want to use this logic in our reviews controller, but we should keep it out of the controller file itself ("skinny controllers"). Instead, let's create a *module* Voting in /lib/voting.rb. A module is a group of methods that can be included in a class.
    Let's define the logic behind how upvotes and downvotes work and how we interface with Redis. We're going to want to use this logic in our reviews controller, but we should keep it out of the controller file itself ("skinny controllers"). Instead, let's create a *module* Votable in /app/controllers/concerns/votable.rb. A module is a group of methods that can be included in a class.

    Redis, like most other NoSQL databases, behaves like a (very fast) hash table, using key-value pairs. This means it can be significantly faster than SQL when you don't care about how two objects are related to each other. (On the other hand, SQL is far better suited to querying or modifying relationships). It can also be far less code, as we shall see.
    Redis, like most other NoSQL databases, behaves like a (very fast) hash table, using key-value pairs. This means it can be significantly faster than SQL when you don't care about how two objects are related to each other. (On the other hand, SQL is far better suited to querying or modifying relationships). NoSQL databases also typically are not as strict about writing database insertions to disk before returning that they were successful, so they can be considerably quicker for writing relatively unimportant data. It can also be far less code, as we shall see.

    There are a number of different datatypes a value can have in a Redis key-value pair. Here we'll be using *sets*, which are essentially arrays, and *strings*, which can be either a simple string or an integer.

    @@ -99,7 +99,9 @@ The docs specify the "Big-O" complexity of each command, a concept you might rec
    The following voting logic will allow for logical "Reddit-style" upvoting and downvoting: a second click on the voting arrow will remove one's vote, downvoting after upvoting will reduce the score by 2 and vice versa (because the upvote is removed and the downvote is added), and each user can only increase or decrease the score by a net of 1. Remember that Redis#srem returns true or false if this logic confuses you.

    ```ruby
    module Voting
    module Votable
    extend ActiveSupport::Concern

    def send_vote(review_id, user_id, direction, opposite)
    unless REDIS.srem("review_#{direction}votes_#{review_id}", user_id)
    REDIS.sadd("review_#{direction}votes_#{review_id}", user_id)
    @@ -142,18 +144,16 @@ end
    AJAX
    ====

    Now let's define controller actions that correspond to these routes, at /app/controllers/reviews_controller.rb. We're going to need to include both the modules we just defined in the class ReviewsController. Files in app/helpers are automatically required, but we need to explicitly require the module Voting, which we've saved in /lib.
    Now let's define controller actions that correspond to these routes, at /app/controllers/reviews_controller.rb. We're going to need to include both the modules we just defined in the class ReviewsController.

    Note that we're deliberately avoiding ActiveRecord queries. That's because we want to accomplish what we're doing with *no SQL queries at all!*

    Since we're using AJAX for this action, we need to tell Rails to respond with JSON instead of HTML. We do this by using the respond_to method and then calling #json, and then tell Rails to create the JSON we want (in this case, the review's updated score). It is possible to tell Rails to respond with still other filetypes, too (CSS, Javascript, XML, etc. etc.).

    ```ruby
    require "voting"

    class ReviewsController < ApplicationController
    include ScoreHelper
    include Voting
    include Votable

    # ...

  2. rayhamel revised this gist Apr 15, 2015. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -7,6 +7,8 @@ Install Redis and the Redis gem (in terminal).

    ```sh
    $ brew install redis
    $ ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
    $ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
    $ gem install redis
    ```

  3. rayhamel revised this gist Apr 15, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -176,7 +176,7 @@ end

    Now we can create the voting buttons in our view. Note that the links do not actually go to anything! That's because we'll be using AJAX. Instead we'll create two HTML attributes, "reviewID" and "path", that we will pass to our script.

    /app/views/tutorials/_reviews.html.erb
    /app/views/tutorials/_reviews.html.erb (in my group project app)

    ```rhtml
    <!-- ... -->
  4. rayhamel revised this gist Apr 15, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -90,7 +90,7 @@ Removes item from the set stored at the key. Returns true if successful, false i

    Returns the number of items in the set stored at the key. Returns 0 if there is no set present at the key, raises an exception if another datatype is stored at the key.

    **I strongly suggest you read the [excellent Redis docs](http://redis.io/commands) to learn about all the Redis commands and datatypes.** You can play around with Redis commands by running "rails c" in the terminal, which will open an irb or pry session with all files in the app required. When you're done playing around, **Redis#flushdb deletes all entries from the database.**
    **I strongly suggest you read the [excellent Redis docs](http://redis.io/commands) to learn about all the Redis commands and datatypes.** You can play around with Redis commands by running "rails c" in the terminal, which will open an irb or pry session with all files in the app required. When you're done playing around, **Redis#flushdb deletes all entries from the database.** The equivalent of "rails c" in Heroku is "heroku run console".

    The docs specify the "Big-O" complexity of each command, a concept you might recall from [my algorithms talk/cheatsheet](https://gist.github.com/rayhamel/784d6864dd59981ced63). You'll note that each command we execute here has a Big-O complexity of Θ(1). In general, you should aim for your Redis commands to be completed in Θ(1) time. If not, you should consider whether it is possible to accomplish the same thing using different commands, a different Redis datatype, or whether SQL would be a better option.

  5. rayhamel revised this gist Apr 15, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -92,7 +92,7 @@ Returns the number of items in the set stored at the key. Returns 0 if there is

    **I strongly suggest you read the [excellent Redis docs](http://redis.io/commands) to learn about all the Redis commands and datatypes.** You can play around with Redis commands by running "rails c" in the terminal, which will open an irb or pry session with all files in the app required. When you're done playing around, **Redis#flushdb deletes all entries from the database.**

    The docs specify the "Big-O" complexity of each command, a concept you might recall from [my algorithms talk/cheatsheet](https://gist.github.com/rayhamel/784d6864dd59981ced63). You'll note that each command we execute here has a Big-O complexity of Θ(1). In general, you should aim for your Redis commands to be completed in Θ(1) time. If not, you should consider whether it is possible to accomplish the same thing using different commands, a different Redis datatype, or whether SQL would be a better option (most SQL queries take).
    The docs specify the "Big-O" complexity of each command, a concept you might recall from [my algorithms talk/cheatsheet](https://gist.github.com/rayhamel/784d6864dd59981ced63). You'll note that each command we execute here has a Big-O complexity of Θ(1). In general, you should aim for your Redis commands to be completed in Θ(1) time. If not, you should consider whether it is possible to accomplish the same thing using different commands, a different Redis datatype, or whether SQL would be a better option.

    The following voting logic will allow for logical "Reddit-style" upvoting and downvoting: a second click on the voting arrow will remove one's vote, downvoting after upvoting will reduce the score by 2 and vice versa (because the upvote is removed and the downvote is added), and each user can only increase or decrease the score by a net of 1. Remember that Redis#srem returns true or false if this logic confuses you.

  6. rayhamel revised this gist Apr 15, 2015. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    This walkthrough uses our group project as an example; we will be creating an upvote/downvote system.

    Redis
    =====

  7. rayhamel revised this gist Apr 15, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -142,7 +142,7 @@ Now let's define controller actions that correspond to these routes, at /app/con

    Note that we're deliberately avoiding ActiveRecord queries. That's because we want to accomplish what we're doing with *no SQL queries at all!*

    Since we're using AJAX for this action, we need to tell Rails to respond with JSON instead of CSS. We do this by using the respond_to method and then .json, and then tell Rails to create the JSON we want (in this case, the review's updated score).
    Since we're using AJAX for this action, we need to tell Rails to respond with JSON instead of HTML. We do this by using the respond_to method and then calling #json, and then tell Rails to create the JSON we want (in this case, the review's updated score). It is possible to tell Rails to respond with still other filetypes, too (CSS, Javascript, XML, etc. etc.).

    ```ruby
    require "voting"
  8. rayhamel created this gist Apr 15, 2015.
    281 changes: 281 additions & 0 deletions ajax_and_redis.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,281 @@
    Redis
    =====

    Install Redis and the Redis gem (in terminal).

    ```sh
    $ brew install redis
    $ gem install redis
    ```

    Add to Gemfile

    ```ruby
    # ...

    gem 'redis'

    # ...
    ```

    Create a new file /config/initializers/redis.rb

    We can now call all the Redis methods using the REDIS constant anywhere in the application.

    Note: Make sure you specify different Redis databases locally if you're using it in more than one app!

    ```ruby
    if Rails.env.development?
    REDIS = Redis.new(host: 'localhost', port: 6379, db: 0)
    elsif Rails.env.test?
    REDIS = Redis.new(host: 'localhost', port: 6379, db: 1)
    else
    uri = URI.parse(ENV["REDISCLOUD_URL"])
    REDIS = Redis.new(host: uri.host, port: uri.port, password: uri.password)
    end
    ```

    Configure Heroku (in terminal).

    Note: Obviously, this won't do anything unless you have already deployed the app to Heroku, and are logged in as the person who deployed the app.

    ```sh
    $ heroku addons:add rediscloud:25
    ```

    Add to .travis.yml

    ```yaml
    # ...

    services:
    - redis-server

    # ...
    ```

    And you're ready for action!

    Let's define the logic behind how upvotes and downvotes work and how we interface with Redis. We're going to want to use this logic in our reviews controller, but we should keep it out of the controller file itself ("skinny controllers"). Instead, let's create a *module* Voting in /lib/voting.rb. A module is a group of methods that can be included in a class.

    Redis, like most other NoSQL databases, behaves like a (very fast) hash table, using key-value pairs. This means it can be significantly faster than SQL when you don't care about how two objects are related to each other. (On the other hand, SQL is far better suited to querying or modifying relationships). It can also be far less code, as we shall see.

    There are a number of different datatypes a value can have in a Redis key-value pair. Here we'll be using *sets*, which are essentially arrays, and *strings*, which can be either a simple string or an integer.

    I'm going to be using the following Redis commands:

    **String:**

    **Redis#set**(key, value)

    Sets a key-value pair using the string datatype. Returns the string "OK", can overwrite other datatypes.

    **Redis#get**(key)

    Returns the string stored at the key. Returns nil if no string found, raises an exception if another datatype is stored at the key.

    **Set:**

    **Redis#sadd**(key, item)

    Adds item to the set stored at the key. Returns true if successful, raises an exception if another datatype is stored at the key.

    **Redis#srem**(key, item)

    Removes item from the set stored at the key. Returns true if successful, false if the item or a set is not present at the key, raises an exception if another datatype is stored at the key.

    **Redis#scard**(key)

    Returns the number of items in the set stored at the key. Returns 0 if there is no set present at the key, raises an exception if another datatype is stored at the key.

    **I strongly suggest you read the [excellent Redis docs](http://redis.io/commands) to learn about all the Redis commands and datatypes.** You can play around with Redis commands by running "rails c" in the terminal, which will open an irb or pry session with all files in the app required. When you're done playing around, **Redis#flushdb deletes all entries from the database.**

    The docs specify the "Big-O" complexity of each command, a concept you might recall from [my algorithms talk/cheatsheet](https://gist.github.com/rayhamel/784d6864dd59981ced63). You'll note that each command we execute here has a Big-O complexity of Θ(1). In general, you should aim for your Redis commands to be completed in Θ(1) time. If not, you should consider whether it is possible to accomplish the same thing using different commands, a different Redis datatype, or whether SQL would be a better option (most SQL queries take).

    The following voting logic will allow for logical "Reddit-style" upvoting and downvoting: a second click on the voting arrow will remove one's vote, downvoting after upvoting will reduce the score by 2 and vice versa (because the upvote is removed and the downvote is added), and each user can only increase or decrease the score by a net of 1. Remember that Redis#srem returns true or false if this logic confuses you.

    ```ruby
    module Voting
    def send_vote(review_id, user_id, direction, opposite)
    unless REDIS.srem("review_#{direction}votes_#{review_id}", user_id)
    REDIS.sadd("review_#{direction}votes_#{review_id}", user_id)
    REDIS.srem("review_#{opposite}votes_#{review_id}", user_id)
    end

    REDIS.set(
    "review_score_#{review_id}", REDIS.scard("review_upvotes_#{review_id}") -
    REDIS.scard("review_downvotes_#{review_id}")
    )
    end
    end
    ```

    We also want to be able to display the score of a review. Since we want to be able to access this method from a view, we will put it in a *helper module* ScoreHelper in /app/helpers/score_helper.rb. Helper modules in the helpers directory are automatically included in all views. Remember that Redis#get returns nil if no value is found if this logic confuses you.

    ```ruby
    module ScoreHelper
    def score(id)
    REDIS.get("review_score_#{id}") || "0"
    end
    end
    ```

    Let's define some routes for upvoting and downvoting in /config/routes.rb.

    Note: Custom routes aren't necessary for Redis or AJAX. However, if you're using the generic "update" route and using jQuery, you can't use $.get or $.post, you must use $.ajax and specify "method" as "PUT" or "PATCH" in the options JSON.

    ```ruby
    Rails.application.routes.draw do
    # ...

    post "reviews/:id/upvote", to: "reviews#upvote", as: "upvote"
    post "reviews/:id/downvote", to: "reviews#downvote", as: "downvote"

    # ...
    end
    ```

    AJAX
    ====

    Now let's define controller actions that correspond to these routes, at /app/controllers/reviews_controller.rb. We're going to need to include both the modules we just defined in the class ReviewsController. Files in app/helpers are automatically required, but we need to explicitly require the module Voting, which we've saved in /lib.

    Note that we're deliberately avoiding ActiveRecord queries. That's because we want to accomplish what we're doing with *no SQL queries at all!*

    Since we're using AJAX for this action, we need to tell Rails to respond with JSON instead of CSS. We do this by using the respond_to method and then .json, and then tell Rails to create the JSON we want (in this case, the review's updated score).

    ```ruby
    require "voting"

    class ReviewsController < ApplicationController
    include ScoreHelper
    include Voting

    # ...

    def upvote
    vote("up", "down")
    end

    def downvote
    vote("down", "up")
    end

    private

    def vote(direction, opposite)
    send_vote(params[:id], current_user.id, direction, opposite)
    respond_to { |format| format.json { render json: score(params[:id]) } }
    end

    # ...
    end
    ```

    Now we can create the voting buttons in our view. Note that the links do not actually go to anything! That's because we'll be using AJAX. Instead we'll create two HTML attributes, "reviewID" and "path", that we will pass to our script.

    /app/views/tutorials/_reviews.html.erb

    ```rhtml
    <!-- ... -->
    <div class="small-1 column">
    <%= link_to (image_tag("chevron-up.png")), '#', class: "upvote",
    reviewID: "#{review.id}", path: "#{upvote_path(review)}" %>
    <h6 class="vote-score" id="review-<%= review.id %>"><%= score(review.id) %></h6>
    <%= link_to (image_tag("chevron-down.png")), '#', class: "downvote",
    reviewID: "#{review.id}", path: "#{downvote_path(review)}" %>
    </div>
    <!-- ... -->
    ```

    Now we can write our script for the AJAX function.

    Here's what this one does, line by line.

    1. After the page is fully loaded and rendered in the browser,
    2. When elements matching the CSS selector ".upvote, .downvote"* are clicked,
    3. Set the variable "reviewID" to that element's HTML attribute reviewID,
    4. Set the variable "path" to that element's HTML attribute path,
    5. Send an AJAX post request to "path", then handle the data the server sends back,
    6. Replace the element representing the score for that review with the new data,
    7. And return from the function. (This prevents snapping to the top of the screen each time the script is run).

    *items with the class "upvote" or the class "downvote"

    /app/assets/javascripts/voting.js

    ```javascript
    $(document).ready(function () {
    $(".upvote, .downvote").click(function () {
    var reviewID = $(this).attr("reviewID");
    var path = $(this).attr("path");
    $.post(path, function (data) {
    $("#review-" + reviewID).text(data);
    });
    return false;
    });
    });
    ```

    And we're done! With Redis and AJAX this is *lightning-fast*; the entire request can be completed in about 12ms on Heroku and 8ms locally.

    Testing AJAX
    ============

    Fact is, Capybara just doesn't play nice with AJAX. Instead of trying to fight Capybara, it's likely better just to write unit tests in pure Rspec.

    Assuming you require authentication for your AJAX function, add this to your /spec/rails_helper.rb file, if it's not already present.

    ```ruby
    RSpec.configure do |config|
    # ...
    config.include Devise::TestHelpers, type: :controller
    end
    ```

    **YOU NEED NOT/SHOULD NOT USE POLTERGEIST OR DATABASE CLEANER OR ANY OTHER GEM, AND YOU NEED NOT MAKE ANY OTHER CHANGES TO YOUR RSPEC CONFIGUATION!**

    Now that we're in Rspec, we have a few bits of functionality that are not available in Capybara, including the Devise #sign_in helper, and the ability to directly make requests to the server.

    Create a new folder /spec/controllers. Our spec file will be /spec/controllers/reviews_controller_spec.rb. We need to require "reviews_controller" as well as "rails_helper", and then our tests should run inside a "describe ReviewsController, type: :controller" block (obviously, substitute "reviews" as needed).

    Flush the Redis database before each test or you may get random failures, and add the "js: true" parameter to each test.

    Next we can sign in as a user, and make our HTTP requests (post, in this case). The first parameter specifies the name of the path, the second is an options hash containing any params (id, in this case) and other options (in this case, that we expect the server response to be in JSON format).

    Then we compare the server response to the value we expect.

    This is not a full feature test, since it does not test that the Javascript we wrote works correctly, but it does check that everything works server-side.

    ```ruby
    require "rails_helper"
    require "reviews_controller"

    # As a user
    # I want to vote on a tutorial's review
    # So that I can voice my opinion on its usefulness

    describe ReviewsController, type: :controller do
    let!(:review) { FactoryGirl.create(:review) }

    before(:each) do
    REDIS.flushdb
    end

    it "should upvote correctly", js: true do
    sign_in review.user
    post(:upvote, id: review.id, format: "json")
    expect(response.body).to eq "1"
    post(:upvote, id: review.id, format: "json")
    expect(response.body).to eq "0"
    end

    it "should downvote correctly", js: true do
    sign_in review.user
    post(:downvote, id: review.id, format: "json")
    expect(response.body).to eq "-1"
    post(:downvote, id: review.id, format: "json")
    expect(response.body).to eq "0"
    end
    end
    ```