From 96b2403204d288fe5d5b3c7d18f09f9a2e078414 Mon Sep 17 00:00:00 2001 From: Naoise Golden Date: Sat, 7 Feb 2015 20:52:08 +0100 Subject: [PATCH 01/33] Add slack notifications for travis --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 566e3b3..f2a39ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,7 @@ before_script: addons: code_climate: - repo_token: be20bf0ededc1a9bfd7ffcdbae0e6dbbb59f31fbc64c18dccb62309f095ead9b \ No newline at end of file + repo_token: be20bf0ededc1a9bfd7ffcdbae0e6dbbb59f31fbc64c18dccb62309f095ead9b + +notifications: + slack: divebook:BVocZwhc0uFYb5iGkASLim1H \ No newline at end of file From 3d91ea6e545b1e5f4adc23a5e4c2b10b1562c372 Mon Sep 17 00:00:00 2001 From: Naoise Golden Date: Sun, 8 Feb 2015 18:18:59 +0100 Subject: [PATCH 02/33] Test after making repo private From a6149000fbb714f1f305fd96a4e869c23c9f9ea6 Mon Sep 17 00:00:00 2001 From: Naoise Golden Date: Sun, 8 Feb 2015 18:27:11 +0100 Subject: [PATCH 03/33] Test after making repo public again From 67ebb3b5bda91f04b37b3f927d755302c7ab5602 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Mon, 9 Feb 2015 23:24:50 +0300 Subject: [PATCH 04/33] [add] gems for development --- .gitignore | 1 + Gemfile | 25 +++++++++++- Gemfile.lock | 65 +++++++++++++++++++++++++++++- app/models/location.rb | 11 +++++ app/models/user.rb | 28 ++++++++++++- config/environments/development.rb | 11 ++--- 6 files changed, 129 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 2d5ff0a..4cf024e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /log/*.log /tmp config/app_environment_variables.rb +.env diff --git a/Gemfile b/Gemfile index 09346b8..0d62452 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,9 @@ gem 'geocoder' # monitoring gem 'newrelic_rpm' +# ENV variables +gem 'dotenv-deployment' + #assets gem 'uglifier', '>= 2.7.0' @@ -32,5 +35,23 @@ gem 'modular-scale', '1.0.2' # fixes incompatibility with old foundation # Code Climate test coverage gem "codeclimate-test-reporter", group: :test, require: nil -# To use debugger -# gem 'debugger' \ No newline at end of file +group :development do + gem 'web-console', '~> 2.0' + gem 'rails-footnotes' + + # Console + gem 'pry-rails' + gem 'hirb-unicode' + gem 'awesome_print' + + # Chrome extensions + gem 'meta_request', '~> 0.3' + gem 'better_errors' + gem 'binding_of_caller' + gem 'bullet' + gem 'annotate' +end + +group :development, :test do + gem 'letter_opener' +end diff --git a/Gemfile.lock b/Gemfile.lock index ba776fc..05ecbfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,12 +45,28 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + addressable (2.3.7) + annotate (2.6.5) + activerecord (>= 2.3.0) + rake (>= 0.8.7) arel (6.0.0) + awesome_print (1.6.1) bcrypt (3.1.10) + better_errors (2.1.1) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + rack (>= 0.9.0) + binding_of_caller (0.7.2) + debug_inspector (>= 0.0.1) builder (3.2.2) + bullet (4.14.4) + activesupport (>= 3.0.0) + uniform_notifier (>= 1.6.0) + callsite (0.0.11) chunky_png (1.3.3) codeclimate-test-reporter (0.4.6) simplecov (>= 0.7.1, < 1.0.0) + coderay (1.1.0) compass (0.12.7) chunky_png (~> 1.2) fssm (>= 0.2.7) @@ -62,6 +78,7 @@ GEM country_select (2.1.1) countries (>= 0.9.3, < 0.10.0) currencies (0.4.2) + debug_inspector (0.0.2) devise (3.4.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -70,23 +87,39 @@ GEM thread_safe (~> 0.1) warden (~> 1.2.3) docile (1.1.5) + dotenv (1.0.2) + dotenv-deployment (0.2.0) + dotenv (~> 1.0) erubis (2.7.0) execjs (2.3.0) faraday (0.9.1) multipart-post (>= 1.2, < 3) fssm (0.2.10) geocoder (1.2.7) - globalid (0.3.0) + globalid (0.3.2) activesupport (>= 4.1.0) hashie (3.4.0) hike (1.2.3) + hirb (0.7.3) + hirb-unicode (0.0.5) + hirb (~> 0.5) + unicode-display_width (~> 0.1.1) i18n (0.7.0) json (1.8.2) jwt (1.2.1) + launchy (2.4.3) + addressable (~> 2.3) + letter_opener (1.3.0) + launchy (~> 2.2) loofah (2.0.1) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) + meta_request (0.3.4) + callsite (~> 0.0, >= 0.0.11) + rack-contrib (~> 1.1) + railties (>= 3.0.0, < 5.0.0) + method_source (0.8.2) mime-types (2.4.3) mini_portile (0.6.2) minitest (5.5.1) @@ -117,7 +150,15 @@ GEM omniauth (~> 1.2) orm_adapter (0.5.0) pg (0.18.1) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-rails (0.3.3) + pry (>= 0.9.10) rack (1.6.0) + rack-contrib (1.2.0) + rack (>= 0.9.1) rack-test (0.6.3) rack (>= 1.0) rails (4.2.0) @@ -137,6 +178,8 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) + rails-footnotes (4.1.5) + rails (>= 3.2) rails-html-sanitizer (1.0.1) loofah (~> 2.0) railties (4.2.0) @@ -163,6 +206,7 @@ GEM multi_json (~> 1.0) simplecov-html (~> 0.8.0) simplecov-html (0.8.0) + slop (3.6.0) sprockets (2.11.3) hike (~> 1.2) multi_json (~> 1.0) @@ -180,8 +224,15 @@ GEM uglifier (2.7.0) execjs (>= 0.3.0) json (>= 1.8.0) + unicode-display_width (0.1.1) + uniform_notifier (1.7.0) warden (1.2.3) rack (>= 1.0) + web-console (2.0.0) + activemodel (~> 4.0) + binding_of_caller (>= 0.7.2) + railties (~> 4.0) + sprockets-rails (>= 2.0, < 4.0) zurb-foundation (3.1.1) compass (>= 0.12.2) modular-scale (>= 1.0.2) @@ -192,20 +243,32 @@ PLATFORMS ruby DEPENDENCIES + annotate + awesome_print + better_errors + binding_of_caller + bullet codeclimate-test-reporter compass-rails country_select devise (~> 3.4.1) + dotenv-deployment foundation-icons-sass-rails! geocoder + hirb-unicode + letter_opener + meta_request (~> 0.3) modular-scale (= 1.0.2) newrelic_rpm oauth2 omniauth omniauth-facebook pg + pry-rails rails (= 4.2.0) + rails-footnotes sass-rails (~> 4.0.2) simple_form uglifier (>= 2.7.0) + web-console (~> 2.0) zurb-foundation (~> 3.1.1) diff --git a/app/models/location.rb b/app/models/location.rb index 870dec1..d30326f 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -1,3 +1,14 @@ +# == Schema Information +# +# Table name: locations +# +# id :integer not null, primary key +# address :string +# latitude :float +# longitude :float +# created_at :datetime +# updated_at :datetime +# class Location < ActiveRecord::Base geocoded_by :address do |obj,results| if geo = results.first diff --git a/app/models/user.rb b/app/models/user.rb index fc25e7a..7aa3d8a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,29 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# name :string +# email :string default(""), not null +# created_at :datetime +# updated_at :datetime +# encrypted_password :string default(""), not null +# reset_password_token :string +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default("0") +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string +# last_sign_in_ip :string +# country :string +# provider :string +# uid :string +# confirmation_token :string +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string +# class User < ActiveRecord::Base # Include default devise modules. Others available are: # :token_authenticatable, :lockable, :timeoutable @@ -24,4 +50,4 @@ def self.find_for_facebook_oauth(auth, signed_in_resource=nil) user end -end \ No newline at end of file +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 17af9a7..d4a1d63 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,14 +14,9 @@ config.action_controller.perform_caching = false # mailer configuration - config.action_mailer.default_url_options = { :host => 'localhost:3000' } - config.action_mailer.raise_delivery_errors = true - config.action_mailer.smtp_settings = { - :address => "smtp.mandrillapp.com", - :port => 587, - :user_name => ENV["MANDRILL_USERNAME"], - :password => ENV["MANDRILL_API_KEY"] - } + config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :letter_opener + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Print deprecation notices to the Rails logger config.active_support.deprecation = :log From b123291767558042716d90e099b10dadfcff742e Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 00:04:40 +0300 Subject: [PATCH 05/33] [add] Pagination --- Gemfile | 5 +++++ Gemfile.lock | 9 +++++++++ app/controllers/api/v1/users_controller.rb | 7 +++++++ config/routes.rb | 5 +++++ db/seeds.rb | 17 ++++++++++------- 5 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 app/controllers/api/v1/users_controller.rb diff --git a/Gemfile b/Gemfile index 0d62452..c74f0e7 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,11 @@ gem 'newrelic_rpm' # ENV variables gem 'dotenv-deployment' +gem 'faker', '~> 1.4' + +# API +gem 'kaminari' +gem 'api-pagination' #assets gem 'uglifier', '>= 2.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 05ecbfa..8e8aa93 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,6 +49,7 @@ GEM annotate (2.6.5) activerecord (>= 2.3.0) rake (>= 0.8.7) + api-pagination (4.1.0) arel (6.0.0) awesome_print (1.6.1) bcrypt (3.1.10) @@ -92,6 +93,8 @@ GEM dotenv (~> 1.0) erubis (2.7.0) execjs (2.3.0) + faker (1.4.3) + i18n (~> 0.5) faraday (0.9.1) multipart-post (>= 1.2, < 3) fssm (0.2.10) @@ -107,6 +110,9 @@ GEM i18n (0.7.0) json (1.8.2) jwt (1.2.1) + kaminari (0.16.2) + actionpack (>= 3.0.0) + activesupport (>= 3.0.0) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.3.0) @@ -244,6 +250,7 @@ PLATFORMS DEPENDENCIES annotate + api-pagination awesome_print better_errors binding_of_caller @@ -253,9 +260,11 @@ DEPENDENCIES country_select devise (~> 3.4.1) dotenv-deployment + faker (~> 1.4) foundation-icons-sass-rails! geocoder hirb-unicode + kaminari letter_opener meta_request (~> 0.3) modular-scale (= 1.0.2) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb new file mode 100644 index 0000000..7fcfae7 --- /dev/null +++ b/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,7 @@ +class Api::V1::UsersController < ApplicationController + def index + users = paginate User.all + + render json: users + end +end diff --git a/config/routes.rb b/config/routes.rb index 04bdaa6..25e55c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,9 @@ Divebook::Application.routes.draw do + namespace :api do + namespace :v1 do + resources :users + end + end #Locations resources :locations diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e8..9180bcf 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,10 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). -# -# Examples: -# -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) +count = 0 +15.times do + User.create( + name: Faker::Name.name, + email: "user#{count}@example.com", + password: '12345678', + country: Faker::Address.country) + + count += 1 +end From 82449a007d7cb5edc4b093896a2c04db07b93734 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 00:06:36 +0300 Subject: [PATCH 06/33] [add] Serializer for Users --- Gemfile | 1 + Gemfile.lock | 3 +++ app/controllers/api/v1/users_controller.rb | 2 +- app/serializers/user_serializer.rb | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 app/serializers/user_serializer.rb diff --git a/Gemfile b/Gemfile index c74f0e7..286aa06 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'faker', '~> 1.4' # API gem 'kaminari' gem 'api-pagination' +gem 'active_model_serializers', '~> 0.9' #assets gem 'uglifier', '>= 2.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 8e8aa93..9d9a66c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,6 +29,8 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.1) + active_model_serializers (0.9.3) + activemodel (>= 3.2) activejob (4.2.0) activesupport (= 4.2.0) globalid (>= 0.3.0) @@ -249,6 +251,7 @@ PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.9) annotate api-pagination awesome_print diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 7fcfae7..48f09f3 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -2,6 +2,6 @@ class Api::V1::UsersController < ApplicationController def index users = paginate User.all - render json: users + render json: users, each_serializer: UserSerializer end end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 0000000..8de9120 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,3 @@ +class UserSerializer < ActiveModel::Serializer + attributes :id, :name +end From 7fef148061e8a89630d3f67b0bca6a9b30c547c5 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 00:07:06 +0300 Subject: [PATCH 07/33] [add] Usefull rake task 'rake color_routes' --- lib/tasks/color_routes.rake | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 lib/tasks/color_routes.rake diff --git a/lib/tasks/color_routes.rake b/lib/tasks/color_routes.rake new file mode 100644 index 0000000..95f4e9b --- /dev/null +++ b/lib/tasks/color_routes.rake @@ -0,0 +1,38 @@ +desc 'Pretty version on rails rake routes.' + +EMK="\033[1;30m" +EMR="\033[1;31m" +EMY="\033[1;33m" +EMB="\033[1;34m" +EMM="\033[1;35m" +EMC="\033[1;36m" +EMW="\033[1;37m" +NOCOLOR = "\033[0m" + +task :color_routes => :environment do + + Rails.application.reload_routes! + all_routes = Rails.application.routes.routes.to_a + all_routes.reject! { |route| route.verb.nil? || route.path.spec.to_s == '/assets' } + all_routes.select! { |route| ENV['CONTROLLER'].nil? || route.defaults[:controller].to_s == ENV['CONTROLLER'] } + + max_widths = { + names: (all_routes.map { |route| route.name.to_s.length }.max), + verbs: (6), + paths: (all_routes.map { |route| route.path.spec.to_s.length }.max), + controllers: (all_routes.map { |route| route.defaults[:controller].to_s.length }.max), + actions: (all_routes.map { |route| route.defaults[:action].to_s.length }.max) + } + + all_routes.group_by { |route| route.defaults[:controller] }.each_value do |group| + puts EMK + "\nCONTROLLER: " + EMW + group.first.defaults[:controller].to_s + NOCOLOR + group.each do |route| + name = EMC + route.name.to_s.rjust(max_widths[:names]) + NOCOLOR + verb = EMY + route.verb.inspect.gsub(/^.{2}|.{2}$/, "").center(max_widths[:verbs]) + NOCOLOR + path = EMR + route.path.spec.to_s.ljust(max_widths[:paths]).gsub(/\.?:\w+/){|s|EMB + s + EMR} + NOCOLOR + action = EMW + route.defaults[:action].to_s.ljust(max_widths[:actions]) + NOCOLOR + + puts "#{name} | #{verb} | #{path} | #{action}" + end + end +end From ee20a68d49382868751549d39e8cbcb3c00edffc Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 00:26:42 +0300 Subject: [PATCH 08/33] [add] 404 handling for users#show --- app/controllers/api/base_controller.rb | 9 +++++++++ app/controllers/api/v1/users_controller.rb | 22 +++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/base_controller.rb diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb new file mode 100644 index 0000000..15ef46c --- /dev/null +++ b/app/controllers/api/base_controller.rb @@ -0,0 +1,9 @@ +class Api::BaseController < ApplicationController + rescue_from ActiveRecord::RecordNotFound, with: :record_not_found + + private + + def record_not_found(error) + render json: { error: error.message }, status: :not_found + end +end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 48f09f3..2f12759 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -1,7 +1,23 @@ -class Api::V1::UsersController < ApplicationController +class Api::V1::UsersController < Api::BaseController + before_action :set_user, only: :show + before_action :return_user, only: :show + def index - users = paginate User.all + @users = paginate User.all + + render json: @users, each_serializer: UserSerializer + end + + def show + end + + private + + def set_user + @user = User.find(params[:id]) + end - render json: users, each_serializer: UserSerializer + def return_user + render json: @user, serializer: UserSerializer end end From 6c3a527f112ca3bdc577edb0578bf522fed65ac6 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 00:57:12 +0300 Subject: [PATCH 09/33] [add] Simple CRUD for Users --- app/controllers/api/base_controller.rb | 8 ++++++ app/controllers/api/v1/users_controller.rb | 30 +++++++++++++++++++++- app/models/user.rb | 12 ++++++--- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 15ef46c..0688cf7 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -1,6 +1,14 @@ class Api::BaseController < ApplicationController rescue_from ActiveRecord::RecordNotFound, with: :record_not_found + def unprocessable_entity(record) + render json: { error: record.errors.full_messages }, status: :unprocessable_entity + end + + def unexpected_error + render json: { error: 'Unexpected Error' }, status: :internal_server_error + end + private def record_not_found(error) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 2f12759..90f3741 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -1,5 +1,5 @@ class Api::V1::UsersController < Api::BaseController - before_action :set_user, only: :show + before_action :set_user, only: [:show, :update, :destroy] before_action :return_user, only: :show def index @@ -11,6 +11,30 @@ def index def show end + def create + @user = User.new(user_params) + + if @user.save + return_user + elsif @user.invalid? + unprocessable_entity(@user) + else + unexpected_error + end + end + + def update + if @user.update(user_params) + return_user + else + unprocessable_entity(@user) + end + end + + def destroy + return_user if @user.destroy + end + private def set_user @@ -20,4 +44,8 @@ def set_user def return_user render json: @user, serializer: UserSerializer end + + def user_params + params.permit(:name, :email, :password) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 7aa3d8a..16b03f6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,10 +25,14 @@ # unconfirmed_email :string # class User < ActiveRecord::Base - # Include default devise modules. Others available are: - # :token_authenticatable, :lockable, :timeoutable - devise :database_authenticatable, :registerable, :omniauthable, - :recoverable, :rememberable, :trackable, :validatable, :confirmable + devise :database_authenticatable, + :registerable, + :omniauthable, + :recoverable, + :rememberable, + :trackable, + :validatable, + :confirmable def to_s name ? name : email From 140ee19d5ae11ba4ff4011bb675063c9f181f4f9 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 01:18:59 +0300 Subject: [PATCH 10/33] [feature] API authentication by access token --- app/controllers/api/base_controller.rb | 9 +++++++++ app/controllers/api/v1/users_controller.rb | 3 ++- app/models/user.rb | 17 +++++++++++++++++ db/migrate/20150209220550_add_token_to_users.rb | 8 ++++++++ db/schema.rb | 4 +++- 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150209220550_add_token_to_users.rb diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 0688cf7..9af51b1 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -9,6 +9,15 @@ def unexpected_error render json: { error: 'Unexpected Error' }, status: :internal_server_error end + def authenticate! + render json: { error: '401 Unauthorized' }, status: :unauthorized unless authenticated + end + + def authenticated + return true if warden.authenticated? + params[:access_token] && @user = User.find_by(authentication_token: params[:access_token]) + end + private def record_not_found(error) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 90f3741..af27186 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -1,4 +1,5 @@ class Api::V1::UsersController < Api::BaseController + before_action :authenticate! before_action :set_user, only: [:show, :update, :destroy] before_action :return_user, only: :show @@ -46,6 +47,6 @@ def return_user end def user_params - params.permit(:name, :email, :password) + params.permit(:name, :email, :password, :access_token) end end diff --git a/app/models/user.rb b/app/models/user.rb index 16b03f6..0b01537 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,8 +23,11 @@ # confirmed_at :datetime # confirmation_sent_at :datetime # unconfirmed_email :string +# authentication_token :string # class User < ActiveRecord::Base + before_save :ensure_authentication_token + devise :database_authenticatable, :registerable, :omniauthable, @@ -54,4 +57,18 @@ def self.find_for_facebook_oauth(auth, signed_in_resource=nil) user end + private + + def ensure_authentication_token + if authentication_token.blank? + self.authentication_token = generate_authentication_token + end + end + + def generate_authentication_token + loop do + token = Devise.friendly_token + break token unless User.where(authentication_token: token).first + end + end end diff --git a/db/migrate/20150209220550_add_token_to_users.rb b/db/migrate/20150209220550_add_token_to_users.rb new file mode 100644 index 0000000..d57cae7 --- /dev/null +++ b/db/migrate/20150209220550_add_token_to_users.rb @@ -0,0 +1,8 @@ +class AddTokenToUsers < ActiveRecord::Migration + def change + add_column :users, :authentication_token, :string + add_index :users, :authentication_token + + User.find_each(&:save) + end +end diff --git a/db/schema.rb b/db/schema.rb index a6385ac..e35c41c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20121022231250) do +ActiveRecord::Schema.define(version: 20150209220550) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -45,8 +45,10 @@ t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" + t.string "authentication_token" end + add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree From 73e194c7338621b11d83ccdcd8bab1e2da263e88 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 18:43:00 +0300 Subject: [PATCH 11/33] [feature] Auth action --- app/controllers/api/v1/users_controller.rb | 33 +++++++++++++--------- config/routes.rb | 3 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index af27186..35f0336 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -1,6 +1,6 @@ class Api::V1::UsersController < Api::BaseController - before_action :authenticate! - before_action :set_user, only: [:show, :update, :destroy] + before_action :authenticate!, except: :auth + before_action :set_user, except: [:index, :auth] before_action :return_user, only: :show def index @@ -12,18 +12,6 @@ def index def show end - def create - @user = User.new(user_params) - - if @user.save - return_user - elsif @user.invalid? - unprocessable_entity(@user) - else - unexpected_error - end - end - def update if @user.update(user_params) return_user @@ -36,6 +24,23 @@ def destroy return_user if @user.destroy end + def auth + @user = User.new(user_params) + + if @user.save + render json: { + id: @user.id, + name: @user.name, + email: @user.email, + access_token: @user.authentication_token + } + elsif @user.invalid? + unprocessable_entity(@user) + else + unexpected_error + end + end + private def set_user diff --git a/config/routes.rb b/config/routes.rb index 25e55c6..1c9b9fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,8 @@ Divebook::Application.routes.draw do namespace :api do namespace :v1 do - resources :users + resources :users, except: [:create, :new, :edit] + post 'auth', to: 'users#auth' end end From 5296405f37339057875e7833f48542e37fa1fa71 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 19:16:09 +0300 Subject: [PATCH 12/33] [add] CRUD for Divesites --- app/controllers/api/base_controller.rb | 4 ++ .../api/v1/divesites_controller.rb | 52 +++++++++++++++++++ app/models/divesite.rb | 19 +++++++ app/serializers/divesite_serializer.rb | 3 ++ config/routes.rb | 1 + db/migrate/20150210154934_create_divesites.rb | 12 +++++ db/schema.rb | 11 +++- db/seeds.rb | 7 +++ 8 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/divesites_controller.rb create mode 100644 app/models/divesite.rb create mode 100644 app/serializers/divesite_serializer.rb create mode 100644 db/migrate/20150210154934_create_divesites.rb diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 9af51b1..b34babe 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -23,4 +23,8 @@ def authenticated def record_not_found(error) render json: { error: error.message }, status: :not_found end + + def default_serializer_options + {root: false} + end end diff --git a/app/controllers/api/v1/divesites_controller.rb b/app/controllers/api/v1/divesites_controller.rb new file mode 100644 index 0000000..1c533f2 --- /dev/null +++ b/app/controllers/api/v1/divesites_controller.rb @@ -0,0 +1,52 @@ +class Api::V1::DivesitesController < Api::BaseController + before_action :authenticate! + before_action :set_divesite, except: [:index, :create] + before_action :return_divesite, only: :show + + def index + @divesites = paginate Divesite.all + + render json: @divesites, each_serializer: DivesiteSerializer + end + + def create + @divesite = Divesite.new(divesite_params) + + if @divesite.save + return_divesite + elsif @divesite.invalid? + unprocessable_entity(@divesite) + else + unexpected_error + end + end + + def show + end + + def update + if @divesite.update(divesite_params) + return_divesite + else + unprocessable_entity(@divesite) + end + end + + def destroy + return_divesite if @divesite.destroy + end + + private + + def set_divesite + @divesite = Divesite.find(params[:id]) + end + + def return_divesite + render json: @divesite, serializer: DivesiteSerializer + end + + def divesite_params + params.permit(:name, :address) + end +end diff --git a/app/models/divesite.rb b/app/models/divesite.rb new file mode 100644 index 0000000..da31776 --- /dev/null +++ b/app/models/divesite.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: divesites +# +# id :integer not null, primary key +# name :string +# address :string +# latitude :float +# longitude :float +# created_at :datetime not null +# updated_at :datetime not null +# +class Divesite < ActiveRecord::Base + geocoded_by :address + + validates :address, presence: true + + after_validation :geocode, if: :address_changed? +end diff --git a/app/serializers/divesite_serializer.rb b/app/serializers/divesite_serializer.rb new file mode 100644 index 0000000..ecd19bc --- /dev/null +++ b/app/serializers/divesite_serializer.rb @@ -0,0 +1,3 @@ +class DivesiteSerializer < ActiveModel::Serializer + attributes :id, :name, :address, :latitude, :longitude +end diff --git a/config/routes.rb b/config/routes.rb index 1c9b9fb..c32727c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ Divebook::Application.routes.draw do namespace :api do namespace :v1 do + resources :divesites, except: [:new, :edit] resources :users, except: [:create, :new, :edit] post 'auth', to: 'users#auth' end diff --git a/db/migrate/20150210154934_create_divesites.rb b/db/migrate/20150210154934_create_divesites.rb new file mode 100644 index 0000000..1d14d19 --- /dev/null +++ b/db/migrate/20150210154934_create_divesites.rb @@ -0,0 +1,12 @@ +class CreateDivesites < ActiveRecord::Migration + def change + create_table :divesites do |t| + t.string :name + t.string :address + t.float :latitude + t.float :longitude + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e35c41c..fd534e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,20 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150209220550) do +ActiveRecord::Schema.define(version: 20150210154934) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "divesites", force: :cascade do |t| + t.string "name" + t.string "address" + t.float "latitude" + t.float "longitude" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "locations", force: :cascade do |t| t.string "address" t.float "latitude" diff --git a/db/seeds.rb b/db/seeds.rb index 9180bcf..5c77811 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -8,3 +8,10 @@ count += 1 end + +15.times do + address = "#{Faker::Address.city}, #{Faker::Address.country}" + Divesite.create( + name: Faker::Company.name, + address: address,) +end From 8341c6a7031fb286574f7748de771485b3b48b57 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 20:45:14 +0300 Subject: [PATCH 13/33] [add] CRUD for Dives --- app/controllers/api/v1/dives_controller.rb | 52 ++++++++++++++++++++++ app/models/dive.rb | 25 +++++++++++ app/models/divesite.rb | 3 ++ app/models/user.rb | 3 ++ app/serializers/dive_serializer.rb | 3 ++ config/routes.rb | 1 + db/migrate/20150210162340_create_dives.rb | 11 +++++ db/schema.rb | 13 +++++- db/seeds.rb | 9 +++- 9 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/dives_controller.rb create mode 100644 app/models/dive.rb create mode 100644 app/serializers/dive_serializer.rb create mode 100644 db/migrate/20150210162340_create_dives.rb diff --git a/app/controllers/api/v1/dives_controller.rb b/app/controllers/api/v1/dives_controller.rb new file mode 100644 index 0000000..59146ff --- /dev/null +++ b/app/controllers/api/v1/dives_controller.rb @@ -0,0 +1,52 @@ +class Api::V1::DivesController < Api::BaseController + before_action :authenticate! + before_action :set_dive, except: [:index, :create] + before_action :return_dive, only: :show + + def index + @dives = paginate Dive.all + + render json: @dives, each_serializer: DiveSerializer + end + + def create + @dive = Dive.new(dive_params) + + if @dive.save + return_dive + elsif @dive.invalid? + unprocessable_entity(@dive) + else + unexpected_error + end + end + + def show + end + + def update + if @dive.update(dive_params) + return_dive + else + unprocessable_entity(@dive) + end + end + + def destroy + return_dive if @dive.destroy + end + + private + + def set_dive + @dive = Dive.find(params[:id]) + end + + def return_dive + render json: @dive, serializer: DiveSerializer + end + + def dive_params + params.permit(:user_id, :divesite_id, :date) + end +end diff --git a/app/models/dive.rb b/app/models/dive.rb new file mode 100644 index 0000000..26365f9 --- /dev/null +++ b/app/models/dive.rb @@ -0,0 +1,25 @@ +# == Schema Information +# +# Table name: dives +# +# id :integer not null, primary key +# user_id :integer +# divesite_id :integer +# date :datetime +# created_at :datetime not null +# updated_at :datetime not null +# +class Dive < ActiveRecord::Base + belongs_to :divesite + belongs_to :user + + validates :divesite, :user, presence: true + + before_save :set_date + + private + + def set_date + self.date = created_at unless date.present? + end +end diff --git a/app/models/divesite.rb b/app/models/divesite.rb index da31776..232d1d9 100644 --- a/app/models/divesite.rb +++ b/app/models/divesite.rb @@ -11,6 +11,9 @@ # updated_at :datetime not null # class Divesite < ActiveRecord::Base + has_many :dives, class_name: 'Dive' + has_many :users, through: :dives + geocoded_by :address validates :address, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 0b01537..a0956a5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,9 @@ # authentication_token :string # class User < ActiveRecord::Base + has_many :dives, class_name: 'Dive' + has_many :divesites, through: :dives + before_save :ensure_authentication_token devise :database_authenticatable, diff --git a/app/serializers/dive_serializer.rb b/app/serializers/dive_serializer.rb new file mode 100644 index 0000000..9cf39e6 --- /dev/null +++ b/app/serializers/dive_serializer.rb @@ -0,0 +1,3 @@ +class DiveSerializer < ActiveModel::Serializer + attributes :id, :user_id, :divesite_id, :date +end diff --git a/config/routes.rb b/config/routes.rb index c32727c..1526728 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ Divebook::Application.routes.draw do namespace :api do namespace :v1 do + resources :dives, except: [:new, :edit] resources :divesites, except: [:new, :edit] resources :users, except: [:create, :new, :edit] post 'auth', to: 'users#auth' diff --git a/db/migrate/20150210162340_create_dives.rb b/db/migrate/20150210162340_create_dives.rb new file mode 100644 index 0000000..5d8ec23 --- /dev/null +++ b/db/migrate/20150210162340_create_dives.rb @@ -0,0 +1,11 @@ +class CreateDives < ActiveRecord::Migration + def change + create_table :dives do |t| + t.integer :user_id, index: true + t.integer :divesite_id, index: true + t.datetime :date + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fd534e6..b2fea94 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,22 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150210154934) do +ActiveRecord::Schema.define(version: 20150210162340) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "dives", force: :cascade do |t| + t.integer "user_id" + t.integer "divesite_id" + t.datetime "date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "dives", ["divesite_id"], name: "index_dives_on_divesite_id", using: :btree + add_index "dives", ["user_id"], name: "index_dives_on_user_id", using: :btree + create_table "divesites", force: :cascade do |t| t.string "name" t.string "address" diff --git a/db/seeds.rb b/db/seeds.rb index 5c77811..4476ffe 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -13,5 +13,12 @@ address = "#{Faker::Address.city}, #{Faker::Address.country}" Divesite.create( name: Faker::Company.name, - address: address,) + address: address) +end + +20.times do + Dive.create( + user_id: rand(1...15), + divesite_id: rand(1...15), + date: Faker::Date.between(5.month.ago, Date.today)) end From e8974e841c11731b447b4dc9e809535540904d37 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 21:05:47 +0300 Subject: [PATCH 14/33] [add] Divesites by User --- app/controllers/api/base_controller.rb | 2 +- app/controllers/api/v1/divesites_controller.rb | 8 +++++++- config/routes.rb | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index b34babe..2f3d8c9 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -15,7 +15,7 @@ def authenticate! def authenticated return true if warden.authenticated? - params[:access_token] && @user = User.find_by(authentication_token: params[:access_token]) + params[:access_token] && @current_user = User.find_by(authentication_token: params[:access_token]) end private diff --git a/app/controllers/api/v1/divesites_controller.rb b/app/controllers/api/v1/divesites_controller.rb index 1c533f2..61ff98c 100644 --- a/app/controllers/api/v1/divesites_controller.rb +++ b/app/controllers/api/v1/divesites_controller.rb @@ -4,7 +4,9 @@ class Api::V1::DivesitesController < Api::BaseController before_action :return_divesite, only: :show def index - @divesites = paginate Divesite.all + set_user if params[:user_id].present? + + @divesites = paginate(@user.present? ? @user.divesites : Divesite.all) render json: @divesites, each_serializer: DivesiteSerializer end @@ -42,6 +44,10 @@ def set_divesite @divesite = Divesite.find(params[:id]) end + def set_user + @user = User.find(params[:user_id]) + end + def return_divesite render json: @divesite, serializer: DivesiteSerializer end diff --git a/config/routes.rb b/config/routes.rb index 1526728..e177f3c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,10 @@ namespace :api do namespace :v1 do resources :dives, except: [:new, :edit] + resources :divesites, except: [:new, :edit] + get 'users/:user_id/divesites', to: 'divesites#index' + resources :users, except: [:create, :new, :edit] post 'auth', to: 'users#auth' end From 0f2c8cda34332f4e027a05cd8a188436eb3267e1 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 21:15:38 +0300 Subject: [PATCH 15/33] [add] Users by Dive Site --- app/controllers/api/v1/users_controller.rb | 8 +++++++- config/routes.rb | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 35f0336..b23661c 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -4,7 +4,9 @@ class Api::V1::UsersController < Api::BaseController before_action :return_user, only: :show def index - @users = paginate User.all + set_divesite + + @users = paginate @divesite.users render json: @users, each_serializer: UserSerializer end @@ -43,6 +45,10 @@ def auth private + def set_divesite + @divesite = Divesite.find(params[:divesite_id]) + end + def set_user @user = User.find(params[:id]) end diff --git a/config/routes.rb b/config/routes.rb index e177f3c..e6dcb33 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,14 +6,15 @@ resources :divesites, except: [:new, :edit] get 'users/:user_id/divesites', to: 'divesites#index' - resources :users, except: [:create, :new, :edit] + resources :users, only: [:show, :update, :destroy] + get 'divesites/:divesite_id/users', to: 'users#index' post 'auth', to: 'users#auth' end end #Locations resources :locations - +devise_for # Users devise_for :users, :controllers => { :registrations => :registrations, :omniauth_callbacks => 'users/omniauth_callbacks' } resources :users, :only => :show From de2b6cf23df8f9dcdba5a6d2dc78a243d54371e3 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Tue, 10 Feb 2015 21:46:55 +0300 Subject: [PATCH 16/33] [add] Dives by Dive Site and by User --- app/controllers/api/v1/dives_controller.rb | 21 ++++++++++++++++++++- app/controllers/api/v1/users_controller.rb | 8 ++++---- config/routes.rb | 2 ++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/dives_controller.rb b/app/controllers/api/v1/dives_controller.rb index 59146ff..b508f60 100644 --- a/app/controllers/api/v1/dives_controller.rb +++ b/app/controllers/api/v1/dives_controller.rb @@ -4,7 +4,18 @@ class Api::V1::DivesController < Api::BaseController before_action :return_dive, only: :show def index - @dives = paginate Dive.all + set_user if params[:user_id].present? + set_divesite if params[:divesite_id].present? + + @dives = paginate begin + if @user.present? + @user.dives + elsif @divesite.present? + @divesite.dives + else + Dive.all + end + end render json: @dives, each_serializer: DiveSerializer end @@ -42,6 +53,14 @@ def set_dive @dive = Dive.find(params[:id]) end + def set_divesite + @divesite = Divesite.find(params[:divesite_id]) + end + + def set_user + @user = User.find(params[:user_id]) + end + def return_dive render json: @dive, serializer: DiveSerializer end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index b23661c..d193f34 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -45,14 +45,14 @@ def auth private - def set_divesite - @divesite = Divesite.find(params[:divesite_id]) - end - def set_user @user = User.find(params[:id]) end + def set_divesite + @divesite = Divesite.find(params[:divesite_id]) + end + def return_user render json: @user, serializer: UserSerializer end diff --git a/config/routes.rb b/config/routes.rb index e6dcb33..cc5128e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,8 @@ namespace :api do namespace :v1 do resources :dives, except: [:new, :edit] + get 'divesites/:divesite_id/dives', to: 'dives#index' + get 'users/:user_id/dives', to: 'dives#index' resources :divesites, except: [:new, :edit] get 'users/:user_id/divesites', to: 'divesites#index' From 270a10dc5a4e3e192753d7e3179cabd6d13e9044 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 10:53:26 +0300 Subject: [PATCH 17/33] [add test] Users#show --- .rspec | 3 + Gemfile | 6 ++ Gemfile.lock | 27 +++++++++ app/controllers/api/base_controller.rb | 1 - config/environments/test.rb | 2 + .../api/v1/users_controller_spec.rb | 25 +++++++++ spec/factories/dives.rb | 18 ++++++ spec/factories/divesites.rb | 18 ++++++ spec/factories/users.rb | 34 ++++++++++++ spec/rails_helper.rb | 55 +++++++++++++++++++ spec/spec_helper.rb | 24 ++++++++ spec/support/database_cleaner.rb | 25 +++++++++ spec/support/factory_girl.rb | 3 + spec/support/request_helpers.rb | 8 +++ 14 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 .rspec create mode 100644 spec/controllers/api/v1/users_controller_spec.rb create mode 100644 spec/factories/dives.rb create mode 100644 spec/factories/divesites.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/database_cleaner.rb create mode 100644 spec/support/factory_girl.rb create mode 100644 spec/support/request_helpers.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..72cf692 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--color +--require spec_helper +--format progress diff --git a/Gemfile b/Gemfile index 286aa06..45f48f2 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,12 @@ group :development do gem 'annotate' end +group :test do + gem 'rspec-rails' + gem 'database_cleaner' + gem 'factory_girl_rails' +end + group :development, :test do gem 'letter_opener' end diff --git a/Gemfile.lock b/Gemfile.lock index 9d9a66c..7c7fdbe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,6 +81,7 @@ GEM country_select (2.1.1) countries (>= 0.9.3, < 0.10.0) currencies (0.4.2) + database_cleaner (1.4.0) debug_inspector (0.0.2) devise (3.4.1) bcrypt (~> 3.0) @@ -89,12 +90,18 @@ GEM responders thread_safe (~> 0.1) warden (~> 1.2.3) + diff-lcs (1.2.5) docile (1.1.5) dotenv (1.0.2) dotenv-deployment (0.2.0) dotenv (~> 1.0) erubis (2.7.0) execjs (2.3.0) + factory_girl (4.5.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.5.0) + factory_girl (~> 4.5.0) + railties (>= 3.0.0) faker (1.4.3) i18n (~> 0.5) faraday (0.9.1) @@ -198,6 +205,23 @@ GEM rake (10.4.2) responders (2.1.0) railties (>= 4.2.0, < 5) + rspec-core (3.2.0) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-rails (3.2.0) + actionpack (>= 3.0, <= 4.2) + activesupport (>= 3.0, <= 4.2) + railties (>= 3.0, <= 4.2) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.1) sass (3.2.19) sass-rails (4.0.4) railties (>= 4.0.0, < 5.0) @@ -261,8 +285,10 @@ DEPENDENCIES codeclimate-test-reporter compass-rails country_select + database_cleaner devise (~> 3.4.1) dotenv-deployment + factory_girl_rails faker (~> 1.4) foundation-icons-sass-rails! geocoder @@ -279,6 +305,7 @@ DEPENDENCIES pry-rails rails (= 4.2.0) rails-footnotes + rspec-rails sass-rails (~> 4.0.2) simple_form uglifier (>= 2.7.0) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 2f3d8c9..9d3c307 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -14,7 +14,6 @@ def authenticate! end def authenticated - return true if warden.authenticated? params[:access_token] && @current_user = User.find_by(authentication_token: params[:access_token]) end diff --git a/config/environments/test.rb b/config/environments/test.rb index b5c4ca9..a00af12 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -28,6 +28,8 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/spec/controllers/api/v1/users_controller_spec.rb b/spec/controllers/api/v1/users_controller_spec.rb new file mode 100644 index 0000000..19a6e4c --- /dev/null +++ b/spec/controllers/api/v1/users_controller_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +describe Api::V1::UsersController do + describe 'GET #show' do + let(:user) { create(:user) } + + it 'returns unauthorized' do + get :show, id: user.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) { get :show, id: user.id, access_token: user.authentication_token } + + it 'returns success' do + expect(response).to have_http_status(:ok) + end + + it 'returns the User object' do + user_json = { 'id' => user.id, 'name' => user.name } + + expect(json).to eq(user_json) + end + end +end diff --git a/spec/factories/dives.rb b/spec/factories/dives.rb new file mode 100644 index 0000000..a30bcd4 --- /dev/null +++ b/spec/factories/dives.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: dives +# +# id :integer not null, primary key +# user_id :integer +# divesite_id :integer +# date :datetime +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryGirl.define do + factory :dive do + date Faker::Date.between(5.month.ago, Date.today) + user + divesite + end +end diff --git a/spec/factories/divesites.rb b/spec/factories/divesites.rb new file mode 100644 index 0000000..cd8b11f --- /dev/null +++ b/spec/factories/divesites.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: divesites +# +# id :integer not null, primary key +# name :string +# address :string +# latitude :float +# longitude :float +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryGirl.define do + factory :divesite do + name Faker::Company.name + address Faker::Address.city + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..b696767 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# name :string +# email :string default(""), not null +# created_at :datetime +# updated_at :datetime +# encrypted_password :string default(""), not null +# reset_password_token :string +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default("0") +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string +# last_sign_in_ip :string +# country :string +# provider :string +# uid :string +# confirmation_token :string +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string +# authentication_token :string +# +FactoryGirl.define do + factory :user do + name Faker::Name.name + email Faker::Internet.email + password Faker::Internet.password(8) + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..e5e7280 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,55 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV['RAILS_ENV'] ||= 'test' +require 'spec_helper' +require File.expand_path('../../config/environment', __FILE__) +require 'support/database_cleaner' +require 'support/factory_girl' +require 'support/request_helpers' +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } + +# Checks for pending migrations before tests are run. +# If you are not using ActiveRecord, you can remove this line. +# ActiveRecord::Migration.maintain_test_schema! + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + config.include Requests::JsonHelpers, type: :controller +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..697c7ff --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,24 @@ +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end +end diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb new file mode 100644 index 0000000..10e6bf8 --- /dev/null +++ b/spec/support/database_cleaner.rb @@ -0,0 +1,25 @@ +RSpec.configure do |config| + config.before(:suite) do + DatabaseCleaner.clean_with(:deletion) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each, :js => true) do + DatabaseCleaner.strategy = :deletion + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end + + config.after(:suite) do + DatabaseCleaner.clean_with(:truncation) + end +end diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb new file mode 100644 index 0000000..eec437f --- /dev/null +++ b/spec/support/factory_girl.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryGirl::Syntax::Methods +end diff --git a/spec/support/request_helpers.rb b/spec/support/request_helpers.rb new file mode 100644 index 0000000..2eceb1d --- /dev/null +++ b/spec/support/request_helpers.rb @@ -0,0 +1,8 @@ +# spec/support/request_helpers.rb +module Requests + module JsonHelpers + def json + @json ||= JSON.parse(response.body) + end + end +end From 357d6ee7c73d4a25e852a520d06c54c1aa8498de Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 10:54:19 +0300 Subject: [PATCH 18/33] [feature] Return 201 Created after successful #create request --- app/controllers/api/v1/dives_controller.rb | 6 +++--- app/controllers/api/v1/divesites_controller.rb | 6 +++--- app/controllers/api/v1/users_controller.rb | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v1/dives_controller.rb b/app/controllers/api/v1/dives_controller.rb index b508f60..1b2c23e 100644 --- a/app/controllers/api/v1/dives_controller.rb +++ b/app/controllers/api/v1/dives_controller.rb @@ -24,7 +24,7 @@ def create @dive = Dive.new(dive_params) if @dive.save - return_dive + return_dive(:created) elsif @dive.invalid? unprocessable_entity(@dive) else @@ -61,8 +61,8 @@ def set_user @user = User.find(params[:user_id]) end - def return_dive - render json: @dive, serializer: DiveSerializer + def return_dive(status=:ok) + render json: @dive, serializer: DiveSerializer, status: status end def dive_params diff --git a/app/controllers/api/v1/divesites_controller.rb b/app/controllers/api/v1/divesites_controller.rb index 61ff98c..e07743d 100644 --- a/app/controllers/api/v1/divesites_controller.rb +++ b/app/controllers/api/v1/divesites_controller.rb @@ -15,7 +15,7 @@ def create @divesite = Divesite.new(divesite_params) if @divesite.save - return_divesite + return_divesite(:created) elsif @divesite.invalid? unprocessable_entity(@divesite) else @@ -48,8 +48,8 @@ def set_user @user = User.find(params[:user_id]) end - def return_divesite - render json: @divesite, serializer: DivesiteSerializer + def return_divesite(status=:ok) + render json: @divesite, serializer: DivesiteSerializer, status: status end def divesite_params diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index d193f34..52d8ddb 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -35,7 +35,7 @@ def auth name: @user.name, email: @user.email, access_token: @user.authentication_token - } + }, status: :created elsif @user.invalid? unprocessable_entity(@user) else From e9d868966e42544d9e601c6be723ba7d217c38ae Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 11:09:43 +0300 Subject: [PATCH 19/33] [add test] Users#update --- app/controllers/api/v1/users_controller.rb | 2 +- config/environments/test.rb | 3 +-- .../api/v1/users_controller_spec.rb | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 52d8ddb..240dfd3 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -58,6 +58,6 @@ def return_user end def user_params - params.permit(:name, :email, :password, :access_token) + params.permit(:name, :email, :password) end end diff --git a/config/environments/test.rb b/config/environments/test.rb index a00af12..ee8be54 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -28,8 +28,7 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } - + config.action_mailer.default_url_options = { host: 'divebook.herokuapp.com' } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/spec/controllers/api/v1/users_controller_spec.rb b/spec/controllers/api/v1/users_controller_spec.rb index 19a6e4c..6c23a4c 100644 --- a/spec/controllers/api/v1/users_controller_spec.rb +++ b/spec/controllers/api/v1/users_controller_spec.rb @@ -22,4 +22,28 @@ expect(json).to eq(user_json) end end + + describe 'PUT #update' do + let(:user) { create(:user) } + + it 'returns unauthorized' do + put :update, id: user.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + put :update, id: user.id, name: 'Batman', access_token: user.authentication_token + end + + it 'returns success' do + expect(response).to have_http_status(:ok) + end + + it 'returns the updated User object' do + user_json = { 'id' => user.id, 'name' => 'Batman' } + + expect(json).to eq(user_json) + end + end end From 247c48dfa905ef9552e6040bab846dde3d799686 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 14:43:25 +0300 Subject: [PATCH 20/33] [add test] Users#destroy --- config/environments/production.rb | 3 +- config/environments/test.rb | 2 ++ .../api/v1/users_controller_spec.rb | 33 +++++++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index c9ed7e7..76fb54c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -2,7 +2,8 @@ # Settings specified here will take precedence over those in config/application.rb # mailer config - config.action_mailer.default_url_options = { :host => 'divebook.herokuapp.com' } + config.action_mailer.default_url_options = { host: 'divebook.herokuapp.com' } + config.action_mailer.default_options = { from: 'no-reply@divebook.herokuapp.com' } config.action_mailer.raise_delivery_errors = false config.action_mailer.smtp_settings = { :address => "smtp.mandrillapp.com", diff --git a/config/environments/test.rb b/config/environments/test.rb index ee8be54..10be454 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -27,8 +27,10 @@ # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. + config.action_mailer.raise_delivery_errors = false config.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'divebook.herokuapp.com' } + config.action_mailer.default_options = { from: 'no-reply@divebook.herokuapp.com' } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/spec/controllers/api/v1/users_controller_spec.rb b/spec/controllers/api/v1/users_controller_spec.rb index 6c23a4c..5b1a63e 100644 --- a/spec/controllers/api/v1/users_controller_spec.rb +++ b/spec/controllers/api/v1/users_controller_spec.rb @@ -12,7 +12,7 @@ before(:each) { get :show, id: user.id, access_token: user.authentication_token } - it 'returns success' do + it 'returns 200 OK' do expect(response).to have_http_status(:ok) end @@ -36,7 +36,7 @@ put :update, id: user.id, name: 'Batman', access_token: user.authentication_token end - it 'returns success' do + it 'returns 200 OK' do expect(response).to have_http_status(:ok) end @@ -46,4 +46,33 @@ expect(json).to eq(user_json) end end + + describe 'DELETE #destroy' do + let(:user) { create(:user) } + + it 'returns unauthorized' do + delete :destroy, id: user.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + delete :destroy, id: user.id, access_token: user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'delete User from DB' do + expect(User.all).not_to include user + end + + let(:new_user) { create(:user) } + it 'returns 404 Not Found for second same request' do + delete :destroy, id: user.id, access_token: new_user.authentication_token + + expect(response).to have_http_status(:not_found) + end + end end From 0dbf35350a563e827292528a2493d7ece178de0c Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 15:19:51 +0300 Subject: [PATCH 21/33] [add tests] Users#index and Users#auth --- .../api/v1/users_controller_spec.rb | 58 +++++++++++++++++++ spec/factories/divesites.rb | 8 +++ 2 files changed, 66 insertions(+) diff --git a/spec/controllers/api/v1/users_controller_spec.rb b/spec/controllers/api/v1/users_controller_spec.rb index 5b1a63e..293a2aa 100644 --- a/spec/controllers/api/v1/users_controller_spec.rb +++ b/spec/controllers/api/v1/users_controller_spec.rb @@ -1,6 +1,40 @@ require 'rails_helper' describe Api::V1::UsersController do + describe 'GET #index' do + let(:divesite) { create(:divesite_with_5_users) } + let(:user) { divesite.users.first} + + it 'returns unauthorized' do + get :index, divesite_id: divesite.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + get :index, divesite_id: divesite.id, access_token: user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns array of Dive Site users' do + expect(json.count).to eq(divesite.users.count) + end + + it 'returns only two Dive Site users' do + per_page = 2 + + get :index, + divesite_id: divesite.id, + per_page: per_page, + access_token: user.authentication_token + + expect(json.count).to eq(per_page) + end + end + describe 'GET #show' do let(:user) { create(:user) } @@ -75,4 +109,28 @@ expect(response).to have_http_status(:not_found) end end + + describe 'POST #auth' do + let(:user) { build(:user) } + + before(:each) do + post :auth, + name: user.name, + email: user.email, + password: user.password + end + + it 'returns 201 Created' do + expect(response).to have_http_status(:created) + end + + it 'returns created User' do + expect(json['name']).to eq(user.name) + expect(json['email']).to eq(user.email) + end + + it 'returns access token for the User' do + expect(json['access_token']).to be + end + end end diff --git a/spec/factories/divesites.rb b/spec/factories/divesites.rb index cd8b11f..fcde9fa 100644 --- a/spec/factories/divesites.rb +++ b/spec/factories/divesites.rb @@ -14,5 +14,13 @@ factory :divesite do name Faker::Company.name address Faker::Address.city + + factory :divesite_with_5_users do + after(:create) do |divesite| + 5.times do + divesite.users << FactoryGirl.create(:user, email: Faker::Internet.email) + end + end + end end end From 77b64faf69098d689d7a972751ed0006fe62c597 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 17:20:20 +0300 Subject: [PATCH 22/33] [add tests] For DivesitesController --- Gemfile | 2 +- .../api/v1/divesites_controller_spec.rb | 173 ++++++++++++++++++ .../api/v1/users_controller_spec.rb | 22 ++- spec/factories/users.rb | 8 + 4 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 spec/controllers/api/v1/divesites_controller_spec.rb diff --git a/Gemfile b/Gemfile index 45f48f2..1f9d772 100644 --- a/Gemfile +++ b/Gemfile @@ -61,9 +61,9 @@ end group :test do gem 'rspec-rails' gem 'database_cleaner' - gem 'factory_girl_rails' end group :development, :test do + gem 'factory_girl_rails' gem 'letter_opener' end diff --git a/spec/controllers/api/v1/divesites_controller_spec.rb b/spec/controllers/api/v1/divesites_controller_spec.rb new file mode 100644 index 0000000..28bd1d4 --- /dev/null +++ b/spec/controllers/api/v1/divesites_controller_spec.rb @@ -0,0 +1,173 @@ +require 'rails_helper' + +describe Api::V1::DivesitesController do + describe 'GET #index' do + let(:user) { create(:user) } + + it 'returns unauthorized' do + get :index + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + @divesites = create_list(:divesite, 5) + get :index, access_token: user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns array of Dive Sites' do + expect(json.count).to eq(@divesites.count) + end + + it 'returns only two Dive Sites' do + per_page = 2 + + get :index, + per_page: per_page, + access_token: user.authentication_token + + expect(json.count).to eq(per_page) + end + end + + describe 'GET #index by User' do + let(:user) { create(:user_with_3_divesites) } + + it 'returns array of Dive Sites by User' do + get :index, + user_id: user.id, + access_token: user.authentication_token + + expect(json.count).to eq(user.divesites.count) + end + end + + describe 'POST #create' do + let(:divesite) { build(:divesite) } + let(:user) { create(:user) } + + it 'returns unauthorized' do + post :create, + name: divesite.name, + address: divesite.address + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + post :create, + name: divesite.name, + address: divesite.address, + access_token: user.authentication_token + end + + it 'returns 201 Created' do + expect(response).to have_http_status(:created) + end + + it 'returns created Dive Site' do + expect(json['name']).to eq(divesite.name) + expect(json['address']).to eq(divesite.address) + end + + it 'returns 422 Unprocessable Entity if address not set' do + post :create, + name: divesite.name, + access_token: user.authentication_token + + expect(response).to have_http_status(:unprocessable_entity) + end + end + + describe 'GET #show' do + let(:divesite) { create(:divesite) } + let(:user) { create(:user) } + + it 'returns unauthorized' do + get :show, id: user.id + + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 404 Not Found' do + get :show, id: 1000000, access_token: user.authentication_token + + expect(response).to have_http_status(:not_found) + end + + before(:each) { get :show, id: divesite.id, access_token: user.authentication_token } + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns the Dive Site object' do + divesite_json = { + 'id' => divesite.id, + 'name' => divesite.name, + 'address' => divesite.address, + 'latitude' => divesite.latitude, + 'longitude' => divesite.longitude + } + + expect(json).to eq(divesite_json) + end + end + + describe 'PUT #update' do + let(:divesite) { create(:divesite) } + let(:new_name) { Faker::Company.name } + let(:user) { create(:user) } + + it 'returns unauthorized' do + put :update, id: divesite.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + put :update, id: divesite.id, name: new_name, access_token: user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns the updated Dive Site object' do + expect(json['name']).to eq(new_name) + end + end + + describe 'DELETE #destroy' do + let(:divesite) { create(:divesite) } + let(:user) { create(:user) } + + it 'returns unauthorized' do + delete :destroy, id: divesite.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + delete :destroy, id: divesite.id, access_token: user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'delete Dive Site from DB' do + expect(Divesite.all).not_to include divesite + end + + it 'returns 404 Not Found for second same request' do + delete :destroy, id: divesite.id, access_token: user.authentication_token + + expect(response).to have_http_status(:not_found) + end + end +end diff --git a/spec/controllers/api/v1/users_controller_spec.rb b/spec/controllers/api/v1/users_controller_spec.rb index 293a2aa..f3a0367 100644 --- a/spec/controllers/api/v1/users_controller_spec.rb +++ b/spec/controllers/api/v1/users_controller_spec.rb @@ -44,6 +44,12 @@ expect(response).to have_http_status(:unauthorized) end + it 'returns 404 Not Found' do + get :show, id: 100000, access_token: user.authentication_token + + expect(response).to have_http_status(:not_found) + end + before(:each) { get :show, id: user.id, access_token: user.authentication_token } it 'returns 200 OK' do @@ -59,6 +65,7 @@ describe 'PUT #update' do let(:user) { create(:user) } + let(:new_name) { Faker::Name.name } it 'returns unauthorized' do put :update, id: user.id @@ -67,7 +74,7 @@ end before(:each) do - put :update, id: user.id, name: 'Batman', access_token: user.authentication_token + put :update, id: user.id, name: new_name, access_token: user.authentication_token end it 'returns 200 OK' do @@ -75,9 +82,7 @@ end it 'returns the updated User object' do - user_json = { 'id' => user.id, 'name' => 'Batman' } - - expect(json).to eq(user_json) + expect(json['name']).to eq(new_name) end end @@ -132,5 +137,14 @@ it 'returns access token for the User' do expect(json['access_token']).to be end + + it 'returns 422 Unprocessable Entity after same request' do + post :auth, + name: user.name, + email: user.email, + password: user.password + + expect(response).to have_http_status(:unprocessable_entity) + end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index b696767..4a03c98 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -30,5 +30,13 @@ name Faker::Name.name email Faker::Internet.email password Faker::Internet.password(8) + + factory :user_with_3_divesites do + after(:create) do |user| + 3.times do + user.divesites << FactoryGirl.create(:divesite, address: Faker::Address.city) + end + end + end end end From 5619008beb997fc0bb8b6f70b0bb7be36370ce33 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 17:44:32 +0300 Subject: [PATCH 23/33] [add tests] For DivesController --- .../api/v1/dives_controller_spec.rb | 193 ++++++++++++++++++ .../api/v1/divesites_controller_spec.rb | 14 +- 2 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 spec/controllers/api/v1/dives_controller_spec.rb diff --git a/spec/controllers/api/v1/dives_controller_spec.rb b/spec/controllers/api/v1/dives_controller_spec.rb new file mode 100644 index 0000000..fa08c58 --- /dev/null +++ b/spec/controllers/api/v1/dives_controller_spec.rb @@ -0,0 +1,193 @@ +require 'rails_helper' + +describe Api::V1::DivesController do + describe 'GET #index by User' do + let(:user) { create(:user_with_3_divesites) } + + it 'returns unauthorized' do + get :index + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + get :index, + user_id: user.id, + access_token: user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns array of Dives by User' do + expect(json.count).to eq(user.dives.count) + expect(json[0]['user_id']).to eq(user.id) + end + + it 'returns only two Dives' do + per_page = 2 + + get :index, + user_id: user.id, + per_page: per_page, + access_token: user.authentication_token + + expect(json.count).to eq(per_page) + end + end + + describe 'GET #index by Dive Site' do + let(:user) { create(:user) } + let(:divesite) { create(:divesite_with_5_users) } + + before(:each) do + get :index, + divesite_id: divesite.id, + access_token: user.authentication_token + end + + it 'returns array of Dives by Dive Site' do + expect(json.count).to eq(divesite.dives.count) + expect(json[0]['divesite_id']).to eq(divesite.id) + end + + it 'returns only two Dives' do + per_page = 2 + + get :index, + divesite_id: divesite.id, + per_page: per_page, + access_token: user.authentication_token + + expect(json.count).to eq(per_page) + end + end + + + describe 'POST #create' do + let(:dive) { build(:dive) } + let(:user) { create(:user, email: Faker::Internet.email) } + + it 'returns unauthorized' do + post :create, + user_id: dive.user_id, + divesite_id: dive.divesite_id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + post :create, + user_id: dive.user_id, + divesite_id: dive.divesite_id, + date: dive.date, + access_token: user.authentication_token + end + + it 'returns 201 Created' do + expect(response).to have_http_status(:created) + end + + it 'returns created Dive' do + expect(json['user_id']).to eq(dive.user_id) + expect(json['divesite_id']).to eq(dive.divesite_id) + end + + it 'returns 422 Unprocessable Entity if User not set' do + post :create, + divesite_id: dive.divesite_id, + date: dive.date, + access_token: user.authentication_token + + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns 422 Unprocessable Entity if Dive Site not set' do + post :create, + user_id: dive.user_id, + date: dive.date, + access_token: user.authentication_token + + expect(response).to have_http_status(:unprocessable_entity) + end + end + + describe 'GET #show' do + let(:dive) { create(:dive) } + + it 'returns unauthorized' do + get :show, id: dive.id + + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 404 Not Found' do + get :show, id: 1000000, access_token: dive.user.authentication_token + + expect(response).to have_http_status(:not_found) + end + + before(:each) { get :show, id: dive.id, access_token: dive.user.authentication_token } + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns the Dive Site object' do + expect(json['user_id']).to eq(dive.user_id) + expect(json['divesite_id']).to eq(dive.divesite_id) + end + end + + describe 'PUT #update' do + let(:dive) { create(:dive) } + let(:new_user) { create(:user, email: Faker::Internet.email) } + + it 'returns unauthorized' do + put :update, id: dive.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + put :update, id: dive.id, user_id: new_user.id, access_token: dive.user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'returns the updated Dive object' do + expect(json['user_id']).to eq(new_user.id) + end + end + + describe 'DELETE #destroy' do + let(:dive) { create(:dive) } + + it 'returns unauthorized' do + delete :destroy, id: dive.id + + expect(response).to have_http_status(:unauthorized) + end + + before(:each) do + delete :destroy, id: dive.id, access_token: dive.user.authentication_token + end + + it 'returns 200 OK' do + expect(response).to have_http_status(:ok) + end + + it 'delete Dive from DB' do + expect(Dive.all).not_to include dive + end + + it 'returns 404 Not Found for second same request' do + delete :destroy, id: dive.id, access_token: dive.user.authentication_token + + expect(response).to have_http_status(:not_found) + end + end +end diff --git a/spec/controllers/api/v1/divesites_controller_spec.rb b/spec/controllers/api/v1/divesites_controller_spec.rb index 28bd1d4..e53acb5 100644 --- a/spec/controllers/api/v1/divesites_controller_spec.rb +++ b/spec/controllers/api/v1/divesites_controller_spec.rb @@ -88,7 +88,7 @@ let(:user) { create(:user) } it 'returns unauthorized' do - get :show, id: user.id + get :show, id: divesite.id expect(response).to have_http_status(:unauthorized) end @@ -106,15 +106,9 @@ end it 'returns the Dive Site object' do - divesite_json = { - 'id' => divesite.id, - 'name' => divesite.name, - 'address' => divesite.address, - 'latitude' => divesite.latitude, - 'longitude' => divesite.longitude - } - - expect(json).to eq(divesite_json) + expect(json['id']).to eq(divesite.id) + expect(json['name']).to eq(divesite.name) + expect(json['address']).to eq(divesite.address) end end From 8782d2b74be28fb5f5868748f52717859a427d23 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 21:08:34 +0300 Subject: [PATCH 24/33] [add] Gems for production --- Gemfile | 5 +++++ Gemfile.lock | 9 +++++++++ config/application.rb | 2 ++ 3 files changed, 16 insertions(+) diff --git a/Gemfile b/Gemfile index 1f9d772..69c2baa 100644 --- a/Gemfile +++ b/Gemfile @@ -67,3 +67,8 @@ group :development, :test do gem 'factory_girl_rails' gem 'letter_opener' end + +group :production do + gem 'rails_12factor' + gem 'heroku-deflater' +end diff --git a/Gemfile.lock b/Gemfile.lock index 7c7fdbe..581e9a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -111,6 +111,8 @@ GEM globalid (0.3.2) activesupport (>= 4.1.0) hashie (3.4.0) + heroku-deflater (0.5.3) + rack (>= 1.4.5) hike (1.2.3) hirb (0.7.3) hirb-unicode (0.0.5) @@ -197,6 +199,11 @@ GEM rails (>= 3.2) rails-html-sanitizer (1.0.1) loofah (~> 2.0) + rails_12factor (0.0.3) + rails_serve_static_assets + rails_stdout_logging + rails_serve_static_assets (0.0.4) + rails_stdout_logging (0.0.3) railties (4.2.0) actionpack (= 4.2.0) activesupport (= 4.2.0) @@ -292,6 +299,7 @@ DEPENDENCIES faker (~> 1.4) foundation-icons-sass-rails! geocoder + heroku-deflater hirb-unicode kaminari letter_opener @@ -305,6 +313,7 @@ DEPENDENCIES pry-rails rails (= 4.2.0) rails-footnotes + rails_12factor rspec-rails sass-rails (~> 4.0.2) simple_form diff --git a/config/application.rb b/config/application.rb index 3846304..45a792e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -48,5 +48,7 @@ class Application < Rails::Application # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' + + config.middleware.use Rack::Deflater end end From 2d47494e38b2788abf96370ced33a9fc521e7840 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 21:22:16 +0300 Subject: [PATCH 25/33] Prepare for deployment --- Gemfile | 3 +++ Gemfile.lock | 2 ++ bin/rails | 10 ++++++++++ bin/rake | 7 +++++++ bin/spring | 18 ++++++++++++++++++ config/environments/production.rb | 4 +++- config/environments/test.rb | 2 +- config/initializers/devise.rb | 4 ++-- 8 files changed, 46 insertions(+), 4 deletions(-) create mode 100755 bin/rails create mode 100755 bin/rake create mode 100755 bin/spring diff --git a/Gemfile b/Gemfile index 69c2baa..08b3df7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,7 @@ source 'https://rubygems.org' +ruby '2.2.0' + # basic gem 'rails', '4.2.0' gem 'pg' # thanks to http://railscasts.com/episodes/342-migrating-to-postgresql @@ -66,6 +68,7 @@ end group :development, :test do gem 'factory_girl_rails' gem 'letter_opener' + gem 'spring' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 581e9a2..0517525 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -246,6 +246,7 @@ GEM simplecov-html (~> 0.8.0) simplecov-html (0.8.0) slop (3.6.0) + spring (1.3.0) sprockets (2.11.3) hike (~> 1.2) multi_json (~> 1.0) @@ -317,6 +318,7 @@ DEPENDENCIES rspec-rails sass-rails (~> 4.0.2) simple_form + spring uglifier (>= 2.7.0) web-console (~> 2.0) zurb-foundation (~> 3.1.1) diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..1c894d5 --- /dev/null +++ b/bin/rails @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..0fb4e07 --- /dev/null +++ b/bin/rake @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +begin + load File.expand_path("../spring", __FILE__) +rescue LoadError +end +require 'bundler/setup' +load Gem.bin_path('rake', 'rake') diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000..de6070b --- /dev/null +++ b/bin/spring @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast +# It gets overwritten when you run the `spring binstub` command + +unless defined?(Spring) + require "rubygems" + require "bundler" + + if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) + ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) + ENV["GEM_HOME"] = nil + Gem.paths = ENV + + gem "spring", match[1] + require "spring/binstub" + end +end diff --git a/config/environments/production.rb b/config/environments/production.rb index 76fb54c..de0495d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -3,7 +3,7 @@ # mailer config config.action_mailer.default_url_options = { host: 'divebook.herokuapp.com' } - config.action_mailer.default_options = { from: 'no-reply@divebook.herokuapp.com' } + config.action_mailer.default_options = { from: ENV['ADMIN_EMAIL'] } config.action_mailer.raise_delivery_errors = false config.action_mailer.smtp_settings = { :address => "smtp.mandrillapp.com", @@ -18,6 +18,8 @@ # Eager loading config.eager_load = true + config.log_level = :info + # Full error reports are disabled and caching is turned on config.consider_all_requests_local = false config.action_controller.perform_caching = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 10be454..3a93d3c 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,7 +30,7 @@ config.action_mailer.raise_delivery_errors = false config.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'divebook.herokuapp.com' } - config.action_mailer.default_options = { from: 'no-reply@divebook.herokuapp.com' } + config.action_mailer.default_options = { from: ENV['ADMIN_EMAIL'] } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ca43487..081f958 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -7,7 +7,7 @@ # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class with default "from" parameter. - config.mailer_sender = ENV['MAIL_FROM_DEVISE'] + config.mailer_sender = ENV['ADMIN_EMAIL'] # Configure the class responsible to send e-mails. # config.mailer = "Devise::Mailer" @@ -236,4 +236,4 @@ # When using omniauth, Devise cannot automatically set Omniauth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = "/my_engine/users/auth" -end \ No newline at end of file +end From 2f1a662195d562577b743055c3d6906a8f45dd5b Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 11 Feb 2015 21:31:03 +0300 Subject: [PATCH 26/33] [add] Dives attribute to JSON for Dive Sites --- app/serializers/divesite_serializer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/serializers/divesite_serializer.rb b/app/serializers/divesite_serializer.rb index ecd19bc..045ea69 100644 --- a/app/serializers/divesite_serializer.rb +++ b/app/serializers/divesite_serializer.rb @@ -1,3 +1,7 @@ class DivesiteSerializer < ActiveModel::Serializer - attributes :id, :name, :address, :latitude, :longitude + attributes :id, :name, :address, :latitude, :longitude, :dives + + def dives + object.dives.count + end end From 294ba70bc3c6f1457a56e2d81e4689e05126f815 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Thu, 12 Feb 2015 20:20:03 +0300 Subject: [PATCH 27/33] [add] Image model --- .gitignore | 1 + Gemfile | 4 ++++ Gemfile.lock | 16 +++++++++++++--- app/models/dive.rb | 1 + app/models/divesite.rb | 1 + app/models/image.rb | 15 +++++++++++++++ app/models/user.rb | 1 + app/serializers/dive_serializer.rb | 2 ++ app/serializers/image_serializer.rb | 19 +++++++++++++++++++ config/application.rb | 3 +++ db/migrate/20150212165142_create_images.rb | 10 ++++++++++ db/schema.rb | 14 +++++++++++++- public/cloud.jpg | Bin 0 -> 55565 bytes 13 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 app/models/image.rb create mode 100644 app/serializers/image_serializer.rb create mode 100644 db/migrate/20150212165142_create_images.rb create mode 100644 public/cloud.jpg diff --git a/.gitignore b/.gitignore index 4cf024e..8a081b3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ /tmp config/app_environment_variables.rb .env +/public/system diff --git a/Gemfile b/Gemfile index 08b3df7..5ee7bb9 100644 --- a/Gemfile +++ b/Gemfile @@ -19,11 +19,15 @@ gem 'omniauth-facebook' # geocoding gem 'geocoder' +# File Upload +gem 'paperclip' + # monitoring gem 'newrelic_rpm' # ENV variables gem 'dotenv-deployment' + gem 'faker', '~> 1.4' # API diff --git a/Gemfile.lock b/Gemfile.lock index 0517525..f46cab2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,7 +51,7 @@ GEM annotate (2.6.5) activerecord (>= 2.3.0) rake (>= 0.8.7) - api-pagination (4.1.0) + api-pagination (4.1.1) arel (6.0.0) awesome_print (1.6.1) bcrypt (3.1.10) @@ -67,6 +67,10 @@ GEM uniform_notifier (>= 1.6.0) callsite (0.0.11) chunky_png (1.3.3) + climate_control (0.0.3) + activesupport (>= 3.0) + cocaine (0.5.5) + climate_control (>= 0.0.3, < 1.0) codeclimate-test-reporter (0.4.6) simplecov (>= 0.7.1, < 1.0.0) coderay (1.1.0) @@ -121,7 +125,7 @@ GEM i18n (0.7.0) json (1.8.2) jwt (1.2.1) - kaminari (0.16.2) + kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) launchy (2.4.3) @@ -166,6 +170,11 @@ GEM oauth2 (~> 1.0) omniauth (~> 1.2) orm_adapter (0.5.0) + paperclip (4.2.1) + activemodel (>= 3.0.0) + activesupport (>= 3.0.0) + cocaine (~> 0.5.3) + mime-types pg (0.18.1) pry (0.10.1) coderay (~> 1.1.0) @@ -246,7 +255,7 @@ GEM simplecov-html (~> 0.8.0) simplecov-html (0.8.0) slop (3.6.0) - spring (1.3.0) + spring (1.3.1) sprockets (2.11.3) hike (~> 1.2) multi_json (~> 1.0) @@ -310,6 +319,7 @@ DEPENDENCIES oauth2 omniauth omniauth-facebook + paperclip pg pry-rails rails (= 4.2.0) diff --git a/app/models/dive.rb b/app/models/dive.rb index 26365f9..a12d143 100644 --- a/app/models/dive.rb +++ b/app/models/dive.rb @@ -12,6 +12,7 @@ class Dive < ActiveRecord::Base belongs_to :divesite belongs_to :user + has_many :images validates :divesite, :user, presence: true diff --git a/app/models/divesite.rb b/app/models/divesite.rb index 232d1d9..5ee49e8 100644 --- a/app/models/divesite.rb +++ b/app/models/divesite.rb @@ -12,6 +12,7 @@ # class Divesite < ActiveRecord::Base has_many :dives, class_name: 'Dive' + has_many :images, through: :dives has_many :users, through: :dives geocoded_by :address diff --git a/app/models/image.rb b/app/models/image.rb new file mode 100644 index 0000000..9da4a85 --- /dev/null +++ b/app/models/image.rb @@ -0,0 +1,15 @@ +class Image < ActiveRecord::Base + belongs_to :dive + + has_attached_file :file, + styles: { thumbnail: '150x150>', + medium: '300x300>', + standard: '600x600>'}, + convert_options: { thumbnail: '-quality 85 -strip', + medium: '-quality 85 -strip', + standard: '-quality 80 -strip' } + + validates_attachment :file, + presence: true, + content_type: { content_type: ['image/jpeg', 'image/png'] } +end diff --git a/app/models/user.rb b/app/models/user.rb index a0956a5..a7047cb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,6 +28,7 @@ class User < ActiveRecord::Base has_many :dives, class_name: 'Dive' has_many :divesites, through: :dives + has_many :images, through: :dives before_save :ensure_authentication_token diff --git a/app/serializers/dive_serializer.rb b/app/serializers/dive_serializer.rb index 9cf39e6..c3f57cb 100644 --- a/app/serializers/dive_serializer.rb +++ b/app/serializers/dive_serializer.rb @@ -1,3 +1,5 @@ class DiveSerializer < ActiveModel::Serializer attributes :id, :user_id, :divesite_id, :date + + has_many :images end diff --git a/app/serializers/image_serializer.rb b/app/serializers/image_serializer.rb new file mode 100644 index 0000000..bbf562b --- /dev/null +++ b/app/serializers/image_serializer.rb @@ -0,0 +1,19 @@ +class ImageSerializer < ActiveModel::Serializer + attributes :id, :thumbnail, :medium, :standard, :original + + def thumbnail + object.file.url :thumbnail + end + + def medium + object.file.url :medium + end + + def standard + object.file.url :standard + end + + def original + object.file.url + end +end diff --git a/config/application.rb b/config/application.rb index 45a792e..d292a8b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -46,6 +46,9 @@ class Application < Rails::Application # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + # Do not swallow errors in after_commit/after_rollback callbacks. + config.active_record.raise_in_transactional_callbacks = true + # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/db/migrate/20150212165142_create_images.rb b/db/migrate/20150212165142_create_images.rb new file mode 100644 index 0000000..3343ee2 --- /dev/null +++ b/db/migrate/20150212165142_create_images.rb @@ -0,0 +1,10 @@ +class CreateImages < ActiveRecord::Migration + def change + create_table :images do |t| + t.attachment :file + t.integer :dive_id, index: true + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b2fea94..53c168b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150210162340) do +ActiveRecord::Schema.define(version: 20150212165142) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -36,6 +36,18 @@ t.datetime "updated_at", null: false end + create_table "images", force: :cascade do |t| + t.string "file_file_name" + t.string "file_content_type" + t.integer "file_file_size" + t.datetime "file_updated_at" + t.integer "dive_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "images", ["dive_id"], name: "index_images_on_dive_id", using: :btree + create_table "locations", force: :cascade do |t| t.string "address" t.float "latitude" diff --git a/public/cloud.jpg b/public/cloud.jpg new file mode 100644 index 0000000000000000000000000000000000000000..befd06305d724e568ab286e305e6d6cd1c48401a GIT binary patch literal 55565 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 LhwyJ)*316^$}_ah literal 0 HcmV?d00001 From 0fced9cb0b92d2fda11af412276a10ff18314bef Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Thu, 12 Feb 2015 20:31:07 +0300 Subject: [PATCH 28/33] [add] Images controller --- app/controllers/api/v1/images_controller.rb | 58 +++++++++++++++++++++ config/routes.rb | 7 ++- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/images_controller.rb diff --git a/app/controllers/api/v1/images_controller.rb b/app/controllers/api/v1/images_controller.rb new file mode 100644 index 0000000..5cd9b82 --- /dev/null +++ b/app/controllers/api/v1/images_controller.rb @@ -0,0 +1,58 @@ +class Api::V1::ImagesController < Api::BaseController + before_action :authenticate! + before_action :set_image, except: :index + before_action :return_image, only: :show + + def index + set_user if params[:user_id].present? + set_dive if params[:dive_id].present? + set_divesite if params[:divesite_id].present? + + @images = paginate begin + if @user.present? + @user.images + elsif @dive.present? + @dive.images + elsif @divesite.present? + @divesite.images + else + Image.all + end + end + + render json: @images, each_serializer: ImageSerializer + end + + def show + end + + def destroy + return_image if @image.destroy + end + + private + + def set_image + @image = Image.find(params[:id]) + end + + def set_user + @user = User.find(params[:user_id]) + end + + def set_dive + @dive = Dive.find(params[:dive_id]) + end + + def set_divesite + @divesite = Divesite.find(params[:divesite_id]) + end + + def return_image(status=:ok) + render json: @image, serializer: ImageSerializer, status: status + end + + def image_params + params.permit(:file) + end +end diff --git a/config/routes.rb b/config/routes.rb index cc5128e..94d15b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,9 +1,14 @@ Divebook::Application.routes.draw do namespace :api do namespace :v1 do + resources :images, only: [:index, :show, :destroy] + get 'dives/:dive_id/images', to: 'images#index' + get 'divesites/:divesite_id/images', to: 'images#index' + get 'users/:user_id/images', to: 'images#index' + resources :dives, except: [:new, :edit] get 'divesites/:divesite_id/dives', to: 'dives#index' - get 'users/:user_id/dives', to: 'dives#index' + get 'users/:user_id/dives', to: 'dives#index' resources :divesites, except: [:new, :edit] get 'users/:user_id/divesites', to: 'divesites#index' From 642b6e9b2cd5090c6a46b4bec3601cf8e605a05e Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Thu, 12 Feb 2015 20:47:21 +0300 Subject: [PATCH 29/33] [feature] Upload images to Amazon S3 --- Gemfile | 1 + Gemfile.lock | 6 ++++++ config/aws.yml | 11 +++++++++++ config/initializers/paperclip.rb | 12 ++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 config/aws.yml create mode 100644 config/initializers/paperclip.rb diff --git a/Gemfile b/Gemfile index 5ee7bb9..7734ddf 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'geocoder' # File Upload gem 'paperclip' +gem 'aws-sdk', '< 2' # monitoring gem 'newrelic_rpm' diff --git a/Gemfile.lock b/Gemfile.lock index f46cab2..0bd0798 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,11 @@ GEM api-pagination (4.1.1) arel (6.0.0) awesome_print (1.6.1) + aws-sdk (1.61.0) + aws-sdk-v1 (= 1.61.0) + aws-sdk-v1 (1.61.0) + json (~> 1.4) + nokogiri (>= 1.4.4) bcrypt (3.1.10) better_errors (2.1.1) coderay (>= 1.0.0) @@ -296,6 +301,7 @@ DEPENDENCIES annotate api-pagination awesome_print + aws-sdk (< 2) better_errors binding_of_caller bullet diff --git a/config/aws.yml b/config/aws.yml new file mode 100644 index 0000000..5a703cd --- /dev/null +++ b/config/aws.yml @@ -0,0 +1,11 @@ +development: + access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> + secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> + +production: + access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> + secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> + +test: + access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> + secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb new file mode 100644 index 0000000..2be5f67 --- /dev/null +++ b/config/initializers/paperclip.rb @@ -0,0 +1,12 @@ +# config/initializers/paperclip.rb +Paperclip::Attachment.default_options.merge!( + storage: :s3, + s3_host_name: ENV['S3_BUCKET_ENDPOINT'], + bucket: ENV['S3_BUCKET_NAME'] +) + +Rails.application.config.paperclip_defaults = { + s3_credentials: { + bucket: ENV['S3_BUCKET_NAME'] + } +} From b6378caeb9eddda983916206151d02a467d5cb53 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Thu, 12 Feb 2015 21:03:05 +0300 Subject: [PATCH 30/33] [add] Images#create --- app/controllers/api/v1/images_controller.rb | 16 ++++++++++++++-- app/models/image.rb | 1 + app/serializers/image_serializer.rb | 7 ++++++- config/routes.rb | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/images_controller.rb b/app/controllers/api/v1/images_controller.rb index 5cd9b82..3075c1b 100644 --- a/app/controllers/api/v1/images_controller.rb +++ b/app/controllers/api/v1/images_controller.rb @@ -1,6 +1,6 @@ class Api::V1::ImagesController < Api::BaseController before_action :authenticate! - before_action :set_image, except: :index + before_action :set_image, except: [:index, :create] before_action :return_image, only: :show def index @@ -23,6 +23,18 @@ def index render json: @images, each_serializer: ImageSerializer end + def create + @image = Image.new(image_params) + + if @image.save + return_image(:created) + elsif @image.invalid? + unprocessable_entity(@image) + else + unexpected_error + end + end + def show end @@ -53,6 +65,6 @@ def return_image(status=:ok) end def image_params - params.permit(:file) + params.permit(:file, :dive_id) end end diff --git a/app/models/image.rb b/app/models/image.rb index 9da4a85..465e7c4 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -9,6 +9,7 @@ class Image < ActiveRecord::Base medium: '-quality 85 -strip', standard: '-quality 80 -strip' } + validates :dive, presence: true validates_attachment :file, presence: true, content_type: { content_type: ['image/jpeg', 'image/png'] } diff --git a/app/serializers/image_serializer.rb b/app/serializers/image_serializer.rb index bbf562b..57f34d0 100644 --- a/app/serializers/image_serializer.rb +++ b/app/serializers/image_serializer.rb @@ -1,5 +1,10 @@ class ImageSerializer < ActiveModel::Serializer - attributes :id, :thumbnail, :medium, :standard, :original + attributes :id, + :dive_id, + :thumbnail, + :medium, + :standard, + :original def thumbnail object.file.url :thumbnail diff --git a/config/routes.rb b/config/routes.rb index 94d15b0..75ad463 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Divebook::Application.routes.draw do namespace :api do namespace :v1 do - resources :images, only: [:index, :show, :destroy] + resources :images, only: [:index, :show, :create, :destroy] get 'dives/:dive_id/images', to: 'images#index' get 'divesites/:divesite_id/images', to: 'images#index' get 'users/:user_id/images', to: 'images#index' From f9f113fd34788a610fe8e7296341ba8a8edc6649 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Thu, 12 Feb 2015 21:03:42 +0300 Subject: [PATCH 31/33] [add] Divesite id and User id to Image JSON --- app/controllers/api/v1/images_controller.rb | 8 ++++++++ app/serializers/image_serializer.rb | 10 ++++++++++ config/routes.rb | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/images_controller.rb b/app/controllers/api/v1/images_controller.rb index 3075c1b..2354c4e 100644 --- a/app/controllers/api/v1/images_controller.rb +++ b/app/controllers/api/v1/images_controller.rb @@ -38,6 +38,14 @@ def create def show end + def update + if @image.update(image_params) + return_image + else + unprocessable_entity(@image) + end + end + def destroy return_image if @image.destroy end diff --git a/app/serializers/image_serializer.rb b/app/serializers/image_serializer.rb index 57f34d0..27ab975 100644 --- a/app/serializers/image_serializer.rb +++ b/app/serializers/image_serializer.rb @@ -1,11 +1,21 @@ class ImageSerializer < ActiveModel::Serializer attributes :id, :dive_id, + :divesite_id, + :user_id, :thumbnail, :medium, :standard, :original + def divesite_id + object.dive.divesite.id + end + + def user_id + object.dive.user.id + end + def thumbnail object.file.url :thumbnail end diff --git a/config/routes.rb b/config/routes.rb index 75ad463..d8fc4e1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Divebook::Application.routes.draw do namespace :api do namespace :v1 do - resources :images, only: [:index, :show, :create, :destroy] + resources :images, except: [:new, :edit] get 'dives/:dive_id/images', to: 'images#index' get 'divesites/:divesite_id/images', to: 'images#index' get 'users/:user_id/images', to: 'images#index' From e4e1ebe19a02d312aa539cc7275d0f9a0555413b Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Fri, 13 Feb 2015 01:24:49 +0300 Subject: [PATCH 32/33] [add] API Documentation --- API-specs.md | 852 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 706 insertions(+), 146 deletions(-) diff --git a/API-specs.md b/API-specs.md index d55cc8a..184c3b7 100644 --- a/API-specs.md +++ b/API-specs.md @@ -1,45 +1,212 @@ # Divebook REST API Specs -Ruby Version is 2.2.0
-Rails Version is 4.2.0 +Ruby version is 2.2.0
+Ruby on Rails version is 4.2.0 -API code should be namespaced to `API::`.
-API URIs should be namespaced to `/api/`. - -Session authentication is done with Devise. +API code should be namespaced to `Api::V1`
+API URIs should be namespaced to `/api/v1/` ## Table of Contents -- Dive Sites - - All Dive Sites - - Add Dive Site - - Get Dive Site - - Update Dive Site - - Delete Dive Site - - Dive Sites by User - Users - - Add User (?) - - Get User - - Update User - - Delete User - - Users by Dive Site + - [Authentication](#authentication) + - [Get User](#get-user) + - [Update User](#update-user) + - [Delete User](#delete-user) + - [Users by Dive Site](#users-by-dive-site) + +- Dive Sites + - [All Dive Sites](#all-dive-sites) + - [Add Dive Site](#add-dive-site) + - [Get Dive Site](#get-dive-site) + - [Update Dive Site](#update-dive-site) + - [Delete Dive Site](#delete-dive-site) + - [Dive Sites by User](#dive-sites-by-user) + - Dives - - Add Dive - - Get Dive - - Update Dive - - Delete Dive - - Dives by User - - Dives by Dive Site + - [Add Dive](#add-dive) + - [Get Dive](#get-dive) + - [Update Dive](#update-dive) + - [Delete Dive](#delete-dive) + - [Dives by User](#dives-by-user) + - [Dives by Dive Site](#dives-by-dive-site) + +- Images + - [Add Image](#add-image) + - [Get Image](#get-image) + - [Update Image](#update-image) + - [Delete Image](#delete-image) + - [Images by Dive](#images-by-dive) + - [Images by Dive Site](#images-by-dive-site) + - [Images by User](#images-by-user) + +- [Pagination](#pagination) + + +*** + +## Users +### Authentication + +`POST http://divebook.herokuapp.com/api/v1/auth` + +#### Parameters + +- **name** +- **email** _(required)_ +- **password** _(required)_ + +#### Response + +Status code: `201 Created`
+The created User object. + +```json +{ + "id": 1, + "name": "User Name", + "email": "user@example.com", + "access_token": "y786wY4Q-bVyYFFNm5QA" +} +``` + +#### Errors + +- **422 Unprocessable Entity** — The system had trouble saving the User. + +*** + +### Get User + +`GET http://divebook.herokuapp.com/api/users/:id` + +#### Parameters + +- **id** *(required)* — ID of the user. +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+The User object. + +```json +{ + "id": 1, + "name": "Naoise Golden" +} +``` + +#### Errors + +- **404 Not Found** — User with the specified ID does not exist. + +*** + +### Update User + +`PUT http://divebook.herokuapp.com/api/users/:id` + +#### Parameters + +- **id** *(required)* — ID of the user. +- **access_token** *(required)* +- **name** +- **email** + +#### Response + +Status code: `200 OK`
+The updated User object. + +```json +{ + "id": 1, + "name": "Naoise Golden" +} +``` + +#### Errors + +- **404 Not Found** — User with the specified ID does not exist. +- **422 Unprocessable Entity** — The system had trouble updating the User. + +*** + +### Delete User + +`DELETE http://divebook.herokuapp.com/api/users/:id` + +#### Parameters + +- **id** *(required)* — ID of the user. +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+The deleted User object. + +```json +{ + "id": 1, + "name": "Naoise Golden" +} +``` + +#### Errors + +- **404 Not Found** — User with the specified ID does not exist. + +*** + +### Users by Dive Site + +`GET http://divebook.herokuapp.com/api/divesites/:divesite_id/users` + +#### Parameters + +- **divesite_id** *(required)* — ID of the Dive Site. +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+Array of User objects. + +```json +[ + { + "id": 1, + "name": "Naoise Golden" + }, + { + "id": 2, + "name": "Paula Sanchez" + } +] +``` + +#### Errors + +- **404 Not Found** — Dive Site with the specified ID does not exist. + +*** ## Dive Sites + ### All Dive Sites -#### http://divebook.herokuapp.com/api/divesites -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +`GET http://divebook.herokuapp.com/api/divesites` + +#### Parameters + +- **access_token** *(required)* + +#### Response -Response: array of Dive Site objects. +Status code: `200 OK`
+Array of Dive Site objects. ```json [ @@ -62,32 +229,58 @@ Response: array of Dive Site objects. ] ``` +*** + ### Add Dive Site -#### http://divebook.herokuapp.com/api/divesites/add -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| POST | Yes | +`POST http://divebook.herokuapp.com/api/divesites` -Response: the created Dive Site object. +#### Parameters +- **name** +- **address** *(required)* +- **access_token** *(required)* -```json +#### Response + +Status code: `201 Created`
+The created Dive Site object. +```json +{ + "id": 1, + "name": "Blue Hole", + "address": "Dahab, Egypt", + "latitude": "28.572179", + "longitude": "34.537062", + "dives": 0 +} ``` +#### Errors + +- **422 Unprocessable Entity** — The system had trouble saving the Dive Site. + +*** + ### Get Dive Site -#### http://divebook.herokuapp.com/api/divesites/DIVESITE_ID -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +`GET http://divebook.herokuapp.com/api/divesites/:id` + +#### Parameters + +- **id** *(required)* +- **access_token** *(required)* -Response: the Dive Site object. +#### Response + +Status code: `200 OK`
+The Dive Site object. ```json { + "id": 1, "name": "Blue Hole", "address": "Dahab, Egypt", "latitude": "28.572179", @@ -96,229 +289,596 @@ Response: the Dive Site object. } ``` +#### Errors + +- **404 Not Found** — Dive Site with the specified ID does not exist. + +*** ### Update Dive Site -#### http://divebook.herokuapp.com/api/divesites/DIVESITE_ID +`PUT http://divebook.herokuapp.com/api/divesites/:id` + +#### Parameters -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| PUT | Yes | +- **id** *(required)* +- **access_token** *(required)* +- **name** +- **address** -Response: the updated Dive Site object. +#### Response + +Status code: `200 OK`
+The updated Dive Site object. ```json +{ + "id": 1, + "name": "Blue Hole", + "address": "Dahab, Egypt", + "latitude": "28.572179", + "longitude": "34.537062", + "dives": 5 +} ``` +#### Errors + +- **404 Not Found** — Dive Site with the specified ID does not exist. +- **422 Unprocessable Entity** — The system had trouble updating the Dive Site. + +*** + ### Delete Dive Site -#### http://divebook.herokuapp.com/api/divesites/ +`DELETE http://divebook.herokuapp.com/api/divesites/:id` + +#### Parameters + +- **id** *(required)* +- **access_token** *(required)* -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| DELETE | Yes | +#### Response -Response: success or error message. +Status code: `200 OK`
+The deleted Dive Site object. ```json +{ + "id": 1, + "name": "Blue Hole", + "address": "Dahab, Egypt", + "latitude": "28.572179", + "longitude": "34.537062", + "dives": 5 +} ``` +#### Errors + +- **404 Not Found** — Dive Site with the specified ID does not exist. + +*** + ### Dive Sites by User -#### http://divebook.herokuapp.com/api/users/USER_ID/divesites +`GET http://divebook.herokuapp.com/api/users/:user_id/divesites` + +#### Parameters -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +- **user_id** *(required)* — ID of the User. +- **access_token** *(required)* -Response: array of Dive Site objects. +#### Response + +Status code: `200 OK`
+Array of Dive Site objects. ```json +[ + { + "id": 1, + "name": "Blue Hole", + "address": "Dahab, Egypt", + "latitude": "28.572179", + "longitude": "34.537062", + "dives": 5 + }, + { + "id": 2, + "name": "Eel Garden", + "address": "Utila, Honduras", + "latitude": "16.114262", + "longitude": "-86.945522", + "dives": 3 + } +] ``` -## Users -### Add User -#### http://divebook.herokuapp.com/api/users +#### Errors -NOTE: maybe it's not possible with Devise. +- **404 Not Found** — User with the specified ID does not exist. -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| POST | Yes | +*** -Response: the created User object. + +## Dives + +### Add Dive + +`POST http://divebook.herokuapp.com/api/dives` + +#### Parameters + +- **user_id** *(required)* +- **divesite_id** *(required)* +- **date** +- **access_token** *(required)* + +#### Response + +Status code: `201 Created`
+The created Dive object. ```json +{ + "id": 1, + "user_id": 1, + "divesite_id": 1, + "date": "2014-04-23T18:25:43.511Z", + "images": [ ] +} ``` -### Get User -#### http://divebook.herokuapp.com/api/users/USER_ID +#### Errors + +- **422 Unprocessable Entity** — The system had trouble saving the Dive. + +*** + +### Get Dive -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +`GET http://divebook.herokuapp.com/api/dives/:id` -Response: the User object. +#### Parameters + +- **id** *(required)* +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+The Dive object. ```json { "id": 1, - "name": "Naoise Golden" + "user_id": 1, + "divesite_id": 1, + "date": "2014-04-23T18:25:43.511Z", + "images": [ ] } ``` -### Update User -#### http://divebook.herokuapp.com/api/users/USER_ID +#### Errors + +- **404 Not Found** — Dive with the specified ID does not exist. + +*** + +### Update Dive +`PUT http://divebook.herokuapp.com/api/dives/:id` -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| PUT | Yes | +#### Parameters -Response: the updated User object. +- **id** *(required)* +- **access_token** *(required)* +- **user_id** +- **divesite_id** +- **date** + +#### Response + +Status code: `200 OK`
+The updated Dive object. ```json +{ + "id": 1, + "user_id": 1, + "divesite_id": 1, + "date": "2014-04-23T18:25:43.511Z", + "images": [ ] +} ``` -## Delete User -#### http://divebook.herokuapp.com/api/users/USER_ID +#### Errors + +- **404 Not Found** — Dive with the specified ID does not exist. +- **422 Unprocessable Entity** — The system had trouble updating the Dive. + +*** + +### Delete Dive +`DELETE http://divebook.herokuapp.com/api/dives/:id` + +#### Parameters + +- **id** *(required)* +- **access_token** *(required)* -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| DELETE | Yes | +#### Response -Response: success or error message. +Status code: `200 OK`
+The deleted Dive object. ```json +{ + "id": 1, + "user_id": 1, + "divesite_id": 1, + "date": "2014-04-23T18:25:43.511Z", + "images": [ ] +} ``` -### Users by Dive Site -#### http://divebook.herokuapp.com/api/divesites/DIVESITE_ID/users +#### Errors + +- **404 Not Found** — Dive with the specified ID does not exist. -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +*** -Response: array of User objects. +### Dives by User +`GET http://divebook.herokuapp.com/api/users/:user_id/dives` + +#### Parameters + +- **user_id** *(required)* — ID of the User. +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+Array of Dive objects. ```json [ { "id": 1, - "name": "Naoise Golden" + "user_id": 1, + "divesite_id": 1, + "date": "2014-04-23T18:25:43.511Z", + "images": [ ] }, { "id": 2, - "name": "Paula Sanchez" + "user_id": 1, + "divesite_id": 2, + "date": "2014-04-23T15:00:00.511Z", + "images": [ ] } ] ``` -## Dives -### Add Dive -#### http://divebook.herokuapp.com/api/dives +#### Errors + +- **404 Not Found** — User with the specified ID does not exist. + +*** + +### Dives by Dive Site +`GET http://divebook.herokuapp.com/api/divesites/:divesite_id/dives` + +#### Parameters + +- **divesite_id** *(required)* — ID of the Dive Site. +- **access_token** *(required)* -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| POST | Yes | +#### Response -Response: created Dive object. +Status code: `200 OK`
+Array of Dive objects. + +```json +[ + { + "id": 1, + "user_id": 1, + "divesite_id": 1, + "date": "2014-04-23T18:25:43.511Z", + "images": [ ] + }, + { + "id": 2, + "user_id": 2, + "divesite_id": 1, + "date": "2014-04-23T15:00:00.511Z", + "images": [ ] + } +] +``` + +#### Errors + +- **404 Not Found** — Dive Site with the specified ID does not exist. + +*** + + +## Images + +### Add Image + +`POST http://divebook.herokuapp.com/api/images` + +#### Parameters + +- **dive_id** *(required)* +- **file** *(required)* +- **access_token** *(required)* + +#### Response + +Status code: `201 Created`
+The created Image object. + +- **thumbnail** — Image with 150x150> size. +- **medium** — Image with 300x300> size. +- **standard** — Image with 600x600> size. +- **original** — Original Image. ```json { "id": 1, - "user_id": 1, - "divesite_id": 1, - "date": "2012-04-23T18:25:43.511Z" + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" } ``` -### Dive -#### http://divebook.herokuapp.com/api/dives/DIVE_ID +#### Errors -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +- **422 Unprocessable Entity** — The system had trouble saving the Image. -Response: the Dive object. +*** + +### Get Image + +`GET http://divebook.herokuapp.com/api/images/:id` + +#### Parameters + +- **id** *(required)* +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+The Image object. ```json { "id": 1, - "user_id": 1, - "divesite_id": 1, - "date": "2012-04-23T18:25:43.511Z" + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" } ``` -### Update Dive -#### http://divebook.herokuapp.com/api/dives/DIVE_ID +#### Errors + +- **404 Not Found** — Image with the specified ID does not exist. -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| PUT | Yes | +*** -Response: the updated Dive object. +### Update Image +`PUT http://divebook.herokuapp.com/api/images/:id` + +#### Parameters + +- **id** *(required)* +- **dive_id** +- **file** +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+The updated Image object. ```json +{ + "id": 1, + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" +} ``` -### Delete Dive -#### http://divebook.herokuapp.com/api/dives/DIVE_ID +#### Errors + +- **404 Not Found** — Image with the specified ID does not exist. +- **422 Unprocessable Entity** — The system had trouble updating the Image. + +*** + +### Delete Image +`DELETE http://divebook.herokuapp.com/api/images/:id` -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| DELETE | Yes | +#### Parameters -Response: success or error message. +- **id** *(required)* +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+The deleted Image object. ```json +{ + "id": 1, + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" +} ``` -### Dives by User -#### http://divebook.herokuapp.com/api/users/USER_ID/dives +#### Errors + +- **404 Not Found** — Image with the specified ID does not exist. + +*** + +### Images by Dive +`GET http://divebook.herokuapp.com/api/dives/:dive_id/images` + +#### Parameters -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +- **dive_id** *(required)* — ID of the Dive. +- **access_token** *(required)* -Response: array of Dive objects. +#### Response + +Status code: `200 OK`
+Array of Image objects. ```json [ { "id": 1, - "user_id": 1, - "divesite_id": 1, - "date": "2012-04-23T18:25:43.511Z" + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" }, { "id": 2, - "user_id": 1, - "divesite_id": 2, - "date": "2012-04-23T15:00:00.511Z" + "dive_id": 3, + "divesite_id": 6, + "user_id": 8, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/002/thumbnail/egypt.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/002/medium/egypt.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/002/standard/egypt.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/002/original/egypt.jpg" } ] ``` -### Dives by Dive Site -#### http://divebook.herokuapp.com/api/divesites/USER_ID/dives +#### Errors + +- **404 Not Found** — Dive with the specified ID does not exist. + +*** + +### Images by Dive Site +`GET http://divebook.herokuapp.com/api/divesites/:divesite_id/images` + +#### Parameters + +- **divesite_id** *(required)* — ID of the Dive Site. +- **access_token** *(required)* -| HTTP Method | Requires acting user | -| ------------- | -------------------- | -| GET | Yes | +#### Response -Response: array of Dive objects. +Status code: `200 OK`
+Array of Image objects. ```json [ { "id": 1, - "user_id": 1, - "divesite_id": 1, - "date": "2012-04-23T18:25:43.511Z" + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" }, { - "id": 3, - "user_id": 2, - "divesite_id": 1, - "date": "2012-03-12T11:10:55.511Z" + "id": 2, + "dive_id": 4, + "divesite_id": 5, + "user_id": 8, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/002/thumbnail/egypt.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/002/medium/egypt.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/002/standard/egypt.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/002/original/egypt.jpg" + } +] +``` + +#### Errors + +- **404 Not Found** — Dive Site with the specified ID does not exist. + +*** + +### Images by User +`GET http://divebook.herokuapp.com/api/users/:user_id/images` + +#### Parameters + +- **user_id** *(required)* — ID of the User. +- **access_token** *(required)* + +#### Response + +Status code: `200 OK`
+Array of Image objects. + +```json +[ + { + "id": 1, + "dive_id": 3, + "divesite_id": 5, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/001/thumbnail/sample.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/001/medium/sample.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/001/standard/sample.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/001/original/sample.jpg" + }, + { + "id": 2, + "dive_id": 4, + "divesite_id": 6, + "user_id": 7, + "thumbnail": "http://s3.amazonaws.com/divebook/images/files/000/000/002/thumbnail/egypt.jpg", + "medium": "http://s3.amazonaws.com/divebook/images/files/000/000/002/medium/egypt.jpg", + "standard": "http://s3.amazonaws.com/divebook/images/files/000/000/002/standard/egypt.jpg", + "original": "http://s3.amazonaws.com/divebook/images/files/000/000/002/original/egypt.jpg" } ] ``` + +#### Errors + +- **404 Not Found** — User with the specified ID does not exist. + +*** + +## Pagination + +Requests that return multiple items will be paginated to 25 items by default. You can specify further pages with the `?page` parameter. For some resources, you can also set a custom page size up to 100 with the `?per_page` parameter. + +`GET http://divebook.herokuapp.com/api/dives?per_page=10&page=2` + +Note that page numbering is 1-based and that omitting the ?page parameter will return the first page. From 29b33973ad938343f6213484a8c480671031814a Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Fri, 13 Feb 2015 11:19:28 +0300 Subject: [PATCH 33/33] [add] Connection through HTTPS --- API-specs.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/API-specs.md b/API-specs.md index 184c3b7..b81c17b 100644 --- a/API-specs.md +++ b/API-specs.md @@ -48,7 +48,7 @@ API URIs should be namespaced to `/api/v1/` ## Users ### Authentication -`POST http://divebook.herokuapp.com/api/v1/auth` +`POST https://divebook.herokuapp.com/api/v1/auth` #### Parameters @@ -78,7 +78,7 @@ The created User object. ### Get User -`GET http://divebook.herokuapp.com/api/users/:id` +`GET https://divebook.herokuapp.com/api/users/:id` #### Parameters @@ -105,7 +105,7 @@ The User object. ### Update User -`PUT http://divebook.herokuapp.com/api/users/:id` +`PUT https://divebook.herokuapp.com/api/users/:id` #### Parameters @@ -135,7 +135,7 @@ The updated User object. ### Delete User -`DELETE http://divebook.herokuapp.com/api/users/:id` +`DELETE https://divebook.herokuapp.com/api/users/:id` #### Parameters @@ -162,7 +162,7 @@ The deleted User object. ### Users by Dive Site -`GET http://divebook.herokuapp.com/api/divesites/:divesite_id/users` +`GET https://divebook.herokuapp.com/api/divesites/:divesite_id/users` #### Parameters @@ -197,7 +197,7 @@ Array of User objects. ### All Dive Sites -`GET http://divebook.herokuapp.com/api/divesites` +`GET https://divebook.herokuapp.com/api/divesites` #### Parameters @@ -233,7 +233,7 @@ Array of Dive Site objects. ### Add Dive Site -`POST http://divebook.herokuapp.com/api/divesites` +`POST https://divebook.herokuapp.com/api/divesites` #### Parameters @@ -265,7 +265,7 @@ The created Dive Site object. ### Get Dive Site -`GET http://divebook.herokuapp.com/api/divesites/:id` +`GET https://divebook.herokuapp.com/api/divesites/:id` #### Parameters @@ -296,7 +296,7 @@ The Dive Site object. *** ### Update Dive Site -`PUT http://divebook.herokuapp.com/api/divesites/:id` +`PUT https://divebook.herokuapp.com/api/divesites/:id` #### Parameters @@ -329,7 +329,7 @@ The updated Dive Site object. *** ### Delete Dive Site -`DELETE http://divebook.herokuapp.com/api/divesites/:id` +`DELETE https://divebook.herokuapp.com/api/divesites/:id` #### Parameters @@ -359,7 +359,7 @@ The deleted Dive Site object. *** ### Dive Sites by User -`GET http://divebook.herokuapp.com/api/users/:user_id/divesites` +`GET https://divebook.herokuapp.com/api/users/:user_id/divesites` #### Parameters @@ -403,7 +403,7 @@ Array of Dive Site objects. ### Add Dive -`POST http://divebook.herokuapp.com/api/dives` +`POST https://divebook.herokuapp.com/api/dives` #### Parameters @@ -435,7 +435,7 @@ The created Dive object. ### Get Dive -`GET http://divebook.herokuapp.com/api/dives/:id` +`GET https://divebook.herokuapp.com/api/dives/:id` #### Parameters @@ -464,7 +464,7 @@ The Dive object. *** ### Update Dive -`PUT http://divebook.herokuapp.com/api/dives/:id` +`PUT https://divebook.herokuapp.com/api/dives/:id` #### Parameters @@ -497,7 +497,7 @@ The updated Dive object. *** ### Delete Dive -`DELETE http://divebook.herokuapp.com/api/dives/:id` +`DELETE https://divebook.herokuapp.com/api/dives/:id` #### Parameters @@ -526,7 +526,7 @@ The deleted Dive object. *** ### Dives by User -`GET http://divebook.herokuapp.com/api/users/:user_id/dives` +`GET https://divebook.herokuapp.com/api/users/:user_id/dives` #### Parameters @@ -564,7 +564,7 @@ Array of Dive objects. *** ### Dives by Dive Site -`GET http://divebook.herokuapp.com/api/divesites/:divesite_id/dives` +`GET https://divebook.herokuapp.com/api/divesites/:divesite_id/dives` #### Parameters @@ -606,7 +606,7 @@ Array of Dive objects. ### Add Image -`POST http://divebook.herokuapp.com/api/images` +`POST https://divebook.herokuapp.com/api/images` #### Parameters @@ -645,7 +645,7 @@ The created Image object. ### Get Image -`GET http://divebook.herokuapp.com/api/images/:id` +`GET https://divebook.herokuapp.com/api/images/:id` #### Parameters @@ -677,7 +677,7 @@ The Image object. *** ### Update Image -`PUT http://divebook.herokuapp.com/api/images/:id` +`PUT https://divebook.herokuapp.com/api/images/:id` #### Parameters @@ -712,7 +712,7 @@ The updated Image object. *** ### Delete Image -`DELETE http://divebook.herokuapp.com/api/images/:id` +`DELETE https://divebook.herokuapp.com/api/images/:id` #### Parameters @@ -744,7 +744,7 @@ The deleted Image object. *** ### Images by Dive -`GET http://divebook.herokuapp.com/api/dives/:dive_id/images` +`GET https://divebook.herokuapp.com/api/dives/:dive_id/images` #### Parameters @@ -788,7 +788,7 @@ Array of Image objects. *** ### Images by Dive Site -`GET http://divebook.herokuapp.com/api/divesites/:divesite_id/images` +`GET https://divebook.herokuapp.com/api/divesites/:divesite_id/images` #### Parameters @@ -832,7 +832,7 @@ Array of Image objects. *** ### Images by User -`GET http://divebook.herokuapp.com/api/users/:user_id/images` +`GET https://divebook.herokuapp.com/api/users/:user_id/images` #### Parameters @@ -879,6 +879,6 @@ Array of Image objects. Requests that return multiple items will be paginated to 25 items by default. You can specify further pages with the `?page` parameter. For some resources, you can also set a custom page size up to 100 with the `?per_page` parameter. -`GET http://divebook.herokuapp.com/api/dives?per_page=10&page=2` +`GET https://divebook.herokuapp.com/api/dives?per_page=10&page=2` Note that page numbering is 1-based and that omitting the ?page parameter will return the first page.