Project

General

Profile

« Previous | Next » 

Revision 363cab56

Added by Ohad Levy over 7 years ago

fixes #17545 - adds UI notification support

- Initial data model for notification support, based on manageiq design.
- addes notification JSON endpoints.

View differences:

app/controllers/concerns/foreman/controller/parameters/notification_recipient.rb
module Foreman::Controller::Parameters::NotificationRecipient
extend ActiveSupport::Concern
class_methods do
def notification_recipient_params_filter
Foreman::ParameterFilter.new(::NotificationRecipient).tap do |filter|
filter.permit :seen
end
end
end
def notification_recipient_params
self.class.notification_recipient_params_filter.filter_params(params, parameter_filter_context)
end
end
app/controllers/notification_recipients_controller.rb
class NotificationRecipientsController < Api::V2::BaseController
include Foreman::Controller::Parameters::NotificationRecipient
before_action :require_login
before_action :find_resource, :only => [:update, :destroy]
def index
@notifications = NotificationRecipient.
where(:user_id => User.current.id).
order(:created_at).
eager_load(:notification, :notification_blueprint)
render :json => {
:notifications => @notifications.paginate(paginate_options).map(&:payload),
:total => @notifications.count
}
end
def update
process_response @notification_recipient.update_attributes(notification_recipient_params)
end
def destroy
process_response @notification_recipient.destroy
end
private
def require_login
not_found unless SETTINGS[:login]
end
def find_resource
super
@notification_recipient.current_user? || not_found
end
end
app/models/notification.rb
# frozen_string_literal: true
# Stores the information related to serving the notification to multiple users
# This class' responsibility is, given a notification blueprint, to determine
# who are the notification recipients
class Notification < ActiveRecord::Base
AUDIENCE_USER = 'user'
AUDIENCE_GROUP = 'usergroup'
AUDIENCE_TAXONOMY = 'taxonomy'
AUDIENCE_GLOBAL = 'global'
AUDIENCE_ADMIN = 'admin'
belongs_to :notification_blueprint
belongs_to :initiator, :class_name => User, :foreign_key => 'user_id'
has_many :notification_recipients, :dependent => :delete_all
has_many :recipients, :class_name => User, :through => :notification_recipients, :source => :user
validates :notification_blueprint, :presence => true
validates :initiator, :presence => true
validates :audience, :inclusion => {
:in => [AUDIENCE_USER, AUDIENCE_GROUP, AUDIENCE_TAXONOMY,
AUDIENCE_GLOBAL, AUDIENCE_ADMIN]
}, :presence => true
before_create :calculate_expiry, :set_notification_recipients,
:set_default_initiator
scope :active, -> { where('expired_at >= :now', {:now => Time.now.utc}) }
scope :expired, -> { where('expired_at < :now', {:now => Time.now.utc}) }
def expired?
Time.now.utc > expired_at
end
def subscriber_ids
case audience
when AUDIENCE_GLOBAL
User.reorder('').pluck(:id)
when AUDIENCE_TAXONOMY
notification_blueprint.subject.user_ids.uniq
when AUDIENCE_USER
[initiator.id]
when AUDIENCE_ADMIN
User.only_admin.reorder('').uniq.pluck(:id)
when AUDIENCE_GROUP
notification_blueprint.subject.all_users.uniq.map(&:id) # This needs to be rewritten in usergroups.
end
end
private
def calculate_expiry
self.expired_at = Time.now.utc + notification_blueprint.expires_in
end
def set_default_initiator
self.initiator = User.anonymous_admin
end
def set_notification_recipients
subscribers = subscriber_ids
notification_recipients.build subscribers.map{|id| { :user_id => id}}
end
end
app/models/notification_blueprint.rb
# Model to store all information related to a notification.
# The audience for this notification is calculated on real time by
# Notification.
#
# Foreman and plugins should define NotificationBlueprints for various
# actions that are notification-worthy. This keeps the responsibilities
# separate, Notifications taking care of serving the content, and
# NotificationBlueprint of storing it.
class NotificationBlueprint < ActiveRecord::Base
has_many :notifications
belongs_to :subject, :polymorphic => true
validates :message, :presence => true
validates :group, :presence => true
validates :name, :presence => true
validates :level, :inclusion => { :in => %w(success error warning info) }, :presence => true
validates :expires_in, :numericality => {:greater_than => 0}
before_validation :set_default_expiry
private
def set_default_expiry
self.expires_in ||= 24.hours
end
end
app/models/notification_recipient.rb
class NotificationRecipient < ActiveRecord::Base
belongs_to :notification
belongs_to :user
has_one :notification_blueprint, :through => :notification
validates :notification, :presence => true
validates :user, :presence => true
scope :unseen, -> { where(:seen => false) }
def payload
{
:id => id,
:seen => seen,
:level => notification_blueprint.level,
:text => notification_blueprint.message,
:subject => notification_blueprint.subject,
:created_at => notification.created_at,
:group => notification_blueprint.group,
}
end
def current_user?
return true unless SETTINGS[:login]
User.current.id == user_id
end
end
app/services/foreman/access_permissions.rb
Foreman::AccessControl.map do |permission_set|
permission_set.security_block :public do |map|
map.permission :user_logout, { :users => [:logout] }, :public => true
map.permission :my_account, { :users => [:edit] }, :public => true
map.permission :my_account, { :users => [:edit],
:notification_recipients => [:index, :update, :destroy] }, :public => true
map.permission :api_status, { :"api/v1/home" => [:status], :"api/v2/home" => [:status]}, :public => true
map.permission :about_index, { :about => [:index] }, :public => true
end
config/routes.rb
get :random_name
end
end
resources :notification_recipients, :only => [:index, :update, :destroy]
end
db/migrate/20160927071039_create_notification_blueprints.rb
class CreateNotificationBlueprints < ActiveRecord::Migration
def change
create_table :notification_blueprints do |t|
t.string :group, index: true
t.string :level
t.string :message
t.text :name
t.integer :expires_in
t.timestamps null: false
t.references :subject, polymorphic: true
end
end
end
db/migrate/20160927071147_create_notifications.rb
class CreateNotifications < ActiveRecord::Migration
def change
create_table :notifications do |t|
t.references :notification_blueprint, index: true, foreign_key: true, null: false
t.references :user, index: true, foreign_key: true
t.string :audience
t.timestamp :expired_at
t.timestamps null: false
end
end
end
db/migrate/20160927073233_create_notification_recipients.rb
class CreateNotificationRecipients < ActiveRecord::Migration
def change
create_table :notification_recipients do |t|
t.references :notification, index: true, foreign_key: true
t.references :user, index: true, foreign_key: true
t.boolean :seen, default: false, index: true
t.timestamps null: false
end
end
end
test/controllers/notification_recipients_controller_test.rb
require 'test_helper'
class NotificationRecipientsControllerTest < ActionController::TestCase
setup do
@request.headers['Accept'] = Mime::JSON
@request.headers['Content-Type'] = Mime::JSON.to_s
end
test "should get index" do
get :index, { :format => 'json' }, set_session_user
assert_response :success
response = ActiveSupport::JSON.decode(@response.body)
assert_empty response['notifications']
assert_equal 0, response['total']
end
test "should get index" do
notification = add_notification
get :index, { :format => 'json' }, set_session_user
assert_response :success
assert_equal notification.notification_blueprint.message, response['notifications'][0]['text']
end
test "should be able to update seen flag" do
add_notification
get :index, { :format=>'json' }, set_session_user
put :update, { :format => 'json', :id => first_notification,
:notification_recipient => {:seen => true} }, set_session_user
assert_response :success
assert response['seen']
end
test "should be able to delete notification" do
add_notification
get :index, { :format=>'json' }, set_session_user
notice_id = first_notification
assert NotificationRecipient.find(notice_id)
delete :destroy, { :id => notice_id }
refute NotificationRecipient.find_by_id(notice_id)
assert_response :success
end
test "should get 404 on invalid notification deletion" do
get :index, { :format=>'json' }, set_session_user
notice_id = 1
refute response['notifications'].map{|n| n['id']}.include?(notice_id)
refute NotificationRecipient.find_by_id(notice_id)
delete :destroy, { :id => notice_id }
assert_response :not_found
end
test "should not get notifications if settings login is disabled" do
SETTINGS[:login] = false
get :index, { :format=>'json' }, {}
SETTINGS[:login] = true
assert_response :not_found
end
private
def add_notification
type = FactoryGirl.create(:notification_blueprint,
:message => 'this test just executed successfully')
FactoryGirl.create(:notification, :notification_blueprint => type, :audience => 'global')
end
def response
ActiveSupport::JSON.decode(@response.body)
end
def first_notification
response['notifications'].first['id']
end
end
test/factories/notification.rb
FactoryGirl.define do
factory :notification do
notification_blueprint
association :initiator, :factory => :user
audience 'user'
end
end
test/factories/notification_blueprint.rb
FactoryGirl.define do
factory :notification_blueprint do
sequence(:group) { |n| "notification_blueprint_#{n}" }
sequence(:message) { |n| "message_#{n}" }
sequence(:name) { |n| "name_#{n}" }
subject User.first
level 'info'
expires_in 24.hours
end
end
test/factories/notification_recipient.rb
FactoryGirl.define do
factory :notification_recipient do
notification
user
end
end
test/models/notification_blueprint_test.rb
require 'test_helper'
class NotificationBlueprintTest < ActiveSupport::TestCase
should validate_presence_of(:message)
should validate_presence_of(:level)
should validate_presence_of(:group)
should validate_presence_of(:name)
end
test/models/notification_recipient_test.rb
require 'test_helper'
class NotificationRecipientTest < ActiveSupport::TestCase
should validate_presence_of(:notification)
should validate_presence_of(:user)
test "seen should defaults to false" do
recipient = NotificationRecipient.new
assert_equal false, recipient.seen
end
test "should return unseen notifications" do
id = FactoryGirl.create(:notification_recipient).id
assert NotificationRecipient.unseen.pluck(:id).include?(id)
end
end
test/models/notification_test.rb
require 'test_helper'
class NotificationTest < ActiveSupport::TestCase
setup do
User.current = users :admin
end
should validate_presence_of(:initiator)
should validate_presence_of(:notification_blueprint)
should validate_presence_of(:audience)
test 'should be able to create notification' do
blueprint = FactoryGirl.create(
:notification_blueprint,
:message => 'this test just executed successfully',
:subject => nil
)
notice = FactoryGirl.create(:notification,
:audience => 'global',
:notification_blueprint => blueprint)
assert notice.valid?
assert_equal blueprint.message, notice.notification_blueprint.message
assert_equal User.all, notice.recipients
end
test 'should return active notifications' do
blueprint = FactoryGirl.create(
:notification_blueprint,
:expires_in => 5.minutes
)
notice = FactoryGirl.create(:notification,
:audience => Notification::AUDIENCE_ADMIN,
:notification_blueprint => blueprint)
assert_includes Notification.active, notice
end
test 'user notifications should subscribe only to itself' do
notification = FactoryGirl.build(:notification,
:audience => Notification::AUDIENCE_USER)
assert_equal [notification.initiator.id], notification.subscriber_ids
end
test 'usergroup notifications should subscribe to all of its members' do
group = FactoryGirl.create(:usergroup)
group.users = FactoryGirl.create_list(:user,25)
notification = FactoryGirl.build(:notification,
:audience => Notification::AUDIENCE_GROUP)
notification.notification_blueprint.subject = group
assert group.all_users.any?
assert_equal group.all_users.map(&:id),
notification.subscriber_ids
end
test 'Organization notifications should subscribe to all of its members' do
org = FactoryGirl.create(:organization)
org.users = FactoryGirl.create_list(:user,25)
notification = FactoryGirl.build(:notification,
:audience => Notification::AUDIENCE_TAXONOMY)
notification.notification_blueprint.subject = org
assert org.user_ids.any?
assert_equal org.user_ids, notification.subscriber_ids
end
test 'Location notifications should subscribe to all of its members' do
loc = FactoryGirl.create(:location)
loc.users = FactoryGirl.create_list(:user,25)
notification = FactoryGirl.build(:notification,
:audience => Notification::AUDIENCE_TAXONOMY)
notification.notification_blueprint.subject = loc
assert loc.user_ids.any?
assert_equal loc.user_ids, notification.subscriber_ids
end
test 'Global notifications should subscribe to all users' do
notification = FactoryGirl.build(:notification,
:audience => Notification::AUDIENCE_GLOBAL)
assert User.count > 0
assert_equal User.reorder('').pluck(:id),
notification.subscriber_ids
end
test 'Admin notifications should subscribe to all admin users' do
notification = FactoryGirl.build(:notification,
:audience => Notification::AUDIENCE_ADMIN)
assert User.only_admin.count > 0
assert_equal User.only_admin.reorder('').pluck(:id).sort,
notification.subscriber_ids.sort
end
end

Also available in: Unified diff