JSON API #71

Merged
merged 45 commits into from Aug 4, 2015

Projects

None yet

5 participants

@jaysonvirissimo

This pull request addresses this issue. It adds a RESTful API for getting recent predictions and creating new ones. If it is adequate for this purpose, I will extend it to cover the remaining CRUD operations on predictions.

It works by allowing the user to generate an API key on the settings page and then supplying the key along with each new request. The endpoints, parameter names, and parameter types can be found in the API markdown file in the top level directory.

RSpec tests have been added for the new API controller and the settings view.

jaysonvirissimo added some commits Jul 1, 2015
@jaysonvirissimo jaysonvirissimo add migration for API token 264af29
@jaysonvirissimo jaysonvirissimo add API token column to database schema ad82577
@jaysonvirissimo jaysonvirissimo add methods for generating API tokens to user model 81c6e28
@jaysonvirissimo jaysonvirissimo create predictions controller for API with index and create actions b5efc84
@jaysonvirissimo jaysonvirissimo shorten create action of API controller edce4d4
@jaysonvirissimo jaysonvirissimo add specs for index action of API predictions controller b9fff00
@jaysonvirissimo jaysonvirissimo add method to allow custom limits 0d0a4e7
@jaysonvirissimo jaysonvirissimo prevent user from requesting unlimited amount of predictions and add …
…constant for hard limit
f098ab3
@jaysonvirissimo jaysonvirissimo render auth errors as guard clause in callback 511e33d
@jaysonvirissimo jaysonvirissimo add specs for index action for content type b33d8ec
@jaysonvirissimo jaysonvirissimo create tests for create action of API controller 006e5d1
@jaysonvirissimo jaysonvirissimo fix api specs 918ab7e
@jaysonvirissimo jaysonvirissimo add markdown API documentation 9c7da44
@jaysonvirissimo jaysonvirissimo add new specs for User model 7651bc1
@jaysonvirissimo jaysonvirissimo add generate_api_token action to users controller 3346234
@jaysonvirissimo jaysonvirissimo add route for generating a new API token 6664162
@jaysonvirissimo jaysonvirissimo refactor user model, users controller, and add api token button to se…
…ttings view; broke some tests
7cf468d
@jaysonvirissimo jaysonvirissimo remove placeholder methods bb546f5
@jaysonvirissimo jaysonvirissimo make sure limit option is a integer, not a string 59ab95f
@jaysonvirissimo jaysonvirissimo use stubs in api controller test 799d449
@jaysonvirissimo jaysonvirissimo repair settings view tests 327d26e
@jaysonvirissimo jaysonvirissimo don't test for non-existent methods 2386281
@jaysonvirissimo jaysonvirissimo add test to show API token on settings view cf8c64c
@jaysonvirissimo jaysonvirissimo simplify settings view button and shorted settings view tests bfdbc0c
@jaysonvirissimo jaysonvirissimo add API documentation 9360cd3
@jaysonvirissimo jaysonvirissimo remove unused model factory method 94d878a
This was referenced Jul 24, 2015
@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
@@ -0,0 +1,54 @@
+module Api
+ class PredictionsController < ApplicationController
+ PREDICTIONS_LIMIT = 1000
+
+ before_filter :authenticate_by_api_token
+ before_filter :build_predictions, only: [:index]
+
+ def index
+ render json: @predictions, status: 200
@wezm
wezm Jul 24, 2015 TrikeApps member

status isn't necessary here

@wezm
TrikeApps member
@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+ def authenticate_by_api_token
+ @user = User.find_by_api_token(params[:api_token]) rescue nil
+
+ if @user.nil? || params[:api_token].nil?
+ render json: invalid_message, status: 401
+ end
+ end
+
+ def build_new_prediction
+ prediction_params = params[:prediction] || {}
+
+ unless prediction_params[:private] && @user
+ prediction_params[:private] = @user.private_default
+ end
+
+ @prediction = Prediction.new(prediction_params.merge(creator: @user))
@wezm
wezm Jul 24, 2015 TrikeApps member

@prediction shouldn't be assigned here as the caller does that.

@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+ end
+
+ def create
+ @prediction = build_new_prediction
+
+ if @prediction.save
+ render json: @prediction, status: 200
+ else
+ render json: @prediction.errors, status: 422
+ end
+ end
+
+ private
+
+ def authenticate_by_api_token
+ @user = User.find_by_api_token(params[:api_token]) rescue nil
@wezm
wezm Jul 24, 2015 TrikeApps member

rescue used like this is discouraged. In this case find_by_api_token will return nil if there is no user with the given token.

@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+ @prediction = build_new_prediction
+
+ if @prediction.save
+ render json: @prediction, status: 200
+ else
+ render json: @prediction.errors, status: 422
+ end
+ end
+
+ private
+
+ def authenticate_by_api_token
+ @user = User.find_by_api_token(params[:api_token]) rescue nil
+
+ if @user.nil? || params[:api_token].nil?
+ render json: invalid_message, status: 401
@wezm
wezm Jul 24, 2015 TrikeApps member
@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+module Api
+ class PredictionsController < ApplicationController
+ PREDICTIONS_LIMIT = 1000
+
+ before_filter :authenticate_by_api_token
+ before_filter :build_predictions, only: [:index]
+
+ def index
+ render json: @predictions, status: 200
+ end
+
+ def create
+ @prediction = build_new_prediction
+
+ if @prediction.save
+ render json: @prediction, status: 200
@wezm
wezm Jul 24, 2015 TrikeApps member

Remove status

@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+ PREDICTIONS_LIMIT = 1000
+
+ before_filter :authenticate_by_api_token
+ before_filter :build_predictions, only: [:index]
+
+ def index
+ render json: @predictions, status: 200
+ end
+
+ def create
+ @prediction = build_new_prediction
+
+ if @prediction.save
+ render json: @prediction, status: 200
+ else
+ render json: @prediction.errors, status: 422
@wezm
wezm Jul 24, 2015 TrikeApps member

Use named status

@wezm
wezm Jul 24, 2015 TrikeApps member

Suggest adding a test for this situation

@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+ render json: invalid_message, status: 401
+ end
+ end
+
+ def build_new_prediction
+ prediction_params = params[:prediction] || {}
+
+ unless prediction_params[:private] && @user
+ prediction_params[:private] = @user.private_default
+ end
+
+ @prediction = Prediction.new(prediction_params.merge(creator: @user))
+ end
+
+ def build_predictions
+ if params[:limit] && params[:limit].to_i <= PREDICTIONS_LIMIT
@wezm
wezm Jul 24, 2015 TrikeApps member

You can call to_i on nil so this can be simplified to:

If (1..PREDICTIONS_LIMIT).include?(params[:limit].to_i)
@wezm wezm commented on an outdated diff Jul 24, 2015
app/controllers/api/predictions_controller.rb
+
+ def build_new_prediction
+ prediction_params = params[:prediction] || {}
+
+ unless prediction_params[:private] && @user
+ prediction_params[:private] = @user.private_default
+ end
+
+ @prediction = Prediction.new(prediction_params.merge(creator: @user))
+ end
+
+ def build_predictions
+ if params[:limit] && params[:limit].to_i <= PREDICTIONS_LIMIT
+ @predictions = Prediction.limit(params[:limit].to_i).recent
+ else
+ @predictions = Prediction.limit(100).recent
@wezm
wezm Jul 24, 2015 TrikeApps member

Suggest making the 100 a constant too.

@jaysonvirissimo

@wezm, thanks for reviewing my code. I've attempted to address each of your comments and the PR is ready for another look now.

@wezm
TrikeApps member

Thanks for the improvements Jayson. It's looking good. I had to stop reviewing mid-way on Friday to go to a meeting so I have a couple more comments.

@wezm wezm commented on an outdated diff Jul 27, 2015
app/controllers/users_controller.rb
@@ -71,6 +71,18 @@ def due_for_judgement
@predictions = @predictions.not_private unless current_user == @user
@predictions = @predictions.select { |x| x.due_for_judgement? }
end
+
+ def generate_api_token
+ @user = User.find_by_login(params[:login])
+
+ if @user && @user.update_attributes(api_token: User.generate_api_token)
+ flash[:notice] = "Generated a new API token!"
+ else
+ flash[:error] = "We couldn't generate a new API token, sorry."
@wezm
wezm Jul 27, 2015 TrikeApps member

When providing an error message like this it's good to keep in mind what it would be like as the user to receive the message. In this case it does not give them any idea what to do to remedy the situation (I'm, aware that the other messages in this controller aren't great but may as well not make it worse).

First of all I would suggest adding generate_api_token to the list of actions that require login. You can then assume that current_user will be set with the logged in user and use it instead of @user. The implementation as it stands would allow anyone to generate a new API token for any user they know the username of.

With that change made you know that it was the update_attributes call that failed, which should provide errors. So the message could become something along these lines:

"Unable to generate new API token due to these errors: #{current_user.errors.full_messages.to_sentence}. Please ensure your user profile is complete"
@wezm wezm commented on an outdated diff Jul 27, 2015
app/views/users/settings.html.erb
@@ -14,3 +14,9 @@
<%= f.submit 'Change password' %>
<% end -%>
<h2><%= link_to 'View notifications', [@user,:deadline_notifications] %></h2>
+<br>
+<% if @user.api_token.present? %>
+ <h2>API Token: <%= @user.api_token %></h2>
+<% else %>
+ <%= button_to("Generate API Token", generate_api_token_user_url(login: @user.login)) %>
@wezm
wezm Jul 27, 2015 TrikeApps member

With the previous change made the login param is no longer necessary. Also convention is to use the _path URL builder unless an absolute URL is necessary. E.g. generate_api_token_user_path

@wezm wezm and 1 other commented on an outdated diff Jul 27, 2015
config/routes.rb
@@ -47,5 +48,9 @@
root :to => 'predictions#home'
match '/healthcheck' => 'content#healthcheck'
+
+ namespace :api do
+ resources :predictions
@wezm
wezm Jul 27, 2015 TrikeApps member

Please fix indentation here.

@jaysonvirissimo
jaysonvirissimo Jul 27, 2015

I am confused about why it shows up this way on Github. My editor (Atom) shows no such whitespace.

@wezm wezm commented on an outdated diff Jul 27, 2015
spec/controllers/api/predictions_controller_spec.rb
+
+describe Api::PredictionsController, type: :controller do
+ before(:each) do
+ controller.stub(:set_timezone)
+ end
+
+ describe 'index' do
+ before(:each) do
+ @prediction = build(:prediction)
+ @predictions = [@prediction]
+ end
+
+ context 'with valid API token' do
+ before(:each) do
+ @user = build(:user_with_email)
+ @user.stub(:api_token).and_return('token')
@wezm
wezm Jul 27, 2015 TrikeApps member

Don't think you need to stub api_token here, just assign the token you want to use. E.g. @user.api_token = 'token'. It's better to avoid stubbing if possible because it exercises the actual code.

@wezm wezm commented on an outdated diff Jul 27, 2015
spec/controllers/api/predictions_controller_spec.rb
+ end
+
+ it 'should respond with JSON content type' do
+ get :index, api_token: @user.api_token
+ response.content_type == Mime::JSON
+ end
+
+ it 'should respond with predictions' do
+ get :index, api_token: @user.api_token
+ response.body.should == @recent.to_json
+ end
+ end
+
+ context 'with invalid API token' do
+ before(:each) do
+ User.stub(:find_by_api_token).and_return(nil)
@wezm
wezm Jul 27, 2015 TrikeApps member

The test description and implementation aren't quite aligned here. The description is invalid API token so I think that there is no need to stub find_by_api_token. Instead just pass an invalid token in the get call. E.g. get :index, api_toke: 'thisisnotavalidtoken'

@wezm wezm commented on an outdated diff Jul 27, 2015
spec/controllers/api/predictions_controller_spec.rb
+ get :index
+ response.response_code.should == 401
+ end
+
+ it 'should respond with JSON content type' do
+ get :index
+ response.content_type == Mime::JSON
+ end
+ end
+ end
+
+ describe 'create' do
+ context 'with valid API token' do
+ before(:each) do
+ @user = build(:user_with_email)
+ @user.stub(:api_token).and_return('token')
@wezm
wezm Jul 27, 2015 TrikeApps member

Same comment about stubbing api token here.

@wezm wezm commented on an outdated diff Jul 27, 2015
spec/controllers/api/predictions_controller_spec.rb
+
+ it 'should respond with HTTP failure' do
+ post :create, prediction: @prediction, api_token: @user.api_token
+ response.response_code.should == 422
+ end
+
+ it 'should respond with error messages' do
+ post :create, prediction: @prediction, api_token: @user.api_token
+ response.body.should include('a probability is between 0 and 100%')
+ end
+ end
+ end
+
+ context 'with invalid API token' do
+ before(:each) do
+ User.stub(:find_by_api_token).and_return(nil)
@wezm
wezm Jul 27, 2015 TrikeApps member

Same comment as before about invalid token.

@wezm
TrikeApps member

Ok I'm done now. A few more tweaks and it should be good to go.

@jaysonvirissimo

@wezm, I've attempted to implement all of the suggestions you made regarding my pull request. I really appreciate your detailed feedback. Please let me know if there are any other areas that need improvement and I'll see what I can do.

@jaysonvirissimo

@wezm, do you have any idea when you might have the time to take a look at my changes? I'm eager to get working on a CLI that makes use of the JSON API. Thanks!

@moggyboy

Hi @jaysonvirissimo - @wezm is on leave until Monday week. I'll try to get one of the other guys to take a look at your PR today or on Monday. Cheers.

@jaysonvirissimo
@RenWenshan RenWenshan and 1 other commented on an outdated diff Aug 3, 2015
app/views/users/settings.html.erb
@@ -14,3 +14,9 @@
<%= f.submit 'Change password' %>
<% end -%>
<h2><%= link_to 'View notifications', [@user,:deadline_notifications] %></h2>
+<br>
+<% if @user.api_token.present? %>
+ <h2>API Token: <%= @user.api_token %></h2>
+<% else %>
+ <%= button_to("Generate API Token", generate_api_token_user_path) %>
@RenWenshan
RenWenshan Aug 3, 2015

Shouldn't a user be allowed to generate new tokens?

@RenWenshan RenWenshan merged commit 140d8fa into tricycle:master Aug 4, 2015
@RenWenshan

@jaysonvirissimo merged, thanks a lot for your contribution

@btrettel

Is there a way to add the outcome via the API? Seems like the next logical step.

@jaysonvirissimo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment