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/membership.rb b/app/models/membership.rb new file mode 100644 index 0000000..42eb6a9 --- /dev/null +++ b/app/models/membership.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +class Membership < ApplicationRecord + belongs_to :subscriber, class_name: 'User' + belongs_to :subscribed, class_name: 'User' + + validates :name, presence: true, length: { maximum: 255 } + validates :subscribed_id, uniqueness: { scope: :subscriber_id } +end diff --git a/app/models/user.rb b/app/models/user.rb index b7b91bf..12eb493 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +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 :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/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/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 1b28a3e..616e7a7 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: 20161004163616) do + + create_table "memberships", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| + 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 ["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| t.string "name" diff --git a/spec/factories/membership.rb b/spec/factories/membership.rb new file mode 100644 index 0000000..e89df0a --- /dev/null +++ b/spec/factories/membership.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +FactoryGirl.define do + factory :membership do + name { Faker::Name.name } + end +end diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb new file mode 100644 index 0000000..d16b5fa --- /dev/null +++ b/spec/models/membership_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +RSpec.describe Membership, type: :model do + subject { FactoryGirl.build(:membership) } + + 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 fdeb17a..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,9 +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 '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 'fetch right subscriber' do + expect(catwoman.subscribers.first).to eq batman + expect(spiderman.subscribers.first).to eq batman end end end