WIP Refactor/notifications (#1131)

* Add new columns to notifications table

* Remove stream from notifications and add new methods

* Turn off moderation comment notifications

* Add notifiable_type index to migration

* Stop creation of Stream-based notification model instances

* Add notification model create hooks

* Add badge achievement notification creation hook

* Fix some specs

* Add missing @ symbol

* Fix for tests and rename a few things

* forgot to uncomment code sigh
This commit is contained in:
Andy Zhao 2018-11-19 16:09:02 -05:00 committed by Ben Halpern
parent faf1c6719b
commit 2db2dd1e5a
25 changed files with 686 additions and 213 deletions

View file

@ -15,6 +15,7 @@ module Api
reactable_type: params[:reactable_type],
category: params[:category] || "like",
)
Notification.send_reaction_notification(@reaction) if @reaction.reactable.user_id != current_user.id
render json: { reaction: @reaction.to_json }
end

View file

@ -112,10 +112,10 @@ class ArticlesController < ApplicationController
handle_org_assignment
handle_hiring_tag
if @article.published
Notification.send_all(@article, "Published") if @article.previous_changes.include?("published")
Notification.send_to_followers(@article, @user.followers, "Published") if @article.saved_changes["published_at"]&.include?(nil)
path = @article.path
else
Notification.remove_all(@article, "Published")
Notification.remove_all(id: @article.id, class_name: "Article", action: "Published")
path = "/#{@article.username}/#{@article.slug}?preview=#{@article.password}"
end
redirect_to (params[:destination] || path)
@ -132,6 +132,7 @@ class ArticlesController < ApplicationController
def destroy
authorize @article
@article.destroy!
Notification.remove_all(id: @article.id, class_name: "Article", action: "Published")
respond_to do |format|
format.html { redirect_to "/dashboard", notice: "Article was successfully deleted." }
format.json { head :no_content }

View file

@ -63,6 +63,7 @@ class CommentsController < ApplicationController
current_user.update(checked_code_of_conduct: true)
end
Mention.create_all(@comment)
Notification.send_new_comment_notifications(@comment)
if @comment.invalid?
@comment.destroy
render json: { status: "comment already exists" }
@ -115,6 +116,7 @@ class CommentsController < ApplicationController
def destroy
authorize @comment
@commentable_path = @comment.commentable.path
Notification.remove_all(id: @comment.id, class_name: "Comment")
if @comment.is_childless?
@comment.destroy
else

View file

@ -24,10 +24,12 @@ class FollowsController < ApplicationController
User.find(params[:followable_id])
end
@result = if params[:verb] == "unfollow"
current_user.stop_following(followable)
follow = current_user.stop_following(followable)
Notification.remove_all(id: follow.id, class_name: "Follow")
"unfollowed"
else
current_user.follow(followable)
follow = current_user.follow(followable)
Notification.send_new_follower_notification(follow)
"followed"
end
current_user.save

View file

@ -41,26 +41,29 @@ class ReactionsController < ApplicationController
def create
authorize Reaction
Rails.cache.delete "count_for_reactable-#{params[:reactable_type]}-#{params[:reactable_id]}"
category = params[:category] || "like"
reaction = Reaction.where(
user_id: current_user.id,
reactable_id: params[:reactable_id],
reactable_type: params[:reactable_type],
category: params[:category] || "like",
category: category,
).first
if reaction
reaction.user.touch
reaction.destroy
Notification.remove_all(id: reaction.id, class_name: "Reaction", action: category) unless reaction.reactable.user_id == current_user.id
@result = "destroy"
else
Reaction.create!(
reaction = Reaction.create!(
user_id: current_user.id,
reactable_id: params[:reactable_id],
reactable_type: params[:reactable_type],
category: params[:category] || "like",
category: category,
)
Notification.send_reaction_notification(reaction) unless reaction.reactable.user_id == current_user.id
@result = "create"
end
render json: { result: @result, category: params[:category] || "like" }
render json: { result: @result, category: category }
end
def cached_user_positive_reactions(user)

View file

@ -16,7 +16,7 @@ class Article < ApplicationRecord
has_many :comments, as: :commentable
has_many :buffer_updates
has_many :reactions, as: :reactable, dependent: :destroy
has_many :notifications, as: :notifiable
has_many :notifications, as: :notifiable
validates :slug, presence: { if: :published? }, format: /\A[0-9a-z-]*\z/,
uniqueness: { scope: :user_id }
@ -52,6 +52,7 @@ class Article < ApplicationRecord
after_save :bust_cache
after_save :update_main_image_background_hex
after_save :detect_human_language
after_update :update_notifications, if: Proc.new { |article| article.notifications.length.positive? && !article.saved_changes.empty? }
# after_save :send_to_moderator
# turned off for now
before_destroy :before_destroy_actions
@ -379,6 +380,10 @@ class Article < ApplicationRecord
private
def update_notifications
Notification.update_notifications(self, "Published")
end
# def send_to_moderator
# ModerationService.new.send_moderation_notification(self) if published
# turned off for now

View file

@ -10,6 +10,7 @@ class BadgeAchievement < ApplicationRecord
include StreamRails::Activity
as_activity
after_create :notify_recipient
after_create :send_email_notification
before_validation :render_rewarding_context_message_html
@ -54,6 +55,10 @@ class BadgeAchievement < ApplicationRecord
private
def notify_recipient
Notification.send_new_badge_notification(self)
end
def send_email_notification
if user.class.name == "User" && user.email.present? && user.email_badge_notifications
NotifyMailer.new_badge_email(self).deliver

View file

@ -4,28 +4,6 @@ class Broadcast < ApplicationRecord
validates :title, :type_of, :processed_html, presence: true
validates :type_of, inclusion: { in: %w(Announcement Onboarding) }
def self.send_welcome_notification(user_id)
welcome_broadcast = Broadcast.find_by(title: "Welcome Notification")
return if welcome_broadcast == nil
Notification.create(
user_id: user_id,
notifiable_id: welcome_broadcast.id,
notifiable_type: "Broadcast",
action: welcome_broadcast.type_of,
)
end
# method not in use; will be in use if we choose to use markdown
def evaluate_markdown
return if body_markdown.blank?
begin
parsed_markdown = MarkdownParser.new(body_markdown)
self.processed_html = get_inner_body(parsed_markdown.finalize)
rescue StandardError => e
errors[:base] << ErrorMessageCleaner.new(e.message).clean
end
end
def get_inner_body(content)
Nokogiri::HTML(content).at("body").inner_html
end

View file

@ -26,6 +26,8 @@ class Comment < ApplicationRecord
after_create :send_to_moderator
before_save :set_markdown_character_count
before_create :adjust_comment_parent_based_on_depth
after_update :update_notifications, if: Proc.new { |comment| comment.saved_changes.include? "body_markdown" }
after_update :remove_notifications, if: :deleted
before_validation :evaluate_markdown
validate :permissions
@ -152,7 +154,15 @@ class Comment < ApplicationRecord
id.to_s(26)
end
# notifications
def custom_css
MarkdownParser.new(body_markdown).tags_used.map do |tag|
Rails.application.assets["ltags/#{tag}.css"].to_s
end.join
end
def title
ActionController::Base.helpers.truncate(ActionController::Base.helpers.strip_tags(processed_html), length: 60)
end
def activity_notify
user_ids = ancestors.map(&:user_id).to_set
@ -160,12 +170,6 @@ class Comment < ApplicationRecord
user_ids.delete(user_id).map { |id| StreamNotifier.new(id).notify }
end
def custom_css
MarkdownParser.new(body_markdown).tags_used.map do |tag|
Rails.application.assets["ltags/#{tag}.css"].to_s
end.join
end
def activity_object
self
end
@ -189,10 +193,6 @@ class Comment < ApplicationRecord
end
end
def title
ActionController::Base.helpers.truncate(ActionController::Base.helpers.strip_tags(processed_html), length: 60)
end
def video
nil
end
@ -235,9 +235,17 @@ class Comment < ApplicationRecord
private
def update_notifications
Notification.update_notifications(self)
end
def remove_notifications
Notification.remove_all(id: id, class_name: "Comment")
end
def send_to_moderator
return if user && user.comments_count > 10
ModerationService.new.send_moderation_notification(self)
Notification.send_moderation_notification(self)
end
def evaluate_markdown

View file

@ -23,7 +23,7 @@ class Mention < ApplicationRecord
mentions = []
doc.css(".comment-mentioned-user").each do |link|
username = link.text.gsub("@", "").downcase
if user = User.find_by_username(link.text.gsub("@", "").downcase)
if user = User.find_by_username(username)
usernames << username
mentions << create_mention(user)
end
@ -36,12 +36,16 @@ class Mention < ApplicationRecord
private
def delete_removed_mentions(usernames)
users = User.where(username: usernames)
@notifiable.mentions.where.not(user_id: users.pluck(:id)).destroy_all
user_ids = User.where(username: usernames).pluck(:id)
mentions = @notifiable.mentions.where.not(user_id: user_ids).destroy_all
Notification.remove_each(mentions) unless mentions.blank?
end
def create_mention(user)
Mention.create(user_id: user.id, mentionable_id: @notifiable.id, mentionable_type: @notifiable.class.name)
mention = Mention.create(user_id: user.id, mentionable_id: @notifiable.id, mentionable_type: @notifiable.class.name)
# mentionable_type = model that created the mention, user = user to be mentioned
Notification.send_mention_notification(mention)
mention
end
end

View file

@ -2,77 +2,245 @@ class Notification < ApplicationRecord
belongs_to :notifiable, polymorphic: true
belongs_to :user
include StreamRails::Activity
as_activity
validates :user_id, presence: true,
uniqueness: { scope: %i[notifiable_id
notifiable_type
action] }
validates :user_id, uniqueness: { scope: %i[notifiable_id notifiable_type action] }
class << self
def send_all(notifiable, action)
if notifiable.class.name == "Article"
return if notifiable.created_at < Time.new(2017, 0o7, 0o7, 0o0, 0o0, 0o0, "+00:00")
notifiable.user.followers.each do |follower|
Notification.create(
user_id: follower.id,
notifiable_id: notifiable.id,
notifiable_type: "Article",
action: action,
)
end
elsif notifiable.class.name == "Broadcast"
if action == "Announcement"
User.all.each do |user|
Notification.create!(
user_id: user.id,
notifiable_id: notifiable.id,
notifiable_type: "Broadcast",
action: action,
)
end
end
def send_new_follower_notification(follow)
json_data = { user: user_data(follow.follower) }
Notification.create(
user_id: follow.followable.id,
notifiable_id: follow.id,
notifiable_type: follow.class.name,
action: nil,
json_data: json_data,
)
end
handle_asynchronously :send_new_follower_notification
def send_to_followers(notifiable, followers, action = nil)
# followers is an array and not an activerecord object
followers.sort_by(&:updated_at).reverse[0..2500].each do |follower|
json_data = {
user: user_data(notifiable.user),
article: article_data(notifiable)
}
Notification.create(
user_id: follower.id,
notifiable_id: notifiable.id,
notifiable_type: notifiable.class.name,
action: action,
json_data: json_data,
)
end
end
handle_asynchronously :send_all
handle_asynchronously :send_to_followers
def remove_all(notifiable, action)
Notification.where(
def send_new_comment_notifications(notifiable)
user_ids = notifiable.ancestors.map(&:user_id).to_set
user_ids.add(notifiable.commentable.user.id) if user_ids.empty?
user_ids.delete(notifiable.user_id).each do |user_id|
json_data = {
user: user_data(notifiable.user),
comment: comment_data(notifiable)
}
Notification.create(
user_id: user_id,
notifiable_id: notifiable.id,
notifiable_type: notifiable.class.name,
action: nil,
json_data: json_data,
)
end
end
handle_asynchronously :send_new_comment_notifications
def send_new_badge_notification(notifiable)
json_data = {
user: user_data(notifiable.user),
badge_achievement: {
badge_id: notifiable.badge_id,
rewarding_context_message: notifiable.rewarding_context_message,
badge: {
title: notifiable.badge.title,
description: notifiable.badge.description,
badge_image_url: notifiable.badge.badge_image_url
}
}
}
Notification.create(
user_id: notifiable.user.id,
notifiable_id: notifiable.id,
notifiable_type: "BadgeAchievement",
action: nil,
json_data: json_data,
)
end
handle_asynchronously :send_new_badge_notification
def send_reaction_notification(notifiable)
json_data = {
user: user_data(notifiable.user),
reaction: {
category: notifiable.category,
reactable_type: notifiable.reactable_type,
reactable_id: notifiable.reactable_id,
reactable: {
path: notifiable.reactable.path,
title: notifiable.reactable.title
},
updated_at: notifiable.updated_at
}
}
Notification.create(
user_id: notifiable.reactable.user.id,
notifiable_id: notifiable.id,
notifiable_type: "Reaction",
action: notifiable.category,
json_data: json_data,
)
end
handle_asynchronously :send_reaction_notification
def send_mention_notification(notifiable)
mentioner = notifiable.mentionable.user
json_data = {
user: user_data(mentioner)
}
json_data[:comment] = comment_data(notifiable.mentionable) if notifiable.mentionable_type == "Comment"
Notification.create(
user_id: notifiable.user_id,
notifiable_id: notifiable.id,
notifiable_type: "Mention",
action: nil,
json_data: json_data,
)
end
handle_asynchronously :send_mention_notification
def send_welcome_notification(receiver_id)
welcome_broadcast = Broadcast.find_by(title: "Welcome Notification")
return if welcome_broadcast == nil
dev_account = User.find_by_id(ENV["DEVTO_USER_ID"])
json_data = {
user: user_data(dev_account),
broadcast: {
processed_html: welcome_broadcast.processed_html
}
}
Notification.create(
user_id: receiver_id,
notifiable_id: welcome_broadcast.id,
notifiable_type: "Broadcast",
action: welcome_broadcast.type_of,
json_data: json_data,
)
end
handle_asynchronously :send_welcome_notification
def send_moderation_notification(notifiable)
available_moderators = User.with_role(:trusted).where("last_moderation_notification < ?", 28.hours.ago)
return if available_moderators.empty?
moderator = available_moderators.sample
dev_account = User.find_by_id(ENV["DEVTO_USER_ID"])
json_data = {
user: user_data(dev_account)
}
json_data[notifiable.class.name.downcase] = send "#{notifiable.class.name.downcase}_data", notifiable
Notification.create(
user_id: moderator.id,
notifiable_id: notifiable.id,
notifiable_type: notifiable.class.name,
action: action,
action: "Moderation",
json_data: json_data,
)
moderator.update_column(:last_moderation_notification, Time.current)
end
handle_asynchronously :send_moderation_notification
def remove_all(notifiable_hash)
Notification.where(
notifiable_id: notifiable_hash[:id],
notifiable_type: notifiable_hash[:class_name],
action: notifiable_hash[:action],
).destroy_all
end
handle_asynchronously :remove_all
end
def activity_actor
if notifiable.class.name == "Broadcast" || action == "Moderation"
User.find(ApplicationConfig["DEVTO_USER_ID"])
else
notifiable&.user
def remove_each(notifiable_collection, action = nil)
# only used for mentions since it's an array
notifiable_collection.each do |notifiable|
Notification.where(
notifiable_id: notifiable.id,
notifiable_type: notifiable.class.name,
action: action,
).destroy_all
end
end
handle_asynchronously :remove_each
def update_notifications(notifiable, action = nil)
notifications = Notification.where(
notifiable_id: notifiable.id,
notifiable_type: notifiable.class.name,
action: action,
)
return if notifications.blank?
new_json_data = notifications.first.json_data
new_json_data[notifiable.class.name.downcase] = send("#{notifiable.class.name.downcase}_data", notifiable)
notifications.update_all(json_data: new_json_data)
end
handle_asynchronously :update_notifications
private
def user_data(user)
{
id: user.id,
class: { name: "User" },
name: user.name,
username: user.username,
path: user.path,
profile_image_90: user.profile_image_90
}
end
def comment_data(comment)
{
id: comment.id,
class: { name: "Comment" },
path: comment.path,
processed_html: comment.processed_html,
updated_at: comment.updated_at,
commentable: {
id: comment.commentable.id,
title: comment.commentable.title,
path: comment.commentable.path,
class: {
name: comment.commentable.class.name
}
}
}
end
def article_data(article)
{
id: article.id,
cached_tag_list_array: article.decorate.cached_tag_list_array,
class: { name: "Article" },
title: article.title,
path: article.path,
updated_at: article.updated_at
}
end
end
def activity_object
notifiable
end
# instance methods
def activity_verb
"#{notifiable_type}_#{action}"
end
def activity_target
"#{notifiable_type.downcase}_#{Time.current.utc}"
end
def activity_notify
[StreamNotifier.new(user_id).notify]
end
def remove_from_feed
super
User.find_by(id: user_id)&.touch(:last_notification_activity)
def aggregation_format
if notifiable_type == "Reaction"
"#{created_at.beginning_of_day}-#{created_at.end_of_day}_#{json_data['reaction']['reactable_id']}_#{json_data['reaction']['reactable_type']}"
elsif notifiable_type == "Follow"
"#{created_at.beginning_of_day}-#{created_at.end_of_day}"
end
end
end

View file

@ -378,7 +378,7 @@ class User < ApplicationRecord
private
def send_welcome_notification
Broadcast.send_welcome_notification(id)
Notification.send_welcome_notification(id)
end
def set_username

View file

@ -19,7 +19,7 @@ class ArticleCreationService
create_job_opportunity(article)
if article.save
if article.published
Notification.send_all(article, "Published")
Notification.send_to_followers(article, user.followers, "Published")
end
end
article.decorate

View file

@ -0,0 +1,10 @@
class AddColumnsToNotifications < ActiveRecord::Migration[5.1]
def change
add_index :notifications, :notifiable_id
add_index :notifications, :user_id
add_index :notifications, :notifiable_type
add_column :notifications, :json_data, :jsonb
add_index :notifications, :json_data, using: :gin
add_column :notifications, :read?, :boolean, default: false
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
@ -454,10 +456,16 @@ ActiveRecord::Schema.define(version: 20181116223239) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.string "action"
t.datetime "created_at", null: false
t.jsonb "json_data"
t.integer "notifiable_id"
t.string "notifiable_type"
t.boolean "read?", default: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["json_data"], name: "index_notifications_on_json_data", using: :gin
t.index ["notifiable_id"], name: "index_notifications_on_notifiable_id"
t.index ["notifiable_type"], name: "index_notifications_on_notifiable_type"
t.index ["user_id"], name: "index_notifications_on_user_id"
end
create_table "organizations", id: :serial, force: :cascade do |t|

View file

@ -34,4 +34,12 @@ FactoryBot.define do
HEREDOC
end
end
trait :video do
after(:build) do |article|
article.video = "https://video.com"
article.user.add_role :video_permission
article.save
end
end
end

View file

@ -1,37 +0,0 @@
# rubocop:disable RSpec/InstanceVariable
require "rails_helper"
describe "Send a broadcast" do
describe "Onboarding/welcome broadcast" do
describe "sent broadcast" do
before do
@broadcast = FactoryBot.create(:broadcast, :onboarding, :sent)
@new_user = FactoryBot.create(:user)
@welcome_notification = Broadcast.send_welcome_notification(@new_user.id)
end
it "has a welcome notification" do
expect(@welcome_notification.notifiable).to eq @broadcast
end
it "is properly sent to the new user" do
expect(@welcome_notification.user).to eq @new_user
end
it "has a link to the welcome thread" do
expect(@broadcast.processed_html.html_safe).to include("/welcome")
end
end
describe "unsent broadcast" do
before do
FactoryBot.create(:broadcast, :onboarding)
end
it "is an unsent broadcast that doesn't create a notification" do
expect(Notification.all.empty?).to be true
end
end
end
end
# rubocop:enable RSpec/InstanceVariable

View file

@ -1,8 +1,8 @@
require "rails_helper"
RSpec.describe "Deleting Article", js: true do
let(:author) { create(:user) }
let(:article) { create(:article, user_id: author.id) }
let(:author) { create(:user) }
let(:article) { create(:article, user_id: author.id) }
def delete_article_via_dashboard
visit "/dashboard"
@ -13,7 +13,7 @@ RSpec.describe "Deleting Article", js: true do
end
before do
article
Notification.send_to_followers(article, [author], "Published")
end
it "author of article deletes own article" do

View file

@ -6,7 +6,8 @@ RSpec.describe "Editing A Comment", type: :feature, js: true do
let(:new_comment_text) { Faker::Lorem.paragraph }
before do
create(:comment, commentable: article, user: user, body_markdown: Faker::Lorem.paragraph)
comment = create(:comment, commentable: article, user: user, body_markdown: Faker::Lorem.paragraph)
Notification.send_new_comment_notifications(comment)
sign_in user
end

View file

@ -2,14 +2,19 @@
require "rails_helper"
RSpec.describe Comment, type: :model do
let(:user) { create(:user) }
let(:article) { create(:article, user_id: user.id, published: true) }
let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) }
let(:comment_2) { create(:comment, user_id: user.id, commentable_id: article.id) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:article) { create(:article, user_id: user.id, published: true) }
let(:article_with_video) { create(:article, :video, user_id: user.id, published: true) } # :video is a trait, see articles.rb
let(:comment) { create(:comment, user_id: user2.id, commentable_id: article.id) }
let(:video_comment) { create(:comment, user_id: user2.id, commentable_id: article_with_video.id) }
let(:comment_2) { create(:comment, user_id: user2.id, commentable_id: article.id) }
let(:child_comment) do
build(:comment, user_id: user.id, commentable_id: article.id, parent_id: comment.id)
end
before { Notification.send_new_comment_notifications(comment) }
it "gets proper generated ID code" do
expect(comment.id_code_generated).to eq(comment.id.to_s(26))
end
@ -18,8 +23,9 @@ RSpec.describe Comment, type: :model do
expect(comment.markdown_character_count).to eq(comment.body_markdown.size)
end
context "when comment is already posted " do
context "when comment is already posted" do
before do
Notification.send_new_comment_notifications(comment_2)
comment_2.update(ancestry: comment.ancestry,
body_markdown: comment.body_markdown,
commentable_type: comment.commentable_type,
@ -49,25 +55,23 @@ RSpec.describe Comment, type: :model do
end
it "adds timestamp url if commentable has video and timestamp" do
article.video = "https://video.com"
article.user.add_role(:video_permission)
article.save!
comment.body_markdown = "I like the part at 4:30"
comment.save
expect(comment.processed_html.include?(">4:30</a>")).to eq(true)
comment.body_markdown = "I like the part at 4:30 and 5:50"
comment.save
expect(comment.processed_html.include?(">5:50</a>")).to eq(true)
comment.body_markdown = "I like the part at 5:30 and :55"
comment.save
expect(comment.processed_html.include?(">:55</a>")).to eq(true)
comment.body_markdown = "I like the part at 52:30"
comment.save
expect(comment.processed_html.include?(">52:30</a>")).to eq(true)
comment.body_markdown = "I like the part at 1:52:30 and 1:20"
comment.save
expect(comment.processed_html.include?(">1:52:30</a>")).to eq(true)
expect(comment.processed_html.include?(">1:20</a>")).to eq(true)
Notification.send_new_comment_notifications(video_comment)
video_comment.body_markdown = "I like the part at 4:30"
video_comment.save
expect(video_comment.processed_html.include?(">4:30</a>")).to eq(true)
video_comment.body_markdown = "I like the part at 4:30 and 5:50"
video_comment.save
expect(video_comment.processed_html.include?(">5:50</a>")).to eq(true)
video_comment.body_markdown = "I like the part at 5:30 and :55"
video_comment.save
expect(video_comment.processed_html.include?(">:55</a>")).to eq(true)
video_comment.body_markdown = "I like the part at 52:30"
video_comment.save
expect(video_comment.processed_html.include?(">52:30</a>")).to eq(true)
video_comment.body_markdown = "I like the part at 1:52:30 and 1:20"
video_comment.save
expect(video_comment.processed_html.include?(">1:52:30</a>")).to eq(true)
expect(video_comment.processed_html.include?(">1:20</a>")).to eq(true)
end
it "does not add timestamp if commentable does not have video" do
@ -163,7 +167,7 @@ RSpec.describe Comment, type: :model do
end
it "returns the root parent comment's user if root parent comment exists" do
expect(child_comment.parent_user).to eq(user)
expect(child_comment.parent_user).to eq(user2)
end
end

View file

@ -14,6 +14,8 @@ RSpec.describe Mention, type: :model do
)
end
before { Notification.send_new_comment_notifications(comment) }
it "creates mention if there is a user mentioned" do
comment.body_markdown = "Hello @#{user.username}, you are cool."
comment.save

View file

@ -1,35 +1,30 @@
require "rails_helper"
RSpec.describe Notification, type: :model do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:follow_instance) { user.follow(user2) }
it "sends notifications to all followers of article user" do
user2.follow(user)
user3.follow(user)
Notification.send_all_without_delay(article, "Published")
expect(Notification.all.size).to eq(2)
describe "#send_new_follower_notification" do
before { Notification.send_new_follower_notification(follow_instance) }
it "creates a notification belonging to the person being followed" do
expect(Notification.first.user_id).to eq user2.id
end
it "creates a notification from the follow instance" do
notifiable_data = { notifiable_id: Notification.first.notifiable_id, notifiable_type: Notification.first.notifiable_type }
follow_data = { notifiable_id: follow_instance.id, notifiable_type: follow_instance.class.name }
expect(notifiable_data).to eq follow_data
end
end
# rubocop:disable RSpec/ExampleLength
it "sends sends a broadcast to everyone" do
user2.follow(user)
user3.follow(user)
broadcast = Broadcast.create!(
processed_html: "Hello", title: "test broadcast", type_of: "Announcement",
)
Notification.send_all_without_delay(broadcast, "Announcement")
expect(Notification.all.size).to eq(3)
end
# rubocop:enable RSpec/ExampleLength
it "removes all notifications" do
user2.follow(user)
user3.follow(user)
Notification.send_all_without_delay(article, "Published")
Notification.remove_all_without_delay(article, "Published")
expect(Notification.all.size).to eq(0)
end
# describe "#send_to_followers" do
# before do
# user2.follow user
# Notification.send_to_followers(article, user.followers, "Published")
# end
# end
end

View file

@ -7,6 +7,7 @@ RSpec.describe "CommentsUpdate", type: :request do
before do
sign_in user
Notification.send_new_comment_notifications(comment)
end
it "updates ordinary article with proper params" do

View file

@ -1,21 +1,325 @@
require "rails_helper"
# vcr_option = {
# cassette_name: "getstream-index",
# allow_playback_repeats: "true",
# }
RSpec.describe "NotificationsIndex", type: :request do
let(:user) { create(:user) }
describe "GET logged-in notifications index" do
before do
sign_in user
end
it "renders page with proper sidebar" do
describe "GET notifications" do
xit "renders page with the proper heading" do
get "/notifications"
expect(response.body).to include("Notifications")
end
context "when signed out" do
xit "renders the signup cue" do
get "/notifications"
expect(response.body).to include "<div class=\"signup-cue"
end
end
context "when signed in" do
before { sign_in user }
xit "does not render the signup cue" do
get "/notifications"
expect(response.body).not_to include "Create your account"
end
end
context "when a user has new follow notifications" do
before do
sign_in user
end
def mock_follow_notifications(amount)
create_list :user, amount
follow_instances = User.last(amount).map { |follower| follower.follow(user) }
follow_instances.each { |follow| Notification.send_new_follower_notification(follow) }
end
xit "renders the proper message for a single notification" do
mock_follow_notifications(1)
get "/notifications"
follow_message = "#{User.last.name}</a> followed you!"
expect(response.body).to include follow_message
end
xit "renders the proper message for two notifications in the same day" do
mock_follow_notifications(2)
get "/notifications"
follow_message = "#{User.last.name}</a> and\n <a href=\"/#{User.second_to_last.username}\">#{User.second_to_last.name}</a> followed you!"
expect(response.body).to include follow_message
end
xit "renders the proper message for three or more notifications in the same day" do
mock_follow_notifications(rand(3..10))
get "/notifications"
follow_message = "others followed you!"
expect(response.body).to include follow_message
end
xit "groups two notifications on the same day" do
mock_follow_notifications(2)
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
# only one notification object containing a group of notifications
expect(notifications.count).to eq 1
end
xit "groups three or more notifications on the same day" do
amount = rand(3..10)
mock_follow_notifications(amount)
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
expect(notifications.count).to eq 1
end
xit "does not group notifications that occur on different days" do
mock_follow_notifications(2)
Notification.last.update(created_at: Notification.last.created_at - 1.day)
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
expect(notifications.count).to eq 2
end
end
context "when a user has new reaction notifications" do
let(:article1) { create(:article, user_id: user.id) }
let(:article2) { create(:article, user_id: user.id) }
before do
sign_in user
end
def mock_heart_reaction_notifications(amount, categories, reactable = article1)
create_list :user, amount
reactions = User.last(amount).map do |user|
create(
:reaction,
user_id: user.id,
reactable_id: reactable.id,
reactable_type: reactable.class.name,
category: categories.sample,
)
end
reactions.each { |reaction| Notification.send_reaction_notification(reaction) }
end
xit "renders the proper message for a single public reaction" do
mock_heart_reaction_notifications(1, %w(like unicorn))
get "/notifications"
message = "#{User.last.name}</strong></a> reacted to"
expect(response.body).to include message
end
xit "renders the proper message for a single private reaction" do
mock_heart_reaction_notifications(1, %w(readinglist))
get "/notifications"
message = "Someone reacted to"
expect(response.body).to include message
end
xit "renders the proper message for two or more public reactions" do
mock_heart_reaction_notifications(2, %w(like unicorn))
get "/notifications"
message = "#{User.last.name}</a> and <a href=\"/#{User.second_to_last.username}\">#{User.second_to_last.name}</a>\n reacted to"
expect(response.body).to include message
end
xit "renders the proper message for two or more reactions where at least one is private" do
mock_heart_reaction_notifications(1, %w(readinglist))
mock_heart_reaction_notifications(1, %w(unicorn like))
get "/notifications"
message = "Devs\n reacted to"
expect(response.body).to include message
end
xit "renders the proper message for multiple public reactions" do
mock_heart_reaction_notifications(3, %w(unicorn like))
get "/notifications"
message = "#{User.last.name}</a> and 2 others\n reacted to"
expect(response.body).to include message
end
xit "properly groups two notifications that have the same day and reactable" do
mock_heart_reaction_notifications(2, %w(unicorn like readinglist))
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
expect(notifications.count).to eq 1
end
xit "properly groups three or more notifications that have the same day and reactable" do
amount = rand(3..10)
mock_heart_reaction_notifications(amount, %w(unicorn like readinglist))
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
expect(notifications.count).to eq 1
end
xit "does not group notifications that are on different days but same reactable" do
mock_heart_reaction_notifications(2, %w(unicorn like readinglist))
Notification.last.update(created_at: Notification.last.created_at - 1.day)
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
expect(notifications.count).to eq 2
end
xit "does not group notifications that are on the same day but different reactables" do
mock_heart_reaction_notifications(1, %w(unicorn like readinglist), article1)
mock_heart_reaction_notifications(1, %w(unicorn like readinglist), article2)
get "/notifications"
notifications = controller.instance_variable_get(:@notifications)
expect(notifications.count).to eq 2
end
end
context "when a user has a new comment notification" do
let(:user2) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:comment) { create(:comment, user_id: user2.id, commentable_id: article.id, commentable_type: "Article") }
before do
sign_in user
Notification.send_new_comment_notifications(comment)
get "/notifications"
end
xit "renders the correct message" do
expect(response.body).to include "commented on"
end
xit "does not render the moderation message" do
expect(response.body).not_to include "As a trusted member"
end
xit "renders the original article's title" do
expect(response.body).to include article.title
end
xit "renders the comment's processed HTML" do
expect(response.body).to include comment.processed_html
end
end
context "when a user has a new moderation notification" do
let(:user2) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:comment) { create(:comment, user_id: user2.id, commentable_id: article.id, commentable_type: "Article") }
before do
user.update(id: 1)
user.add_role :trusted
sign_in user
Notification.send_moderation_notification(comment)
get "/notifications"
end
xit "renders the proper message" do
expect(response.body).to include "As a trusted member"
end
xit "renders the article's title" do
expect(response.body).to include article.title
end
xit "renders the comment's processed HTML" do
expect(response.body).to include comment.processed_html
end
end
context "when a user has a new welcome notification" do
before do
user.update(id: 1)
sign_in user
end
xit "renders the welcome notification" do
broadcast = create(:broadcast, :onboarding)
Notification.send_welcome_notification(user.id)
get "/notifications"
expect(response.body).to include broadcast.processed_html
end
end
context "when a user has a new badge notification" do
before do
sign_in user
badge = create(:badge)
badge_achievement = create(:badge_achievement, user: user, badge: badge)
Notification.send_new_badge_notification(badge_achievement)
get "/notifications"
end
xit "renders the proper message with the badge's title" do
message = "You received the <strong>#{Badge.first.title}"
expect(response.body).to include message
end
xit "renders the rewarding context message" do
expect(response.body).to include user.badge_achievements.first.rewarding_context_message
end
xit "renders the badge's description" do
expect(response.body).to include Badge.first.description
end
xit "renders the CHECK YOUR PROFILE button" do
expect(response.body).to include "CHECK YOUR PROFILE"
end
end
context "when a user has a new mention notification" do
let(:user2) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:comment) do
create(
:comment,
user_id: user2.id,
commentable_id: article.id,
commentable_type: "Article",
body_markdown: "@#{user.username}",
)
end
before do
user.update(id: 1)
sign_in user
comment
Mention.create_all(comment)
Notification.send_mention_notification(Mention.first)
get "/notifications"
end
xit "renders the proper message" do
expect(response.body).to include "mentioned you in a comment"
end
xit "renders the processed HTML of the comment where they were mentioned" do
expect(response.body).to include comment.processed_html
end
end
context "when a user has a new article notification" do
let(:user2) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
before do
user2.follow(user)
Notification.send_to_followers(article, article.user.followers, "Published")
sign_in user2
get "/notifications"
end
xit "renders the proper message" do
expect(response.body).to include "made a new post:"
end
xit "renders the article's title" do
expect(response.body).to include article.title
end
xit "renders the author's name" do
expect(response.body).to include article.user.name
end
end
end
end

View file

@ -2,12 +2,12 @@
require "rails_helper"
RSpec.describe "ArticlesApi", type: :request do
let(:user) { create(:user) }
let(:user) { create(:user, :super_admin) }
let(:article) { create(:article) }
before do
user.update(secret: "TEST_SECRET")
user.add_role(:super_admin)
sign_in user
end
describe "POST /api/reactions" do