Commit 89b44582 by tady

Merge pull request #102 from tadyjp/feat/watch

watchings 追加
parents f1da4188 0483e78a
...@@ -87,6 +87,7 @@ group :development, :test do ...@@ -87,6 +87,7 @@ group :development, :test do
gem 'teaspoon' gem 'teaspoon'
gem 'guard-teaspoon' gem 'guard-teaspoon'
gem 'byebug'
end end
group :test do group :test do
......
...@@ -64,6 +64,9 @@ GEM ...@@ -64,6 +64,9 @@ GEM
breadcrumble (4.1.0) breadcrumble (4.1.0)
rails (>= 4.0.0) rails (>= 4.0.0)
builder (3.2.2) builder (3.2.2)
byebug (3.1.2)
columnize (~> 0.8)
debugger-linecache (~> 1.2)
capistrano (3.2.1) capistrano (3.2.1)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
...@@ -95,6 +98,7 @@ GEM ...@@ -95,6 +98,7 @@ GEM
execjs execjs
coffee-script-source (1.7.1) coffee-script-source (1.7.1)
colorize (0.7.3) colorize (0.7.3)
columnize (0.8.9)
coveralls (0.7.0) coveralls (0.7.0)
multi_json (~> 1.3) multi_json (~> 1.3)
rest-client rest-client
...@@ -106,6 +110,7 @@ GEM ...@@ -106,6 +110,7 @@ GEM
daemons (1.1.9) daemons (1.1.9)
database_rewinder (0.2.0) database_rewinder (0.2.0)
debug_inspector (0.0.2) debug_inspector (0.0.2)
debugger-linecache (1.2.0)
devise (3.2.4) devise (3.2.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -391,6 +396,7 @@ DEPENDENCIES ...@@ -391,6 +396,7 @@ DEPENDENCIES
better_errors better_errors
binding_of_caller binding_of_caller
breadcrumble breadcrumble
byebug
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1.2) capistrano-bundler (~> 1.1.2)
capistrano-rails (~> 1.1) capistrano-rails (~> 1.1)
......
...@@ -11,4 +11,6 @@ $ -> ...@@ -11,4 +11,6 @@ $ ->
# TODO # TODO
prettyPrint() prettyPrint()
$(document).on 'ajax:success', '.ajax_link', (data, res, xhr) ->
console.log(res)
$('#yield').html(res)
...@@ -9,7 +9,7 @@ $.extend ...@@ -9,7 +9,7 @@ $.extend
dataType: 'json' dataType: 'json'
done: (e, data) -> done: (e, data) ->
$.each data.result.files, (index, _file) -> $.each data.result.files, (index, _file) ->
settings.$textarea.val(settings.$textarea.val() + "![" + _file.name + "](" + _file.url + ")\n") settings.$textarea.val(settings.$textarea.val() + "\n![" + _file.name + "](" + _file.url + ")\n")
settings.$textarea.trigger("change") settings.$textarea.trigger("change")
# $('<p/>').text(file.name).appendTo('#files') # TODO # $('<p/>').text(file.name).appendTo('#files') # TODO
progressall: (e, data) -> progressall: (e, data) ->
......
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
// Place all the styles related to the watchings controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
require 'nkf' require 'nkf'
class PostsController < ApplicationController class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy, :slideshow] before_action :set_post, only: [:show, :edit, :update, :destroy, :slideshow, :watch]
include RV::Mailer include RV::Mailer
...@@ -108,6 +108,19 @@ class PostsController < ApplicationController ...@@ -108,6 +108,19 @@ class PostsController < ApplicationController
render layout: 'slideshow' render layout: 'slideshow'
end end
def watch
if current_user.watching?(post: @post)
current_user.unwatch!(post: @post)
else
current_user.watch!(post: @post)
end
respond_to do |format|
format.html { render action: :show, layout: false }
format.json { head :no_content }
end
end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
......
class WatchingsController < ApplicationController
def show
@posts = current_user.watching_posts.order(updated_at: :desc).page(params[:page]).decorate
end
end
class WatchingDecorator < Draper::Decorator
delegate_all
# Define presentation-specific methods here. Helpers are accessed through
# `helpers` (aka `h`). You can override attributes, for example:
#
# def created_at
# helpers.content_tag :span, class: 'time' do
# object.created_at.strftime("%a %m/%d/%y")
# end
# end
end
module WatchingsHelper
end
...@@ -14,13 +14,21 @@ class Comment < ActiveRecord::Base ...@@ -14,13 +14,21 @@ class Comment < ActiveRecord::Base
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
belongs_to :post belongs_to :post
######################################################################
# validations
######################################################################
validates :author_id, presence: true validates :author_id, presence: true
validates :post_id, presence: true validates :post_id, presence: true
validates :body, presence: true validates :body, presence: true
### Callback ### ######################################################################
# Callback
######################################################################
after_save :notify_author after_save :notify_author
######################################################################
# Instance method
######################################################################
private private
def notify_author def notify_author
......
# == Schema Information
#
# Table name: footprints
#
# id :integer not null, primary key
# user_id :integer not null
# post_id :integer not null
# created_at :datetime
# updated_at :datetime
#
class Footprint < ActiveRecord::Base class Footprint < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :post belongs_to :post
......
...@@ -22,7 +22,8 @@ class Post < ActiveRecord::Base ...@@ -22,7 +22,8 @@ class Post < ActiveRecord::Base
has_many :comments has_many :comments
has_many :footprints has_many :footprints
# default_scope { where(is_draft: false).order(:updated_at => :desc) } has_many :watches, :as => :watchable, :dependent => :destroy
has_many :watchers, :through => :watches
###################################################################### ######################################################################
# validations # validations
...@@ -31,6 +32,11 @@ class Post < ActiveRecord::Base ...@@ -31,6 +32,11 @@ class Post < ActiveRecord::Base
validates :body, presence: true validates :body, presence: true
###################################################################### ######################################################################
# Callback
######################################################################
after_save :notify_watchers
######################################################################
# Named scope # Named scope
###################################################################### ######################################################################
scope :search, (lambda do |query| scope :search, (lambda do |query|
...@@ -100,4 +106,12 @@ class Post < ActiveRecord::Base ...@@ -100,4 +106,12 @@ class Post < ActiveRecord::Base
def visited_user_count def visited_user_count
footprints.select(:user_id).uniq.count footprints.select(:user_id).uniq.count
end end
private
def notify_watchers
watchers.each do |watcher|
watcher.push_notification(decorate.show_path, "#{author.name}さんが「#{title}」を編集しました")
end
end
end end
...@@ -15,6 +15,9 @@ class Tag < ActiveRecord::Base ...@@ -15,6 +15,9 @@ class Tag < ActiveRecord::Base
has_many :post_tags has_many :post_tags
has_many :posts, through: :post_tags has_many :posts, through: :post_tags
has_many :watches, :as => :watchable, :dependent => :destroy
has_many :watchers, :through => :watches
# for tree structure # for tree structure
has_ancestry has_ancestry
......
...@@ -33,13 +33,20 @@ class User < ActiveRecord::Base ...@@ -33,13 +33,20 @@ class User < ActiveRecord::Base
devise :omniauthable, omniauth_providers: [:google_oauth2] devise :omniauthable, omniauth_providers: [:google_oauth2]
###################################################################### ######################################################################
# association # Associations
###################################################################### ######################################################################
has_many :posts, foreign_key: 'author_id' has_many :posts, foreign_key: 'author_id'
has_many :comments, foreign_key: 'author_id' has_many :comments, foreign_key: 'author_id'
has_many :notifications has_many :notifications
has_many :footprints has_many :footprints
has_many :watches, :as => :watchable, :dependent => :destroy
has_many :watchers, :through => :watches
has_many :watchings, class_name: 'Watch', foreign_key: 'watcher_id'
has_many :watching_posts, :through => :watchings, :source => :watchable, :source_type => "Post"
# has_many :watchings, :as => :resource
###################################################################### ######################################################################
# scope # scope
###################################################################### ######################################################################
...@@ -53,7 +60,7 @@ class User < ActiveRecord::Base ...@@ -53,7 +60,7 @@ class User < ActiveRecord::Base
###################################################################### ######################################################################
# validations # Validations
###################################################################### ######################################################################
validates :name, presence: true validates :name, presence: true
validates :email, presence: true validates :email, presence: true
...@@ -119,4 +126,47 @@ class User < ActiveRecord::Base ...@@ -119,4 +126,47 @@ class User < ActiveRecord::Base
def visit_post!(post) def visit_post!(post)
footprints.create!(post: post) footprints.create!(post: post)
end end
def watch!(hash)
if hash[:post]
watching_posts << hash[:post] unless watching_posts.include?(hash[:post])
elsif hash[:tag]
raise 'Not Implemented.'
elsif hash[:user]
raise 'Not Implemented.'
else
raise 'No hash argument set.'
end
end
def unwatch!(hash)
if hash[:post]
hash[:post].watches.where(watcher: self).destroy_all
elsif hash[:tag]
raise 'Not Implemented.'
elsif hash[:user]
raise 'Not Implemented.'
else
raise 'No hash argument set.'
end
end
# check if user watching post/tag/user
# TODO: tag/user
def watching?(hash)
if hash[:post]
hash[:post].watches.where(watcher: self).exists?
elsif hash[:tag]
raise 'Not Implemented.'
elsif hash[:user]
raise 'Not Implemented.'
else
raise 'No hash argument set.'
end
end
# def watching_posts
# ids = watching_items.where(resource_type: "Post").pluck(:resource_id)
# Post.where(id: ids)
# end
end end
class Watch < ActiveRecord::Base
######################################################################
# Associations
######################################################################
belongs_to :watcher, class_name: 'User'
belongs_to :watchable, polymorphic: true
######################################################################
# Validations
######################################################################
validates :watcher_id, uniqueness: { scope: [:watchable_type, :watchable_id] }
end
...@@ -9,6 +9,7 @@ html lang="ja" ...@@ -9,6 +9,7 @@ html lang="ja"
link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.css" rel="stylesheet" / link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.css" rel="stylesheet" /
link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.print.css" rel="stylesheet" / link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.print.css" rel="stylesheet" /
link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet" link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet"
script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"
= stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "application", media: "all"
= render_style = render_style
= csrf_meta_tags = csrf_meta_tags
...@@ -16,12 +17,11 @@ html lang="ja" ...@@ -16,12 +17,11 @@ html lang="ja"
= render partial: 'partials/header_notifications' = render partial: 'partials/header_notifications'
- if params[:controller] != 'welcome' - if params[:controller] != 'welcome'
= render partial: 'partials/app_header' = render partial: 'partials/app_header'
.container.container-main .container.container-main#yield
= yield = yield
script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js" script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/underscore.string/2.3.3/underscore.string.min.js" script src="//cdnjs.cloudflare.com/ajax/libs/underscore.string/2.3.3/underscore.string.min.js"
script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"
script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js" script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js" script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.min.js" script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.min.js"
......
...@@ -41,6 +41,8 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation" ...@@ -41,6 +41,8 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation"
span.badge.pull-right = current_user.decorate.draft_count span.badge.pull-right = current_user.decorate.draft_count
li li
a href=edit_user_path マイページ a href=edit_user_path マイページ
li
a href=watching_path Watchings
li.divider li.divider
li li
a href=destroy_user_session_path data-method="delete" rel="nofollow" SignOut a href=destroy_user_session_path data-method="delete" rel="nofollow" SignOut
......
...@@ -8,7 +8,7 @@ a.list-group-item.post-list.mod-hover-hidden data-post-id=post.id href=post_path ...@@ -8,7 +8,7 @@ a.list-group-item.post-list.mod-hover-hidden data-post-id=post.id href=post_path
h4.text-link #{post.title} h4.text-link #{post.title}
.col-xs-3 .col-xs-3
span.label.label-danger = post.display_specified_date if post.specified_date span.label.label-danger = post.display_specified_date if post.specified_date
small.pull-right "##{post.id}" small.pull-right ##{post.id}
.row .row
.col-xs-8 .col-xs-8
small.text-success small.text-success
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.col-xs-3 .col-xs-3
p.btn-group .btn-group
a.btn.btn-primary href=edit_post_path(@post) a.btn.btn-primary href=edit_post_path(@post)
| 編集&nbsp; | 編集&nbsp;
span.glyphicon.glyphicon-pencil span.glyphicon.glyphicon-pencil
...@@ -29,7 +29,16 @@ ...@@ -29,7 +29,16 @@
li.divider li.divider
li= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' } li= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' }
.well | &nbsp;
.btn-group
- if current_user.watching?(post: @post)
= link_to 'Watching <span class="glyphicon glyphicon-eye-open"></span>'.html_safe, watch_post_path, :remote => true, :'data-type' => :html, :class => 'btn btn-warning ajax_link'
- else
= link_to 'Watch <span class="glyphicon glyphicon-eye-open"></span>'.html_safe, watch_post_path, :remote => true, :'data-type' => :html, :class => 'btn btn-default ajax_link'
.well style="margin-top:20px"
dl dl
dt 作成者 dt 作成者
dd dd
...@@ -64,8 +73,6 @@ ...@@ -64,8 +73,6 @@
dd dd
= @post.comments.count = @post.comments.count
.row .row
.panel.panel-success .panel.panel-success
.panel-heading .panel-heading
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.col-xs-9 .col-xs-9
h4.text-link #{post.title} h4.text-link #{post.title}
.col-xs-3 .col-xs-3
small.pull-right "##{post.id}" small.pull-right ##{post.id}
.row .row
.col-xs-8 .col-xs-8
p.small.text-success p.small.text-success
......
/! view:flow/show
.row
h1
| Watchings
small - ウォッチ中の項目
.col-xs-8 role="navigation"
.list-group
- @posts.each do |_post|
= render partial: 'posts/large_item', locals: { post: _post }
= paginate(@posts)
Rendezvous::Application.routes.draw do Rendezvous::Application.routes.draw do
get 'templates/show'
post 'apis/markdown_preview' post 'apis/markdown_preview'
post 'apis/file_receiver' post 'apis/file_receiver'
get 'apis/user_mention' get 'apis/user_mention'
...@@ -14,11 +12,13 @@ Rendezvous::Application.routes.draw do ...@@ -14,11 +12,13 @@ Rendezvous::Application.routes.draw do
get 'flow' => 'flow#show', as: 'flow' get 'flow' => 'flow#show', as: 'flow'
get 'search' => 'search#show', as: 'search' get 'search' => 'search#show', as: 'search'
get 'templates' => 'templates#show', as: 'templates' get 'templates' => 'templates#show', as: 'templates'
get 'watchings' => 'watchings#show', as: 'watching'
get 'posts/:id/fork' => 'posts#fork', as: 'fork_post' get 'posts/:id/fork' => 'posts#fork', as: 'fork_post'
post 'posts/:id/mail' => 'posts#mail', as: 'mail_post' post 'posts/:id/mail' => 'posts#mail', as: 'mail_post'
post 'posts/:id/comment' => 'posts#comment', as: 'comment_post' post 'posts/:id/comment' => 'posts#comment', as: 'comment_post'
get 'posts/:id/slideshow' => 'posts#slideshow', as: 'slideshow_post' get 'posts/:id/slideshow' => 'posts#slideshow', as: 'slideshow_post'
get 'posts/:id/watch' => 'posts#watch', as: 'watch_post'
resources :posts, except: [:index] resources :posts, except: [:index]
get 'notification_bridge/:id' => 'notifications#bridge', as: 'notification_bridge' get 'notification_bridge/:id' => 'notifications#bridge', as: 'notification_bridge'
......
class CreateWatches < ActiveRecord::Migration
def change
create_table :watches do |t|
t.integer :watcher_id, null: false
t.string :watchable_type, null: false
t.integer :watchable_id, null: false
t.timestamps
end
add_index :watches, [:watcher_id, :watchable_type, :watchable_id], unique: true
add_index :watches, [:watchable_type, :watchable_id]
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140719132802) do ActiveRecord::Schema.define(version: 20140719145016) do
create_table "comments", force: true do |t| create_table "comments", force: true do |t|
t.integer "author_id" t.integer "author_id"
...@@ -115,4 +115,15 @@ ActiveRecord::Schema.define(version: 20140719132802) do ...@@ -115,4 +115,15 @@ ActiveRecord::Schema.define(version: 20140719132802) do
add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id", using: :btree add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id", using: :btree
create_table "watches", force: true do |t|
t.integer "watcher_id", null: false
t.string "watchable_type", null: false
t.integer "watchable_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "watches", ["watchable_type", "watchable_id"], name: "index_watches_on_watchable_type_and_watchable_id", using: :btree
add_index "watches", ["watcher_id", "watchable_type", "watchable_id"], name: "index_watches_on_watcher_id_and_watchable_type_and_watchable_id", unique: true, using: :btree
end end
require 'rails_helper'
RSpec.describe WatchingsController, :type => :controller do
describe "GET 'show' without login" do
it "returns http redirect" do
get 'show'
expect(response).to redirect_to('/')
end
end
describe "GET 'show' with login" do
it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show'
expect(response).to be_success
end
end
end
require 'spec_helper'
describe WatchingDecorator do
end
require 'rails_helper'
# Specs in this file have access to a helper object that includes
# the WatchingsHelper. For example:
#
# describe WatchingsHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
RSpec.describe WatchingsHelper, :type => :helper do
pending "add some examples to (or delete) #{__FILE__}"
end
# == Schema Information
#
# Table name: footprints
#
# id :integer not null, primary key
# user_id :integer not null
# post_id :integer not null
# created_at :datetime
# updated_at :datetime
#
require 'rails_helper' require 'rails_helper'
RSpec.describe Footprint, :type => :model do RSpec.describe Footprint, :type => :model do
......
...@@ -109,7 +109,22 @@ describe Post do ...@@ -109,7 +109,22 @@ describe Post do
expect(Post.search('ruby')).to include(@post1) expect(Post.search('ruby')).to include(@post1)
expect(Post.search('ruby')).to include(@post3) expect(Post.search('ruby')).to include(@post3)
end end
end
describe '#notify_watchers' do
before :each do
@alice = create(:alice)
@bob = create(:bob)
@post = @alice.posts.create id: 1001, title: 'ruby rspec', body: 'This is first espec test: ruby'
end end
it do
@post.watchers << @bob
expect(@bob.watching_posts.size).to eq(1)
expect(@bob.notifications.size).to eq(0)
@post.update!(title: @post.title + '+')
expect(@bob.notifications.size).to eq(1)
end
end
end end
...@@ -31,9 +31,9 @@ describe User do ...@@ -31,9 +31,9 @@ describe User do
let(:alice) { create(:alice) } let(:alice) { create(:alice) }
let(:bob) { create(:bob) } let(:bob) { create(:bob) }
let(:post) { create(:post) }
describe '#google_oauth_token_expired?' do describe '#google_oauth_token_expired?' do
it 'not expired' do it 'not expired' do
expect(alice.google_oauth_token_expired?).to be_falsey expect(alice.google_oauth_token_expired?).to be_falsey
end end
...@@ -41,7 +41,29 @@ describe User do ...@@ -41,7 +41,29 @@ describe User do
it 'expired' do it 'expired' do
expect(bob.google_oauth_token_expired?).to be_truthy expect(bob.google_oauth_token_expired?).to be_truthy
end end
end
describe '#unwatch / #watch / #watching?' do
it 'not watching' do
expect(alice.watching?(post: post)).to be_falsey
end
it '#watch!' do
alice.watch!(post: post)
expect(alice.watching?(post: post)).to be_truthy
end
it '#watch! (uniqueness)' do
alice.watch!(post: post)
alice.watch!(post: post)
expect(alice.watching_posts.size).to be(1)
end
it '#unwatch!' do
alice.watch!(post: post)
alice.unwatch!(post: post)
expect(alice.watching?(post: post)).to be_falsey
end
end end
end end
......
require 'rails_helper'
RSpec.describe Watch, :type => :model do
# describe "validations" do
# let(:alice) { create(:alice) }
# let(:post) { create(:post) }
# describe "watcher_id" do
# it "uniqueness" do
# alice.watch!(post: post)
# alice.watch!(post: post)
# expect(alice.watching_posts.size).to be(1)
# end
# end
# end
end
require 'rails_helper'
RSpec.describe "watchings/show.html.erb", :type => :view do
pending "add some examples to (or delete) #{__FILE__}"
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment