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 @@ +
    + +
    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