From b4841b3887e0f64368cf85ee8a8b8481d9cdbc3c Mon Sep 17 00:00:00 2001 From: max-borisov Date: Mon, 19 Sep 2016 10:00:29 +0200 Subject: [PATCH 1/2] Contacts page - create Contact model - create association between users and contacts - create Contacts controller. Add contact - update spec tests --- .gitignore | 2 + Gemfile | 1 + Gemfile.lock | 3 ++ app/assets/stylesheets/application.scss | 8 ++++ app/controllers/flows/contacts_controller.rb | 37 +++++++++++++++++++ app/models/contact.rb | 11 ++++++ app/models/membership.rb | 9 +++++ app/models/user.rb | 2 + app/views/flows/contacts/_contact.html.slim | 6 +++ app/views/flows/contacts/_form.html.slim | 11 ++++++ app/views/flows/contacts/index.html.slim | 15 ++++++++ config/{database.yml => database.example.yml} | 0 config/routes.rb | 1 + .../20160918121902_create_users_contacts.rb | 8 ++++ .../20160924203226_create_memberships.rb | 10 +++++ ...0160925080106_drop_users_contacts_table.rb | 8 ++++ ...5080729_add_unique_index_to_memberships.rb | 5 +++ .../20161001091106_add_name_to_memberships.rb | 5 +++ db/schema.rb | 13 ++++++- spec/factories/contact.rb | 7 ++++ spec/factories/membership.rb | 7 ++++ spec/models/contact_spec.rb | 12 ++++++ spec/models/membership_spec.rb | 14 +++++++ spec/models/user_spec.rb | 8 ++++ 24 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 app/controllers/flows/contacts_controller.rb create mode 100644 app/models/contact.rb create mode 100644 app/models/membership.rb create mode 100644 app/views/flows/contacts/_contact.html.slim create mode 100644 app/views/flows/contacts/_form.html.slim create mode 100644 app/views/flows/contacts/index.html.slim rename config/{database.yml => database.example.yml} (100%) create mode 100644 db/migrate/20160918121902_create_users_contacts.rb create mode 100644 db/migrate/20160924203226_create_memberships.rb create mode 100644 db/migrate/20160925080106_drop_users_contacts_table.rb create mode 100644 db/migrate/20160925080729_add_unique_index_to_memberships.rb create mode 100644 db/migrate/20161001091106_add_name_to_memberships.rb create mode 100644 spec/factories/contact.rb create mode 100644 spec/factories/membership.rb create mode 100644 spec/models/contact_spec.rb create mode 100644 spec/models/membership_spec.rb diff --git a/.gitignore b/.gitignore index f57f5b1..81fcb14 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ bower.json *.swp *.swo .vimrc + +config/database.yml diff --git a/Gemfile b/Gemfile index 8549a6c..0d558a8 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,7 @@ gem 'sass-rails', '~> 5.0' gem 'sextant', group: [:development] gem 'slim-rails' gem 'spring', group: [:development] +gem 'spring-commands-rspec', group: [:test] gem 'spring-watcher-listen', '~> 2.0.0', group: [:development] gem 'turbolinks', '~> 5' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock index ef90e87..57706aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -222,6 +222,8 @@ GEM railties (>= 3.1) slim (~> 3.0) spring (1.7.2) + spring-commands-rspec (1.0.4) + spring (>= 0.9.1) spring-watcher-listen (2.0.0) listen (>= 2.7, < 4.0) spring (~> 1.2) @@ -286,6 +288,7 @@ DEPENDENCIES shoulda-matchers (~> 3.1) slim-rails spring + spring-commands-rspec spring-watcher-listen (~> 2.0.0) turbolinks (~> 5) tzinfo-data diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 6622598..ec7840a 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,3 +16,11 @@ label.required:after { content: " *"; color: red; } + +.form-inline { + .form-group { + input { + width: 100%; + } + } +} diff --git a/app/controllers/flows/contacts_controller.rb b/app/controllers/flows/contacts_controller.rb new file mode 100644 index 0000000..cbe4d35 --- /dev/null +++ b/app/controllers/flows/contacts_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +class Flows::ContactsController < ApplicationController + def index + @contacts = contacts + @contact = current_user.contacts.build + end + + def create + contact = Contact.find_or_initialize_by(email: contact_params[:email]) + contact.name = contact_params[:name] + + if contact.save + membership = current_user.memberships.build(contact_id: contact.id) + return redirect_to flows_contacts_path, alert: 'Contact has already been added' unless membership.valid? + membership.save + redirect_to flows_contacts_path, notice: 'Contact has been added' + else + @contacts = contacts + @contact = contact + render :index + end + end + + def edit; end + + def destroy; end + + private + + def contacts + @contacts = current_user.contacts.order(id: :asc) + end + + def contact_params + params.require(:contact).permit(:name, :email) + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb new file mode 100644 index 0000000..12410c1 --- /dev/null +++ b/app/models/contact.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +class Contact < ApplicationRecord + self.table_name = 'users' + + has_many :memberships + has_many :users, through: :memberships + + validates :name, :email, presence: true, length: { maximum: 255 } + + delegate :name, to: :memberships +end diff --git a/app/models/membership.rb b/app/models/membership.rb new file mode 100644 index 0000000..a407ef8 --- /dev/null +++ b/app/models/membership.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +class Membership < ApplicationRecord + belongs_to :user + belongs_to :contact + + validates :name, presence: true, length: { maximum: 255 } + validates :contact_id, uniqueness: { scope: :user_id, + message: 'has already been added' } +end diff --git a/app/models/user.rb b/app/models/user.rb index b7b91bf..b996349 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,6 +4,8 @@ class User < ApplicationRecord :trackable, :validatable has_many :newsletters + has_many :memberships + has_many :contacts, through: :memberships validates :name, presence: true, length: { maximum: 255 } end diff --git a/app/views/flows/contacts/_contact.html.slim b/app/views/flows/contacts/_contact.html.slim new file mode 100644 index 0000000..d8fd3cc --- /dev/null +++ b/app/views/flows/contacts/_contact.html.slim @@ -0,0 +1,6 @@ +tr + td= contact.id + td= link_to contact.name, '#' + td= contact.email + td= 0 + td= 0 diff --git a/app/views/flows/contacts/_form.html.slim b/app/views/flows/contacts/_form.html.slim new file mode 100644 index 0000000..c8f0378 --- /dev/null +++ b/app/views/flows/contacts/_form.html.slim @@ -0,0 +1,11 @@ +.panel.panel-success + .panel-body + form.form-inline + = form_for contact, url: url, html: { class: 'for-inline' } do |f| + = render 'shared/errors', model: contact + .form-group.col-lg-5 + = f.text_field :name, placeholder: 'Name', class: 'form-control' + .form-group.col-lg-5 + = f.text_field :email, placeholder: 'E-mail', class: 'form-control' + .form-group.col-lg-2 + = f.submit 'Добавить', class: 'btn btn-success' diff --git a/app/views/flows/contacts/index.html.slim b/app/views/flows/contacts/index.html.slim new file mode 100644 index 0000000..ae4bf97 --- /dev/null +++ b/app/views/flows/contacts/index.html.slim @@ -0,0 +1,15 @@ +h1 Мои Контакты + += render 'form', contact: @contact, url: flows_contacts_path + +- if @contacts.empty? + .alert.alert-warning.text-center Нет добавленных контактов +-else + table.table.table-hover.table-striped + tr + th ID + th Имя + th E-mail + th Кол-во подписок + th Кол-во отписанных + = render @contacts diff --git a/config/database.yml b/config/database.example.yml similarity index 100% rename from config/database.yml rename to config/database.example.yml diff --git a/config/routes.rb b/config/routes.rb index f7b2523..058a6cb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ devise_for :users namespace :flows do resources :newsletters, except: [:edit] + resources :contacts, only: [:index, :create] end resources :flows, only: [:index, :show] root to: 'flows#index' diff --git a/db/migrate/20160918121902_create_users_contacts.rb b/db/migrate/20160918121902_create_users_contacts.rb new file mode 100644 index 0000000..c2bd5b8 --- /dev/null +++ b/db/migrate/20160918121902_create_users_contacts.rb @@ -0,0 +1,8 @@ +class CreateUsersContacts < ActiveRecord::Migration[5.0] + def change + create_table :users_contacts, id: false do |t| + t.belongs_to :user, index: true + t.belongs_to :contact, index: true + end + end +end diff --git a/db/migrate/20160924203226_create_memberships.rb b/db/migrate/20160924203226_create_memberships.rb new file mode 100644 index 0000000..ec2735a --- /dev/null +++ b/db/migrate/20160924203226_create_memberships.rb @@ -0,0 +1,10 @@ +class CreateMemberships < ActiveRecord::Migration[5.0] + def change + create_table :memberships do |t| + t.belongs_to :user, index: true + t.belongs_to :contact, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20160925080106_drop_users_contacts_table.rb b/db/migrate/20160925080106_drop_users_contacts_table.rb new file mode 100644 index 0000000..52aa142 --- /dev/null +++ b/db/migrate/20160925080106_drop_users_contacts_table.rb @@ -0,0 +1,8 @@ +class DropUsersContactsTable < ActiveRecord::Migration[5.0] + def change + drop_table :users_contacts, id: false do |t| + t.belongs_to :user, index: true + t.belongs_to :contact, index: true + end + end +end diff --git a/db/migrate/20160925080729_add_unique_index_to_memberships.rb b/db/migrate/20160925080729_add_unique_index_to_memberships.rb new file mode 100644 index 0000000..f0e81e6 --- /dev/null +++ b/db/migrate/20160925080729_add_unique_index_to_memberships.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexToMemberships < ActiveRecord::Migration[5.0] + def change + add_index :memberships, [:user_id, :contact_id], unique: true + end +end diff --git a/db/migrate/20161001091106_add_name_to_memberships.rb b/db/migrate/20161001091106_add_name_to_memberships.rb new file mode 100644 index 0000000..3f1af85 --- /dev/null +++ b/db/migrate/20161001091106_add_name_to_memberships.rb @@ -0,0 +1,5 @@ +class AddNameToMemberships < ActiveRecord::Migration[5.0] + def change + add_column :memberships, :name, :string, limit: 255 + end +end diff --git a/db/schema.rb b/db/schema.rb index 1b28a3e..6a16df4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,18 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160917154254) do +ActiveRecord::Schema.define(version: 20161001091106) do + + create_table "memberships", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| + t.integer "user_id" + t.integer "contact_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "name" + t.index ["contact_id"], name: "index_memberships_on_contact_id", using: :btree + t.index ["user_id", "contact_id"], name: "index_memberships_on_user_id_and_contact_id", unique: true, using: :btree + t.index ["user_id"], name: "index_memberships_on_user_id", using: :btree + end create_table "newsletters", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| t.string "name" diff --git a/spec/factories/contact.rb b/spec/factories/contact.rb new file mode 100644 index 0000000..3d397a5 --- /dev/null +++ b/spec/factories/contact.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +FactoryGirl.define do + factory :contact do + name { Faker::Name.name } + email { Faker::Internet.email } + end +end diff --git a/spec/factories/membership.rb b/spec/factories/membership.rb new file mode 100644 index 0000000..c04c7e9 --- /dev/null +++ b/spec/factories/membership.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +FactoryGirl.define do + factory :membership do + user + contact + end +end diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb new file mode 100644 index 0000000..9cb101b --- /dev/null +++ b/spec/models/contact_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +RSpec.describe Contact, type: :model do + subject { FactoryGirl.build(:contact) } + + it { should have_many(:memberships) } + it { should have_many(:users).through(:memberships) } + + it { should validate_presence_of(:name) } + it { should validate_presence_of(:email) } + it { should validate_length_of(:name).is_at_most(255) } + it { should validate_length_of(:email).is_at_most(255) } +end diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb new file mode 100644 index 0000000..efcceb6 --- /dev/null +++ b/spec/models/membership_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +RSpec.describe Membership, type: :model do + subject { FactoryGirl.build(:membership) } + + it { should belong_to(:user) } + it { should belong_to(:contact) } + + it { should validate_presence_of(:name) } + it { should validate_length_of(:name).is_at_most(255) } + it do + should validate_uniqueness_of(:contact_id).scoped_to(:user_id) + .with_message('has already been added') + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fdeb17a..93ef966 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -32,5 +32,13 @@ it 'responds to newsletters association' do expect(subject).to respond_to(:newsletters) end + + it 'responds to memberships association' do + expect(subject).to respond_to(:memberships) + end + + it 'responds to contacts association' do + expect(subject).to respond_to(:contacts) + end end end From c98a0fdbd53cb10cd1c34fa1ff2ff35b7408fd11 Mon Sep 17 00:00:00 2001 From: MpaK Date: Wed, 5 Oct 2016 00:24:37 +0700 Subject: [PATCH 2/2] contact removed, only Membership - User (subscriber or subscribed relations) --- app/models/contact.rb | 11 ---- app/models/membership.rb | 9 ++-- app/models/user.rb | 10 ++-- ...1004163616_rename_columns_in_membership.rb | 6 +++ db/schema.rb | 16 +++--- spec/factories/contact.rb | 7 --- spec/factories/membership.rb | 3 +- spec/models/contact_spec.rb | 12 ----- spec/models/membership_spec.rb | 51 ++++++++++++++++--- spec/models/user_spec.rb | 30 ++++++++--- 10 files changed, 91 insertions(+), 64 deletions(-) delete mode 100644 app/models/contact.rb create mode 100644 db/migrate/20161004163616_rename_columns_in_membership.rb delete mode 100644 spec/factories/contact.rb delete mode 100644 spec/models/contact_spec.rb diff --git a/app/models/contact.rb b/app/models/contact.rb deleted file mode 100644 index 12410c1..0000000 --- a/app/models/contact.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true -class Contact < ApplicationRecord - self.table_name = 'users' - - has_many :memberships - has_many :users, through: :memberships - - validates :name, :email, presence: true, length: { maximum: 255 } - - delegate :name, to: :memberships -end diff --git a/app/models/membership.rb b/app/models/membership.rb index a407ef8..42eb6a9 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true class Membership < ApplicationRecord - belongs_to :user - belongs_to :contact - + belongs_to :subscriber, class_name: 'User' + belongs_to :subscribed, class_name: 'User' + validates :name, presence: true, length: { maximum: 255 } - validates :contact_id, uniqueness: { scope: :user_id, - message: 'has already been added' } + validates :subscribed_id, uniqueness: { scope: :subscriber_id } end diff --git a/app/models/user.rb b/app/models/user.rb index b996349..12eb493 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true class User < ApplicationRecord - devise :database_authenticatable, :registerable, :recoverable, :rememberable, - :trackable, :validatable + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable has_many :newsletters - has_many :memberships - has_many :contacts, through: :memberships + has_many :ownerships, class_name: 'Membership', foreign_key: :subscriber_id + has_many :memberships, foreign_key: :subscribed_id + + has_many :subscribers, class_name: 'User', through: :memberships, foreign_key: :subscriber_id, dependent: :destroy + has_many :subscribeds, class_name: 'User', through: :ownerships, foreign_key: :subscribed_id, dependent: :destroy validates :name, presence: true, length: { maximum: 255 } end diff --git a/db/migrate/20161004163616_rename_columns_in_membership.rb b/db/migrate/20161004163616_rename_columns_in_membership.rb new file mode 100644 index 0000000..6fe45ab --- /dev/null +++ b/db/migrate/20161004163616_rename_columns_in_membership.rb @@ -0,0 +1,6 @@ +class RenameColumnsInMembership < ActiveRecord::Migration[5.0] + def change + rename_column :memberships, :user_id, :subscriber_id + rename_column :memberships, :contact_id, :subscribed_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 6a16df4..616e7a7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,17 +10,17 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161001091106) do +ActiveRecord::Schema.define(version: 20161004163616) do create_table "memberships", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| - t.integer "user_id" - t.integer "contact_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "subscriber_id" + t.integer "subscribed_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "name" - t.index ["contact_id"], name: "index_memberships_on_contact_id", using: :btree - t.index ["user_id", "contact_id"], name: "index_memberships_on_user_id_and_contact_id", unique: true, using: :btree - t.index ["user_id"], name: "index_memberships_on_user_id", using: :btree + t.index ["subscribed_id"], name: "index_memberships_on_subscribed_id", using: :btree + t.index ["subscriber_id", "subscribed_id"], name: "index_memberships_on_subscriber_id_and_subscribed_id", unique: true, using: :btree + t.index ["subscriber_id"], name: "index_memberships_on_subscriber_id", using: :btree end create_table "newsletters", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| diff --git a/spec/factories/contact.rb b/spec/factories/contact.rb deleted file mode 100644 index 3d397a5..0000000 --- a/spec/factories/contact.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -FactoryGirl.define do - factory :contact do - name { Faker::Name.name } - email { Faker::Internet.email } - end -end diff --git a/spec/factories/membership.rb b/spec/factories/membership.rb index c04c7e9..e89df0a 100644 --- a/spec/factories/membership.rb +++ b/spec/factories/membership.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true FactoryGirl.define do factory :membership do - user - contact + name { Faker::Name.name } end end diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb deleted file mode 100644 index 9cb101b..0000000 --- a/spec/models/contact_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true -RSpec.describe Contact, type: :model do - subject { FactoryGirl.build(:contact) } - - it { should have_many(:memberships) } - it { should have_many(:users).through(:memberships) } - - it { should validate_presence_of(:name) } - it { should validate_presence_of(:email) } - it { should validate_length_of(:name).is_at_most(255) } - it { should validate_length_of(:email).is_at_most(255) } -end diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb index efcceb6..d16b5fa 100644 --- a/spec/models/membership_spec.rb +++ b/spec/models/membership_spec.rb @@ -2,13 +2,48 @@ RSpec.describe Membership, type: :model do subject { FactoryGirl.build(:membership) } - it { should belong_to(:user) } - it { should belong_to(:contact) } - - it { should validate_presence_of(:name) } - it { should validate_length_of(:name).is_at_most(255) } - it do - should validate_uniqueness_of(:contact_id).scoped_to(:user_id) - .with_message('has already been added') + let(:batman){ FactoryGirl.create(:user, name: 'Batman') } + let(:catwoman){ FactoryGirl.create(:user, name: 'Catwoman') } + let(:spiderman){ FactoryGirl.create(:user, name: 'spiderman') } + + context 'with relations' do + it { should belong_to(:subscriber) } + it { should belong_to(:subscribed) } + end + + context 'with validations' do + before do + subject.subscriber = batman + subject.subscribed = catwoman + subject.save + end + + it { should validate_presence_of(:name) } + it { should validate_length_of(:name).is_at_most(255) } + + it 'subscribed user should be uniq for subscriber' do + should validate_uniqueness_of(:subscribed_id).scoped_to(:subscriber_id) + end + end + + context 'with membership subscriptions' do + before do + subject.subscriber = batman + subject.subscribed = catwoman + subject.save + end + + it 'subscribe 1 person' do + expect(Membership.count).to eq 1 + end + + it 'subscribe 2 people' do + meme = FactoryGirl.build(:membership) + meme.subscriber = batman + meme.subscribed = spiderman + meme.save + + expect(Membership.count).to eq 2 + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 93ef966..93dcb71 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,6 +2,10 @@ RSpec.describe User, type: :model do subject { FactoryGirl.build(:user) } + let(:batman){ FactoryGirl.create(:user, name: 'Batman') } + let(:catwoman){ FactoryGirl.create(:user, name: 'Catwoman') } + let(:spiderman){ FactoryGirl.create(:user, name: 'spiderman') } + context 'with validation' do it 'checks empty name' do subject.name = nil @@ -28,17 +32,29 @@ end end - context 'association' do - it 'responds to newsletters association' do - expect(subject).to respond_to(:newsletters) + context 'with associations' do + it { expect(subject).to respond_to(:newsletters) } + it { respond_to(:memberships) } + it { respond_to(:ownerships) } + it { respond_to(:subscribers) } + it { respond_to(:subscribeds) } + end + + context 'with memberships' do + before do + batman.ownerships.create!(subscribed: catwoman, name: catwoman.name) + batman.ownerships.create!(subscribed: spiderman, name: spiderman.name) end - it 'responds to memberships association' do - expect(subject).to respond_to(:memberships) + it 'fetch right subscribeds' do + expect(batman.subscribeds.count).to eq 2 + expect(batman.subscribeds).to include catwoman + expect(batman.subscribeds).to include spiderman end - it 'responds to contacts association' do - expect(subject).to respond_to(:contacts) + it 'fetch right subscriber' do + expect(catwoman.subscribers.first).to eq batman + expect(spiderman.subscribers.first).to eq batman end end end