From ba0ce4e14b59283977a9d4296c42c1c973135d4a Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 12:05:34 +0200 Subject: [PATCH 01/21] =?UTF-8?q?a=C3=B1adir=20track=5Factivity=20para=20m?= =?UTF-8?q?ostrar=20futuras=20notificaciones=20a=20los=20usuarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/application_controller.rb | 8 ++++++++ app/controllers/comments_controller.rb | 1 + app/models/activity.rb | 2 -- app/models/user.rb | 2 +- db/migrate/20150928075646_create_activities.rb | 9 +++++++++ db/schema.rb | 1 + 6 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20150928075646_create_activities.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 841b32498..b2bd92af1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -108,4 +108,12 @@ class ApplicationController < ActionController::Base store_location_for(:user, request.path) end end + + def track_activity(trackable) + if trackable.is_a? Comment + action = trackable.root? ? "debate_comment" : "comment_reply" + activity = current_user.activities.create! action: action, trackable: trackable + add_notifications_for activity + end + end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 35406faf0..e05be448a 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -9,6 +9,7 @@ class CommentsController < ApplicationController def create if @comment.save CommentNotifier.new(comment: @comment).process + track_activity @comment else render :new end diff --git a/app/models/activity.rb b/app/models/activity.rb index 977204669..047ccb7dd 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -1,5 +1,4 @@ class Activity < ActiveRecord::Base - belongs_to :actionable, -> { with_hidden }, polymorphic: true belongs_to :user, -> { with_hidden } @@ -24,5 +23,4 @@ class Activity < ActiveRecord::Base def self.by(user) where(user: user) end - end diff --git a/app/models/user.rb b/app/models/user.rb index dd2bc5cf0..0d07643bd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -199,7 +199,7 @@ class User < ActiveRecord::Base def email_required? !erased? end - + def has_official_email? domain = Setting.value_for 'email_domain_for_officials' !email.blank? && ( (email.end_with? "@#{domain}") || (email.end_with? ".#{domain}") ) diff --git a/db/migrate/20150928075646_create_activities.rb b/db/migrate/20150928075646_create_activities.rb new file mode 100644 index 000000000..89b0edcbc --- /dev/null +++ b/db/migrate/20150928075646_create_activities.rb @@ -0,0 +1,9 @@ +class CreateActivities < ActiveRecord::Migration + def change + create_table :activities do |t| + t.belongs_to :user, index: true, foreign_key: true + t.string :action + t.belongs_to :trackable, polymorphic: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ca180b297..16b50e4e9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -396,6 +396,7 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree + add_foreign_key "activities", "users" add_foreign_key "administrators", "users" add_foreign_key "annotations", "legislations" add_foreign_key "annotations", "users" From d9ba3edc2a54b0a7d546e6f87ecb5221a154ebfd Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 12:11:20 +0200 Subject: [PATCH 02/21] mostrar notificaciones a los usuarios cuando alguien comenta en su debate o responde a su comentario --- app/controllers/application_controller.rb | 10 ++ app/controllers/notifications_controller.rb | 9 ++ app/helpers/notifications_helper.rb | 16 +++ app/models/notification.rb | 12 +++ app/models/user.rb | 1 + app/views/devise/menu/_login_items.html.erb | 3 + app/views/notifications/index.html.erb | 11 +++ config/locales/en.yml | 3 + config/locales/es.yml | 3 + config/routes.rb | 3 + .../20150928115005_create_notifications.rb | 9 ++ db/schema.rb | 13 +++ .../notifications_controller_spec.rb | 16 +++ spec/factories.rb | 6 ++ spec/features/notifications_spec.rb | 97 +++++++++++++++++++ spec/helpers/notifications_helper_spec.rb | 48 +++++++++ spec/models/notification_spec.rb | 44 +++++++++ 17 files changed, 304 insertions(+) create mode 100644 app/controllers/notifications_controller.rb create mode 100644 app/helpers/notifications_helper.rb create mode 100644 app/models/notification.rb create mode 100644 app/views/notifications/index.html.erb create mode 100644 db/migrate/20150928115005_create_notifications.rb create mode 100644 spec/controllers/notifications_controller_spec.rb create mode 100644 spec/features/notifications_spec.rb create mode 100644 spec/helpers/notifications_helper_spec.rb create mode 100644 spec/models/notification_spec.rb 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 From 9a5f525dc95fd974a025ad1807c9d9f524d2ad55 Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 20:48:46 +0200 Subject: [PATCH 03/21] marcar notificaciones como leidas despues de ser vistas --- app/controllers/notifications_controller.rb | 1 + app/models/notification.rb | 4 ++++ spec/controllers/notifications_controller_spec.rb | 15 ++++++++++----- spec/models/notification_spec.rb | 10 ++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 2fb715287..199893bd7 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -4,6 +4,7 @@ class NotificationsController < ApplicationController def index @notifications = current_user.notifications.unread.recent.for_render + @notifications.each { |notification| notification.mark_as_read! } end end diff --git a/app/models/notification.rb b/app/models/notification.rb index aff5c38fa..0f387f61f 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -9,4 +9,8 @@ class Notification < ActiveRecord::Base def timestamp activity.trackable.created_at end + + def mark_as_read! + update_attribute :read, true + end end diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb index c4ff5f247..fa6104a9c 100644 --- a/spec/controllers/notifications_controller_spec.rb +++ b/spec/controllers/notifications_controller_spec.rb @@ -4,13 +4,18 @@ describe NotificationsController do describe "#index" do let(:user) { create :user } - let(:notification) { create :notification, user: user } - it "assigns @notifications" do + it "mark all notifications as read" do + notifications = [create(:notification, user: user), create(:notification, user: user)] + Notification.all.each do |notification| + expect(notification.read).to be false + end + 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 + get :index + Notification.all.each do |notification| + expect(notification.read).to be true + end end end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index ee5f00e6d..8d5e3dd97 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -41,4 +41,14 @@ describe Notification do expect(notification.timestamp).to eq comment.created_at end end + + describe "#mark_as_read" do + it "set up read flag to true" do + notification = create :notification + expect(notification.read).to be false + + notification.mark_as_read! + expect(notification.read).to be true + end + end end From 7a8ffe2d4a9e692d3e1e244ff296b57213475f9b Mon Sep 17 00:00:00 2001 From: Julian Herrero Date: Tue, 6 Oct 2015 20:56:11 +0200 Subject: [PATCH 04/21] cleanup --- app/views/notifications/index.html.erb | 2 +- config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 222907c7e..ab4351c50 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -2,7 +2,7 @@
      <% @notifications.each do |notification| %>
    • -  •  +  •  <%= notification.activity.username %> <%= notification_text_for(notification) %> <%= link_to notification.activity.trackable.debate.title, notification.activity.trackable.debate %>
    • diff --git a/config/locales/en.yml b/config/locales/en.yml index a6a4d55bc..35af31bbc 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -268,7 +268,7 @@ en: one: "1 vote" other: "%{count} votes" notifications: - commented_on_your_debate: "commented on yout debate" + commented_on_your_debate: "commented on your debate" replied_to_your_comment: "replied to your comment on" comments_helper: comment_link: "Comment" From b5e9113718d2f6fd143e615058f1eee8beb3cc05 Mon Sep 17 00:00:00 2001 From: rgarcia Date: Wed, 6 Jan 2016 12:33:37 +0100 Subject: [PATCH 05/21] merges activities into notifications --- app/controllers/application_controller.rb | 17 ----------------- app/controllers/comments_controller.rb | 11 ++++++++++- app/controllers/notifications_controller.rb | 3 +-- app/helpers/notifications_helper.rb | 11 +++++------ app/models/comment.rb | 4 ++++ app/models/notification.rb | 15 +++++++++------ app/views/notifications/index.html.erb | 4 ++-- db/migrate/20150928075646_create_activities.rb | 9 --------- ...170113_merge_activities_and_notifications.rb | 10 ++++++++++ db/schema.rb | 13 ++++--------- spec/factories.rb | 5 ++--- spec/helpers/notifications_helper_spec.rb | 17 +++++++++-------- spec/models/comment_spec.rb | 15 +++++++++++++++ spec/models/notification_spec.rb | 16 ++++++++++++---- 14 files changed, 83 insertions(+), 67 deletions(-) delete mode 100644 db/migrate/20150928075646_create_activities.rb create mode 100644 db/migrate/20160105170113_merge_activities_and_notifications.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 312617c4e..5150f97ec 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -109,21 +109,4 @@ class ApplicationController < ActionController::Base end end - def track_activity(trackable) - if trackable.is_a? Comment - action = trackable.root? ? "debate_comment" : "comment_reply" - activity = current_user.activities.create! action: action, trackable: trackable - 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/comments_controller.rb b/app/controllers/comments_controller.rb index e05be448a..c30ddbaf2 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -9,7 +9,7 @@ class CommentsController < ApplicationController def create if @comment.save CommentNotifier.new(comment: @comment).process - track_activity @comment + add_notification @comment else render :new end @@ -63,4 +63,13 @@ class CommentsController < ApplicationController ["1", true].include?(comment_params[:as_moderator]) && can?(:comment_as_moderator, @commentable) end + def add_notification(comment) + if comment.reply? + author = comment.parent.author + else + author = comment.commentable.author + end + author.notifications.create!(notifiable: comment) unless comment.made_by? author + end + end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 199893bd7..8ecb16104 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -3,8 +3,7 @@ class NotificationsController < ApplicationController load_and_authorize_resource class: "User" def index - @notifications = current_user.notifications.unread.recent.for_render - @notifications.each { |notification| notification.mark_as_read! } + @notifications = current_user.notifications.recent.for_render end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index d0e7e71a9..45ef6422f 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,16 +1,15 @@ 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") + if notification.notifiable.reply? + t("comments.notifications.replied_to_your_comment") + else + t("comments.notifications.commented_on_your_debate") end end def notifications_class_for(user) - user.notifications.unread.count > 0 ? "with_notifications" : "without_notifications" + user.notifications.count > 0 ? "with_notifications" : "without_notifications" end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 3771af84a..44e16b729 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -90,6 +90,10 @@ class Comment < ActiveRecord::Base !root? end + def made_by?(user) + self.user == user + end + def call_after_commented self.commentable.try(:after_commented) end diff --git a/app/models/notification.rb b/app/models/notification.rb index 0f387f61f..63f44dedc 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,16 +1,19 @@ class Notification < ActiveRecord::Base belongs_to :user - belongs_to :activity + belongs_to :notifiable, polymorphic: true - scope :unread, -> { where(read: false) } + scope :unread, -> { all } scope :recent, -> { order(id: :desc) } - scope :for_render, -> { includes(activity: [:user, :trackable]) } + scope :for_render, -> { includes(notifiable: [:user]) } + + def username + notifiable.user.username + end def timestamp - activity.trackable.created_at + notifiable.created_at end def mark_as_read! - update_attribute :read, true end -end +end \ No newline at end of file diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index ab4351c50..0155f0521 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -3,8 +3,8 @@ <% @notifications.each do |notification| %>
    •  •  - <%= notification.activity.username %> <%= notification_text_for(notification) %> - <%= link_to notification.activity.trackable.debate.title, notification.activity.trackable.debate %> + <%= notification.username %> <%= notification_text_for(notification) %> + <%= link_to notification.notifiable.commentable.title, notification.notifiable.commentable %>
    • <% end %>
    diff --git a/db/migrate/20150928075646_create_activities.rb b/db/migrate/20150928075646_create_activities.rb deleted file mode 100644 index 89b0edcbc..000000000 --- a/db/migrate/20150928075646_create_activities.rb +++ /dev/null @@ -1,9 +0,0 @@ -class CreateActivities < ActiveRecord::Migration - def change - create_table :activities do |t| - t.belongs_to :user, index: true, foreign_key: true - t.string :action - t.belongs_to :trackable, polymorphic: true - end - end -end diff --git a/db/migrate/20160105170113_merge_activities_and_notifications.rb b/db/migrate/20160105170113_merge_activities_and_notifications.rb new file mode 100644 index 000000000..fac5b213d --- /dev/null +++ b/db/migrate/20160105170113_merge_activities_and_notifications.rb @@ -0,0 +1,10 @@ +class MergeActivitiesAndNotifications < ActiveRecord::Migration + def change + change_table :notifications do |t| + t.remove :read + t.remove :activity_id + t.references :notifiable, polymorphic: true + end + end + +end diff --git a/db/schema.rb b/db/schema.rb index cb1c5aa65..35aa4b68c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151215165824) do +ActiveRecord::Schema.define(version: 20160105170113) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -205,14 +205,11 @@ 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 + t.integer "user_id" + t.integer "notifiable_id" + t.string "notifiable_type" 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| @@ -407,7 +404,6 @@ ActiveRecord::Schema.define(version: 20151215165824) do add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree - add_foreign_key "activities", "users" add_foreign_key "administrators", "users" add_foreign_key "annotations", "legislations" add_foreign_key "annotations", "users" @@ -416,7 +412,6 @@ 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/factories.rb b/spec/factories.rb index 8cdd3a3e4..d48aba942 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -291,9 +291,8 @@ FactoryGirl.define do end factory :notification do - association :user, factory: :user - association :activity, factory: :activity - read false + user + association :notifiable, factory: :comment end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 120369bf4..dd5cd1b78 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -3,20 +3,21 @@ 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" } + let(:debate) { create :debate } + let(:debate_comment) { create :comment, commentable: debate } + let(:comment_reply) { create :comment, commentable: debate, parent: debate_comment } 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") + it "returns correct text when someone comments on your debate" do + notification = create :notification, notifiable: debate_comment + expect(notification_text_for(notification)).to eq "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") + it "returns correct text when someone replies to your comment" do + notification = create :notification, notifiable: comment_reply + expect(notification_text_for(notification)).to eq "replied to your comment on" end end end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 11a02400a..684de6438 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -128,4 +128,19 @@ describe Comment do expect(Comment.not_as_admin_or_moderator.first).to eq(comment1) end end + + describe "#made_by?" do + let(:author) { create :user } + let(:comment) { create :comment, user: author } + + it "returns true if comment was made by user" do + expect(comment.made_by?(author)).to be true + end + + it "returns false if comment was not made by user" do + not_author = create :user + expect(comment.made_by?(not_author)).to be false + end + end + end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 8d5e3dd97..d93dcf6f3 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -26,8 +26,8 @@ describe Notification do 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) + it "returns notifications including notifiable and user" do + expect(Notification).to receive(:includes).with(notifiable: [:user]).exactly(:once) Notification.for_render end end @@ -35,8 +35,7 @@ describe Notification do 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 + notification = create :notification, notifiable: comment expect(notification.timestamp).to eq comment.created_at end @@ -51,4 +50,13 @@ describe Notification do expect(notification.read).to be true end end + + describe "#username" do + it "returns the username of the activity's author" do + comment = create :comment + notification = create :notification, notifiable: comment + expect(notification.username).to eq comment.author.username + end + end + end From e2f419e625f1996cbe14e461ce487e221ca5f2ce Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 12:02:01 +0100 Subject: [PATCH 06/21] destroy notifications when marked as read --- app/controllers/notifications_controller.rb | 21 ++++++- app/helpers/notifications_helper.rb | 8 +-- app/models/notification.rb | 3 +- .../notifications/_notification.html.erb | 8 +++ app/views/notifications/index.html.erb | 25 ++++---- config/locales/en.yml | 9 ++- config/locales/es.yml | 9 ++- config/routes.rb | 4 +- .../notifications_controller_spec.rb | 21 ------- spec/features/notifications_spec.rb | 63 ++++++++++++++----- spec/helpers/notifications_helper_spec.rb | 17 ++--- spec/models/notification_spec.rb | 16 ++--- 12 files changed, 120 insertions(+), 84 deletions(-) create mode 100644 app/views/notifications/_notification.html.erb delete mode 100644 spec/controllers/notifications_controller_spec.rb diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 8ecb16104..3820d7e36 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,9 +1,26 @@ class NotificationsController < ApplicationController before_action :authenticate_user! - load_and_authorize_resource class: "User" + after_action :mark_as_read, only: :show + skip_authorization_check def index - @notifications = current_user.notifications.recent.for_render + @notifications = current_user.notifications.unread.recent.for_render end + def show + @notification = current_user.notifications.find(params[:id]) + redirect_to url_for(@notification.notifiable.commentable) + end + + def mark_all_as_read + current_user.notifications.each { |notification| notification.mark_as_read } + redirect_to notifications_path + end + + private + + def mark_as_read + @notification.mark_as_read + end + end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 45ef6422f..1945d7c5b 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,11 +1,7 @@ module NotificationsHelper - def notification_text_for(notification) - if notification.notifiable.reply? - t("comments.notifications.replied_to_your_comment") - else - t("comments.notifications.commented_on_your_debate") - end + def notification_action(notification) + notification.notifiable.reply? ? "replied_to_your_comment" : "commented_on_your_debate" end def notifications_class_for(user) diff --git a/app/models/notification.rb b/app/models/notification.rb index 63f44dedc..e22e68ad2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -14,6 +14,7 @@ class Notification < ActiveRecord::Base notifiable.created_at end - def mark_as_read! + def mark_as_read + self.destroy end end \ No newline at end of file diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb new file mode 100644 index 000000000..313a5458c --- /dev/null +++ b/app/views/notifications/_notification.html.erb @@ -0,0 +1,8 @@ +
  • + <%= link_to notification do %> +  •  + <%= notification.username %> + <%= t("notifications.index.#{notification_action(notification)}") %> + <%= notification.notifiable.commentable.title %> + <% end %> +
  • \ No newline at end of file diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 0155f0521..5eaebefb9 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -1,11 +1,14 @@ -
    -
      - <% @notifications.each do |notification| %> -
    • -  •  - <%= notification.username %> <%= notification_text_for(notification) %> - <%= link_to notification.notifiable.commentable.title, notification.notifiable.commentable %> -
    • - <% end %> -
    -
    +<% if @notifications.empty? %> +
    <%= t("notifications.index.empty_notifications") %>
    +<% else %> +
    +
      + <%= render @notifications %> +
    + +
    + <%= link_to t("notifications.index.mark_all_as_read"), + mark_all_as_read_notifications_path, method: :put %> +
    +
    +<% end %> \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 35af31bbc..683db0454 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -267,9 +267,6 @@ en: zero: "No votes" one: "1 vote" other: "%{count} votes" - notifications: - commented_on_your_debate: "commented on your debate" - replied_to_your_comment: "replied to your comment on" comments_helper: comment_link: "Comment" comment_button: "Publish comment" @@ -313,6 +310,12 @@ en: user_permission_votes: "Participate on final voting" user_permission_verify: "To perform all the actions verify your account." user_permission_verify_info: "* Only for users on Madrid City Census." + notifications: + index: + mark_all_as_read: "Mark all as read" + empty_notifications: "There are no new notifications." + commented_on_your_debate: "commented on your debate" + replied_to_your_comment: "replied to your comment on" simple_captcha: placeholder: "Enter the text from the image" label: "Enter the text from the image in the box below" diff --git a/config/locales/es.yml b/config/locales/es.yml index c10d8a493..f6fd907de 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -267,9 +267,6 @@ 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" @@ -313,6 +310,12 @@ es: user_permission_votes: "Participar en las votaciones finales*" user_permission_verify: "Para poder realizar todas las acciones verifica tu cuenta." user_permission_verify_info: "* Sólo usuarios empadronados en el municipio de Madrid." + notifications: + index: + mark_all_as_read: "Marcar todas como leídas" + empty_notifications: "No hay notificaciones nuevas." + commented_on_your_debate: "ha comentado en tu debate" + replied_to_your_comment: "ha respondido a tu comentario en" simple_captcha: placeholder: "Introduce el texto de la imagen" label: "Introduce el texto de la imagen en la siguiente caja" diff --git a/config/routes.rb b/config/routes.rb index 681eff89b..29b63e91b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,7 +75,9 @@ Rails.application.routes.draw do collection { get :erase } end - resources :notifications, only: :index + resources :notifications, only: [:index, :show] do + collection { put :mark_all_as_read } + end resource :verification, controller: "verification", only: [:show] diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/controllers/notifications_controller_spec.rb deleted file mode 100644 index fa6104a9c..000000000 --- a/spec/controllers/notifications_controller_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails_helper' - -describe NotificationsController do - - describe "#index" do - let(:user) { create :user } - - it "mark all notifications as read" do - notifications = [create(:notification, user: user), create(:notification, user: user)] - Notification.all.each do |notification| - expect(notification.read).to be false - end - - sign_in user - get :index - Notification.all.each do |notification| - expect(notification.read).to be true - end - end - end -end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 4b5b48f0e..22e4b320d 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -21,10 +21,10 @@ feature "Notifications" do expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" click_link "Notificaciones" + expect(page).to have_css ".notification", count: 1 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) + expect(page).to have_content "commented on your debate" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end scenario "User replied to my comment", :js do @@ -48,10 +48,10 @@ feature "Notifications" do expect(page).to have_xpath "//a[@class='with_notifications' and @href='#{notifications_path}' and text()='Notificaciones']" visit notifications_path + expect(page).to have_css ".notification", count: 1 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) + expect(page).to have_content "replied to your comment on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end scenario "Author commented on his own debate", :js do @@ -66,10 +66,7 @@ feature "Notifications" do 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) + expect(page).to have_css ".notification", count: 0 end scenario "Author replied to his own comment", :js do @@ -89,9 +86,47 @@ feature "Notifications" do 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) + expect(page).to have_css ".notification", count: 0 end + + context "mark as read" do + + scenario "mark a single notification as read" do + user = create :user + notification = create :notification, user: user + + login_as user + visit notifications_path + + expect(page).to have_css ".notification", count: 1 + + first(".notification a").click + visit notifications_path + + expect(page).to have_css ".notification", count: 0 + end + + scenario "mark all notifications as read" do + user = create :user + 2.times { create :notification, user: user } + + login_as user + visit notifications_path + + expect(page).to have_css ".notification", count: 2 + click_link "Mark all as read" + + expect(page).to have_css ".notification", count: 0 + expect(current_path).to eq(notifications_path) + end + + end + + scenario "no notifications" do + login_as user + visit notifications_path + + expect(page).to have_content "There are no new notifications" + end + end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index dd5cd1b78..fa378b2ec 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe NotificationsHelper do - describe "#notification_text_for" do + describe "#notification_action" do let(:debate) { create :debate } let(:debate_comment) { create :comment, commentable: debate } let(:comment_reply) { create :comment, commentable: debate, parent: debate_comment } @@ -10,14 +10,14 @@ describe NotificationsHelper do context "when action was comment on a debate" do it "returns correct text when someone comments on your debate" do notification = create :notification, notifiable: debate_comment - expect(notification_text_for(notification)).to eq "commented on your debate" + expect(notification_action(notification)).to eq "commented_on_your_debate" end end context "when action was comment on a debate" do it "returns correct text when someone replies to your comment" do notification = create :notification, notifiable: comment_reply - expect(notification_text_for(notification)).to eq "replied to your comment on" + expect(notification_action(notification)).to eq "replied_to_your_comment" end end end @@ -25,20 +25,13 @@ describe NotificationsHelper do describe "#notifications_class_for" do let(:user) { create :user } - context "when user doesn't have any notification" do + context "when user doesn't have notifications" 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 + context "when user has notifications" do it "returns class 'with_notifications'" do notification = create :notification, user: user expect(notifications_class_for(user)).to eq "with_notifications" diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index d93dcf6f3..0b8354e83 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -4,12 +4,8 @@ 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 + 2.times { create :notification } + expect(Notification.unread.size).to be 2 end end @@ -42,12 +38,12 @@ describe Notification do end describe "#mark_as_read" do - it "set up read flag to true" do + it "destroys notification" do notification = create :notification - expect(notification.read).to be false + expect(Notification.unread.size).to eq 1 - notification.mark_as_read! - expect(notification.read).to be true + notification.mark_as_read + expect(Notification.unread.size).to eq 0 end end From 816a95b7b72df3a041536cb8e9dc43864b5e6efc Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 14:19:05 +0100 Subject: [PATCH 07/21] adds comment show view --- app/controllers/comments_controller.rb | 7 ++++++- app/helpers/comments_helper.rb | 7 +++++-- app/models/abilities/everyone.rb | 1 + app/views/comments/_comment.html.erb | 13 +++++++------ app/views/comments/show.html.erb | 12 ++++++++++++ config/locales/en.yml | 2 ++ config/locales/es.yml | 2 ++ config/routes.rb | 2 +- spec/features/comments/debates_spec.rb | 17 ++++++++++++++++- spec/features/comments/proposals_spec.rb | 15 +++++++++++++++ spec/models/abilities/everyone_spec.rb | 2 ++ 11 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 app/views/comments/show.html.erb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 35406faf0..050962414 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,5 +1,5 @@ class CommentsController < ApplicationController - before_action :authenticate_user! + before_action :authenticate_user!, only: :create before_action :load_commentable, only: :create before_action :build_comment, only: :create @@ -14,6 +14,11 @@ class CommentsController < ApplicationController end end + def show + @comment = Comment.find(params[:id]) + set_comment_flags(@comment.subtree) + end + def vote @comment.vote_by(voter: current_user, vote: params[:value]) respond_with @comment diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index 9ac29e4da..9d9d08075 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -13,8 +13,11 @@ module CommentsHelper end def child_comments_of(parent) - return [] unless @comment_tree - @comment_tree.children_of(parent) + if @comment_tree.present? + @comment_tree.ordered_children_of(parent) + else + parent.children + end end def user_level_class(comment) diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index d6b2a6d57..122d5db2a 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -5,6 +5,7 @@ module Abilities def initialize(user) can :read, Debate can :read, Proposal + can :read, Comment can :read, Legislation can :read, User can [:search, :read], Annotation diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 9178f0b9b..44e15362b 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,9 +1,9 @@ -<% cache [locale_and_user_status(comment), comment, commentable_cache_key(@commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %> +<% cache [locale_and_user_status(comment), comment, commentable_cache_key(comment.commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %>
    <% if comment.hidden? || comment.user.hidden? %> - <% if child_comments_of(comment).size > 0 %> + <% if comment.children.size > 0 %>

    <%= t("comments.comment.deleted") %>

    @@ -49,7 +49,7 @@ <%= t("shared.collective") %> <% end %> - <% if comment.user_id == @commentable.author_id %> + <% if comment.user_id == comment.commentable.author_id %>  •  <%= t("comments.comment.author") %> @@ -63,7 +63,7 @@
    + <%= comment_author_class comment, comment.commentable.author_id %>"> <%= simple_format text_with_links comment.body %>
    @@ -72,7 +72,7 @@
    - <%= t("comments.comment.responses", count: child_comments_of(comment).size) %> + <%= t("comments.comment.responses", count: comment.children.size) %> <% if user_signed_in? %>  |  @@ -81,11 +81,12 @@ <%= render 'comments/actions', comment: comment %> - <%= render 'comments/form', {commentable: @commentable, parent_id: comment.id, toggeable: true} %> + <%= render 'comments/form', {commentable: comment.commentable, parent_id: comment.id, toggeable: true} %> <% end %>
    <% end %> +
    <% child_comments_of(comment).each do |child| %> <%= render 'comments/comment', comment: child %> diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb new file mode 100644 index 000000000..b0c3dfb3f --- /dev/null +++ b/app/views/comments/show.html.erb @@ -0,0 +1,12 @@ +
    + <%= link_to t("comments.show.return_to_commentable") + @comment.commentable.title, + @comment.commentable %> +
    + +
    +
    +
    + <%= render @comment %> +
    +
    +
    \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ee698c91..57a9fb92e 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -244,6 +244,8 @@ en: form: submit_button: "Save changes" comments: + show: + return_to_commentable: "Go back to " select_order: "Sort by" orders: most_voted: "Most voted" diff --git a/config/locales/es.yml b/config/locales/es.yml index 46bb3b88b..65eb1eb8b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -244,6 +244,8 @@ es: form: submit_button: "Guardar cambios" comments: + show: + return_to_commentable: "Volver a " select_order: "Ordenar por" orders: most_voted: "Más votados" diff --git a/config/routes.rb b/config/routes.rb index 3472f1c4e..2da32d479 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,7 +53,7 @@ Rails.application.routes.draw do end end - resources :comments, only: :create, shallow: true do + resources :comments, only: [:create, :show], shallow: true do member do post :vote put :flag diff --git a/spec/features/comments/debates_spec.rb b/spec/features/comments/debates_spec.rb index 5adfa218c..7d807a6c0 100644 --- a/spec/features/comments/debates_spec.rb +++ b/spec/features/comments/debates_spec.rb @@ -20,6 +20,21 @@ feature 'Commenting debates' do end end + scenario 'Show' do + parent_comment = create(:comment, commentable: debate) + first_child = create(:comment, commentable: debate, parent: parent_comment) + second_child = create(:comment, commentable: debate, parent: parent_comment) + + visit comment_path(parent_comment) + + expect(page).to have_css(".comment", count: 3) + expect(page).to have_content parent_comment.body + expect(page).to have_content first_child.body + expect(page).to have_content second_child.body + + expect(page).to have_link "Go back to #{debate.title}", debate_path(debate) + end + scenario 'Comment order' do c1 = create(:comment, :with_confidence_score, commentable: debate, cached_votes_up: 100, cached_votes_total: 120, created_at: Time.now - 2) c2 = create(:comment, :with_confidence_score, commentable: debate, cached_votes_up: 10, cached_votes_total: 12, created_at: Time.now - 1) @@ -254,7 +269,7 @@ feature 'Commenting debates' do fill_in "comment-body-debate_#{debate.id}", with: 'Testing submit button!' click_button 'Publish comment' - + # The button's text should now be "..." # This should be checked before the Ajax request is finished expect(page).to_not have_button 'Publish comment' diff --git a/spec/features/comments/proposals_spec.rb b/spec/features/comments/proposals_spec.rb index 1c277cf4d..d919f0b51 100644 --- a/spec/features/comments/proposals_spec.rb +++ b/spec/features/comments/proposals_spec.rb @@ -20,6 +20,21 @@ feature 'Commenting proposals' do end end + scenario 'Show' do + parent_comment = create(:comment, commentable: proposal) + first_child = create(:comment, commentable: proposal, parent: parent_comment) + second_child = create(:comment, commentable: proposal, parent: parent_comment) + + visit comment_path(parent_comment) + + expect(page).to have_css(".comment", count: 3) + expect(page).to have_content parent_comment.body + expect(page).to have_content first_child.body + expect(page).to have_content second_child.body + + expect(page).to have_link "Go back to #{proposal.title}", proposal_path(proposal) + end + scenario 'Comment order' do c1 = create(:comment, :with_confidence_score, commentable: proposal, cached_votes_up: 100, cached_votes_total: 120, created_at: Time.now - 2) c2 = create(:comment, :with_confidence_score, commentable: proposal, cached_votes_up: 10, cached_votes_total: 12, created_at: Time.now - 1) diff --git a/spec/models/abilities/everyone_spec.rb b/spec/models/abilities/everyone_spec.rb index 4c532b7a4..dacac25e4 100644 --- a/spec/models/abilities/everyone_spec.rb +++ b/spec/models/abilities/everyone_spec.rb @@ -21,4 +21,6 @@ describe "Abilities::Everyone" do it { should_not be_able_to(:vote, Proposal) } it { should_not be_able_to(:flag, Proposal) } it { should_not be_able_to(:unflag, Proposal) } + + it { should be_able_to(:show, Comment) } end From 35704f2be906a1ef9306dba29c44f6a39d117b7f Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 15:18:45 +0100 Subject: [PATCH 08/21] refactors comment tree --- lib/comment_tree.rb | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/comment_tree.rb b/lib/comment_tree.rb index 8d0e072dd..f2eadb8ab 100644 --- a/lib/comment_tree.rb +++ b/lib/comment_tree.rb @@ -2,24 +2,34 @@ class CommentTree ROOT_COMMENTS_PER_PAGE = 10 - attr_accessor :root_comments, :comments + attr_accessor :root_comments, :comments, :commentable, :page, :order def initialize(commentable, page, order = 'confidence_score') - @root_comments = commentable.comments.roots.send("sort_by_#{order}").page(page).per(ROOT_COMMENTS_PER_PAGE).for_render - - root_descendants = @root_comments.each_with_object([]) do |root, col| - col.concat(Comment.descendants_of(root).send("sort_descendants_by_#{order}").for_render.to_a) - end + @commentable = commentable + @page = page + @order = order @comments = root_comments + root_descendants + end - @comments_by_parent_id = @comments.each_with_object({}) do |comment, col| - (col[comment.parent_id] ||= []) << comment + def root_comments + commentable.comments.roots.send("sort_by_#{order}").page(page).per(ROOT_COMMENTS_PER_PAGE).for_render + end + + def root_descendants + root_comments.each_with_object([]) do |root, array| + array.concat(Comment.descendants_of(root).send("sort_descendants_by_#{order}").for_render.to_a) end end - def children_of(parent) - @comments_by_parent_id[parent.id] || [] + def ordered_children_of(parent) + comments_by_parent_id[parent.id] || [] + end + + def comments_by_parent_id + comments.each_with_object({}) do |comment, array| + (array[comment.parent_id] ||= []) << comment + end end def comment_authors From 9c2b5764319cf51b1811433ab889dfddada8c8e8 Mon Sep 17 00:00:00 2001 From: rgarcia Date: Thu, 7 Jan 2016 15:30:48 +0100 Subject: [PATCH 09/21] fixes specs --- config/i18n-tasks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index c6393c23a..5668ee2ce 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -125,6 +125,8 @@ ignore_unused: - 'debates.index.search_form.*' - 'proposals.index.orders.*' - 'proposals.index.search_form.*' + - 'notifications.index.commented_on_your_debate' + - 'notifications.index.replied_to_your_comment' - 'helpers.page_entries_info.*' # kaminari - 'views.pagination.*' # kaminari # - '{devise,kaminari,will_paginate}.*' From 09120b6129d4c86d7b2a611aa538070a913d8e39 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 12:44:40 +0100 Subject: [PATCH 10/21] Updates icons font --- app/assets/fonts/icons.eot | Bin 10856 -> 9544 bytes app/assets/fonts/icons.svg | 7 ++- app/assets/fonts/icons.ttf | Bin 10700 -> 9388 bytes app/assets/fonts/icons.woff | Bin 8300 -> 7468 bytes app/assets/stylesheets/icons.scss | 93 +++++++++++++++--------------- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot index 3046c50cc222d1c158d1f8748287cb28c02f44be..ced98a9b4790e2f0ffb6ab1792a9671d4d63e209 100644 GIT binary patch delta 1603 zcmZ`(T}&KR6h3$UXLojnWp`$m{ejuPnO$&USzwokR;fWyi9&4yrl4lKg@Upyv?Q&@ zbQ@o^iP+XjYOKZ@pGfRO18IEF_!DE&7-AEm4-Gu*gGMFBH(zQjo>>-(X`P#U@AQK_ga-Sc7=SV05YHplhmUk* zhc_m^0wB*L9xav%^VJ*GF#t+J?&Fh%)AQTgK%@RNt_vq;U!5ui2Co4y3_y5fda`hW zd+UccacvWOZW;+DLw>=3y@=z}rNzowafrsv$qghXX3NEbb$4kLfWD4+v{X1d4`<*e zuJo|#B=!Kxj{sW4IZ@NYT-o`8P~HAjM7Gl*VBj3i0=6BWpMlpfxAu1k3U#UG zC)THV1nD6Dl!O)tLo?_Qffz&~j+>;=Bm*p}@n}rMtsUs51TK)l1O+r`05^ER3o7`) z4+bV$paaWi8O20L`!*FNlo}r{t1NMQd<-ysFP{GzF}0=y$Bc zj6eScdy4&@dy)H*yT@nwOZ>XP2r5m$rqU2gR!b&XyGdhTo?|zIJJ-NKytYKBF`0}|fjj*iS&oZ*2vRD6> zx{aXflO%yB6Pj{f)edNC>z&R9z2OBvP3V8N7@8%v)Wf-4PFWIpo{L2Clptt=K*d5q zBS-+cQRatraPE%AAWvS8aDxFufuuoOhmPF69p-FNiNS|P6`d)^js346}yXbHz zE>-n=k}HwhaGIjxR&diI!>w$NKxUtZk{=G)(Pb)>F?V z;;PrH9*QUGQhaH5pZgnr;8pLh*A$<6zy&K{rc51X>(|u2Pw&@d7H`vMb(ur{w)L_Z zG5es^zRHYr15b`Ll!bP=PW%L5PH*{>1?JmXZ0tC ztjkHAv|204b_T|YX8oMhDPmeoYSNlZ^;w^%V%D#zgA2-BIXzcioSB*_78Ym9bJBjm y6=zNrXD5Z7DP6`a5`0Cdn&UDeKX&`kBj{cVM|`mn*`NI$!P>!3Zrk(zr}JOm1qxmO delta 2981 zcmYjTU1%KF6+ZXQ%)PU#)vkVKXEu^nl4qoy$cnYv*`1k^%9bKIF*Hp>Nk~gvD2uB| zF8)z$rTHQ43O|(6LgILf+lEk=f?EhBq!>b>tqY+KDWoY)$xEDv_N}F08c6A*`<=UM z@Ll%abIv_K-}zbTzW0THKB;yO6TQFmn#%0!ooiQL+c?wP`{=np5-q(=L^CIjKl}(i zMGE|7@biy9(M%q{^|e0`iLZix>eSip-rwf`@j8(#LH5&~?!~=3cgTYN_i*p-oO$t? zC!)80O=N=&4|cb^-*DcU{1nV5_-4Bh*iGrc)&sw=dv?F~LgAS8CIk%N=`-g~b+4@b zq()@@3jC92yD#j~x8y6h{~Y-FbKSGsmp zEw6{DJER7v6miB>r&*e#kml(SMYKQ-1dZu_IJC(@6E$2FXp)M6SfWuXQ-#JTpmD(Q zs7gN7=stj(rkVbxb)rZ$*Y2B| zp6UP9I_Ur18tZ@AuC;I4pE++Tsjljh&h$6+dv4KPbKfZVh5f=C#Y>|fmmB3*%D*Xp zTp6oesJuJ&*7(MapV^fHYS9)(pe@Y_jzs1Y|hA)q;)S{67N8>?h_ zmUi+ihjN}Jfe-jT?}0BIXT0XvOsZNr)Q??P3gM+ zy6ehj9qhDI8+V*)x$>1{YxByL&8?*3*Mn*`sQZeI?{OWJjH$+gsUedV#u zEutI0^gghV#3C(qP%ld!HY9XQHTG$4bw zPMrF=7XU^(M~pURvcXw&qLc#*K!V-2-^R1M&BW=rO zPY|<_)pLa?>LOgh3wUr2NZW?8uxu7N!jfv>9In{d(YAC{tjFE~V6p(HIK=Uz&n?4>+XlDgy_I5Y{dlbI2JaXUeX%gTsWA1{!D^#eft>3=wXa zWEIbOHGEW5yvIYZ+Q5>!+4*uXaFNm*n^xaX?!vR%|cNab)kaAK;>A&?Ts z#o`cZ&>A+xop7)@3?9%O{IzAC)38hFgNHd?(=#@NLC<&*%3-(08#^@sz#AbSM$2(g zSt)MaI~Ha>o*yW1c!W{_B$Whm4uOVos_g9Uf{xMTh&@&?WoO(x9YZZk0z0md2@iwd z{~MgRYcOHFxF4**lZH;P8Aj_aViTK1@C~1XEV)gN1{R(!9#GVOP-=gP0kW=%3o8z5 zX{~HkGJ{~6hO{?)74ZY++K_he zeEI#5R*-+xKNcQ}kJDj(IoSm~c8b&=(heRq*N3!%{7?Pwh9@TzTBRd+hw;vIi_-po z!l`1idZe{_Bw1~x{fX$&{>dm5Oa1ReE4J|IWdH4GzW-sgZC~8)Ug&2F4~>@2o!>wG z%;{6z{nO{q^`Bpe3htTixt-^_JKO!&7mgn1HwNBm(qJuv!!s6(c?-`nVIIZ&65+Q9 P*84|){ue&)eVqOugycd~ diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg index c107e740d..d92b9a44a 100644 --- a/app/assets/fonts/icons.svg +++ b/app/assets/fonts/icons.svg @@ -19,7 +19,6 @@ - @@ -30,12 +29,10 @@ - - @@ -45,4 +42,8 @@ + + + + diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf index b933f1c3f1f5f651f93736c4336dbde93769ad04..f3486cf96bec5abc0d42863006781a3c543eb65b 100644 GIT binary patch delta 1620 zcmaJBU1(cn^nCX}$-TKvlY5gkO>^^iZ_=7HP1~f7P0N_6qs&!1TG^_S#k485O>GOC zAWad$2@Z!B98Pc|J`~);SSf;t3Q`a$8x~(i_mCGKEaICl6Iaho(zV5(9B$6=Ok#B|0yM){xbM42D8aR@>HIzfXF zL?8@N^p?Q046xWQ9#7fu4m-vpVt1WjfD9_MfD7E<0R_C^10C9+-Ta&ymi!&@V4zbA zMZ(co!u*MHx6=&E@q(?@?hqxXA*(Gew@2~%^tN`hmwwxs9F?xBjHE zMXzrJx40_vxDQUuF9jE|ZT*q3K7fCU$E)^bj9iLD!aQez;$b~pH}UXX*w6=ZeY%lT z-CmXV@Pv!#Jw(s-KTY(3Y_16d{Yas&TsGrX6%Vg^Rc|iarxKQR`B=s&R~7cgpJKo6 zSG=Mq@MKbzR~7ZRs&w7&ZP8j@^3jC;YnP!}Vo?K}BsrX-jpw;gC`So`DhO1h!!KV# z3qR{~v8>{h-@D%yw%MFwt4o?&mDPY(QIue8;JJYS4yddzRz%cGsf0?3i4sepl?>G)lIQ5U}5^hx7k+KV^*d zU((BkLLATAM5jY~oZ&;yV9={M7v&cHrFo;t!>a;(OIBLiL42cw=>#`4r}=!SaW{=j?Ug;-Sac$8>t zpSWsWlR0t&`S`Fw-SYGi11kh`MNb$S%r;loTD^~&G>eP(Ws~Mme$TwChm0ZUveuii z;hNgvYLjMhF>f|$4&~pQs&S!r5R&*{Bq0N7#94rRWqvtdS)49unN(Ukrp**9^Gk>a zlfB7Ix;JaC86$);zoLSq%T(hj1}+fQd^WC8#4yY8lDQopGJ}bTIg&V$l;MeGX0bS15*jF3F|Q;7Mgp_JN4)tMSx*#fo%rmYg%4iS{N~?orVW|^ delta 2971 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 IoCHGp4|*~XOaK4? delta 8235 zcmXY$Wl$SVwDyBjtfjb1i@R%am*VbDAOuK(1`Y1+PJ!YjP~3~Vmf{Zo;;t!rx%Yi{ z=K1jac4yByJNx16><`gnDZ;NB3JMy!x*7lgEeL=OcpaRkNrdz;%3c6~g~%?@(MLf+ zR~-PL8h@Q4e-)+MoM*qHoV>y-+kf>`uc8I$0#G${xOf2oYRp%!{3=VWL-TeU7b~|{ zru*u10RW`@uKh_9M>{LqS9bo&m|rD`)5U#R~emA2n2Z;nO+%Bi?&%1uY-fwf%u#F8?n59(x zVOYw&vcN@ePhPD8uMNEvyu3`os5eB)QlhBD-jCSw3am;BiY6(UCn+&@b=;J&HFZ4( zRc|a?S>%OQNdgSchE20to>yQCj|AQABnC0WAc3aCUfhv-nl`#5#5wNBR|$yhb{mDn zjmE-931wTj%69@GMu{|p+V*@u4MSIU3zNI+wpi3!RK$Bb^g8bPp% zi8sI=Act-*i-ocw#VG_SvwbNkAxqDlA3};zeCts{5>E3db2G`Q$Rvc73Z9=ro-i;l z!hil1FK(fj~VRk$XOT-?kLyaYA%*iF>3+MWK*b@upA zAzvug#2~g%OE-|^4_I^?^(0=xX9-bW!R!4+H898ta;QcAmf|ums+lQC=}`Ftkg@vX z$CK|V9x3L(HS|-Y>(YNPQHpJGd@oF5YyVI(Q4BY*!>fe5swn`(eR zCNLy038gS4Gwryv(a#hhQvPE~UdDLdL~%WVirv&A4l&N|H%HiDXQLBUi*pn&ptH#g z36Bfem!rP9gjD#F_rz9$gxiVIeSX8!)X+V3>&0?Tz(eWT`X7YhrOniO?n`LL(c@%w zP9MqdO~d#DSgoHyEvQeh2QbUaL3|P!>Zr(!S?aQkz`+@-GnC-UL}Y_qW_gUD{vMs1 znee}XDDV?u#K(T7__$_}1417m$@NxqAG1^zA@@E=LgA=p<6G zy2r|L2iv3@20%o1aXc4zEuDKTc&IPCFSuoGZN&{~(3Ds?RdMC%*f-|3E*@S7|ILD3 z#zN$#sPaIu9}LRAQ&RsXv?XJ1+8z#>)ZCFelX3;wU1pk2?^zOGg1huFqvIeZ_AR_M zq}-~Z6_#y$D_JJYTnBY@CcKW{mZQz`fV+FDS0!kk_>1QV%Po9UF6|~7ng{7E5#cvX zk2@1PadVT}uI^S=KkIwj#wO-0tD9hl@E&sKDD0C{Ki{Aezb#YIj)q3IwvP%Ky%yi; z)@}cMK{4bEYYU|OCh#H0oTQb!Uk#-xk-nnDg>j|3_vcEpugTeo%d?N`UT5dYS6>VF zujJ%DW^YoZE?K+71+}N=v$f4CGdkaEvB?-^oKKHw+C~(!3CcMht>8xg`PQ%oliI-z z@5fSV3y$EF4(m)IEZkOBGOswowKQqh#x3_nqBf@@ZwYB{?n;cOMwv+^(nBX!@5zrq zlbyvO%KW%+=fR8}h$EdlRN)g1Mwswjt%ndsGs1>okg<5r;U;$#6VDOZm}muYR5 z3YUR=0*8#0s!{)o2Z;b~8dm1p{2lLnXWz|wreZVT^f2L8K?PF#r!X@rl&H*Wi4GIg z0->+|7-GNjdrwOv&ZwyPiJzIsqQs}-(*m=e zo^e4juR)`yIIfAO?H|9kSD*j-y#`w~(&YG?+{&HSuA>^bb|<6Mwe;}S^lQP88X%##(yVSV2?ETOs=F^~m5 z%J1=3^edMB#AGiDndob78D^5T6`h=3T(8VLU_3zr_taztp8XRQo~2qwi8vdXlF@ax z{n}~-CVoP$l~h}DMANivq3UrKUkxO7-hqnT3#{U{`F06&G$S6C_X9o-& zelx{T$P+LQz}THBkBxXq49_;ws{)+|f4i@YAx0(SXJd!83ANqL3!zRJn4-lAwYkLM z;$c?WTcadWHCEQ0+sq81T42WloHRYFB--8R{P>SaD!XL;mdVx;PWRKT{y7ylHwLGK zy>BBRCHiV9V}RKxYP|BX0Jq- z$5&|Hw*?Okc|C>o(c{zow*4K&(C8AU7FKQVw5J_-)V_U?&4-x X#}?Ogg%yvg%r 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 axJP0`;@$qg%VIBu9*g7nT5vo7?SBBJVA6>I diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss index eda8b979d..46e24e4cf 100644 --- a/app/assets/stylesheets/icons.scss +++ b/app/assets/stylesheets/icons.scss @@ -38,116 +38,119 @@ } .icon-angle-down:before { - content: "a"; + content: "\61"; } .icon-angle-left:before { - content: "b"; + content: "\62"; } .icon-angle-right:before { - content: "c"; + content: "\63"; } .icon-angle-up:before { - content: "d"; + content: "\64"; } .icon-comments:before { - content: "e"; + content: "\65"; } .icon-twitter:before { - content: "f"; + content: "\66"; } .icon-calendar:before { - content: "g"; + content: "\67"; } .icon-debates:before { - content: "i"; + content: "\69"; } .icon-unlike:before { - content: "j"; + content: "\6a"; } .icon-like:before { - content: "k"; + content: "\6b"; } .icon-check:before { - content: "l"; + content: "\6c"; } .icon-edit:before { - content: "m"; -} -.icon-star:before { - content: "n"; + content: "\6d"; } .icon-user:before { - content: "o"; + content: "\6f"; } .icon-settings:before { - content: "q"; + content: "\71"; } .icon-stats:before { - content: "r"; + content: "\72"; } .icon-proposals:before { - content: "h"; + content: "\68"; } .icon-organizations:before { - content: "s"; + content: "\73"; } .icon-deleted:before { - content: "t"; + content: "\74"; } .icon-tag:before { - content: "u"; + content: "\75"; } .icon-eye:before { - content: "p"; + content: "\70"; } .icon-x:before { - content: "v"; + content: "\76"; } .icon-flag:before { - content: "w"; -} -.icon-notification:before { - content: "x"; + content: "\77"; } .icon-comment:before { - content: "y"; + content: "\79"; } .icon-reply:before { - content: "z"; + content: "\7a"; } .icon-facebook:before { - content: "A"; + content: "\41"; } .icon-google-plus:before { - content: "B"; -} -.icon-language:before { - content: "C"; + content: "\42"; } .icon-search:before { - content: "E"; + content: "\45"; } .icon-external:before { - content: "F"; + content: "\46"; } .icon-video:before { - content: "D"; + content: "\44"; } .icon-document:before { - content: "G"; + content: "\47"; } .icon-print:before { - content: "H"; + content: "\48"; } .icon-blog:before { - content: "J"; + content: "\4a"; } .icon-box:before { - content: "I"; + content: "\49"; } .icon-youtube:before { - content: "K"; + content: "\4b"; } .icon-letter:before { - content: "L"; -} \ No newline at end of file + content: "\4c"; +} +.icon-no-notification:before { + content: "\78"; +} +.icon-notification:before { + content: "\6e"; +} +.icon-circle:before { + content: "\43"; +} +.icon-circle-o:before { + content: "\4d"; +} From e008d1f642cea4e99ac701563ebc3adf9c339061 Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 12:45:37 +0100 Subject: [PATCH 11/21] Adds new styles for notifications --- app/assets/stylesheets/layout.scss | 73 ++++++++++++++++++- app/assets/stylesheets/variables.scss | 1 + app/views/devise/menu/_login_items.html.erb | 14 +++- .../notifications/_notification.html.erb | 10 ++- app/views/notifications/index.html.erb | 28 ++++--- config/locales/en.yml | 4 +- config/locales/es.yml | 4 +- 7 files changed, 112 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index d47189d18..329ab5a0e 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -384,7 +384,6 @@ header { &:hover { background: none; color: white; - text-decoration: underline; transition: text-decoration 275ms; } @@ -408,6 +407,7 @@ header { &:hover, &:focus { background-color: #007095 !important; + text-decoration: underline; } } @@ -935,6 +935,77 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar { } } +.notifications { + position: relative; + + &:hover { + text-decoration: none; + } + + [class^="icon-"] { + font-size: $h4-font-size; + vertical-align: middle; + } + + .icon-circle { + color: #ecf00b; + font-size: $tiny-font-size; + position: absolute; + right: 4px; + top: -6px; + } +} + +.notifications-list:before { + background: $border; + content: ''; + height: 100%; + left: 28px; + position: absolute; + top: 0; + width: 2px; +} + +.notification { + display: block; + padding: $line-height/2 0 $line-height/2 $line-height*1.5; + position: relative; + + &:hover { + + a { + text-decoration: none; + } + + p:not(.time) { + color: $link; + } + + &:before { + content: "\43"; + } + } + + &:before { + background: white; + color: $brand; + content: "\4d"; + font-family: "icons" !important; + left: 6px; + position: absolute; + } + + p { + color: $text; + margin-bottom: 0; + } + + .time { + font-size: $small-font-size; + color: $text-medium; + } +} + // 09. Filters & search // - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss index 72fb27788..336b09919 100644 --- a/app/assets/stylesheets/variables.scss +++ b/app/assets/stylesheets/variables.scss @@ -39,6 +39,7 @@ $h6-font-size: rem-calc(13); $h6-line-height: rem-calc(17); $small-font-size: rem-calc(14); +$tiny-font-size: rem-calc(10); $line-height: rem-calc(24); // 02. Colors diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index 5f6608f2f..d2460d11d 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -1,5 +1,16 @@
      <% if user_signed_in? %> +
    • + <%= link_to notifications_path, class: "notifications" do %> + <% if current_user.notifications.count > 0 %> + + + + <% else %> + + <% end %> + <% end %> +
    • <%= link_to(t("layouts.header.my_activity_link"), user_path(current_user)) %>
    • @@ -9,9 +20,6 @@
    • <%= 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/_notification.html.erb b/app/views/notifications/_notification.html.erb index 313a5458c..7315f899d 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,8 +1,10 @@
    • <%= link_to notification do %> -  •  - <%= notification.username %> - <%= t("notifications.index.#{notification_action(notification)}") %> - <%= notification.notifiable.commentable.title %> +

      + <%= notification.username %> + <%= t("notifications.index.#{notification_action(notification)}") %> + <%= notification.notifiable.commentable.title %> +

      +

      <%= l notification.timestamp, format: :datetime %>

      <% end %>
    • \ No newline at end of file diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 5eaebefb9..cc9170e48 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -1,14 +1,18 @@ -<% if @notifications.empty? %> -
      <%= t("notifications.index.empty_notifications") %>
      -<% else %> -
      -
        - <%= render @notifications %> -
      +
      +
      + <% if @notifications.empty? %> +
      + <%= t("notifications.index.empty_notifications") %> +
      + <% else %> +
      + <%= link_to t("notifications.index.mark_all_as_read"), + mark_all_as_read_notifications_path, method: :put %> +
      -
      - <%= link_to t("notifications.index.mark_all_as_read"), - mark_all_as_read_notifications_path, method: :put %> -
      +
        + <%= render @notifications %> +
      + <% end %>
      -<% end %> \ No newline at end of file +
      diff --git a/config/locales/en.yml b/config/locales/en.yml index 683db0454..d46c6f5ab 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,6 +29,8 @@ en: more_information: "More information" debates: "Debates" proposals: "Proposals" + new_notifications: "You have %{count} new notifications" + no_notifications: "You don't have new notifications" footer: description: "This portal uses the %{consul} which is %{open_source}. From Madrid out into the world." open_source: "open-source software" @@ -313,7 +315,7 @@ en: notifications: index: mark_all_as_read: "Mark all as read" - empty_notifications: "There are no new notifications." + empty_notifications: "You don't have new notifications." commented_on_your_debate: "commented on your debate" replied_to_your_comment: "replied to your comment on" simple_captcha: diff --git a/config/locales/es.yml b/config/locales/es.yml index f6fd907de..531456d8f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -29,6 +29,8 @@ es: more_information: "Más información" debates: "Debates" proposals: "Propuestas" + new_notifications: "Tienes %{count} notificaciones nuevas" + no_notifications: "No tienes notificaciones nuevas" footer: description: "Este portal usa la %{consul} que es %{open_source}. De Madrid, para el mundo entero." open_source: "software libre" @@ -313,7 +315,7 @@ es: notifications: index: mark_all_as_read: "Marcar todas como leídas" - empty_notifications: "No hay notificaciones nuevas." + empty_notifications: "No tienes notificaciones nuevas." commented_on_your_debate: "ha comentado en tu debate" replied_to_your_comment: "ha respondido a tu comentario en" simple_captcha: From 40ffeeaeb01c41ec1c876621bbf8652836e5196a Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 12:46:02 +0100 Subject: [PATCH 12/21] Removes unused method --- app/helpers/notifications_helper.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 1945d7c5b..63618440b 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -3,9 +3,4 @@ module NotificationsHelper def notification_action(notification) notification.notifiable.reply? ? "replied_to_your_comment" : "commented_on_your_debate" end - - def notifications_class_for(user) - user.notifications.count > 0 ? "with_notifications" : "without_notifications" - end - end From ab74bd58faefb6e473724f1f73b27e99783de46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baza=CC=81n?= Date: Fri, 8 Jan 2016 12:35:23 +0100 Subject: [PATCH 13/21] avoids queries when creating a notification --- app/controllers/comments_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index c30ddbaf2..2ce536c5c 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -65,11 +65,11 @@ class CommentsController < ApplicationController def add_notification(comment) if comment.reply? - author = comment.parent.author + notifiable = comment.parent else - author = comment.commentable.author + notifiable = comment.commentable end - author.notifications.create!(notifiable: comment) unless comment.made_by? author + Notification.create!(user_id: notifiable.author_id, notifiable: comment) unless comment.author_id == notifiable.author_id end end From 790d8de5b6335f787ea9bcf533433431b61e7dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 12:35:56 +0100 Subject: [PATCH 14/21] removes unused method --- app/models/comment.rb | 4 ---- spec/models/comment_spec.rb | 14 -------------- 2 files changed, 18 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 44e16b729..3771af84a 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -90,10 +90,6 @@ class Comment < ActiveRecord::Base !root? end - def made_by?(user) - self.user == user - end - def call_after_commented self.commentable.try(:after_commented) end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 684de6438..73b70fb42 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -129,18 +129,4 @@ describe Comment do end end - describe "#made_by?" do - let(:author) { create :user } - let(:comment) { create :comment, user: author } - - it "returns true if comment was made by user" do - expect(comment.made_by?(author)).to be true - end - - it "returns false if comment was not made by user" do - not_author = create :user - expect(comment.made_by?(not_author)).to be false - end - end - end From a976e16ef345acd744410fbeff9bc1b03f7c021b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 12:36:43 +0100 Subject: [PATCH 15/21] redirects notification show to notifiable --- app/controllers/notifications_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 3820d7e36..a4ec31b50 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -9,7 +9,7 @@ class NotificationsController < ApplicationController def show @notification = current_user.notifications.find(params[:id]) - redirect_to url_for(@notification.notifiable.commentable) + redirect_to url_for(@notification.notifiable) end def mark_all_as_read From 5ef9f4de873cf6d507f58e30e716495e86d2523a Mon Sep 17 00:00:00 2001 From: Alberto Garcia Cabeza Date: Fri, 8 Jan 2016 13:41:18 +0100 Subject: [PATCH 16/21] Improves styles for comments show --- app/assets/stylesheets/layout.scss | 10 +++------- app/views/comments/_comment.html.erb | 8 ++++---- app/views/comments/show.html.erb | 12 ++++++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index d47189d18..ffb2c6c76 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -1582,7 +1582,6 @@ table { font-family: $font-sans; font-size: $small-font-size; line-height: $line-height; - margin: rem-calc(10) $line-height/2 $line-height/4 0; a { color: $text-light; @@ -1603,15 +1602,12 @@ table { .comment-body { margin-left: rem-calc(42); - p { - font-size: $small-font-size; - } - .reply { background: white; border: 1px solid $border; - font-family: $font-sans; - font-size: rem-calc(12); + border-left: 0; + border-right: 0; + font-size: $small-font-size; margin: rem-calc(6) 0; padding: rem-calc(6); diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 44e15362b..c001da74e 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -67,11 +67,11 @@ <%= simple_format text_with_links comment.body %>
      - - <%= render 'comments/votes', comment: comment %> - -
      + + <%= render 'comments/votes', comment: comment %> + + <%= t("comments.comment.responses", count: comment.children.size) %> <% if user_signed_in? %> diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb index b0c3dfb3f..4fcbdead4 100644 --- a/app/views/comments/show.html.erb +++ b/app/views/comments/show.html.erb @@ -1,9 +1,13 @@ -
      - <%= link_to t("comments.show.return_to_commentable") + @comment.commentable.title, - @comment.commentable %> +
      +
      + <%= link_to @comment.commentable, class: "left back" do %> + + <%= t("comments.show.return_to_commentable") + @comment.commentable.title %> + <% end %> +
      -
      +
      <%= render @comment %> From 704a038795d2c9714ac0f6c6aaec308f24578555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 14:32:16 +0100 Subject: [PATCH 17/21] groups notifications --- app/controllers/comments_controller.rb | 2 +- app/helpers/notifications_helper.rb | 2 +- app/models/notification.rb | 14 ++-- app/views/comments/_comment.html.erb | 2 +- .../notifications/_notification.html.erb | 5 +- config/locales/en.yml | 8 +- config/locales/es.yml | 8 +- ...0108114750_add_counter_to_notifications.rb | 5 ++ db/schema.rb | 3 +- spec/factories.rb | 2 +- spec/features/notifications_spec.rb | 84 ++++++++++++++++--- spec/helpers/notifications_helper_spec.rb | 25 +----- spec/models/notification_spec.rb | 10 +-- 13 files changed, 111 insertions(+), 59 deletions(-) create mode 100644 db/migrate/20160108114750_add_counter_to_notifications.rb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 2ce536c5c..b728ba729 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -69,7 +69,7 @@ class CommentsController < ApplicationController else notifiable = comment.commentable end - Notification.create!(user_id: notifiable.author_id, notifiable: comment) unless comment.author_id == notifiable.author_id + Notification.add(notifiable.author_id, notifiable) unless comment.author_id == notifiable.author_id end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 63618440b..281163380 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,6 +1,6 @@ module NotificationsHelper def notification_action(notification) - notification.notifiable.reply? ? "replied_to_your_comment" : "commented_on_your_debate" + notification.notifiable_type == "Comment" ? "replies_to" : "comments_on" end end diff --git a/app/models/notification.rb b/app/models/notification.rb index e22e68ad2..7d3ebd146 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -4,11 +4,7 @@ class Notification < ActiveRecord::Base scope :unread, -> { all } scope :recent, -> { order(id: :desc) } - scope :for_render, -> { includes(notifiable: [:user]) } - - def username - notifiable.user.username - end + scope :for_render, -> { includes(:notifiable) } def timestamp notifiable.created_at @@ -17,4 +13,12 @@ class Notification < ActiveRecord::Base def mark_as_read self.destroy end + + def self.add(user_id, notifiable) + if notification = Notification.find_by(user_id: user_id, notifiable: notifiable) + Notification.increment_counter(:counter, notification.id) + else + Notification.create!(user_id: user_id, notifiable: notifiable) + end + end end \ No newline at end of file diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 9178f0b9b..00d62ed5f 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -71,7 +71,7 @@ <%= render 'comments/votes', comment: comment %> -
      +
      <%= t("comments.comment.responses", count: child_comments_of(comment).size) %> <% if user_signed_in? %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 7315f899d..28def42f7 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,9 +1,8 @@
    • <%= link_to notification do %>

      - <%= notification.username %> - <%= t("notifications.index.#{notification_action(notification)}") %> - <%= notification.notifiable.commentable.title %> + <%= t("notifications.index.#{notification_action(notification)}", count: notification.counter) %> + <%= notification.notifiable.is_a?(Comment) ? notification.notifiable.commentable.title : notification.notifiable.title %>

      <%= l notification.timestamp, format: :datetime %>

      <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index e8d82bcf8..d04cba8fe 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -313,8 +313,12 @@ en: index: mark_all_as_read: "Mark all as read" empty_notifications: "You don't have new notifications." - commented_on_your_debate: "commented on your debate" - replied_to_your_comment: "replied to your comment on" + comments_on: + one: "Someone commented on" + other: "There are %{count} new comments on" + replies_to: + one: "Someone replied to your comment on" + other: "There are %{count} new replies to your comment on" simple_captcha: placeholder: "Enter the text from the image" label: "Enter the text from the image in the box below" diff --git a/config/locales/es.yml b/config/locales/es.yml index 6d6f9a72b..bef5af656 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -313,8 +313,12 @@ es: index: mark_all_as_read: "Marcar todas como leídas" empty_notifications: "No tienes notificaciones nuevas." - commented_on_your_debate: "ha comentado en tu debate" - replied_to_your_comment: "ha respondido a tu comentario en" + comments_on: + one: "Hay un nuevo comentario en" + other: "Hay %{count} comentarios nuevos en" + replies_to: + one: "Hay una respuesta nueva a tu comentario en" + other: "Hay %{count} nuevas respuestas a tu comentario en" simple_captcha: placeholder: "Introduce el texto de la imagen" label: "Introduce el texto de la imagen en la siguiente caja" diff --git a/db/migrate/20160108114750_add_counter_to_notifications.rb b/db/migrate/20160108114750_add_counter_to_notifications.rb new file mode 100644 index 000000000..7cf3c434c --- /dev/null +++ b/db/migrate/20160108114750_add_counter_to_notifications.rb @@ -0,0 +1,5 @@ +class AddCounterToNotifications < ActiveRecord::Migration + def change + add_column :notifications, :counter, :integer, default: 1 + end +end diff --git a/db/schema.rb b/db/schema.rb index 35aa4b68c..1ba98a0a3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160105170113) do +ActiveRecord::Schema.define(version: 20160108114750) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -208,6 +208,7 @@ ActiveRecord::Schema.define(version: 20160105170113) do t.integer "user_id" t.integer "notifiable_id" t.string "notifiable_type" + t.integer "counter", default: 1 end add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree diff --git a/spec/factories.rb b/spec/factories.rb index d48aba942..6e910a214 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -292,7 +292,7 @@ FactoryGirl.define do factory :notification do user - association :notifiable, factory: :comment + association :notifiable, factory: :proposal end end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 22e4b320d..1ffc6a854 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -4,6 +4,7 @@ feature "Notifications" do let(:author) { create :user } let(:user) { create :user } let(:debate) { create :debate, author: author } + let(:proposal) { create :proposal, author: author } scenario "User commented on my debate", :js do login_as user @@ -18,12 +19,44 @@ feature "Notifications" do 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" + find(".icon-notification").click + expect(page).to have_css ".notification", count: 1 - expect(page).to have_content user.username - expect(page).to have_content "commented on your debate" + + expect(page).to have_content "Someone commented on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Multiple comments on my proposal", :js do + login_as user + visit proposal_path proposal + + fill_in "comment-body-proposal_#{proposal.id}", with: "I agree" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I agree" + end + + logout + login_as create(:user) + visit proposal_path proposal + + fill_in "comment-body-proposal_#{proposal.id}", with: "I disagree" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I disagree" + end + + logout + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + + expect(page).to have_content "There are 2 new comments on" expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end @@ -45,12 +78,39 @@ feature "Notifications" do 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 + find(".icon-notification").click + expect(page).to have_css ".notification", count: 1 - expect(page).to have_content user.username - expect(page).to have_content "replied to your comment on" + expect(page).to have_content "Someone replied to your comment on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Multiple replies to my comment", :js do + comment = create :comment, commentable: debate, user: author + 3.times do |n| + login_as create(:user) + visit debate_path debate + + within("#comment_#{comment.id}_reply") { click_link "Reply" } + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "Reply number #{n}" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "Reply number #{n}" + end + logout + end + + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There are 3 new replies to your comment on" expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" end @@ -63,9 +123,8 @@ feature "Notifications" do 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" + find(".icon-no-notification").click expect(page).to have_css ".notification", count: 0 end @@ -83,7 +142,8 @@ feature "Notifications" do 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']" + + find(".icon-no-notification") visit notifications_path expect(page).to have_css ".notification", count: 0 @@ -126,7 +186,7 @@ feature "Notifications" do login_as user visit notifications_path - expect(page).to have_content "There are no new notifications" + expect(page).to have_content "You don't have new notifications" end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index fa378b2ec..ccb3b3a9d 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -5,38 +5,21 @@ describe NotificationsHelper do describe "#notification_action" do let(:debate) { create :debate } let(:debate_comment) { create :comment, commentable: debate } - let(:comment_reply) { create :comment, commentable: debate, parent: debate_comment } context "when action was comment on a debate" do it "returns correct text when someone comments on your debate" do - notification = create :notification, notifiable: debate_comment - expect(notification_action(notification)).to eq "commented_on_your_debate" + notification = create :notification, notifiable: debate + expect(notification_action(notification)).to eq "comments_on" end end context "when action was comment on a debate" do it "returns correct text when someone replies to your comment" do - notification = create :notification, notifiable: comment_reply - expect(notification_action(notification)).to eq "replied_to_your_comment" + notification = create :notification, notifiable: debate_comment + expect(notification_action(notification)).to eq "replies_to" end end end - describe "#notifications_class_for" do - let(:user) { create :user } - - context "when user doesn't have notifications" do - it "returns class 'without_notifications'" do - expect(notifications_class_for(user)).to eq "without_notifications" - end - end - - context "when user has 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 index 0b8354e83..361211a51 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -23,7 +23,7 @@ describe Notification do describe "#for_render (scope)" do it "returns notifications including notifiable and user" do - expect(Notification).to receive(:includes).with(notifiable: [:user]).exactly(:once) + expect(Notification).to receive(:includes).with(:notifiable).exactly(:once) Notification.for_render end end @@ -47,12 +47,4 @@ describe Notification do end end - describe "#username" do - it "returns the username of the activity's author" do - comment = create :comment - notification = create :notification, notifiable: comment - expect(notification.username).to eq comment.author.username - end - end - end From 432e9e0d5bf0e64244c0d6d1c6145bc9bb98b84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 14:39:34 +0100 Subject: [PATCH 18/21] adds counter cache for user's notifications --- app/models/notification.rb | 2 +- app/views/devise/menu/_login_items.html.erb | 4 ++-- ...20160108133501_add_notifications_counter_cache_to_user.rb | 5 +++++ db/schema.rb | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb diff --git a/app/models/notification.rb b/app/models/notification.rb index 7d3ebd146..1cb500ccf 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,5 +1,5 @@ class Notification < ActiveRecord::Base - belongs_to :user + belongs_to :user, counter_cache: true belongs_to :notifiable, polymorphic: true scope :unread, -> { all } diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index d2460d11d..7681619e1 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -2,9 +2,9 @@ <% if user_signed_in? %>
    • <%= link_to notifications_path, class: "notifications" do %> - <% if current_user.notifications.count > 0 %> + <% if current_user.notifications_count > 0 %> - + <% else %> diff --git a/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb b/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb new file mode 100644 index 000000000..e106df6ae --- /dev/null +++ b/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb @@ -0,0 +1,5 @@ +class AddNotificationsCounterCacheToUser < ActiveRecord::Migration + def change + add_column :users, :notifications_count, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ba98a0a3..162bd816e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160108114750) do +ActiveRecord::Schema.define(version: 20160108133501) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -339,6 +339,7 @@ ActiveRecord::Schema.define(version: 20160108114750) do t.datetime "erased_at" t.boolean "public_activity", default: true t.boolean "newsletter", default: false + t.integer "notifications_count", default: 0 end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree From 1c801a73989d77ad31cc85a2f6aaac234128ce91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 14:44:22 +0100 Subject: [PATCH 19/21] adds ignore_unused i18n keys --- config/i18n-tasks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 081619394..6b815cd31 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -127,8 +127,8 @@ ignore_unused: - 'proposals.index.select_order' - 'proposals.index.orders.*' - 'proposals.index.search_form.*' - - 'notifications.index.commented_on_your_debate' - - 'notifications.index.replied_to_your_comment' + - 'notifications.index.comments_on*' + - 'notifications.index.replies_to*' - 'helpers.page_entries_info.*' # kaminari - 'views.pagination.*' # kaminari # - '{devise,kaminari,will_paginate}.*' From fb7550c7de93528793f7006574aafb0bb1b8cf6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 15:12:10 +0100 Subject: [PATCH 20/21] fixes build --- app/views/comments/_comment.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index c001da74e..e8c49636b 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -67,7 +67,7 @@ <%= simple_format text_with_links comment.body %>
    • -
      +
      <%= render 'comments/votes', comment: comment %> From c8ad7a8fb2d1624efa9b25062a9a0c2b40cd03b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juanjo=20Baz=C3=A1n?= Date: Fri, 8 Jan 2016 17:28:27 +0100 Subject: [PATCH 21/21] pluralizes notifications alert --- config/locales/en.yml | 4 +++- config/locales/es.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 97494fd81..c40575d6b 100755 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,7 +29,9 @@ en: more_information: "More information" debates: "Debates" proposals: "Proposals" - new_notifications: "You have %{count} new notifications" + new_notifications: + one: "You have a new notification" + other: "You have %{count} new notifications" no_notifications: "You don't have new notifications" footer: description: "This portal uses the %{consul} which is %{open_source}. From Madrid out into the world." diff --git a/config/locales/es.yml b/config/locales/es.yml index 7fb02c729..37be2e03a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -29,7 +29,9 @@ es: more_information: "Más información" debates: "Debates" proposals: "Propuestas" - new_notifications: "Tienes %{count} notificaciones nuevas" + new_notifications: + one: "Tienes una nueva notificación" + other: "Tienes %{count} notificaciones nuevas" no_notifications: "No tienes notificaciones nuevas" footer: description: "Este portal usa la %{consul} que es %{open_source}. De Madrid, para el mundo entero."