diff --git a/.gitignore b/.gitignore index c1f8602..4d9bfcc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ todos/log/*.log todos/tmp/ .gems/ coverage/ +.byebug_history \ No newline at end of file diff --git a/Gemfile b/Gemfile index 70a37bb..b31f2f1 100644 --- a/Gemfile +++ b/Gemfile @@ -14,4 +14,6 @@ gemspec # your gem to rubygems.org. # To use a debugger -# gem 'byebug', group: [:development, :test] +gem 'awesome_print', group: %i[development test] +gem 'byebug', group: %i[development test] +gem 'shoulda-matchers', group: :test diff --git a/Gemfile.lock b/Gemfile.lock index 516ec84..8e749ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,8 +56,10 @@ GEM tzinfo (~> 1.1) arel (9.0.0) ast (2.4.1) + awesome_print (1.8.0) bcrypt (3.1.13) builder (3.2.4) + byebug (11.1.3) committee (3.3.0) json_schema (~> 0.14, >= 0.14.3) openapi_parser (>= 0.6.1) @@ -212,6 +214,8 @@ GEM activerecord (~> 5.0) schema_plus_columns valuable + shoulda-matchers (4.3.0) + activesupport (>= 4.2.0) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -241,6 +245,8 @@ PLATFORMS ruby DEPENDENCIES + awesome_print + byebug croods! gem-release pg @@ -249,6 +255,7 @@ DEPENDENCIES rspec_junit_formatter (~> 0.4.1) rubocop (= 0.80.1) rubocop-rspec (= 1.38.1) + shoulda-matchers simplecov (~> 0.17.0) timecop (= 0.9.1) diff --git a/spec/support/shoulda_matchers.rb b/spec/support/shoulda_matchers.rb new file mode 100644 index 0000000..edcf9dd --- /dev/null +++ b/spec/support/shoulda_matchers.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/spec/todos/notes/create_spec.rb b/spec/todos/notes/create_spec.rb new file mode 100644 index 0000000..18a2900 --- /dev/null +++ b/spec/todos/notes/create_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'POST /notes', type: :request do + subject { response } + + let(:params) { { text: 'Foo Bar', list_id: list.id } } + + let(:project) do + current_user.projects.create! name: 'Foo' + end + + let(:list) do + project.lists.create! name: 'Foo' + end + + let(:task) do + list.tasks.create! name: 'Foo' + end + + let(:assignment) do + task.assignments.create! user: current_user + end + + let(:note) { Note.find_by(text: 'Foo Bar') } + + context 'without the optional association' do + before do + post '/notes', params: params.to_json + end + + it { is_expected.to have_http_status(:created) } + it { expect(response.body).to eq_json(note) } + end + + context 'with the optional association' do + let(:params) do + { text: 'Foo Bar', list_id: list.id, assignment_id: assignment.id } + end + + before do + post '/notes', params: params.to_json + end + + it { is_expected.to have_http_status(:created) } + it { expect(response.body).to eq_json(note) } + end + + context 'without the required association' do + let(:params) do + { text: 'Foo Bar', assignment_id: assignment.id } + end + + before do + post '/notes', params: params.to_json + end + + it { is_expected.to have_http_status(:bad_request) } + it { expect(response.body).to include('list_id', "wasn't supplied.") } + end +end diff --git a/spec/todos/notes/destroy_spec.rb b/spec/todos/notes/destroy_spec.rb new file mode 100644 index 0000000..1927aec --- /dev/null +++ b/spec/todos/notes/destroy_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'DELETE /notes/:id', type: :request do + subject { response } + + let(:project) do + current_user.projects.create! name: 'Foo' + end + + let(:list) do + project.lists.create! name: 'Foo' + end + + let(:task) do + list.tasks.create! name: 'Foo' + end + + let(:assignment) do + task.assignments.create! user: current_user + end + + let(:note) do + list.notes.create!(text: 'foo') + end + + before do + project + list + task + assignment + note + end + + context 'without the optional association' do + before do + delete "/notes/#{note.id}" + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(note) } + it { expect(list.notes.find_by(text: 'foo')).to be_nil } + end + + context 'with the optional association' do + let(:note) do + list.notes.create!(text: 'foo', assignment: assignment) + end + + before do + delete "/notes/#{note.id}" + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(note) } + it { expect(list.notes.find_by(text: 'foo')).to be_nil } + end +end diff --git a/spec/todos/notes/index_spec.rb b/spec/todos/notes/index_spec.rb new file mode 100644 index 0000000..56b9c00 --- /dev/null +++ b/spec/todos/notes/index_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'GET /notes', type: :request do + subject { response } + + let(:project) do + current_user.projects.create! name: 'Foo' + end + + let(:list) do + project.lists.create! name: 'Foo' + end + + let(:task) do + list.tasks.create! name: 'Foo' + end + + let(:assignment) do + task.assignments.create! user: current_user + end + + let(:note) do + list.notes.create!(text: 'foo', assignment: assignment) + end + + let(:notes) do + list.notes.order(:created_at) + end + + context 'with valid request' do + before do + get "/notes?list_id=#{list.id}" + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(notes) } + end + + context 'without the optional association' do + before do + note.update!(assignment: nil) + + get "/notes?list_id=#{list.id}" + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(notes) } + end +end diff --git a/spec/todos/notes/model_spec.rb b/spec/todos/notes/model_spec.rb new file mode 100644 index 0000000..bfe656a --- /dev/null +++ b/spec/todos/notes/model_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Note, type: :model do + it { is_expected.to belong_to(:assignment).optional } + it { is_expected.to belong_to(:list) } + it { is_expected.to validate_presence_of(:text) } +end diff --git a/spec/todos/notes/notes_spec.rb b/spec/todos/notes/notes_spec.rb new file mode 100644 index 0000000..bfe656a --- /dev/null +++ b/spec/todos/notes/notes_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Note, type: :model do + it { is_expected.to belong_to(:assignment).optional } + it { is_expected.to belong_to(:list) } + it { is_expected.to validate_presence_of(:text) } +end diff --git a/spec/todos/notes/update_spec.rb b/spec/todos/notes/update_spec.rb new file mode 100644 index 0000000..a56adab --- /dev/null +++ b/spec/todos/notes/update_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'PUT /notes/:id', type: :request do + subject { response } + + let(:project) do + current_user.projects.create! name: 'Foo' + end + + let(:list) do + project.lists.create! name: 'Foo' + end + + let(:task) do + list.tasks.create! name: 'Foo' + end + + let(:assignment) do + task.assignments.create! user: current_user + end + + let(:note) do + list.notes.create!(text: 'foo', assignment: assignment) + end + + before do + project + list + task + assignment + note + end + + context 'with optional association' do + before do + put "/notes/#{note.id}", params: { text: 'Bar Foo' }.to_json + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(note.reload) } + end + + context 'without optional association' do + let(:note) do + list.notes.create!(text: 'foo') + end + + before do + put "/notes/#{note.id}", params: { text: 'Bar Foo' }.to_json + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(note.reload) } + end + + context 'when removing the optional association' do + before do + put "/notes/#{note.id}", params: { assignment_id: nil }.to_json + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(note.reload) } + end + + context 'when adding an optional association' do + let(:note) do + list.notes.create!(text: 'foo') + end + + before do + put "/notes/#{note.id}", params: { assignment_id: assignment.id }.to_json + end + + it { is_expected.to have_http_status(:ok) } + it { expect(response.body).to eq_json(note.reload) } + end +end diff --git a/todos/app/resources/notes/model.rb b/todos/app/resources/notes/model.rb new file mode 100644 index 0000000..432c3f5 --- /dev/null +++ b/todos/app/resources/notes/model.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Notes + module Model + extend ActiveSupport::Concern + + included do + belongs_to :assignment, optional: true + end + end +end diff --git a/todos/app/resources/notes/resource.rb b/todos/app/resources/notes/resource.rb new file mode 100644 index 0000000..2a3b1ad --- /dev/null +++ b/todos/app/resources/notes/resource.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Notes + class Resource < ApplicationResource + filter_by :list + + extend_model { include Notes::Model } + end +end diff --git a/todos/config/initializers/croods.rb b/todos/config/initializers/croods.rb index e17f150..0a5f5f1 100644 --- a/todos/config/initializers/croods.rb +++ b/todos/config/initializers/croods.rb @@ -7,6 +7,7 @@ :lists, :tasks, :assignments, + :notes, multi_tenancy_by: :organization ) diff --git a/todos/db/migrate/20200626135719_create_notes.rb b/todos/db/migrate/20200626135719_create_notes.rb new file mode 100644 index 0000000..671561d --- /dev/null +++ b/todos/db/migrate/20200626135719_create_notes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateNotes < ActiveRecord::Migration[5.2] + def change + create_table :notes do |t| + t.references :assignment + t.references :list, null: false + t.text :text, null: false + + t.timestamps + end + end +end diff --git a/todos/db/schema.rb b/todos/db/schema.rb index 9b8bb23..f6ce734 100644 --- a/todos/db/schema.rb +++ b/todos/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_25_150646) do +ActiveRecord::Schema.define(version: 2020_06_26_135719) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -84,4 +84,12 @@ t.datetime "updated_at", :null=>false end + create_table "notes", force: :cascade do |t| + t.bigint "assignment_id", :foreign_key=>{:references=>"assignments", :name=>"fk_notes_assignment_id", :on_update=>:no_action, :on_delete=>:no_action}, :index=>{:name=>"fk__notes_assignment_id"} + t.bigint "list_id", :null=>false, :foreign_key=>{:references=>"lists", :name=>"fk_notes_list_id", :on_update=>:no_action, :on_delete=>:no_action}, :index=>{:name=>"fk__notes_list_id"} + t.text "text", :null=>false + t.datetime "created_at", :null=>false + t.datetime "updated_at", :null=>false + end + end