diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 173a7fd36..5a60e6d91 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -18,6 +18,7 @@ class Budget acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases include Relationable + include Notifiable belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :heading diff --git a/app/models/comment.rb b/app/models/comment.rb index ccfc04602..fc0fb35f8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -2,8 +2,9 @@ class Comment < ActiveRecord::Base include Flaggable include HasPublicAuthor include Graphqlable + include Notifiable - COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic Legislation::Proposal Poll).freeze + COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll Topic Legislation::Question Legislation::Annotation Legislation::Proposal).freeze acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb new file mode 100644 index 000000000..f9748c64f --- /dev/null +++ b/app/models/concerns/notifiable.rb @@ -0,0 +1,35 @@ +module Notifiable + extend ActiveSupport::Concern + + def notifiable_title + case self.class.name + when "ProposalNotification" + proposal.title + when "Comment" + commentable.title + else + title + end + end + + def notifiable_available? + case self.class.name + when "ProposalNotification" + check_availability(proposal) + when "Comment" + check_availability(commentable) + else + check_availability(self) + end + end + + def check_availability(resource) + resource.present? && + resource.try(:hidden_at) == nil && + resource.try(:retired_at) == nil + end + + def linkable_resource + is_a?(ProposalNotification) ? proposal : self + end +end diff --git a/app/models/debate.rb b/app/models/debate.rb index e949e8092..188ab2f93 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -10,6 +10,7 @@ class Debate < ActiveRecord::Base include HasPublicAuthor include Graphqlable include Relationable + include Notifiable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/legislation/proposal.rb b/app/models/legislation/proposal.rb index c7196a3f0..3fe6a9d96 100644 --- a/app/models/legislation/proposal.rb +++ b/app/models/legislation/proposal.rb @@ -10,6 +10,7 @@ class Legislation::Proposal < ActiveRecord::Base include Followable include Communitable include Documentable + include Notifiable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, diff --git a/app/models/legislation/question.rb b/app/models/legislation/question.rb index 71ce100ea..2ca5fb39a 100644 --- a/app/models/legislation/question.rb +++ b/app/models/legislation/question.rb @@ -1,6 +1,7 @@ class Legislation::Question < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Notifiable belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id' diff --git a/app/models/notification.rb b/app/models/notification.rb index 84fc72f1c..de0e7c645 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,4 +1,5 @@ class Notification < ActiveRecord::Base + belongs_to :user, counter_cache: true belongs_to :notifiable, polymorphic: true @@ -7,6 +8,9 @@ class Notification < ActiveRecord::Base scope :not_emailed, -> { where(emailed_at: nil) } scope :for_render, -> { includes(:notifiable) } + delegate :notifiable_title, :notifiable_available?, :check_availability, :linkable_resource, + to: :notifiable, allow_nil: true + def timestamp notifiable.created_at end @@ -25,17 +29,6 @@ class Notification < ActiveRecord::Base end end - def notifiable_title - case notifiable.class.name - when "ProposalNotification" - notifiable.proposal.title - when "Comment" - notifiable.commentable.title - else - notifiable.title - end - end - def notifiable_action case notifiable_type when "ProposalNotification" @@ -47,8 +40,4 @@ class Notification < ActiveRecord::Base end end - def linkable_resource - notifiable.is_a?(ProposalNotification) ? notifiable.proposal : notifiable - end - -end +end \ No newline at end of file diff --git a/app/models/poll.rb b/app/models/poll.rb index ffb4e36f4..bf5a5a0a8 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -2,6 +2,7 @@ class Poll < ActiveRecord::Base include Imageable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Notifiable RECOUNT_DURATION = 1.week diff --git a/app/models/proposal.rb b/app/models/proposal.rb index aaf5bea87..7b914ce25 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -12,6 +12,7 @@ class Proposal < ActiveRecord::Base include Communitable include Imageable include Mappable + include Notifiable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, diff --git a/app/models/proposal_notification.rb b/app/models/proposal_notification.rb index bd4211a85..590f19208 100644 --- a/app/models/proposal_notification.rb +++ b/app/models/proposal_notification.rb @@ -1,6 +1,6 @@ class ProposalNotification < ActiveRecord::Base - include Graphqlable + include Notifiable belongs_to :author, class_name: 'User', foreign_key: 'author_id' belongs_to :proposal @@ -21,4 +21,8 @@ class ProposalNotification < ActiveRecord::Base end end + def notifiable + proposal + end + end diff --git a/app/models/topic.rb b/app/models/topic.rb index e0a73d58b..11c921dd4 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,6 +1,7 @@ class Topic < ActiveRecord::Base acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Notifiable belongs_to :community belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb index 9d9767e73..80cd7e2da 100644 --- a/app/views/comments/_form.html.erb +++ b/app/views/comments/_form.html.erb @@ -8,7 +8,7 @@ <%= f.hidden_field :commentable_id, value: commentable.id %> <%= f.hidden_field :parent_id, value: parent_id %> - <%= f.submit comment_button_text(parent_id, commentable), class: "button" %> + <%= f.submit comment_button_text(parent_id, commentable), class: "button", id: "publish_comment" %> <% if can? :comment_as_moderator, commentable %>
diff --git a/app/views/devise/menu/_login_items.html.erb b/app/views/devise/menu/_login_items.html.erb index 186597bc6..f74aede44 100644 --- a/app/views/devise/menu/_login_items.html.erb +++ b/app/views/devise/menu/_login_items.html.erb @@ -1,5 +1,5 @@ <% if user_signed_in? %> -
  • +
  • <%= link_to notifications_path, rel: "nofollow", class: "notifications" do %> <%= t("layouts.header.notifications") %> <% if current_user.notifications_count > 0 %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 29bf00823..a5c69550c 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -1,5 +1,5 @@
  • - <% if notification.notifiable.present? %> + <% if notification.notifiable_available? %> <%= link_to notification do %>

    diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index fa7749a4b..879fcf694 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -3,6 +3,10 @@ require 'sessions_helper' feature 'Budget Investments' do + context "Concerns" do + it_behaves_like 'notifiable in-app', Budget::Investment + end + let(:author) { create(:user, :level_two, username: 'Isabel') } let(:budget) { create(:budget, name: "Big Budget") } let(:other_budget) { create(:budget, name: "What a Budget!") } diff --git a/spec/features/comments/legislation_questions_spec.rb b/spec/features/comments/legislation_questions_spec.rb index d21cefc4e..0c9c868ed 100644 --- a/spec/features/comments/legislation_questions_spec.rb +++ b/spec/features/comments/legislation_questions_spec.rb @@ -2,6 +2,11 @@ require 'rails_helper' include ActionView::Helpers::DateHelper feature 'Commenting legislation questions' do + + context "Concerns" do + it_behaves_like 'notifiable in-app', Legislation::Question + end + let(:user) { create :user, :level_two } let(:process) { create :legislation_process, :in_debate_phase } let(:legislation_question) { create :legislation_question, process: process } diff --git a/spec/features/debates_spec.rb b/spec/features/debates_spec.rb index 39cd84760..0fff1aaa3 100644 --- a/spec/features/debates_spec.rb +++ b/spec/features/debates_spec.rb @@ -9,6 +9,10 @@ feature 'Debates' do Setting['feature.debates'] = true end + context "Concerns" do + it_behaves_like 'notifiable in-app', Debate + end + scenario 'Index' do debates = [create(:debate), create(:debate), create(:debate)] @@ -1047,4 +1051,5 @@ feature 'Debates' do expect(page).to_not have_content("Featured") end end + end diff --git a/spec/features/legislation/proposals_spec.rb b/spec/features/legislation/proposals_spec.rb new file mode 100644 index 000000000..79eb9c386 --- /dev/null +++ b/spec/features/legislation/proposals_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +feature 'Legislation Proposals' do + + context "Concerns" do + it_behaves_like 'notifiable in-app', Legislation::Proposal + end + +end diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index 01c6ec934..42f3399c5 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -1,309 +1,12 @@ require 'rails_helper' feature "Notifications" do - let(:admin_user) { create :user } - let(:administrator) do - create(:administrator, user: admin_user) - admin_user - end - let(:author) { create :user } + let(:user) { create :user } - let(:debate) { create :debate, author: author } - let(:proposal) { create :proposal, author: author } - let(:process) { create :legislation_process, :in_debate_phase } - let(:legislation_question) { create(:legislation_question, process: process, author: administrator) } - let(:legislation_annotation) { create(:legislation_annotation, author: author) } - - let(:topic) do - proposal = create(:proposal) - community = proposal.community - create(:topic, community: community, author: author) - end - - scenario "User commented on my debate", :js do - create(:notification, notifiable: debate, user: author) - login_as author - visit root_path - - find(".icon-notification").click - - expect(page).to have_css ".notification", count: 1 - - expect(page).to have_content "Someone commented on" - expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" - end - - scenario "User commented on my legislation question", :js do - create(:notification, notifiable: legislation_question, user: administrator) - login_as administrator - visit root_path - - find(".icon-notification").click - - expect(page).to have_css ".notification", count: 1 - - expect(page).to have_content "Someone commented on" - expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" - end - - scenario "User commented on my topic", :js do - create(:notification, notifiable: topic, user: author) - login_as author - visit root_path - - find(".icon-notification").click - - expect(page).to have_css ".notification", count: 1 - - 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 - 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 - - 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 - visit root_path - - find(".icon-notification").click - - expect(page).to have_css ".notification", count: 1 - 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 - 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 - - 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 - - find(".icon-no-notification").click - expect(page).to have_css ".notification", count: 0 - 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 - - find(".icon-no-notification") - - visit notifications_path - expect(page).to have_css ".notification", count: 0 - end - - context "Proposal notification" do - - scenario "Voters should receive a notification", :js do - author = create(:user) - - user1 = create(:user) - user2 = create(:user) - user3 = create(:user) - - proposal = create(:proposal, author: author) - - create(:vote, voter: user1, votable: proposal, vote_flag: true) - create(:vote, voter: user2, votable: proposal, vote_flag: true) - - login_as(author) - visit root_path - - visit new_proposal_notification_path(proposal_id: proposal.id) - - fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" - fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" - click_button "Send message" - - expect(page).to have_content "Your message has been sent correctly." - - logout - login_as user1 - visit root_path - visit root_path - - find(".icon-notification").click - - notification_for_user1 = Notification.where(user: user1).first - expect(page).to have_css ".notification", count: 1 - expect(page).to have_content "There is one new notification on #{proposal.title}" - expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']" - - logout - login_as user2 - visit root_path - visit root_path - - find(".icon-notification").click - - notification_for_user2 = Notification.where(user: user2).first - expect(page).to have_css ".notification", count: 1 - expect(page).to have_content "There is one new notification on #{proposal.title}" - expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']" - - logout - login_as user3 - visit root_path - visit root_path - - find(".icon-no-notification").click - - expect(page).to have_css ".notification", count: 0 - end - - scenario "Followers should receive a notification", :js do - author = create(:user) - - user1 = create(:user) - user2 = create(:user) - user3 = create(:user) - - proposal = create(:proposal, author: author) - - create(:follow, :followed_proposal, user: user1, followable: proposal) - create(:follow, :followed_proposal, user: user2, followable: proposal) - - login_as author.reload - visit root_path - - visit new_proposal_notification_path(proposal_id: proposal.id) - - fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" - fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" - click_button "Send message" - - expect(page).to have_content "Your message has been sent correctly." - - logout - login_as user1.reload - visit root_path - - find(".icon-notification").click - - notification_for_user1 = Notification.where(user: user1).first - expect(page).to have_css ".notification", count: 1 - expect(page).to have_content "There is one new notification on #{proposal.title}" - expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']" - - logout - login_as user2.reload - visit root_path - - find(".icon-notification").click - - notification_for_user2 = Notification.where(user: user2).first - expect(page).to have_css ".notification", count: 1 - expect(page).to have_content "There is one new notification on #{proposal.title}" - expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']" - - logout - login_as user3.reload - visit root_path - - find(".icon-no-notification").click - - expect(page).to have_css ".notification", count: 0 - end - - pending "group notifications for the same proposal" - 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 @@ -318,7 +21,6 @@ feature "Notifications" do end scenario "mark all notifications as read" do - user = create :user 2.times { create :notification, user: user } login_as user @@ -333,19 +35,6 @@ feature "Notifications" do end - scenario "Notifiable hidden", :js do - create(:notification, notifiable: debate, user: author) - debate.hide - - login_as author - visit root_path - find(".icon-notification").click - - expect(page).to have_css ".notification", count: 1 - expect(page).to have_content "This resource is not available anymore" - expect(page).to_not have_xpath "//a[@href='#{notification_path(Notification.last)}']" - end - scenario "no notifications" do login_as user visit notifications_path diff --git a/spec/features/polls/polls_spec.rb b/spec/features/polls/polls_spec.rb index 8629a1087..791bc3167 100644 --- a/spec/features/polls/polls_spec.rb +++ b/spec/features/polls/polls_spec.rb @@ -2,6 +2,10 @@ require 'rails_helper' feature 'Polls' do + context "Concerns" do + it_behaves_like 'notifiable in-app', Poll + end + context '#index' do scenario 'Polls can be listed' do diff --git a/spec/features/proposal_notifications_spec.rb b/spec/features/proposal_notifications_spec.rb index 1e93f7e13..5d7ad284c 100644 --- a/spec/features/proposal_notifications_spec.rb +++ b/spec/features/proposal_notifications_spec.rb @@ -195,6 +195,170 @@ feature 'Proposal Notifications' do end + context "In-app notifications from the proposal's author" do + + scenario "Voters should receive a notification", :js do + author = create(:user) + + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + + proposal = create(:proposal, author: author) + + create(:vote, voter: user1, votable: proposal, vote_flag: true) + create(:vote, voter: user2, votable: proposal, vote_flag: true) + + login_as(author) + visit root_path + + visit new_proposal_notification_path(proposal_id: proposal.id) + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + + logout + login_as user1 + visit root_path + visit root_path + + find(".icon-notification").click + + notification_for_user1 = Notification.where(user: user1).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']" + + logout + login_as user2 + visit root_path + visit root_path + + find(".icon-notification").click + + notification_for_user2 = Notification.where(user: user2).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']" + + logout + login_as user3 + visit root_path + visit root_path + + find(".icon-no-notification").click + + expect(page).to have_css ".notification", count: 0 + end + + scenario "Followers should receive a notification", :js do + author = create(:user) + + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + + proposal = create(:proposal, author: author) + + create(:follow, :followed_proposal, user: user1, followable: proposal) + create(:follow, :followed_proposal, user: user2, followable: proposal) + + login_as author.reload + visit root_path + + visit new_proposal_notification_path(proposal_id: proposal.id) + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + + logout + login_as user1.reload + visit root_path + + find(".icon-notification").click + + notification_for_user1 = Notification.where(user: user1).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user1)}']" + + logout + login_as user2.reload + visit root_path + + find(".icon-notification").click + + notification_for_user2 = Notification.where(user: user2).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There is one new notification on #{proposal.title}" + expect(page).to have_xpath "//a[@href='#{notification_path(notification_for_user2)}']" + + logout + login_as user3.reload + visit root_path + + find(".icon-no-notification").click + + expect(page).to have_css ".notification", count: 0 + end + + scenario "Proposal hidden", :js do + author = create(:user) + user = create(:user) + + proposal = create(:proposal, author: author) + + create(:vote, voter: user, votable: proposal, vote_flag: true) + + login_as(author) + visit root_path + + visit new_proposal_notification_path(proposal_id: proposal.id) + + fill_in 'proposal_notification_title', with: "Thank you for supporting my proposal" + fill_in 'proposal_notification_body', with: "Please share it with others so we can make it happen!" + click_button "Send message" + + expect(page).to have_content "Your message has been sent correctly." + + proposal.hide + + logout + login_as user + visit root_path + visit root_path + + find(".icon-notification").click + + notification_for_user = Notification.where(user: user).first + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "This resource is not available anymore" + expect(page).to_not have_xpath "//a[@href='#{notification_path(notification_for_user)}']" + end + + scenario "Proposal retired by author", :js do + author = create(:user) + user = create(:user) + + proposal = create(:proposal, author: author) + + create(:vote, voter: user, votable: proposal, vote_flag: true) + + login_as(author) + visit root_path + + visit new_proposal_notification_path(proposal_id: proposal.id) + end + + pending "group notifications for the same proposal" + end + scenario "Error messages" do author = create(:user) proposal = create(:proposal, author: author) diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb index 886459c38..4bd29a7cd 100644 --- a/spec/features/proposals_spec.rb +++ b/spec/features/proposals_spec.rb @@ -9,6 +9,10 @@ feature 'Proposals' do Setting['feature.proposals'] = true end + context "Concerns" do + it_behaves_like 'notifiable in-app', Proposal + end + context 'Index' do scenario 'Lists featured and regular proposals' do featured_proposals = create_featured_proposals diff --git a/spec/features/topics_specs.rb b/spec/features/topics_specs.rb index 0ba674844..4fd73a4a2 100644 --- a/spec/features/topics_specs.rb +++ b/spec/features/topics_specs.rb @@ -2,6 +2,10 @@ require 'rails_helper' feature 'Topics' do + context "Concerns" do + it_behaves_like 'notifiable in-app', Topic + end + context 'New' do scenario 'Should display disabled button to new topic page without user logged', :js do diff --git a/spec/models/budget/investment_spec.rb b/spec/models/budget/investment_spec.rb index f11c4916b..c10a51418 100644 --- a/spec/models/budget/investment_spec.rb +++ b/spec/models/budget/investment_spec.rb @@ -3,6 +3,10 @@ require 'rails_helper' describe Budget::Investment do let(:investment) { build(:budget_investment) } + describe "Concerns" do + it_behaves_like "notifiable" + end + it "should be valid" do expect(investment).to be_valid end diff --git a/spec/models/debate_spec.rb b/spec/models/debate_spec.rb index 3e1278bd7..a9f47f5a5 100644 --- a/spec/models/debate_spec.rb +++ b/spec/models/debate_spec.rb @@ -4,7 +4,10 @@ require 'rails_helper' describe Debate do let(:debate) { build(:debate) } - it_behaves_like "has_public_author" + describe "Concerns" do + it_behaves_like "has_public_author" + it_behaves_like "notifiable" + end it "should be valid" do expect(debate).to be_valid @@ -763,4 +766,5 @@ describe Debate do end end + end diff --git a/spec/models/legislation/process_spec.rb b/spec/models/legislation/process_spec.rb index 33460832b..5783df2fe 100644 --- a/spec/models/legislation/process_spec.rb +++ b/spec/models/legislation/process_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Legislation::Process, type: :model do +describe Legislation::Process do let(:process) { create(:legislation_process) } it "should be valid" do @@ -115,4 +115,5 @@ RSpec.describe Legislation::Process, type: :model do expect(process.status).to eq(:open) end end + end diff --git a/spec/models/legislation/question_spec.rb b/spec/models/legislation/question_spec.rb index 216b9cbfd..81d789445 100644 --- a/spec/models/legislation/question_spec.rb +++ b/spec/models/legislation/question_spec.rb @@ -1,8 +1,12 @@ require 'rails_helper' -RSpec.describe Legislation::Question, type: :model do +describe Legislation::Question do let(:question) { create(:legislation_question) } + describe "Concerns" do + it_behaves_like "notifiable" + end + it "should be valid" do expect(question).to be_valid end @@ -59,4 +63,9 @@ RSpec.describe Legislation::Question, type: :model do expect(question2.first_question_id).to eq(question1.id) end end + + describe "notifications" do + it_behaves_like 'notifiable' + end + end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 12d315421..0ffa39ae6 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -48,52 +48,21 @@ describe Notification do end describe "#notification_action" do - it "returns correct text when someone comments on your commentable" do - debate = create(:debate) - notification = create(:notification, notifiable: debate) + let(:notifiable) { create(:proposal) } + + it "returns correct action when someone comments on your commentable" do + notification = create(:notification, notifiable: notifiable) expect(notification.notifiable_action).to eq "comments_on" end - it "returns correct text when someone replies to your comment" do - debate = create(:debate) - debate_comment = create(:comment, commentable: debate) - notification = create(:notification, notifiable: debate_comment) + it "returns correct action when someone replies to your comment" do + comment = create(:comment, commentable: notifiable) + notification = create(:notification, notifiable: comment) expect(notification.notifiable_action).to eq "replies_to" end - it "returns correct text when the author created a proposal notification" do - proposal_notification = create(:proposal_notification) - notification = create(:notification, notifiable: proposal_notification) - - expect(notification.notifiable_action).to eq "proposal_notification" - end - end - - describe "#notification_title" do - it "returns the commentable title when it's a root comment" do - debate = create(:debate, title: "Save the whales") - notification = create(:notification, notifiable: debate) - - expect(notification.notifiable_title).to eq "Save the whales" - end - - it "returns the commentable title when it's a reply to a root comment" do - debate = create(:debate, title: "Save the whales") - debate_comment = create(:comment, commentable: debate) - notification = create(:notification, notifiable: debate_comment) - - expect(notification.notifiable_title).to eq "Save the whales" - end - - it "returns the commentable title when it's an author's proposals notification" do - proposal = create(:proposal, title: "Save the whales") - proposal_notification = create(:proposal_notification, proposal: proposal) - notification = create(:notification, notifiable: proposal_notification) - - expect(notification.notifiable_title).to eq "Save the whales" - end end end diff --git a/spec/models/poll/poll_spec.rb b/spec/models/poll/poll_spec.rb index bfb5881f2..d2e00ef18 100644 --- a/spec/models/poll/poll_spec.rb +++ b/spec/models/poll/poll_spec.rb @@ -1,9 +1,13 @@ require 'rails_helper' -describe :poll do +describe Poll do let(:poll) { build(:poll) } + describe "Concerns" do + it_behaves_like "notifiable" + end + describe "validations" do it "should be valid" do expect(poll).to be_valid @@ -199,4 +203,5 @@ describe :poll do end end + end diff --git a/spec/models/proposal_notification_spec.rb b/spec/models/proposal_notification_spec.rb index 88a98ebee..704555bdc 100644 --- a/spec/models/proposal_notification_spec.rb +++ b/spec/models/proposal_notification_spec.rb @@ -78,4 +78,79 @@ describe ProposalNotification do end + describe "notifications in-app" do + + let(:notifiable) { create(model_name(described_class)) } + let(:proposal) { notifiable.proposal } + + describe "#notification_title" do + + it "returns the proposal title" do + notification = create(:notification, notifiable: notifiable) + + expect(notification.notifiable_title).to eq notifiable.proposal.title + end + + end + + describe "#notification_action" do + + it "returns the correct action" do + notification = create(:notification, notifiable: notifiable) + + expect(notification.notifiable_action).to eq "proposal_notification" + end + + end + + describe "notifiable_available?" do + + it "returns true when the proposal is available" do + notification = create(:notification, notifiable: notifiable) + + expect(notification.notifiable_available?).to be(true) + end + + it "returns false when the proposal is not available" do + notification = create(:notification, notifiable: notifiable) + + notifiable.proposal.destroy + + expect(notification.notifiable_available?).to be(false) + end + + end + + describe "check_availability" do + + it "returns true if the resource is present, not hidden, nor retired" do + notification = create(:notification, notifiable: notifiable) + + expect(notification.check_availability(proposal)).to be(true) + end + + it "returns false if the resource is not present" do + notification = create(:notification, notifiable: notifiable) + + notifiable.proposal.really_destroy! + expect(notification.check_availability(proposal)).to be(false) + end + + it "returns false if the resource is hidden" do + notification = create(:notification, notifiable: notifiable) + + notifiable.proposal.hide + expect(notification.check_availability(proposal)).to be(false) + end + + it "returns false if the resource is retired" do + notification = create(:notification, notifiable: notifiable) + + notifiable.proposal.update(retired_at: Time.now) + expect(notification.check_availability(proposal)).to be(false) + end + + end + + end end diff --git a/spec/models/proposal_spec.rb b/spec/models/proposal_spec.rb index 29338a62f..f1da4c673 100644 --- a/spec/models/proposal_spec.rb +++ b/spec/models/proposal_spec.rb @@ -4,7 +4,10 @@ require 'rails_helper' describe Proposal do let(:proposal) { build(:proposal) } - it_behaves_like "has_public_author" + describe "Concerns" do + it_behaves_like "has_public_author" + it_behaves_like "notifiable" + end it "should be valid" do expect(proposal).to be_valid @@ -950,4 +953,5 @@ describe Proposal do end end + end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 5071439fa..bbe21d920 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -3,6 +3,10 @@ require 'rails_helper' describe Topic do let(:topic) { build(:topic) } + describe "Concerns" do + it_behaves_like "notifiable" + end + it "should be valid" do expect(topic).to be_valid end @@ -71,4 +75,7 @@ describe Topic do end + describe "notifications" do + it_behaves_like 'notifiable' + end end diff --git a/spec/shared/features/notifiable_in_app.rb b/spec/shared/features/notifiable_in_app.rb new file mode 100644 index 000000000..64e1e9691 --- /dev/null +++ b/spec/shared/features/notifiable_in_app.rb @@ -0,0 +1,137 @@ +shared_examples "notifiable in-app" do |described_class| + + let(:author) { create(:user, :verified) } + let!(:notifiable) { create(model_name(described_class), author: author) } + + scenario "A user commented on my notifiable", :js do + notification = create(:notification, notifiable: notifiable, user: author) + + login_as author + visit root_path + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "Someone commented on" + expect(page).to have_xpath "//a[@href='#{notification_path(notification)}']" + end + + scenario "Multiple users commented on my notifiable", :js do + 3.times do + login_as(create(:user, :verified)) + + visit path_for(notifiable) + + fill_in comment_body(notifiable), with: "I agree" + click_button "publish_comment" + within "#comments" do + expect(page).to have_content "I agree" + end + end + + logout + login_as author + visit root_path + visit root_path + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There are 3 new comments on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "A user replied to my comment", :js do + comment = create :comment, commentable: notifiable, user: author + + login_as(create(:user, :verified)) + visit path_for(notifiable) + + 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 + visit root_path + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + 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: notifiable, user: author + + 3.times do |n| + login_as(create(:user, :verified)) + visit path_for(notifiable) + + 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 + 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 + + scenario "Author commented on his own notifiable", :js do + login_as(author) + visit path_for(notifiable) + + fill_in comment_body(notifiable), with: "I commented on my own notifiable" + click_button "publish_comment" + within "#comments" do + expect(page).to have_content "I commented on my own notifiable" + end + + within("#notifications") do + find(".icon-no-notification").click + expect(page).to have_css ".notification", count: 0 + end + end + + scenario "Author replied to his own comment", :js do + comment = create :comment, commentable: notifiable, user: author + + login_as author + visit path_for(notifiable) + + 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 + + within("#notifications") do + find(".icon-no-notification").click + expect(page).to have_css ".notification", count: 0 + end + + end + +end \ No newline at end of file diff --git a/spec/shared/models/notifiable.rb b/spec/shared/models/notifiable.rb new file mode 100644 index 000000000..4c09a01ce --- /dev/null +++ b/spec/shared/models/notifiable.rb @@ -0,0 +1,88 @@ +shared_examples "notifiable" do + + let(:notifiable) { create(model_name(described_class)) } + + describe "#notification_title" do + + it "returns the notifiable title when it's a root comment" do + notification = create(:notification, notifiable: notifiable) + + expect(notification.notifiable_title).to eq notifiable.title + end + + it "returns the notifiable title when it's a reply to a root comment" do + comment = create(:comment, commentable: notifiable) + notification = create(:notification, notifiable: comment) + + expect(notification.notifiable_title).to eq notifiable.title + end + + end + + describe "notifiable_available?" do + + it "returns true when it's a root comment and the notifiable is available" do + notification = create(:notification, notifiable: notifiable) + + expect(notification.notifiable_available?).to be(true) + end + + it "returns true when it's a reply to comment and the notifiable is available" do + comment = create(:comment, commentable: notifiable) + notification = create(:notification, notifiable: comment) + + expect(notification.notifiable_available?).to be(true) + end + + it "returns false when it's a root comment and the notifiable has been hidden" do + notification = create(:notification, notifiable: notifiable) + + notifiable.hide + notification.reload + + expect(notification.notifiable_available?).to_not be(true) + end + + it "returns false when it's a reply to comment and the commentable has been hidden" do + comment = create(:comment, commentable: notifiable) + notification = create(:notification, notifiable: comment) + + notifiable.hide + notification.reload + + expect(notification.notifiable_available?).to be(false) + end + + end + + describe "check_availability" do + + it "returns true if the resource is present, not hidden, nor retired" do + notification = create(:notification, notifiable: notifiable) + expect(notification.check_availability(notifiable)).to be(true) + end + + it "returns false if the resource is not present" do + notification = create(:notification, notifiable: notifiable) + notifiable.really_destroy! + expect(notification.check_availability(notifiable)).to be(false) + end + + it "returns false if the resource is not hidden" do + notification = create(:notification, notifiable: notifiable) + notifiable.hide + expect(notification.check_availability(notifiable)).to be(false) + end + + it "returns false if the resource is retired" do + notification = create(:notification, notifiable: notifiable) + + if notifiable.respond_to?(:retired_at) + notifiable.update(retired_at: Time.now) + expect(notification.check_availability(notifiable)).to be(false) + end + end + + end + +end \ No newline at end of file diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index 4b2134a02..b83385a6d 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -330,4 +330,31 @@ module CommonActions expect(Poll::Voter.count).to eq(1) end + def model_name(described_class) + return :proposal_notification if described_class == ProposalNotification + + described_class.name.gsub("::", "_").downcase.to_sym + end + + def comment_body(resource) + "comment-body-#{resource.class.name.gsub("::", "_").downcase.to_sym}_#{resource.id}" + end + + def path_for(resource) + nested_path_for(resource) || url_for([resource, only_path: true]) + end + + def nested_path_for(resource) + case resource.class.name + when "Legislation::Question" + legislation_process_question_path(resource.process, resource) + when "Legislation::Proposal" + legislation_process_proposal_path(resource.process, resource) + when "Budget::Investment" + budget_investment_path(resource.budget, resource) + else + false + end + end + end