diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b2bd92af1..312617c4e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -116,4 +116,14 @@ class ApplicationController < ActionController::Base
add_notifications_for activity
end
end
+
+ def add_notifications_for(activity)
+ case activity.action
+ when "debate_comment"
+ author = activity.trackable.debate.author
+ when "comment_reply"
+ author = activity.trackable.parent.author
+ end
+ author.notifications.create!(activity: activity) unless activity.made_by? author
+ end
end
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
new file mode 100644
index 000000000..2fb715287
--- /dev/null
+++ b/app/controllers/notifications_controller.rb
@@ -0,0 +1,9 @@
+class NotificationsController < ApplicationController
+ before_action :authenticate_user!
+ load_and_authorize_resource class: "User"
+
+ def index
+ @notifications = current_user.notifications.unread.recent.for_render
+ end
+
+end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
new file mode 100644
index 000000000..d0e7e71a9
--- /dev/null
+++ b/app/helpers/notifications_helper.rb
@@ -0,0 +1,16 @@
+module NotificationsHelper
+
+ def notification_text_for(notification)
+ case notification.activity.action
+ when "debate_comment"
+ t("comments.notifications.commented_on_your_debate")
+ when "comment_reply"
+ t("comments.notifications.replied_to_your_comment")
+ end
+ end
+
+ def notifications_class_for(user)
+ user.notifications.unread.count > 0 ? "with_notifications" : "without_notifications"
+ end
+
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
new file mode 100644
index 000000000..aff5c38fa
--- /dev/null
+++ b/app/models/notification.rb
@@ -0,0 +1,12 @@
+class Notification < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :activity
+
+ scope :unread, -> { where(read: false) }
+ scope :recent, -> { order(id: :desc) }
+ scope :for_render, -> { includes(activity: [:user, :trackable]) }
+
+ def timestamp
+ activity.trackable.created_at
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 0d07643bd..276598bf2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
has_many :proposals, -> { with_hidden }, foreign_key: :author_id
has_many :comments, -> { with_hidden }
has_many :failed_census_calls
+ has_many :notifications
validates :username, presence: true, if: :username_required?
validates :username, uniqueness: true, if: :username_required?
diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb
index b0998949f..5f6608f2f 100644
--- a/app/views/devise/menu/_login_items.html.erb
+++ b/app/views/devise/menu/_login_items.html.erb
@@ -9,6 +9,9 @@
<%= link_to(t("devise_views.menu.login_items.logout"), destroy_user_session_path, method: :delete) %>
+
+ <%= link_to 'Notificaciones', notifications_path, class: notifications_class_for(current_user) %>
+
<% else %>
<%= link_to(t("devise_views.menu.login_items.login"), new_user_session_path) %>
diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb
new file mode 100644
index 000000000..222907c7e
--- /dev/null
+++ b/app/views/notifications/index.html.erb
@@ -0,0 +1,11 @@
+
+
+ <% @notifications.each do |notification| %>
+ -
+ •
+ <%= notification.activity.username %> <%= notification_text_for(notification) %>
+ <%= link_to notification.activity.trackable.debate.title, notification.activity.trackable.debate %>
+
+ <% end %>
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3ee698c91..a6a4d55bc 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -267,6 +267,9 @@ en:
zero: "No votes"
one: "1 vote"
other: "%{count} votes"
+ notifications:
+ commented_on_your_debate: "commented on yout debate"
+ replied_to_your_comment: "replied to your comment on"
comments_helper:
comment_link: "Comment"
comment_button: "Publish comment"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 46bb3b88b..c10d8a493 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -267,6 +267,9 @@ es:
zero: "Sin votos"
one: "1 voto"
other: "%{count} votos"
+ notifications:
+ commented_on_your_debate: "ha comentado en tu debate"
+ replied_to_your_comment: "ha respondido a tu comentario en"
comments_helper:
comment_link: "Comentar"
comment_button: "Publicar comentario"
diff --git a/config/routes.rb b/config/routes.rb
index 3472f1c4e..681eff89b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -74,6 +74,9 @@ Rails.application.routes.draw do
resource :account, controller: "account", only: [:show, :update, :delete] do
collection { get :erase }
end
+
+ resources :notifications, only: :index
+
resource :verification, controller: "verification", only: [:show]
scope module: :verification do
diff --git a/db/migrate/20150928115005_create_notifications.rb b/db/migrate/20150928115005_create_notifications.rb
new file mode 100644
index 000000000..42bbbf72e
--- /dev/null
+++ b/db/migrate/20150928115005_create_notifications.rb
@@ -0,0 +1,9 @@
+class CreateNotifications < ActiveRecord::Migration
+ def change
+ create_table :notifications do |t|
+ t.belongs_to :user, index: true, foreign_key: true
+ t.belongs_to :activity, index: true, foreign_key: true
+ t.boolean :read, default: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 16b50e4e9..cb1c5aa65 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -204,6 +204,17 @@ ActiveRecord::Schema.define(version: 20151215165824) do
add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree
+ create_table "notifications", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "activity_id"
+ t.boolean "read", default: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "notifications", ["activity_id"], name: "index_notifications_on_activity_id", using: :btree
+ add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree
+
create_table "organizations", force: :cascade do |t|
t.integer "user_id"
t.string "name", limit: 60
@@ -405,5 +416,7 @@ ActiveRecord::Schema.define(version: 20151215165824) do
add_foreign_key "identities", "users"
add_foreign_key "locks", "users"
add_foreign_key "moderators", "users"
+ add_foreign_key "notifications", "activities"
+ add_foreign_key "notifications", "users"
add_foreign_key "organizations", "users"
end
diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb
new file mode 100644
index 000000000..c4ff5f247
--- /dev/null
+++ b/spec/controllers/notifications_controller_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+describe NotificationsController do
+
+ describe "#index" do
+ let(:user) { create :user }
+ let(:notification) { create :notification, user: user }
+
+ it "assigns @notifications" do
+ sign_in user
+
+ get :index, debate: { title: 'A sample debate', description: 'this is a sample debate', terms_of_service: 1 }
+ expect(assigns(:notifications)).to eq user.notifications.unread.recent.for_render
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index 2c20000ca..8cdd3a3e4 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -290,4 +290,10 @@ FactoryGirl.define do
sequence(:track_id) { |n| "#{n}" }
end
+ factory :notification do
+ association :user, factory: :user
+ association :activity, factory: :activity
+ read false
+ end
+
end
diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb
new file mode 100644
index 000000000..4b5b48f0e
--- /dev/null
+++ b/spec/features/notifications_spec.rb
@@ -0,0 +1,97 @@
+require 'rails_helper'
+
+feature "Notifications" do
+ let(:author) { create :user }
+ let(:user) { create :user }
+ let(:debate) { create :debate, author: author }
+
+ scenario "User commented on my debate", :js do
+ login_as user
+ visit debate_path debate
+
+ fill_in "comment-body-debate_#{debate.id}", with: "I commented on your debate"
+ click_button "Publish comment"
+ within "#comments" do
+ expect(page).to have_content "I commented on your debate"
+ end
+
+ logout
+ login_as author
+ visit root_path
+ expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']"
+
+ click_link "Notificaciones"
+ expect(page).to have_content user.username
+ expect(page).to have_content I18n.t("comments.notifications.commented_on_your_debate")
+ expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment")
+ expect(page).to have_link debate.title, href: debate_path(debate)
+ end
+
+ scenario "User replied to my comment", :js do
+ comment = create :comment, commentable: debate, user: author
+ login_as user
+ visit debate_path debate
+
+ click_link "Reply"
+ within "#js-comment-form-comment_#{comment.id}" do
+ fill_in "comment-body-comment_#{comment.id}", with: "I replied to your comment"
+ click_button "Publish reply"
+ end
+
+ within "#comment_#{comment.id}" do
+ expect(page).to have_content "I replied to your comment"
+ end
+
+ logout
+ login_as author
+ visit root_path
+ expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']"
+
+ visit notifications_path
+ expect(page).to have_content user.username
+ expect(page).to have_content I18n.t("comments.notifications.replied_to_your_comment")
+ expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate")
+ expect(page).to have_link debate.title, href: debate_path(debate)
+ end
+
+ scenario "Author commented on his own debate", :js do
+ login_as author
+ visit debate_path debate
+
+ fill_in "comment-body-debate_#{debate.id}", with: "I commented on my own debate"
+ click_button "Publish comment"
+ within "#comments" do
+ expect(page).to have_content "I commented on my own debate"
+ end
+ expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']"
+
+ click_link "Notificaciones"
+ expect(page).to_not have_content user.username
+ expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate")
+ expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment")
+ expect(page).to_not have_link debate.title, href: debate_path(debate)
+ end
+
+ scenario "Author replied to his own comment", :js do
+ comment = create :comment, commentable: debate, user: author
+ login_as author
+ visit debate_path debate
+
+ click_link "Reply"
+ within "#js-comment-form-comment_#{comment.id}" do
+ fill_in "comment-body-comment_#{comment.id}", with: "I replied to my own comment"
+ click_button "Publish reply"
+ end
+
+ within "#comment_#{comment.id}" do
+ expect(page).to have_content "I replied to my own comment"
+ end
+ expect(page).to have_xpath "//a[@class='without_notifications' and @href='#{notifications_path}' and text()='Notificaciones']"
+
+ visit notifications_path
+ expect(page).to_not have_content user.username
+ expect(page).to_not have_content I18n.t("comments.notifications.replied_to_your_comment")
+ expect(page).to_not have_content I18n.t("comments.notifications.commented_on_your_debate")
+ expect(page).to_not have_link debate.title, href: debate_path(debate)
+ end
+end
diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb
new file mode 100644
index 000000000..120369bf4
--- /dev/null
+++ b/spec/helpers/notifications_helper_spec.rb
@@ -0,0 +1,48 @@
+require 'rails_helper'
+
+describe NotificationsHelper do
+
+ describe "#notification_text_for" do
+ let(:comment_activity) { create :activity, action: "debate_comment" }
+ let(:reply_activity) { create :activity, action: "comment_reply" }
+
+ context "when action was comment on a debate" do
+ it "returns 'commented_on_your_debate' locale text" do
+ notification = create :notification, activity: comment_activity
+ expect(notification_text_for(notification)).to eq t("comments.notifications.commented_on_your_debate")
+ end
+ end
+
+ context "when action was comment on a debate" do
+ it "returns 'replied_to_your_comment' locale text" do
+ notification = create :notification, activity: reply_activity
+ expect(notification_text_for(notification)).to eq t("comments.notifications.replied_to_your_comment")
+ end
+ end
+ end
+
+ describe "#notifications_class_for" do
+ let(:user) { create :user }
+
+ context "when user doesn't have any notification" do
+ it "returns class 'without_notifications'" do
+ expect(notifications_class_for(user)).to eq "without_notifications"
+ end
+ end
+
+ context "when user doesn't have unread notifications" do
+ it "returns class 'without_notifications'" do
+ notification = create :notification, user: user, read: true
+ expect(notifications_class_for(user)).to eq "without_notifications"
+ end
+ end
+
+ context "when user has unread notifications" do
+ it "returns class 'with_notifications'" do
+ notification = create :notification, user: user
+ expect(notifications_class_for(user)).to eq "with_notifications"
+ end
+ end
+ end
+
+end
diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb
new file mode 100644
index 000000000..ee5f00e6d
--- /dev/null
+++ b/spec/models/notification_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+describe Notification do
+
+ describe "#unread (scope)" do
+ it "returns only unread notifications" do
+ unread_notification = create :notification
+ read_notification = create :notification, read: true
+
+ unread_notifications = Notification.unread
+ expect(unread_notifications.size).to be 1
+ expect(unread_notifications.first).to eq unread_notification
+ end
+ end
+
+ describe "#recent (scope)" do
+ it "returns notifications sorted by id descendant" do
+ old_notification = create :notification
+ new_notification = create :notification
+
+ sorted_notifications = Notification.recent
+ expect(sorted_notifications.size).to be 2
+ expect(sorted_notifications.first).to eq new_notification
+ expect(sorted_notifications.last).to eq old_notification
+ end
+ end
+
+ describe "#for_render (scope)" do
+ it "returns notifications with including activity, user and trackable info" do
+ expect(Notification).to receive(:includes).with(activity: [:user, :trackable]).exactly(:once)
+ Notification.for_render
+ end
+ end
+
+ describe "#timestamp" do
+ it "returns the timestamp of the trackable object" do
+ comment = create :comment
+ activity = create :activity, trackable: comment
+ notification = create :notification, activity: activity
+
+ expect(notification.timestamp).to eq comment.created_at
+ end
+ end
+end