From 9d9ffce476c2ada0bb8942d8d6935e52e2e631bd Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sat, 5 Jan 2013 17:44:58 -0500 Subject: [PATCH 01/28] update readme --- README.md | 73 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b027d96..aa36785 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,46 @@ -Temporary Rails API +Secret Revelations API (work in progress) =================== ###[ Staged at wiglepedia.org ]( wiglepedia.org ) -Getting Mods +####Tags +_Anywhere there is a ``, it will hence be referred to as a tag; all tags in this documentation are designed to serve as placeholders for some variable input_ + +####Optional +_Anywhere there is a `[arbitrary text here]`, it should be considered optional._ + +####Routes +_A "route" is the part of the URL that follows the hostname; i.e. the hostname is [ wiglepedia.org ]( wiglepedia.org ), `wiglepedia.org/mods` has a route of `/mods`_ + +Authentication +-------------- +The API uses [ HTTP Digest Authentication ]( http://en.wikipedia.org/wiki/Http_digest_authentication ) for authorizing user-agents. If a user-agent attempts to access a protected resource it will receive an HTTP 401 challenge response with the `WWW_Authorize` header set; therefore, authentication may take place at any time, on any resource. If the user-agent has already successfully authenticated it may access any protected resource (for the respective credentials) by simply setting the `Authorize` header in the request. + +_NOTE: the `status` key/property in the JSON response for authentications is designed to reflect the meaning of the respective HTTP status code, i.e.: 200 = Success/OK, 400 = Bad Request, 201 = Created, etc._ + +###Signing In +If you simply want to authenticate with the API to set your `Authorize` request header, request the `/users` resource with the GET HTTP method. + +####Response +If the request was successful the response will be a JSON object whose structure is: +`{status: 200, message: 'successfully authenticated', user: {username: , email: , password_hash: } } + +If unsuccessful the response will be a 401 Unauthorized with HTML body saying 'HTTP Digest: Access denied.' + +_NOTE: this soon to be replaced with an HTTP 200 response with a JSON body containing something like `{status: 401, message: }`_ + +###Registering +To register a user account with the api submit a request to the `/users` resource with the HTTP POST method. The POST data should look like the following: +`user[email]=&user[password_hash]=::">&user[username]=` + +#####Validation Restrictions +The API will consider any request whose email and/or username are already in use invalid. The request must contain email, username and password (nothing more). + +#####Example Using Curl: +`curl -vd "user%5Bemail%5D=test@example.com%40gmail.com&user%5Bpassword_hash%5D=0101713f36fc52cd5a51dd03f43a7e98user%5Busername%5D=tester" http://localhost:3001/v1/users` + + +Getting Resources ------------ -_Note: This API will respond to HTTP methods other than GET but is intended only for use with GET and will only retrieve resources, not create or modify them_ Currently the root route returns all mods. This data is encoded in JSON, if the response to any request is a single record, that objec is returned, if it is more than one record the response is an array. Schema @@ -14,32 +50,21 @@ Mods ---- Currently a mod has the following schema: - _______________________________ - | FIELD | TYPE | - |-------------------|---------| - | id | integer | - | name | varchar | - | author | varchar | - | minecraft_version | varchar | - | forum_url | varchar | - \-----------------------------/ +| Field | Type | +|-------------------|---------| +| id | integer | +| name | varchar | +| author | varchar | +| minecraft_version | varchar | +| forum_url | varchar | Categories ---------- Categories have the following schema: - _______________________________ - | FIELD | TYPE | - |-------------------|---------| - | name | string | - \-----------------------------/ - -Routes -====== -_A "route" is the part of the URL that follows the hostname; i.e. the hostname is [ secretrevelations.herokuapp.com ]( secretrevelations.herokuapp.com ), `secretrevelations.herokuapp.com/mods` has a route of `/mods`_ - -####Tags -_Anywhere there is a ``, it will hence be referred to as a tag; all tags in this documentation are designed to serve as placeholders for some variable input_ +| Field | Type | +|-------------------|---------| +| name | string | Mods ---- From dcf9b2fd9363c03fb2c0d1c1abb94f12b579f285 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sat, 5 Jan 2013 22:32:19 -0500 Subject: [PATCH 02/28] update users#create to return user object on successful login --- app/controllers/v1/users_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index f748727..9321bc4 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -9,11 +9,11 @@ def index def create user = User.new(params[:user]) begin user.save! - @response = {status: 201, message: 'successfully created user'} + @response = {status: 201, message: 'successfully created user', user: current_user} rescue ActiveRecord::RecordInvalid @response = {status: 400, message: $!.to_s} ensure - render json: {message: @response} + render json: @response end end From 5ac6e1c2d312eddc7abad3164dce362fb3896684 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 00:13:33 -0500 Subject: [PATCH 03/28] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa36785..f88179b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ To register a user account with the api submit a request to the `/users` resourc The API will consider any request whose email and/or username are already in use invalid. The request must contain email, username and password (nothing more). #####Example Using Curl: -`curl -vd "user%5Bemail%5D=test@example.com%40gmail.com&user%5Bpassword_hash%5D=0101713f36fc52cd5a51dd03f43a7e98user%5Busername%5D=tester" http://localhost:3001/v1/users` +`curl -vd "user%5Bemail%5D=test@example.com%40gmail.com&user%5Bpassword_hash%5D=0101713f36fc52cd5a51dd03f43a7e98&user%5Busername%5D=tester" http://localhost:3001/v1/users` Getting Resources From 32b4f2a64e67bd3780b8f7792f0d65f113227e07 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 00:52:15 -0500 Subject: [PATCH 04/28] add else to begin..end block in users#create, return user object in unsuccessful users#create, add jsonp callback to users#create --- app/controllers/v1/users_controller.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 9321bc4..662643e 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -9,11 +9,14 @@ def index def create user = User.new(params[:user]) begin user.save! - @response = {status: 201, message: 'successfully created user', user: current_user} + #TODO: log user in if possible, maybe set session[:user_id] manually + @response = {status: 201, message: 'successfully created user', user: user} rescue ActiveRecord::RecordInvalid + @response = {status: 400, message: $!.to_s, user: user} + else @response = {status: 400, message: $!.to_s} ensure - render json: @response + render json: @response, callback: params[:callback] end end From d0a457c585b80034de9e493061dc8cefb5b7eeb2 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 01:28:46 -0500 Subject: [PATCH 05/28] move mods_controller & categories_controller into v1 & update routes. update readme --- README.md | 32 ++++++++++++++----- .../{ => v1}/categories_controller.rb | 2 +- app/controllers/{ => v1}/mods_controller.rb | 2 +- config/routes.rb | 20 ++++++------ 4 files changed, 36 insertions(+), 20 deletions(-) rename app/controllers/{ => v1}/categories_controller.rb (66%) rename app/controllers/{ => v1}/mods_controller.rb (94%) diff --git a/README.md b/README.md index f88179b..8729a0d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,22 @@ Secret Revelations API (work in progress) =================== ###[ Staged at wiglepedia.org ]( wiglepedia.org ) +###Versioning +####Current Version: v1 +This API is versioned for maintainability. Anywhere where the tag like `` the user-agent should substitute a version (preferably the current, as above). +Any time a request is made to resource of a version older than the current an alert, warning or error object will be appended to any JSON object returned to the user-agent. This object will look like the following: +`{, (alert | warning | error): }` + +####Alert / Warning / Error +An alert is intended to notify the user-agent that a newer version is available +i.e.: `{, alert: "a newer version: v2 is available"}` + +A warning is intended to notify the user-agent that the requested version will soon be deprecated +i.e.: `{, warning: "this resource version will be deprecated soon"}` + +An error is inteded to notify the user-agent that the requested version IS deprecated and cannot be retrieved +i.e.: `{, error: "this resource version is deprecated, please update"}` + ####Tags _Anywhere there is a ``, it will hence be referred to as a tag; all tags in this documentation are designed to serve as placeholders for some variable input_ @@ -72,14 +88,14 @@ Mods ###All Mods *WARNING:* there are currently upwards of 14,000 records in the DB. If you're running low on memory I don't recommend pointing your browser to this resource as they will be returned as text in your browser -`/mods` +`//mods` ###Single Mod by ID -`/mods/` +`//mods/` ###Range of Mods _Note: `[]` (square brackets) indicate optional parameters_ -`/mods/count//[offset/]` +`//mods/count//[offset/]` Searching for Mods ------------------ @@ -87,21 +103,21 @@ _All searches are currently performed using an SQL where clause like this: `WHER _The use of periods and spaces is permitted in all searches_ ###By Name -`/mods/name/` +`//mods/name/` ###By Minecraft Version -`/mods/version/` +`//mods/version/` ###By Author Handle _Author handles are those of the [ curse ]( http://www.curse.com/ ) user who posted the minecraft forum topic that the mod originated from_ -`/mods/author/` +`//mods/author/` ###How Many Mods? _Returns an integer that is the total number of mods in the DB currently_ -`/mods/total` +`//mods/total` Categories ---------- ###All Categories -`/categories` +`//categories` diff --git a/app/controllers/categories_controller.rb b/app/controllers/v1/categories_controller.rb similarity index 66% rename from app/controllers/categories_controller.rb rename to app/controllers/v1/categories_controller.rb index 5dd4e18..58e44c7 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/v1/categories_controller.rb @@ -1,4 +1,4 @@ -class CategoriesController < ApplicationController +class V1::CategoriesController < ApplicationController def index @categories = Category.all render json: @categories, callback: params[:callback] diff --git a/app/controllers/mods_controller.rb b/app/controllers/v1/mods_controller.rb similarity index 94% rename from app/controllers/mods_controller.rb rename to app/controllers/v1/mods_controller.rb index 344492c..3a4a4e2 100644 --- a/app/controllers/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -1,4 +1,4 @@ -class ModsController < ApplicationController +class V1::ModsController < ApplicationController def index @mods = Mod.all render json: @mods, callback: params[:callback] diff --git a/config/routes.rb b/config/routes.rb index 82c2353..032468e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,21 +1,21 @@ SecretrevApi::Application.routes.draw do namespace :v1 do + #TODO: implement alert/warning/error system for user-agents accessing old verisons, etc. resources :categorizations, except: :edit resources :users, except: :edit - end - - resources :categories, except: :edit + resources :categories, except: :edit - get 'mods/total' => 'mods#total' - resources :mods, except: :edit + get 'mods/total' => 'mods#total' + resources :mods, except: :edit - get 'mods/name/:q' => 'mods#name', :q => /.*/ - get 'mods/version/:q' => 'mods#version', :q => /.*/ - get 'mods/author/:q' => 'mods#author', :q => /.*/ - get 'mods/count/:count(/offset/:offset)' => 'mods#count', :count => /\d+/, :offset => /\d+/ - get 'categories' => 'categories#index' + get 'mods/name/:q' => 'mods#name', :q => /.*/ + get 'mods/version/:q' => 'mods#version', :q => /.*/ + get 'mods/author/:q' => 'mods#author', :q => /.*/ + get 'mods/count/:count(/offset/:offset)' => 'mods#count', :count => /\d+/, :offset => /\d+/ + get 'categories' => 'categories#index' + end root :to => 'mods#index' From d9cee6ec3f88feb5914cee181c02c0ddf1406506 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 01:54:41 -0500 Subject: [PATCH 06/28] update readme --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8729a0d..b981379 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,24 @@ Secret Revelations API (work in progress) ###Versioning ####Current Version: v1 -This API is versioned for maintainability. Anywhere where the tag like `` the user-agent should substitute a version (preferably the current, as above). -Any time a request is made to resource of a version older than the current an alert, warning or error object will be appended to any JSON object returned to the user-agent. This object will look like the following: -`{, (alert | warning | error): }` +This API is versioned for maintainability. Anywhere there is a `` tag, the user-agent should substitute a version (preferably the current, as above). -####Alert / Warning / Error -An alert is intended to notify the user-agent that a newer version is available -i.e.: `{, alert: "a newer version: v2 is available"}` +####Version Check +A user-agent may check the current version with this route: `/version` +A user-agent may also check the status of it's the any version with this route: `/version/`. The response will be a JSON object containing an alert, warning or an error. -A warning is intended to notify the user-agent that the requested version will soon be deprecated -i.e.: `{, warning: "this resource version will be deprecated soon"}` +#####Alert / Warning / Error +An alert is intended to notify the user-agent that a newer version is available i.e.: -An error is inteded to notify the user-agent that the requested version IS deprecated and cannot be retrieved -i.e.: `{, error: "this resource version is deprecated, please update"}` +`{alert: "a newer version: v2 is available"}` + +A warning is intended to notify the user-agent that the requested version will soon be deprecated i.e.: + +`{warning: "this resource version will be deprecated soon"}` + +An error is inteded to notify the user-agent that the requested version IS deprecated and cannot be retrieved i.e.: + +`{error: "this resource version is deprecated, please update"}` ####Tags _Anywhere there is a ``, it will hence be referred to as a tag; all tags in this documentation are designed to serve as placeholders for some variable input_ From 3b7d9dfe38913fa14faccb1fc581982f0f576ffb Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 16:11:20 -0500 Subject: [PATCH 07/28] add CORS support and meta_request gem for development (rails panel chrome extension) --- Gemfile | 1 + Gemfile.lock | 6 ++++++ app/controllers/application_controller.rb | 24 ++++++++++++++++++++++- app/controllers/v1/users_controller.rb | 2 ++ config/routes.rb | 1 + 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 3cda477..eb2f22d 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'awesome_print' gem "rspec-rails", ">= 2.11.4", :group => [:development, :test] +gem "meta_request", group: :development gem "database_cleaner", ">= 0.9.1", :group => :test gem "capybara" gem "factory_girl_rails", ">= 4.1.0", :group => [:development, :test]# To use ActiveModel has_secure_password diff --git a/Gemfile.lock b/Gemfile.lock index c1c7a98..541de57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,6 +70,9 @@ GEM i18n (>= 0.4.0) mime-types (~> 1.16) treetop (~> 1.4.8) + meta_request (0.2.1) + rack-contrib + rails method_source (0.8.1) mime-types (1.19) multi_json (1.3.7) @@ -83,6 +86,8 @@ GEM rack (1.4.1) rack-cache (1.2) rack (>= 0.4) + rack-contrib (1.1.0) + rack (>= 0.9.1) rack-ssl (1.3.2) rack rack-test (0.6.2) @@ -146,6 +151,7 @@ DEPENDENCIES capybara database_cleaner (>= 0.9.1) factory_girl_rails (>= 4.1.0) + meta_request pg pry rails (= 3.2.9) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 95c1ad1..8237647 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,7 +7,7 @@ class ApplicationController < ActionController::API def authenticate authenticate_or_request_with_http_digest(REALM) do |username| - user = User.find_by_username(username) || raise('no user found') + user = User.find_by_username(username) || fail('no user found') session[:user_id] = user.id user.password_hash end @@ -16,4 +16,26 @@ def authenticate def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end + + # For all responses in this controller, return the CORS access control headers. + + def cors_set_access_control_headers + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' + headers['Access-Control-Max-Age'] = "1728000" + end + + # If this is a preflight OPTIONS request, then short-circuit the + # request, return only the necessary headers and return an empty + # text/plain. + + def cors_preflight_check + if request.method == :options + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' + headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version' + headers['Access-Control-Max-Age'] = '1728000' + render :text => '', :content_type => 'text/plain' + end + end end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 662643e..380b636 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -1,5 +1,7 @@ class V1::UsersController < ApplicationController before_filter :authenticate, except: :create + before_filter :cors_preflight_check, only: :create + after_filter :cors_set_access_control_headers, only: :create def index #logs the user in using `before_filter` diff --git a/config/routes.rb b/config/routes.rb index 032468e..e23a521 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,7 @@ #TODO: implement alert/warning/error system for user-agents accessing old verisons, etc. resources :categorizations, except: :edit resources :users, except: :edit + match 'users' => 'users#create', :constraints => {:method => 'OPTIONS'} resources :categories, except: :edit From 1251a6d6a90be611931408b3988f110c9760183f Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 16:16:48 -0500 Subject: [PATCH 08/28] @bugfix: POST to users#create always returns {status: 400 ...} --- app/controllers/v1/users_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 380b636..fed7ffe 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -15,7 +15,7 @@ def create @response = {status: 201, message: 'successfully created user', user: user} rescue ActiveRecord::RecordInvalid @response = {status: 400, message: $!.to_s, user: user} - else + rescue @response = {status: 400, message: $!.to_s} ensure render json: @response, callback: params[:callback] From f47a2088bffab5620ef286816926c2f35789021e Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 16:38:37 -0500 Subject: [PATCH 09/28] respond with appropriate HTTP status code matching the JSON responses' status property --- app/controllers/v1/users_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index fed7ffe..a4918b0 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -18,7 +18,7 @@ def create rescue @response = {status: 400, message: $!.to_s} ensure - render json: @response, callback: params[:callback] + render json: @response, callback: params[:callback], status: @response[:status] end end From c645f0dd98bfe7417b089ff577d67924facd98b1 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 6 Jan 2013 22:33:32 -0500 Subject: [PATCH 10/28] add endpoint for marking mods as broken --- app/controllers/application_controller.rb | 6 +++- .../v1/categorizations_controller.rb | 33 +++++-------------- app/controllers/v1/mods_controller.rb | 15 ++++++++- app/models/categorization.rb | 2 ++ app/models/category.rb | 1 + app/models/mod.rb | 8 ++++- db/schema.rb | 14 ++++++-- 7 files changed, 49 insertions(+), 30 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8237647..d788430 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,7 +7,11 @@ class ApplicationController < ActionController::API def authenticate authenticate_or_request_with_http_digest(REALM) do |username| - user = User.find_by_username(username) || fail('no user found') + user = User.find_by_username(username) + unless user + render json: {status: 401, message: 'Access Denied'}, status: 401 + return + end session[:user_id] = user.id user.password_hash end diff --git a/app/controllers/v1/categorizations_controller.rb b/app/controllers/v1/categorizations_controller.rb index 94f9b8b..c0e60e9 100644 --- a/app/controllers/v1/categorizations_controller.rb +++ b/app/controllers/v1/categorizations_controller.rb @@ -7,31 +7,16 @@ def index end def create - #before_filter :restrict_access - - # @categorizations = [] - # params[:category].each do |category| - # category.each do |category_name| - # end - # end - #binding.pry - categorization = Categorization.new - categorization.mod_id = params[:mod_id] - categorization.user_id = params[:user_id] - categorization.category_id = params[:category_id] - if categorization.save - - head :ok - else - head :bad_request + categorization = Categorization.new params[:categorization] + begin + categorization.save! + rescue ActiveRecord::RecordInvalid + + rescue + + ensure + end end - - #private - #def restict_access - # authenticate_or_request_with_http_token do |token, options| - # ApiKey.exists?(access_token: token) - # end - #end end diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 3a4a4e2..960b774 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -1,14 +1,27 @@ class V1::ModsController < ApplicationController + before_filter :authenticate, only: :broken + def index @mods = Mod.all render json: @mods, callback: params[:callback] end + def uncategorized + @mods = Mod.uncategorized.where(broken: false).limit(100) + end + def show - @mod = Mod.find(params['id']) + @mod = Mod.find params[:id] render json: @mod, callback: params[:callback] end + def broken + @mod = Mod.find params[:id] + @mod.broken = true + Break.create {user: current_user, mod: @mod} + @mod.save! + end + def name @mods = Mod.where "name like ?", "%#{params[:q]}%" render json: @mods, callback: params[:callback] diff --git a/app/models/categorization.rb b/app/models/categorization.rb index f1ca260..af65c5d 100644 --- a/app/models/categorization.rb +++ b/app/models/categorization.rb @@ -1,3 +1,5 @@ class Categorization < ActiveRecord::Base attr_accessible :category_id, :mod_id, :user_id + belongs_to :category + belongs_to :mod end diff --git a/app/models/category.rb b/app/models/category.rb index 8ece2ba..5ce4589 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,4 +1,5 @@ class Category < ActiveRecord::Base attr_accessible :name, :description has_and_belongs_to_many :mods + has_many :categorizations end diff --git a/app/models/mod.rb b/app/models/mod.rb index 9d91bbb..cb8e1bc 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -1,4 +1,10 @@ class Mod < ActiveRecord::Base - attr_accessible :author, :forum_url, :minecraft_version, :name, :created_at + attr_readonly :author, :forum_url, :minecraft_version, :name, :created_at + attr_accessible :broken has_and_belongs_to_many :categories + has_many :categorizations + + #scope :uncategorized, all #categorizations.count < 10 + + #where categorizations end diff --git a/db/schema.rb b/db/schema.rb index 90a71d6..cf17163 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20121216234819) do +ActiveRecord::Schema.define(:version => 20130107023717) do create_table "api_keys", :force => true do |t| t.string "access_token" @@ -20,6 +20,13 @@ t.datetime "updated_at", :null => false end + create_table "breaks", :force => true do |t| + t.string "user_id" + t.string "mod_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "categories", :force => true do |t| t.string "name" t.datetime "created_at", :null => false @@ -45,8 +52,9 @@ t.string "minecraft_version" t.string "forum_url" t.string "author" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.boolean "broken", :default => false end create_table "users", :force => true do |t| From 33cd7db3ea53a1b4ef65e767a16d092c29639d4e Mon Sep 17 00:00:00 2001 From: Bryan White Date: Mon, 7 Jan 2013 11:33:34 -0500 Subject: [PATCH 11/28] add endpoint for uncategorized mods, and incomplete mods --- app/controllers/v1/categorizations_controller.rb | 14 ++++++-------- app/controllers/v1/mods_controller.rb | 12 ++++++++++-- app/models/categorization.rb | 6 +++++- app/models/mod.rb | 6 ++++-- app/models/user.rb | 1 + config/routes.rb | 7 +++++-- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/controllers/v1/categorizations_controller.rb b/app/controllers/v1/categorizations_controller.rb index c0e60e9..fc1c6e0 100644 --- a/app/controllers/v1/categorizations_controller.rb +++ b/app/controllers/v1/categorizations_controller.rb @@ -1,5 +1,4 @@ class V1::CategorizationsController < ApplicationController - before_filter :authenticate def index @@ -7,16 +6,15 @@ def index end def create - categorization = Categorization.new params[:categorization] - begin - categorization.save! + categorization = Categorization.new params[:categorization] + begin categorization.save! + @response = {status: 201, message: 'successfully created categorization', categorization: categorization} rescue ActiveRecord::RecordInvalid - + @response = {status: 400, message: $!.to_s, categorization: categorization} rescue - + @response = {status: 400, message: $!.to_s} ensure - + render json: @response, callback: params[:callback], status: @response[:status] end - end end diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 960b774..89ad1db 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -6,8 +6,16 @@ def index render json: @mods, callback: params[:callback] end + # incomplete returns mods that have < 10 categorzations by any user + def incomplete + @mods = Mod.where(broken: false).limit(params[:count]).incomplete + render json: @mods, callback: params[:callback] + end + + # uncategorized returns mod that have not been categorized by a single user def uncategorized - @mods = Mod.uncategorized.where(broken: false).limit(100) + @mods = Mod.uncategorized(current_user.id).where(broken: false).limit(params[:count]) + render json: @mods, callback: params[:callback] end def show @@ -18,7 +26,7 @@ def show def broken @mod = Mod.find params[:id] @mod.broken = true - Break.create {user: current_user, mod: @mod} + Break.create Hash[user: current_user, mod: @mod] @mod.save! end diff --git a/app/models/categorization.rb b/app/models/categorization.rb index af65c5d..cf7b061 100644 --- a/app/models/categorization.rb +++ b/app/models/categorization.rb @@ -1,5 +1,9 @@ class Categorization < ActiveRecord::Base - attr_accessible :category_id, :mod_id, :user_id + attr_accessible :category, :mod, :user + belongs_to :user belongs_to :category belongs_to :mod + + validates_presence_of :category, :mod, :user + validates :user_id, :uniqueness => {:scope => [:mod_id, :category_id]} end diff --git a/app/models/mod.rb b/app/models/mod.rb index cb8e1bc..5d890d4 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -4,7 +4,9 @@ class Mod < ActiveRecord::Base has_and_belongs_to_many :categories has_many :categorizations - #scope :uncategorized, all #categorizations.count < 10 + scope :incomplete, find_by_sql('select * from mods where id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') - #where categorizations + def self.uncategorized user_id + where "select mod_id from categorizations group by mod_id where user_id not #{user_id}" + end end diff --git a/app/models/user.rb b/app/models/user.rb index 511baae..06c04b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,7 @@ class User < ActiveRecord::Base attr_accessible :email, :username, :password_hash #attr_protected :password_hash + has_many :categorizations validates_presence_of :email, :username, :password_hash validates_uniqueness_of :email, :username diff --git a/config/routes.rb b/config/routes.rb index e23a521..3b6e3c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,12 +9,15 @@ resources :categories, except: :edit get 'mods/total' => 'mods#total' - resources :mods, except: :edit - get 'mods/name/:q' => 'mods#name', :q => /.*/ get 'mods/version/:q' => 'mods#version', :q => /.*/ get 'mods/author/:q' => 'mods#author', :q => /.*/ get 'mods/count/:count(/offset/:offset)' => 'mods#count', :count => /\d+/, :offset => /\d+/ + get 'mods/uncategorized(/:count)' => 'mods#uncategorized', :count => /\d+/ + get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ + + resources :mods, except: :edit + get 'categories' => 'categories#index' end From 9d18158088becb7e58f209037235e9400ab2215f Mon Sep 17 00:00:00 2001 From: Bryan White Date: Mon, 7 Jan 2013 12:00:05 -0500 Subject: [PATCH 12/28] forgot to `git add` for last 2 commits --- app/models/break.rb | 5 +++++ db/migrate/20130107021312_add_broken_to_mods.rb | 5 +++++ db/migrate/20130107023717_create_breaks.rb | 10 ++++++++++ spec/factories/breaks.rb | 8 ++++++++ spec/models/break_spec.rb | 5 +++++ 5 files changed, 33 insertions(+) create mode 100644 app/models/break.rb create mode 100644 db/migrate/20130107021312_add_broken_to_mods.rb create mode 100644 db/migrate/20130107023717_create_breaks.rb create mode 100644 spec/factories/breaks.rb create mode 100644 spec/models/break_spec.rb diff --git a/app/models/break.rb b/app/models/break.rb new file mode 100644 index 0000000..1dc38bc --- /dev/null +++ b/app/models/break.rb @@ -0,0 +1,5 @@ +class Break < ActiveRecord::Base + attr_accessible :mod, :user + belongs_to :mod + belongs_to :user +end diff --git a/db/migrate/20130107021312_add_broken_to_mods.rb b/db/migrate/20130107021312_add_broken_to_mods.rb new file mode 100644 index 0000000..7e78214 --- /dev/null +++ b/db/migrate/20130107021312_add_broken_to_mods.rb @@ -0,0 +1,5 @@ +class AddBrokenToMods < ActiveRecord::Migration + def change + add_column :mods, :broken, :boolean, default: false + end +end diff --git a/db/migrate/20130107023717_create_breaks.rb b/db/migrate/20130107023717_create_breaks.rb new file mode 100644 index 0000000..43cbc17 --- /dev/null +++ b/db/migrate/20130107023717_create_breaks.rb @@ -0,0 +1,10 @@ +class CreateBreaks < ActiveRecord::Migration + def change + create_table :breaks do |t| + t.string :user_id + t.string :mod_id + + t.timestamps + end + end +end diff --git a/spec/factories/breaks.rb b/spec/factories/breaks.rb new file mode 100644 index 0000000..c0727ff --- /dev/null +++ b/spec/factories/breaks.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :break do + user_id "MyString" + mod_id "MyString" + end +end diff --git a/spec/models/break_spec.rb b/spec/models/break_spec.rb new file mode 100644 index 0000000..e19edb8 --- /dev/null +++ b/spec/models/break_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Break do + pending "add some examples to (or delete) #{__FILE__}" +end From 7f5ce7b0d5e56299b29129f18aef277239a68cb6 Mon Sep 17 00:00:00 2001 From: nembus Date: Thu, 10 Jan 2013 23:54:47 -0500 Subject: [PATCH 13/28] Update rails gem to 3.2.11. Change Allow-Control-Access-Headers to include X-Requested-With and Content-Type. --- Gemfile | 2 +- Gemfile.lock | 60 +++++++++++------------ app/controllers/application_controller.rb | 1 + 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/Gemfile b/Gemfile index eb2f22d..2947f1d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '3.2.9' +gem 'rails' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' diff --git a/Gemfile.lock b/Gemfile.lock index 541de57..e32c4ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,12 +11,12 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (3.2.9) - actionpack (= 3.2.9) + actionmailer (3.2.11) + actionpack (= 3.2.11) mail (~> 2.4.4) - actionpack (3.2.9) - activemodel (= 3.2.9) - activesupport (= 3.2.9) + actionpack (3.2.11) + activemodel (= 3.2.11) + activesupport (= 3.2.11) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) @@ -24,18 +24,18 @@ GEM rack-cache (~> 1.2) rack-test (~> 0.6.1) sprockets (~> 2.2.1) - activemodel (3.2.9) - activesupport (= 3.2.9) + activemodel (3.2.11) + activesupport (= 3.2.11) builder (~> 3.0.0) - activerecord (3.2.9) - activemodel (= 3.2.9) - activesupport (= 3.2.9) + activerecord (3.2.11) + activemodel (= 3.2.11) + activesupport (= 3.2.11) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.9) - activemodel (= 3.2.9) - activesupport (= 3.2.9) - activesupport (3.2.9) + activeresource (3.2.11) + activemodel (= 3.2.11) + activesupport (= 3.2.11) + activesupport (3.2.11) i18n (~> 0.6) multi_json (~> 1.0) arel (3.0.2) @@ -63,7 +63,7 @@ GEM hike (1.2.1) i18n (0.6.1) journey (1.0.4) - json (1.7.5) + json (1.7.6) libwebsocket (0.1.6) websocket mail (2.4.4) @@ -75,7 +75,7 @@ GEM rails method_source (0.8.1) mime-types (1.19) - multi_json (1.3.7) + multi_json (1.5.0) nokogiri (1.5.5) pg (0.14.1) polyglot (0.3.3) @@ -83,7 +83,7 @@ GEM coderay (~> 1.0.5) method_source (~> 0.8) slop (~> 3.3.1) - rack (1.4.1) + rack (1.4.3) rack-cache (1.2) rack (>= 0.4) rack-contrib (1.1.0) @@ -92,22 +92,22 @@ GEM rack rack-test (0.6.2) rack (>= 1.0) - rails (3.2.9) - actionmailer (= 3.2.9) - actionpack (= 3.2.9) - activerecord (= 3.2.9) - activeresource (= 3.2.9) - activesupport (= 3.2.9) + rails (3.2.11) + actionmailer (= 3.2.11) + actionpack (= 3.2.11) + activerecord (= 3.2.11) + activeresource (= 3.2.11) + activesupport (= 3.2.11) bundler (~> 1.0) - railties (= 3.2.9) - railties (3.2.9) - actionpack (= 3.2.9) - activesupport (= 3.2.9) + railties (= 3.2.11) + railties (3.2.11) + actionpack (= 3.2.11) + activesupport (= 3.2.11) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - rake (10.0.2) + rake (10.0.3) rdoc (3.12) json (~> 1.4) rspec-core (2.12.0) @@ -128,7 +128,7 @@ GEM multi_json (~> 1.0) rubyzip slop (3.3.3) - sprockets (2.2.1) + sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) @@ -154,6 +154,6 @@ DEPENDENCIES meta_request pg pry - rails (= 3.2.9) + rails rails-api! rspec-rails (>= 2.11.4) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d788430..b7033bf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,6 +26,7 @@ def current_user def cors_set_access_control_headers headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' + headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, Content-Type' headers['Access-Control-Max-Age'] = "1728000" end From 75eb380aa76c688361c9f61a084dac9cbed90c24 Mon Sep 17 00:00:00 2001 From: nembus Date: Fri, 11 Jan 2013 21:46:42 -0500 Subject: [PATCH 14/28] In the middle of challenges... --- app/controllers/challenges_controller.rb | 2 ++ app/models/challenge.rb | 3 ++ config/routes.rb | 7 +++++ .../20130112015302_create_challenges.rb | 11 ++++++++ .../controllers/challenges_controller_spec.rb | 28 +++++++++++++++++++ spec/factories/challenges.rb | 9 ++++++ spec/models/challenge_spec.rb | 5 ++++ test/fixtures/categories.yml | 7 +++++ test/functional/categories_controller_test.rb | 7 +++++ test/unit/category_test.rb | 7 +++++ 10 files changed, 86 insertions(+) create mode 100644 app/controllers/challenges_controller.rb create mode 100644 app/models/challenge.rb create mode 100644 db/migrate/20130112015302_create_challenges.rb create mode 100644 spec/controllers/challenges_controller_spec.rb create mode 100644 spec/factories/challenges.rb create mode 100644 spec/models/challenge_spec.rb create mode 100644 test/fixtures/categories.yml create mode 100644 test/functional/categories_controller_test.rb create mode 100644 test/unit/category_test.rb diff --git a/app/controllers/challenges_controller.rb b/app/controllers/challenges_controller.rb new file mode 100644 index 0000000..c74240d --- /dev/null +++ b/app/controllers/challenges_controller.rb @@ -0,0 +1,2 @@ +class ChallengesController < ApplicationController +end diff --git a/app/models/challenge.rb b/app/models/challenge.rb new file mode 100644 index 0000000..006b4db --- /dev/null +++ b/app/models/challenge.rb @@ -0,0 +1,3 @@ +class Challenge < ActiveRecord::Base + attr_accessible :answer, :link, :question +end diff --git a/config/routes.rb b/config/routes.rb index 3b6e3c9..24214ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,10 @@ SecretrevApi::Application.routes.draw do + resources :challenges, except: :edit + + resources :challenges + + namespace :v1 do #TODO: implement alert/warning/error system for user-agents accessing old verisons, etc. resources :categorizations, except: :edit @@ -17,6 +22,8 @@ get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ resources :mods, except: :edit + get 'challenge' => 'challenge#random' + post 'challenge' => 'challenge#check' get 'categories' => 'categories#index' end diff --git a/db/migrate/20130112015302_create_challenges.rb b/db/migrate/20130112015302_create_challenges.rb new file mode 100644 index 0000000..b2ad3e6 --- /dev/null +++ b/db/migrate/20130112015302_create_challenges.rb @@ -0,0 +1,11 @@ +class CreateChallenges < ActiveRecord::Migration + def change + create_table :challenges do |t| + t.text :question + t.string :answer + t.string :link + + t.timestamps + end + end +end diff --git a/spec/controllers/challenges_controller_spec.rb b/spec/controllers/challenges_controller_spec.rb new file mode 100644 index 0000000..beb8c93 --- /dev/null +++ b/spec/controllers/challenges_controller_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ChallengesController do + + describe "#random" do + before :each do + + end + + it "should create a session" do + session[:challenge_id].should_not be blank? + end + it "should return a random challenge" do + pending + end + end + + describe "#check" do + it "should require that a session exist" do + session[:challenge_id].should_not be blank? + end + + it "should accept a response" do + pending + end + + end +end diff --git a/spec/factories/challenges.rb b/spec/factories/challenges.rb new file mode 100644 index 0000000..c1f948f --- /dev/null +++ b/spec/factories/challenges.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :challenge do + question "MyText" + answer "MyString" + link "MyString" + end +end diff --git a/spec/models/challenge_spec.rb b/spec/models/challenge_spec.rb new file mode 100644 index 0000000..1c666f2 --- /dev/null +++ b/spec/models/challenge_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Challenge do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml new file mode 100644 index 0000000..0227c60 --- /dev/null +++ b/test/fixtures/categories.yml @@ -0,0 +1,7 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html + +one: + name: MyString + +two: + name: MyString diff --git a/test/functional/categories_controller_test.rb b/test/functional/categories_controller_test.rb new file mode 100644 index 0000000..12ca7c1 --- /dev/null +++ b/test/functional/categories_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class CategoriesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/unit/category_test.rb b/test/unit/category_test.rb new file mode 100644 index 0000000..4733541 --- /dev/null +++ b/test/unit/category_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class CategoryTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From bd204492eeefe14fbe960db20af6b5b1f1735660 Mon Sep 17 00:00:00 2001 From: nembus Date: Fri, 11 Jan 2013 23:26:21 -0500 Subject: [PATCH 15/28] WIP... Controller testing... --- app/controllers/categories_controller.rb | 6 ---- app/controllers/challenges_controller.rb | 2 -- app/controllers/mods_controller.rb | 36 ------------------- app/controllers/v1/challenges_controller.rb | 5 +++ app/models/challenge.rb | 1 + db/schema.rb | 10 +++++- .../controllers/challenges_controller_spec.rb | 25 +++++++++---- 7 files changed, 34 insertions(+), 51 deletions(-) delete mode 100644 app/controllers/categories_controller.rb delete mode 100644 app/controllers/challenges_controller.rb delete mode 100644 app/controllers/mods_controller.rb create mode 100644 app/controllers/v1/challenges_controller.rb diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb deleted file mode 100644 index ebdc33d..0000000 --- a/app/controllers/categories_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class CategoriesController < ApplicationController - def index - @categories = Category.all - render json: @categories - end -end diff --git a/app/controllers/challenges_controller.rb b/app/controllers/challenges_controller.rb deleted file mode 100644 index c74240d..0000000 --- a/app/controllers/challenges_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ChallengesController < ApplicationController -end diff --git a/app/controllers/mods_controller.rb b/app/controllers/mods_controller.rb deleted file mode 100644 index 901ef2f..0000000 --- a/app/controllers/mods_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class ModsController < ApplicationController - def index - @mods = Mod.all - render json: @mods - end - - def show - @mod = Mod.find(params['id']) - render json: @mod - end - - def name - @mods = Mod.where "name like ?", "%#{params[:q]}%" - render json: @mods - end - - def version - @mods = Mod.where "minecraft_version like ?", "%#{params[:q]}%" - render json: @mods - end - - def author - @mods = Mod.where "author like ?", "%#{params[:q]}%" - render json: @mods - end - - def count - @mods = Mod.limit(params[:count]).offset(params[:offset]) - render json: @mods - end - - def total - @total = Mod.all.length - render json: @total - end -end diff --git a/app/controllers/v1/challenges_controller.rb b/app/controllers/v1/challenges_controller.rb new file mode 100644 index 0000000..796ad50 --- /dev/null +++ b/app/controllers/v1/challenges_controller.rb @@ -0,0 +1,5 @@ +class V1::ChallengesController < ApplicationController + def random + session[:challenge_id] = 1000 + end +end diff --git a/app/models/challenge.rb b/app/models/challenge.rb index 006b4db..bffdeb3 100644 --- a/app/models/challenge.rb +++ b/app/models/challenge.rb @@ -1,3 +1,4 @@ class Challenge < ActiveRecord::Base attr_accessible :answer, :link, :question + validates_presence_of :answer end diff --git a/db/schema.rb b/db/schema.rb index cf17163..d11dce2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130107023717) do +ActiveRecord::Schema.define(:version => 20130112015302) do create_table "api_keys", :force => true do |t| t.string "access_token" @@ -47,6 +47,14 @@ t.datetime "updated_at", :null => false end + create_table "challenges", :force => true do |t| + t.text "question" + t.string "answer" + t.string "link" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "mods", :force => true do |t| t.string "name" t.string "minecraft_version" diff --git a/spec/controllers/challenges_controller_spec.rb b/spec/controllers/challenges_controller_spec.rb index beb8c93..fb1d8a0 100644 --- a/spec/controllers/challenges_controller_spec.rb +++ b/spec/controllers/challenges_controller_spec.rb @@ -1,23 +1,36 @@ require 'spec_helper' -describe ChallengesController do +describe V1::ChallengesController do - describe "#random" do + describe "GET #random" do before :each do end it "should create a session" do - session[:challenge_id].should_not be blank? + pending + session[:challenge_id].blank?.should_not be true end it "should return a random challenge" do - pending + challenges = FactoryGirl.create_list :challenge, 20 + random_challenge_ids = [] + (1..3).each do |count| + get :challenge + + current_id = assigns(:challenge).id + + if random_challenge_ids.find(current_id) + fail + end + + random_challenge_ids << current_id + end end end - describe "#check" do + describe "POST #check" do it "should require that a session exist" do - session[:challenge_id].should_not be blank? + session[:challenge_id].blank?.should_not be true end it "should accept a response" do From cdb51fdc98be7795feeb59d779ee377105a517e1 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Thu, 17 Jan 2013 06:53:15 -0500 Subject: [PATCH 16/28] fix mod 'uncategotized' and 'incomplete' scopes queries. include cookies and session::cookiestore middlewares. rewrite mods_controller#uncategorized to do previous behaviour if current_user exists, execute #incomplete otherwise --- app/controllers/application_controller.rb | 9 ++++----- app/controllers/v1/mods_controller.rb | 10 +++++++--- app/models/mod.rb | 4 ++-- config/application.rb | 3 +++ spec/v1/mods_controller_spec.rb | 13 +++++++++++++ 5 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 spec/v1/mods_controller_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d788430..8956d41 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,14 +7,13 @@ class ApplicationController < ActionController::API def authenticate authenticate_or_request_with_http_digest(REALM) do |username| - user = User.find_by_username(username) - unless user + @user = User.find_by_username(username) + unless @user render json: {status: 401, message: 'Access Denied'}, status: 401 - return end - session[:user_id] = user.id - user.password_hash + @user.password_hash end + @user ? session[:user_id] = @user.id : nil end def current_user diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 89ad1db..334758c 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -12,10 +12,14 @@ def incomplete render json: @mods, callback: params[:callback] end - # uncategorized returns mod that have not been categorized by a single user + # uncategorized returns incomplete if current_user doesn't exist, otherwise returns mods not categorized by current_user def uncategorized - @mods = Mod.uncategorized(current_user.id).where(broken: false).limit(params[:count]) - render json: @mods, callback: params[:callback] + if current_user + @mods = Mod.uncategorized(current_user.id).where(broken: false).limit(params[:count]) + render json: @mods, callback: params[:callback] + else + incomplete + end end def show diff --git a/app/models/mod.rb b/app/models/mod.rb index 5d890d4..99a9a4d 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -4,9 +4,9 @@ class Mod < ActiveRecord::Base has_and_belongs_to_many :categories has_many :categorizations - scope :incomplete, find_by_sql('select * from mods where id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') + scope :incomplete, where('id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') def self.uncategorized user_id - where "select mod_id from categorizations group by mod_id where user_id not #{user_id}" + where "id not in (select mod_id from categorizations where user_id != #{user_id} group by mod_id)" end end diff --git a/config/application.rb b/config/application.rb index 40aea82..81232a8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,9 @@ module SecretrevApi class Application < Rails::Application + config.middleware.use ActionDispatch::Cookies + config.middleware.use ActionDispatch::Session::CookieStore + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/spec/v1/mods_controller_spec.rb b/spec/v1/mods_controller_spec.rb new file mode 100644 index 0000000..725e992 --- /dev/null +++ b/spec/v1/mods_controller_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe "/v1/mods" do + describe "incomplete" do + let(:url) { "v1/mods/incomplete/10" } + + it "returns 10 mods" do + get "#{url}.json" + last_response.status.should eql(200) + JSON.parse(last_response.body).length.should eq 10 + end + end +end From 7a19c6b03cbda9760fe1e386f5d03932a9e3a2a7 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Thu, 17 Jan 2013 08:15:29 -0500 Subject: [PATCH 17/28] fix "Access-Control-Allow-Headers" not allowing "X-Requested-With" and "Content-Type" --- app/controllers/application_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8956d41..768b795 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,6 +25,7 @@ def current_user def cors_set_access_control_headers headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' + headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, Content-Type' headers['Access-Control-Max-Age'] = "1728000" end From ab48db92adb6321fd4db05f3f7c8a3cb704eaddb Mon Sep 17 00:00:00 2001 From: Bryan White Date: Thu, 17 Jan 2013 10:16:38 -0500 Subject: [PATCH 18/28] modify categorizations#create to accept one request with a mod_id and category_ids array and make multiple categorizations out of it --- .../v1/categorizations_controller.rb | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/controllers/v1/categorizations_controller.rb b/app/controllers/v1/categorizations_controller.rb index fc1c6e0..2645cd7 100644 --- a/app/controllers/v1/categorizations_controller.rb +++ b/app/controllers/v1/categorizations_controller.rb @@ -1,20 +1,29 @@ class V1::CategorizationsController < ApplicationController before_filter :authenticate + before_filter :cors_preflight_check, only: :create + after_filter :cors_set_access_control_headers, only: :create def index end def create - categorization = Categorization.new params[:categorization] - begin categorization.save! - @response = {status: 201, message: 'successfully created categorization', categorization: categorization} - rescue ActiveRecord::RecordInvalid - @response = {status: 400, message: $!.to_s, categorization: categorization} - rescue - @response = {status: 400, message: $!.to_s} - ensure - render json: @response, callback: params[:callback], status: @response[:status] + params[:categorization].category_ids.each do |category_id| + hash = {mod_id: params[:categorization].mod_id, category_id: category_id} + categorization = Categorization.new hash + + #categorization = Categorization.new params[:categorization] + begin + categorization.save! + rescue ActiveRecord::RecordInvalid + @response = {status: 400, message: $!.to_s, categorization: categorization} + render json: @response, callback: params[:callback], status: @response[:status] + rescue + @response = {status: 400, message: $!.to_s} + render json: @response, callback: params[:callback], status: @response[:status] + end end + @response = {status: 201, message: 'successfully created categorization', categorization: categorization} + render json: @response, callback: params[:callback], status: @response[:status] end end From a43c57ae754a5218b3cd231a492aa76664f4c500 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Thu, 17 Jan 2013 10:17:58 -0500 Subject: [PATCH 19/28] add options verb to the routes.rb - having a problem with the before_filter :authenticate on the OPTIONS call, browser's not passing Authorization header --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 3b6e3c9..30f2d34 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ namespace :v1 do #TODO: implement alert/warning/error system for user-agents accessing old verisons, etc. resources :categorizations, except: :edit + match 'categorizations' => 'categorizations#create', :constraints => {:method => 'OPTIONS'} resources :users, except: :edit match 'users' => 'users#create', :constraints => {:method => 'OPTIONS'} From 09abd277900a8a19f155e37d032bb32a8ceda2f4 Mon Sep 17 00:00:00 2001 From: nembus Date: Fri, 18 Jan 2013 22:11:42 -0500 Subject: [PATCH 20/28] Ignore db/logfile --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 85de3c8..221bbd4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # Ignore the default SQLite database. /db/*.sqlite3 +/db/logfile # Ignore database config /config/database.yml From f56ed25624040bdc5e27a87fdee092a77301a069 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sat, 19 Jan 2013 04:04:34 -0500 Subject: [PATCH 21/28] configure CORS to allow 'with-credentials' requests. as a result have to set 'allow-control-access-origin' to not '*' .. it's k --- app/controllers/application_controller.rb | 167 +++++++++++++++++- .../v1/categorizations_controller.rb | 23 ++- app/controllers/v1/mods_controller.rb | 3 + app/models/categorization.rb | 2 +- config/application.rb | 1 + config/routes.rb | 1 + 6 files changed, 185 insertions(+), 12 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 768b795..921096c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::API private def authenticate + session[:test] = 'monkeys789' authenticate_or_request_with_http_digest(REALM) do |username| @user = User.find_by_username(username) unless @user @@ -23,9 +24,10 @@ def current_user # For all responses in this controller, return the CORS access control headers. def cors_set_access_control_headers - headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Origin'] = 'http://localhost:8000' headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, Content-Type' + headers['Access-Control-Allow-Credentials'] = 'true' headers['Access-Control-Max-Age'] = "1728000" end @@ -34,12 +36,169 @@ def cors_set_access_control_headers # text/plain. def cors_preflight_check - if request.method == :options - headers['Access-Control-Allow-Origin'] = '*' + if request.method == 'OPTIONS' + headers['Access-Control-Allow-Origin'] = 'http://localhost:8000' headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' - headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version' + headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version, Content-Type' + headers['Access-Control-Allow-Credentials'] = 'true' headers['Access-Control-Max-Age'] = '1728000' render :text => '', :content_type => 'text/plain' end end + + def authenticate_or_request_with_session_digest(realm = "Application", &password_procedure) + # a_w_s_d will return nil unless session[:authorization] exists = "authenticates logged in user" + # r_s_d_a is called if there isn't a valid session[:authorization] and will respond with the "Session-Authenticate" + authenticate_with_session_digest(realm, &password_procedure) || request_session_digest_authentication(realm) + end + + # Authenticate with HTTP Digest, returns true or false + def authenticate_with_session_digest(realm = "Application", &password_procedure) + SessionDigest.authenticate(session, request, realm, &password_procedure) + end + + # Render output including the HTTP Digest authentication header + def request_session_digest_authentication(realm = "Application", message = nil) + SessionDigest.authentication_request(self, realm, message) + end + + module SessionDigest + extend self + + # Returns false on a valid response, true otherwise + def authenticate(session, request, realm, &password_procedure) + session[:authorization] && validate_digest_response(request, realm, &password_procedure) + end + + # Returns false unless the request credentials response value matches the expected value. + # First try the password as a ha1 digest password. If this fails, then try it as a plain + # text password. + def validate_digest_response(request, realm, &password_procedure) + secret_key = secret_token(request) + credentials = decode_credentials_property(session) + valid_nonce = validate_nonce(secret_key, request, credentials[:nonce]) + + if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque] + password = password_procedure.call(credentials[:username]) + return false unless password + + method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] + uri = credentials[:uri][0, 1] == '/' ? request.original_fullpath : request.original_url + + [true, false].any? do |trailing_question_mark| + [true, false].any? do |password_is_ha1| + _uri = trailing_question_mark ? uri + "?" : uri + expected = expected_response(method, _uri, credentials, password, password_is_ha1) + expected == credentials[:response] + end + end + end + end + + # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ + # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead + # of a plain-text password. + def expected_response(http_method, uri, credentials, password, password_is_ha1=true) + ha1 = password_is_ha1 ? password : ha1(credentials, password) + ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':')) + ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':')) + end + + def ha1(credentials, password) + ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + end + + def encode_credentials(http_method, credentials, password, password_is_ha1) + credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) + "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(', ') + end + + def decode_credentials_property(session) + decode_credentials(session.authorization) + end + + def decode_credentials(property) + HashWithIndifferentAccess[property.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair| + key, value = pair.split('=', 2) + [key.strip, value.to_s.gsub(/^"|"$/, '').delete('\'')] + end] + end + + def authentication_header(controller, realm) + secret_key = secret_token(controller.request) + nonce = self.nonce(secret_key) + opaque = opaque(secret_key) + controller.session[:www_authenticate] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}") + binding.pry + end + + def authentication_request(controller, realm, message = nil) + message ||= "HTTP Digest: Access denied.\n" + authentication_header(controller, realm) + controller.response_body = message + controller.status = 401 + end + + def secret_token(request) + secret = request.env["action_dispatch.secret_token"] + raise "You must set config.secret_token in your app's config" if secret.blank? + secret + end + + # Uses an MD5 digest based on time to generate a value to be used only once. + # + # A server-specified data string which should be uniquely generated each time a 401 response is made. + # It is recommended that this string be base64 or hexadecimal data. + # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed. + # + # The contents of the nonce are implementation dependent. + # The quality of the implementation depends on a good choice. + # A nonce might, for example, be constructed as the base 64 encoding of + # + # => time-stamp H(time-stamp ":" ETag ":" private-key) + # + # where time-stamp is a server-generated time or other non-repeating value, + # ETag is the value of the HTTP ETag header associated with the requested entity, + # and private-key is data known only to the server. + # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and + # reject the request if it did not match the nonce from that header or + # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity. + # The inclusion of the ETag prevents a replay request for an updated version of the resource. + # (Note: including the IP address of the client in the nonce would appear to offer the server the ability + # to limit the reuse of the nonce to the same client that originally got it. + # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm. + # Also, IP address spoofing is not that hard.) + # + # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to + # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for + # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 + # of this document. + # + # The nonce is opaque to the client. Composed of Time, and hash of Time with secret + # key from the Rails session secret generated upon creation of project. Ensures + # the time cannot be modified by client. + def nonce(secret_key, time = Time.now) + t = time.to_i + hashed = [t, secret_key] + digest = ::Digest::MD5.hexdigest(hashed.join(":")) + ::Base64.encode64("#{t}:#{digest}").gsub("\n", '') + end + + # Might want a shorter timeout depending on whether the request + # is a PUT or POST, and if client is browser or web service. + # Can be much shorter if the Stale directive is implemented. This would + # allow a user to use new nonce without prompting user again for their + # username and password. + def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) + t = ::Base64.decode64(value).split(":").first.to_i + nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout + end + + # Opaque based on random generation - but changing each request? + def opaque(secret_key) + ::Digest::MD5.hexdigest(secret_key) + end + end + + end diff --git a/app/controllers/v1/categorizations_controller.rb b/app/controllers/v1/categorizations_controller.rb index 2645cd7..e225700 100644 --- a/app/controllers/v1/categorizations_controller.rb +++ b/app/controllers/v1/categorizations_controller.rb @@ -1,6 +1,12 @@ class V1::CategorizationsController < ApplicationController - before_filter :authenticate - before_filter :cors_preflight_check, only: :create + #before_filter :cors_preflight_check, only: :create + before_filter do |controller| + binding.pry + cors_preflight_check + unless controller.request.method == 'OPTIONS' + authenticate + end + end after_filter :cors_set_access_control_headers, only: :create def index @@ -8,8 +14,9 @@ def index end def create - params[:categorization].category_ids.each do |category_id| - hash = {mod_id: params[:categorization].mod_id, category_id: category_id} + binding.pry + params[:categorization][:category_ids].each do |category_id| + hash = {mod_id: params[:categorization][:mod_id], user_id: current_user.id, category_id: category_id} categorization = Categorization.new hash #categorization = Categorization.new params[:categorization] @@ -17,13 +24,15 @@ def create categorization.save! rescue ActiveRecord::RecordInvalid @response = {status: 400, message: $!.to_s, categorization: categorization} - render json: @response, callback: params[:callback], status: @response[:status] + #fail $!.to_s + #render json: @response, callback: params[:callback], status: @response[:status] rescue @response = {status: 400, message: $!.to_s} - render json: @response, callback: params[:callback], status: @response[:status] + #fail $!.to_s + #render json: @response, callback: params[:callback], status: @response[:status] end end - @response = {status: 201, message: 'successfully created categorization', categorization: categorization} + @response = {status: 201, message: 'successfully created categorization'} #, categorization: categorization} render json: @response, callback: params[:callback], status: @response[:status] end end diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 334758c..19b425e 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -1,5 +1,7 @@ class V1::ModsController < ApplicationController before_filter :authenticate, only: :broken + before_filter :cors_preflight_check, only: :uncategorized + after_filter :cors_set_access_control_headers, only: :uncategorized def index @mods = Mod.all @@ -14,6 +16,7 @@ def incomplete # uncategorized returns incomplete if current_user doesn't exist, otherwise returns mods not categorized by current_user def uncategorized + session[:hello_from_rails_api] = 'bananas125' if current_user @mods = Mod.uncategorized(current_user.id).where(broken: false).limit(params[:count]) render json: @mods, callback: params[:callback] diff --git a/app/models/categorization.rb b/app/models/categorization.rb index cf7b061..c793c7f 100644 --- a/app/models/categorization.rb +++ b/app/models/categorization.rb @@ -1,5 +1,5 @@ class Categorization < ActiveRecord::Base - attr_accessible :category, :mod, :user + attr_accessible :category_id, :mod_id, :user_id belongs_to :user belongs_to :category belongs_to :mod diff --git a/config/application.rb b/config/application.rb index 81232a8..96775ed 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,6 +13,7 @@ module SecretrevApi class Application < Rails::Application config.middleware.use ActionDispatch::Cookies config.middleware.use ActionDispatch::Session::CookieStore + config.session_store :cookie_store, key: '_secretrev_api_session' # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers diff --git a/config/routes.rb b/config/routes.rb index 30f2d34..e4584ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,7 @@ get 'mods/author/:q' => 'mods#author', :q => /.*/ get 'mods/count/:count(/offset/:offset)' => 'mods#count', :count => /\d+/, :offset => /\d+/ get 'mods/uncategorized(/:count)' => 'mods#uncategorized', :count => /\d+/ + match 'mods/uncategorized(/:count)' => 'mods#uncategorized', :constraints => {:method => 'OPTIONS'} get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ resources :mods, except: :edit From 296263e5dc39614428a1e98637d82dcfa2472034 Mon Sep 17 00:00:00 2001 From: nembus Date: Sun, 20 Jan 2013 01:47:15 -0500 Subject: [PATCH 22/28] Add a 'broken' route --- config/routes.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 9387185..efc61a4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,6 +21,8 @@ get 'mods/count/:count(/offset/:offset)' => 'mods#count', :count => /\d+/, :offset => /\d+/ get 'mods/uncategorized(/:count)' => 'mods#uncategorized', :count => /\d+/ get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ + #this should be post but because of jsonp we are forced to use get + get 'mods/:id/broken' => 'mods#broken', :id => /\d+/ resources :mods, except: :edit get 'challenge' => 'challenge#random' From e03ce5df751a163c91ee33e62d4bda9f1114dbac Mon Sep 17 00:00:00 2001 From: nembus Date: Sun, 20 Jan 2013 05:55:17 -0500 Subject: [PATCH 23/28] Fix uncategorized scope on Mods Reworking Mod breakage (WIP) --- app/controllers/v1/categorizations_controller.rb | 2 -- app/controllers/v1/mods_controller.rb | 5 +++-- app/models/mod.rb | 7 ++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/controllers/v1/categorizations_controller.rb b/app/controllers/v1/categorizations_controller.rb index e225700..fbbcbb9 100644 --- a/app/controllers/v1/categorizations_controller.rb +++ b/app/controllers/v1/categorizations_controller.rb @@ -1,7 +1,6 @@ class V1::CategorizationsController < ApplicationController #before_filter :cors_preflight_check, only: :create before_filter do |controller| - binding.pry cors_preflight_check unless controller.request.method == 'OPTIONS' authenticate @@ -14,7 +13,6 @@ def index end def create - binding.pry params[:categorization][:category_ids].each do |category_id| hash = {mod_id: params[:categorization][:mod_id], user_id: current_user.id, category_id: category_id} categorization = Categorization.new hash diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 19b425e..7f9f990 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -10,15 +10,16 @@ def index # incomplete returns mods that have < 10 categorzations by any user def incomplete - @mods = Mod.where(broken: false).limit(params[:count]).incomplete + @mods = Mod.broken_by_democracy.limit(params[:count]).incomplete render json: @mods, callback: params[:callback] end # uncategorized returns incomplete if current_user doesn't exist, otherwise returns mods not categorized by current_user def uncategorized + session[:hello_from_rails_api] = 'bananas125' if current_user - @mods = Mod.uncategorized(current_user.id).where(broken: false).limit(params[:count]) + @mods = Mod.uncategorized(current_user.id).broken_by_me(current_user.id).limit(params[:count]) render json: @mods, callback: params[:callback] else incomplete diff --git a/app/models/mod.rb b/app/models/mod.rb index 99a9a4d..913fd12 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -5,8 +5,13 @@ class Mod < ActiveRecord::Base has_many :categorizations scope :incomplete, where('id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') + scope :broken_by_democracy, where('id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') def self.uncategorized user_id - where "id not in (select mod_id from categorizations where user_id != #{user_id} group by mod_id)" + where "id not in (select mod_id from categorizations where user_id = #{user_id} group by mod_id)" + end + + def self.uncategorized user_id + where "id not in (select mod_id from categorizations where user_id = #{user_id} group by mod_id)" end end From 44962a6fce7776d393f53780a999d16b2857d59e Mon Sep 17 00:00:00 2001 From: Bryan White Date: Sun, 20 Jan 2013 08:53:09 -0500 Subject: [PATCH 24/28] bump to MVP --- app/controllers/application_controller.rb | 2 +- app/controllers/v1/mods_controller.rb | 42 ++++++++++++++++++---- app/models/break.rb | 5 ++- app/models/mod.rb | 7 ++-- config/routes.rb | 4 +-- db/migrate/20130107023717_create_breaks.rb | 4 +-- db/schema.rb | 4 +-- 7 files changed, 50 insertions(+), 18 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 921096c..339812c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,7 +6,7 @@ class ApplicationController < ActionController::API private def authenticate - session[:test] = 'monkeys789' + #session[:test] = 'monkeys789' authenticate_or_request_with_http_digest(REALM) do |username| @user = User.find_by_username(username) unless @user diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 7f9f990..442ae70 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -1,7 +1,20 @@ class V1::ModsController < ApplicationController - before_filter :authenticate, only: :broken - before_filter :cors_preflight_check, only: :uncategorized - after_filter :cors_set_access_control_headers, only: :uncategorized + #before_filter :authenticate, only: :break + #before_filter :set_cache_buster, only: :uncategorized + before_filter only: :break do |controller| + cors_preflight_check + unless controller.request.method == 'OPTIONS' + authenticate + end + end + before_filter :cors_preflight_check, only: [:uncategorized, :break, :show] + after_filter :cors_set_access_control_headers, only: [:uncategorized, :break, :show] + + def set_cache_buster + response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + end def index @mods = Mod.all @@ -10,16 +23,16 @@ def index # incomplete returns mods that have < 10 categorzations by any user def incomplete - @mods = Mod.broken_by_democracy.limit(params[:count]).incomplete + @mods = Mod.not_broken_by_democracy.limit(params[:count]).incomplete render json: @mods, callback: params[:callback] end # uncategorized returns incomplete if current_user doesn't exist, otherwise returns mods not categorized by current_user def uncategorized - session[:hello_from_rails_api] = 'bananas125' + #session[:hello_from_rails_api] = 'bananas125' if current_user - @mods = Mod.uncategorized(current_user.id).broken_by_me(current_user.id).limit(params[:count]) + @mods = Mod.uncategorized(current_user.id).not_broken_by_me(current_user.id).limit(params[:count]) render json: @mods, callback: params[:callback] else incomplete @@ -31,13 +44,28 @@ def show render json: @mod, callback: params[:callback] end + # TODO: reserved for future admin use!!! def broken @mod = Mod.find params[:id] @mod.broken = true - Break.create Hash[user: current_user, mod: @mod] + #Break.create Hash[user: current_user, mod: @mod] @mod.save! end + def break + @mod = Mod.find params[:id] + @mod.breaks.new user_id: current_user.id + begin @mod.save! + @response = {status: 201, message: 'successfully created break', resource: @mod} + rescue ActiveRecord::RecordInvalid + @response = {status: 400, message: "You've already flagged this mod as broken", resource: @mod} + rescue + @response = {status: 400, message: $!.to_s, resource: @mod} + ensure + render json: @response, status: @response[:status] + end + end + def name @mods = Mod.where "name like ?", "%#{params[:q]}%" render json: @mods, callback: params[:callback] diff --git a/app/models/break.rb b/app/models/break.rb index 1dc38bc..bad7754 100644 --- a/app/models/break.rb +++ b/app/models/break.rb @@ -1,5 +1,8 @@ class Break < ActiveRecord::Base - attr_accessible :mod, :user + attr_accessible :mod_id, :user_id belongs_to :mod belongs_to :user + + validates_presence_of :user, :mod + validates_uniqueness_of :user_id, scope: :mod_id end diff --git a/app/models/mod.rb b/app/models/mod.rb index 913fd12..40249bf 100644 --- a/app/models/mod.rb +++ b/app/models/mod.rb @@ -3,15 +3,16 @@ class Mod < ActiveRecord::Base attr_accessible :broken has_and_belongs_to_many :categories has_many :categorizations + has_many :breaks scope :incomplete, where('id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') - scope :broken_by_democracy, where('id not in (select mod_id from categorizations group by mod_id having count(mod_id) > 9)') + scope :not_broken_by_democracy, where('id not in (select mod_id from breaks group by mod_id having count(mod_id) > 9)') def self.uncategorized user_id where "id not in (select mod_id from categorizations where user_id = #{user_id} group by mod_id)" end - def self.uncategorized user_id - where "id not in (select mod_id from categorizations where user_id = #{user_id} group by mod_id)" + def self.not_broken_by_me user_id + where "id not in (select mod_id from breaks where user_id = #{user_id} group by mod_id)" end end diff --git a/config/routes.rb b/config/routes.rb index e058d8f..c380eb1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,8 +22,8 @@ get 'mods/uncategorized(/:count)' => 'mods#uncategorized', :count => /\d+/ match 'mods/uncategorized(/:count)' => 'mods#uncategorized', :constraints => {:method => 'OPTIONS'} get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ - #this should be post but because of jsonp we are forced to use get - get 'mods/:id/broken' => 'mods#broken', :id => /\d+/ + match 'mods/:id/break' => 'mods#break', :id => /\d+/, :constraints => {:method => 'OPTIONS'} + post 'mods/:id/break' => 'mods#break', :id => /\d+/ resources :mods, except: :edit get 'challenge' => 'challenge#random' diff --git a/db/migrate/20130107023717_create_breaks.rb b/db/migrate/20130107023717_create_breaks.rb index 43cbc17..7f21c56 100644 --- a/db/migrate/20130107023717_create_breaks.rb +++ b/db/migrate/20130107023717_create_breaks.rb @@ -1,8 +1,8 @@ class CreateBreaks < ActiveRecord::Migration def change create_table :breaks do |t| - t.string :user_id - t.string :mod_id + t.integer :user_id + t.integer :mod_id t.timestamps end diff --git a/db/schema.rb b/db/schema.rb index d11dce2..54834fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -21,8 +21,8 @@ end create_table "breaks", :force => true do |t| - t.string "user_id" - t.string "mod_id" + t.integer "user_id" + t.integer "mod_id" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end From b45e67272289a9c19ff934537c0c9ac1c33f79a0 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 22 Jan 2013 05:29:39 -0500 Subject: [PATCH 25/28] add endpoint for checking availability of mods to be categorized by the current_user --- app/controllers/v1/mods_controller.rb | 14 ++++++++++++-- config/routes.rb | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 442ae70..473717d 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -39,12 +39,21 @@ def uncategorized end end + def available? + if current_user + if Mod.uncategorized(current_user).not_broken_by_me(current_user).where(id: params[:id]); + render json: true + else + render json: false, status: 400 + end + end + end + def show @mod = Mod.find params[:id] render json: @mod, callback: params[:callback] end - # TODO: reserved for future admin use!!! def broken @mod = Mod.find params[:id] @mod.broken = true @@ -55,7 +64,8 @@ def broken def break @mod = Mod.find params[:id] @mod.breaks.new user_id: current_user.id - begin @mod.save! + begin + @mod.save! @response = {status: 201, message: 'successfully created break', resource: @mod} rescue ActiveRecord::RecordInvalid @response = {status: 400, message: "You've already flagged this mod as broken", resource: @mod} diff --git a/config/routes.rb b/config/routes.rb index c380eb1..2084dee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ match 'mods/:id/break' => 'mods#break', :id => /\d+/, :constraints => {:method => 'OPTIONS'} post 'mods/:id/break' => 'mods#break', :id => /\d+/ + get 'mods/available/:user_id' => 'mods#available?', :user_id => /\d+/ resources :mods, except: :edit get 'challenge' => 'challenge#random' From beb8f9d66159a0e3ff63c040251933ca4159f962 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 22 Jan 2013 06:58:00 -0500 Subject: [PATCH 26/28] fix bug in mods#available --- app/controllers/v1/mods_controller.rb | 2 +- config/routes.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 473717d..34b6a66 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -41,7 +41,7 @@ def uncategorized def available? if current_user - if Mod.uncategorized(current_user).not_broken_by_me(current_user).where(id: params[:id]); + unless Mod.uncategorized(current_user.id).not_broken_by_me(current_user.id).where(id: params[:id]).blank?; render json: true else render json: false, status: 400 diff --git a/config/routes.rb b/config/routes.rb index 2084dee..cce7650 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,7 +24,7 @@ get 'mods/incomplete(/:count)' => 'mods#incomplete', :count => /\d+/ match 'mods/:id/break' => 'mods#break', :id => /\d+/, :constraints => {:method => 'OPTIONS'} post 'mods/:id/break' => 'mods#break', :id => /\d+/ - get 'mods/available/:user_id' => 'mods#available?', :user_id => /\d+/ + get 'mods/available/:id' => 'mods#available?', :id => /\d+/ resources :mods, except: :edit get 'challenge' => 'challenge#random' From a89d8b844ff6c8f6046a1ad7f255e369ceb785eb Mon Sep 17 00:00:00 2001 From: Bryan White Date: Tue, 22 Jan 2013 07:54:29 -0500 Subject: [PATCH 27/28] fix mods#available (again) to respond to jsonp --- app/controllers/v1/mods_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/mods_controller.rb b/app/controllers/v1/mods_controller.rb index 34b6a66..3f86bcd 100644 --- a/app/controllers/v1/mods_controller.rb +++ b/app/controllers/v1/mods_controller.rb @@ -42,9 +42,9 @@ def uncategorized def available? if current_user unless Mod.uncategorized(current_user.id).not_broken_by_me(current_user.id).where(id: params[:id]).blank?; - render json: true + render json: true, callback: params[:callback] else - render json: false, status: 400 + render json: false, status: 400, callack: params[:callback] end end end From 3fe6a9125961bec4e1900617ca3776f992086c92 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Wed, 5 Feb 2014 05:38:15 -0500 Subject: [PATCH 28/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b981379..988b698 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Secret Revelations API (work in progress) =================== -###[ Staged at wiglepedia.org ]( wiglepedia.org ) +###[ Staged at categorize.bryanchriswhite.com ]( http://categorize.bryanchriswhite.com ) ###Versioning ####Current Version: v1