[PATCH 1/2] initial implementation of basic notifications

tzumainn at redhat.com tzumainn at redhat.com
Thu Oct 4 13:57:24 UTC 2012


From: Tzu-Mainn Chen <tzumainn at redhat.com>

---
 src/app/controllers/application_controller.rb      |    1 +
 src/app/controllers/notifications_controller.rb    |   64 ++++++++++++
 src/app/models/deployment.rb                       |   17 +++-
 src/app/models/instance.rb                         |   10 ++
 src/app/models/instance_observer.rb                |   52 ++++++++---
 src/app/models/notification.rb                     |   68 +++++++++++++
 src/app/models/user_notification.rb                |   32 ++++++
 src/app/views/notifications/index.html.haml        |   20 ++++
 src/config/application.rb                          |    1 +
 src/config/locales/en.yml                          |   48 +++++++++
 src/config/routes.rb                               |    6 +
 .../migrate/20120920130441_create_notifications.rb |   28 +++++
 src/lib/notifications/notifier.rb                  |  105 ++++++++++++++++++++
 src/lib/notify.rb                                  |   13 +++
 14 files changed, 451 insertions(+), 14 deletions(-)
 create mode 100644 src/app/controllers/notifications_controller.rb
 create mode 100644 src/app/models/notification.rb
 create mode 100644 src/app/models/user_notification.rb
 create mode 100644 src/app/views/notifications/index.html.haml
 create mode 100644 src/db/migrate/20120920130441_create_notifications.rb
 create mode 100644 src/lib/notifications/notifier.rb
 create mode 100644 src/lib/notify.rb

diff --git a/src/app/controllers/application_controller.rb b/src/app/controllers/application_controller.rb
index 0c02521..3b9c425 100644
--- a/src/app/controllers/application_controller.rb
+++ b/src/app/controllers/application_controller.rb
@@ -24,6 +24,7 @@ require 'will_paginate/array'
 class ApplicationController < ActionController::Base
   # FIXME: not sure what we're doing aobut service layer w/ deltacloud
   include ApplicationService
+  include Notifications
   helper_method :current_session, :current_user, :filter_view?
   before_filter :read_breadcrumbs, :set_locale, :check_session_expiration
 
diff --git a/src/app/controllers/notifications_controller.rb b/src/app/controllers/notifications_controller.rb
new file mode 100644
index 0000000..dc1187e
--- /dev/null
+++ b/src/app/controllers/notifications_controller.rb
@@ -0,0 +1,64 @@
+#
+#   Copyright 2012 Red Hat, Inc.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+
+class NotificationsController < ApplicationController
+  before_filter :require_user
+
+  def index
+    clear_breadcrumbs
+    save_breadcrumb(notifications_path)
+
+    load_notifications
+    load_headers
+
+    respond_to do |format|
+      format.html
+      format.js
+    end
+  end
+
+  def filter
+    redirect_to_original({ "source_type" => params[:source_type] })
+  end
+
+  protected
+
+  def load_notifications
+    conditions = ["exists (select 1 from user_notifications
+                                   where user_id = ?
+                                   and notification_id = notifications.id)",
+                  current_user.id
+                 ]
+
+    @notifications = Notification.find(:all,
+                                       :include => :source,
+                                       :conditions => conditions,
+                                       :order => "created_at desc"
+                                       )
+
+    @paginated_notifications = paginate_collection(@notifications, params[:page], PER_PAGE)
+  end
+
+  def load_headers
+    @header = [
+      { :name => t('notifications.index.notification_time'), :sortable => false },
+      { :name => t('notifications.index.level'), :sortable => false },
+      { :name => t('notifications.index.source'), :sortable => false },
+      { :name => t('notifications.index.message'), :sortable => false },
+      { :name => t('notifications.index.details'), :sortable => false },
+    ]
+  end
+end
diff --git a/src/app/models/deployment.rb b/src/app/models/deployment.rb
index dd833b6..05680ed 100644
--- a/src/app/models/deployment.rb
+++ b/src/app/models/deployment.rb
@@ -242,13 +242,18 @@ class Deployment < ActiveRecord::Base
     all_inst_match, account, errs = pick_provider_selection_match
 
     if all_inst_match
+      summary = I18n.t('deployments.events.launch_attempt',
+                           :account => account.name)
       self.events << Event.create(
         :source => self,
         :event_time => DateTime.now,
         :status_code => 'deployment_launch_match',
-        :summary => I18n.t('deployments.events.launch_attempt',
-                           :account => account.name)
+        :summary => summary
       )
+      Notify.notice('notifications.deployments.launch_attempt',
+                    :details => summary,
+                    :source => self,
+                    :user => user)
     else
       if errs.any?
         raise I18n.t('deployments.errors.match_not_found_with_errors',
@@ -775,6 +780,9 @@ class Deployment < ActiveRecord::Base
         :summary => I18n.t('deployments.events.state_changed',
                            :state => self.state)
       )
+      Notify.notice("notifications.deployments.#{self.state}_state_change",
+                    :source => self,
+                    :user => self.owner)
     end
   end
 
@@ -794,6 +802,11 @@ class Deployment < ActiveRecord::Base
           :summary => I18n.t('deployments.errors.launch_failed'),
           :description => $!.message
         )
+        Notify.error('notifications.deployments.launch_failed',
+                     :source => self,
+                     :details => $!.message,
+                     :user => self.owner)
+
         update_attribute(:state, STATE_FAILED)
         cleanup_failed_launch
         logger.error $!.message
diff --git a/src/app/models/instance.rb b/src/app/models/instance.rb
index 8e02002..a011016 100644
--- a/src/app/models/instance.rb
+++ b/src/app/models/instance.rb
@@ -225,6 +225,9 @@ class Instance < ActiveRecord::Base
     event = Event.create!(:source => self, :event_time => Time.now,
                           :summary => "#{action} action queued",
                           :status_code => "#{action}_queued")
+    Notify.notice("notifications.instances.#{action}_action_queued",
+                  :source => self,
+                  :user => user)
 
     return task
   end
@@ -473,6 +476,10 @@ class Instance < ActiveRecord::Base
       :summary => "Failed to stop instance #{self.name}",
       :description => $!.message
     )
+    Notify.error("notifications.instances.stop_failed",
+                 :details => $!.message,
+                 :source => self,
+                 :user => user)
     logger.error $!.message
     logger.error $!.backtrace.join("\n  ")
     false
@@ -493,6 +500,9 @@ class Instance < ActiveRecord::Base
     event = Event.create!(:source => self, :event_time => Time.now,
                           :summary => "Instance is not accessible, state changed to stopped",
                           :status_code => "forced_stop")
+    Notify.warning("notifications.instances.forced_stop",
+                 :source => self,
+                 :user => user)
   end
 
   def deployed?
diff --git a/src/app/models/instance_observer.rb b/src/app/models/instance_observer.rb
index d42fbe6..f19dfed 100644
--- a/src/app/models/instance_observer.rb
+++ b/src/app/models/instance_observer.rb
@@ -89,6 +89,9 @@ class InstanceObserver < ActiveRecord::Observer
     event = Event.new(:source => instance, :event_time => instance.created_at,
                       :summary => "created")
     event.save!
+    Notify.success("notifications.instances.created",
+                 :source => instance,
+                 :user => instance.owner)
   end
 
   def after_update(instance)
@@ -99,31 +102,56 @@ class InstanceObserver < ActiveRecord::Observer
                         :summary => "state changed to #{instance.state}",
                         :change_hash => instance.changes, :status_code => instance.state)
       event.save!
+      Notify.notice("notifications.instances.#{instance.state}_state_change",
+                     :source => instance,
+                     :user => instance.owner)
       if instance.state == Instance::STATE_RUNNING && instance.deployment
-        event2 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
-                        :status_code => "first_running", :summary => "1st instance is running",
-                        :change_hash => instance.changes) if instance.first_running?
-        event3 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
-                        :status_code => "all_running", :summary => "All instances are running",
-                        :change_hash => instance.changes) if instance.deployment.all_instances_running?
-        event2.save! if event2
-        event3.save! if event3
+        if instance.first_running?
+          event2 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
+                             :status_code => "first_running", :summary => "1st instance is running",
+                             :change_hash => instance.changes)
+          event2.save!
+          Notify.success("notifications.deployments.first_instance_running",
+                        :source => instance.deployment,
+                        :user => instance.deployment.owner)
+        end
+        if instance.deployment.all_instances_running?
+          event3 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
+                             :status_code => "all_running", :summary => "All instances are running",
+                             :change_hash => instance.changes)
+          event3.save!
+          Notify.success("notifications.deployments.all_instances_running",
+                        :source => instance.deployment,
+                        :user => instance.deployment.owner)
+        end
       elsif (instance.state == Instance::STATE_STOPPED || instance.state == Instance::STATE_ERROR) && instance.deployment
         if instance.deployment.any_instance_running? && !(instance.deployment.events.empty?)
-          event4 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
-                             :status_code => "some_stopped", :summary => "Some instances are stopped",
-                             :change_hash => instance.changes) if instance.deployment.events.lifetime.last{|e|e.status_code == :all_running}
+          if instance.deployment.events.lifetime.last{|e|e.status_code == :all_running}
+            event4 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
+                               :status_code => "some_stopped", :summary => "Some instances are stopped",
+                               :change_hash => instance.changes)
+            event4.save!
+            Notify.notice("notifications.deployments.some_instances_stopped",
+                          :source => instance.deployment,
+                          :user => instance.deployment.owner)
+          end
         else
           event4 = Event.new(:source => instance.deployment, :event_time => DateTime.now,
                              :status_code => "all_stopped", :summary => "All instances are stopped",
                              :change_hash => instance.changes)
+          event4.save!
+          Notify.notice("notifications.deployments.all_instances_stopped",
+                        :source => instance.deployment,
+                        :user => instance.deployment.owner)
         end
-        event4.save! if event4
       elsif instance.state == Instance::STATE_VANISHED
         Event.create!(
           :source => instance, :event_time => Time.now,
           :status_code => 'vanished', :summary => "Instance disappeared from cloud provider"
         )
+        Notify.error('notifications.instances.vanished',
+                     :source => instance,
+                     :user => instance.owner)
       end
       instance.deployment.update_state(instance) if instance.deployment
 
diff --git a/src/app/models/notification.rb b/src/app/models/notification.rb
new file mode 100644
index 0000000..c5e9b66
--- /dev/null
+++ b/src/app/models/notification.rb
@@ -0,0 +1,68 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+class Notification < ActiveRecord::Base
+
+  has_many :user_notifications
+  has_many :users, :through => :user_notifications
+  belongs_to :source, :polymorphic => true, :conditions => '1=1'
+
+  LEVELS = [:notice, :warning, :success, :error]
+
+  validates_inclusion_of :level, :in => LEVELS + LEVELS.collect{|type| type.to_s}
+  validates_presence_of :source_id
+  validates_presence_of :source_type
+  validates_presence_of :key
+  validates_length_of :user_notifications, :minimum => 1
+
+  before_validation :set_default_notice_level
+
+  scope :readable, lambda { |user| joins(:users).where('users.id' => user) }
+  scope :read, lambda { viewed true }
+  scope :unread, lambda { viewed false }
+
+  def source
+    case source_type
+      when "Deployment" then Deployment.unscoped.find(source_id)
+      when "Instance" then Instance.unscoped.find(source_id)
+      else super
+    end
+  end
+
+  def to_s
+    "#{level}: #{key} #{source.name}"
+  end
+
+  def check_permissions operation
+    logger.debug "CHECKING #{operation}"
+    # anybody can create notices
+    return true if operation == :create
+    if operation == :update or operation == :destroy
+      # TODO: who is a real owner of a notice?
+    end
+    false
+  end
+
+  def message
+    if source.nil?
+      I18n.t("notifications.errors.missing_source")
+    else
+      I18n.t(key, :name => source.name)
+    end
+  end
+
+  private
+
+  def set_default_notice_level
+    self.level ||= TYPES.first
+  end
+end
diff --git a/src/app/models/user_notification.rb b/src/app/models/user_notification.rb
new file mode 100644
index 0000000..80f9e74
--- /dev/null
+++ b/src/app/models/user_notification.rb
@@ -0,0 +1,32 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+class UserNotification < ActiveRecord::Base
+
+  belongs_to :user
+  belongs_to :notification
+
+  def check_permissions operation
+    # anybody can create user_notice relationships
+    return true if operation == :create
+    # only notice owner can update or destroy
+    if operation == :update or operation == :destroy
+      return true if user.id and User.current and user.id == User.current.id
+    end
+    false
+  end
+
+  def read!
+    update_attributes! :viewed => true
+  end
+
+end
diff --git a/src/app/views/notifications/index.html.haml b/src/app/views/notifications/index.html.haml
new file mode 100644
index 0000000..5133e9c
--- /dev/null
+++ b/src/app/views/notifications/index.html.haml
@@ -0,0 +1,20 @@
+= render :partial => 'layouts/nav_history'
+
+%header.page-header
+  %h1.section-index= t('notifications.index.title')
+
+%section.content-section.toggle-view.notifications
+  - content_for :filter_controls do
+    %li
+      = label_tag "source_type", t('filter_table.viewing')
+      = hidden_field_tag :current_path, request.fullpath
+      = restful_submit_tag t("filter_table.apply_filters"), "index", filter_notifications_path, 'POST', :class => 'button', :id => 'apply_notifications_filter'
+      %span.label.badge.dark= @notifications.count
+  = filter_table(@header, @paginated_notifications) do |notification|
+    - source = notification.source
+    %tr{:class => cycle('nostripe','stripe')}
+      %td= notification.created_at.strftime("%d-%b-%Y %H:%M:%S")
+      %td= notification.level
+      %td= source.nil? ? t('notifications.index.not_available') : link_to(source.class.to_s + " (" + source.name + ")", polymorphic_path(source))
+      %td= notification.message
+      %td= notification.details
diff --git a/src/config/application.rb b/src/config/application.rb
index 88d6d1d..88606a5 100644
--- a/src/config/application.rb
+++ b/src/config/application.rb
@@ -38,6 +38,7 @@ module Conductor
 
     # Custom directories with classes and modules you want to be autoloadable.
     # config.autoload_paths += %W(#{config.root}/extras)
+    config.autoload_paths += %W(#{Rails.root}/lib)
 
     # Only load the plugins named here, in the order given (default is alphabetical).
     # :all can be used as a placeholder for all plugins not explicitly named.
diff --git a/src/config/locales/en.yml b/src/config/locales/en.yml
index 1955d2d..60e75b6 100644
--- a/src/config/locales/en.yml
+++ b/src/config/locales/en.yml
@@ -12,6 +12,53 @@ en:
     no_results: "No matching results."
   activity:
     activity: Activity
+  notifications:
+    index:
+      title: Notifications
+      notification_time: Time
+      source: Source
+      message: Message
+      details: Details
+      not_available: N/A
+      level: Level
+    deployments:
+      launch_attempt: Attempting to launch deployment %{name}
+      launch_failed: Failed to launch deployment %{name}
+      first_instance_running: First instance running for deployment %{name}
+      all_instances_running: All instances running for deployment %{name}
+      some_instances_stopped: Some instances stopped for deployment %{name}
+      all_instances_stopped: All instances stopped for deployment %{name}
+      new_state_change: Deployment %{name} created
+      pending_state_change: Deployment %{name} pending
+      running_state_change: Deployment %{name} running
+      shutting_down_state_change: Deployment %{name} shutting down
+      stopped_state_change: Deployment %{name} stopped
+      failed_state_change: Deployment %{name} failed
+      incomplete_state_change: Deployment %{name} incomplete
+      rollback_in_progress: Deployment %{name} rollback in progress
+      rollback_complete: Deployment %{name} rollback complete
+      rollback_failed: Deployment %{name} rollback failed
+    instances:
+      created: Instance %{name} created
+      new_state_change: Instance %{name} state changed to new
+      pending_state_change: Instance %{name} state changed to pending
+      running_state_change: Instance %{name} state changed to running
+      shutting_down_state_change: Instance %{name} state changed to shutting down
+      stopped_state_change: Instance %{name} state changed to stopped
+      stopping_state_change: Instance %{name} state changed to stopping
+      create_failed_state_change: Instance %{name} state changed to create failed
+      error_state_change: Instance %{name} state changed to error
+      vanished_state_change: Instance %{name} state changed to new
+      create_action_queued: Create action queued for instance %{name}
+      start_action_queued: Start action queued for instance %{name}
+      stop_action_queued: Stop action queued for instance %{name}
+      reboot_action_queued: Reboot action queued for instance %{name}
+      destroy_action_queued: Destroy action queued for instance %{name}
+      stop_failed: Failed to stop instance %{name}
+      forced_stop: Instance %{name} is not accessible, state changed to stopped
+      vanished: Instance %{name} vanished
+    errors:
+      missing_source: Notification source is missing
   logs:
     options:
       default_users: All Users
@@ -1576,3 +1623,4 @@ en:
     deployables: Deployables
     deployments: Deployments
     logs: Logs
+    notifications: Notifications
diff --git a/src/config/routes.rb b/src/config/routes.rb
index 4533796..35b7b6f 100644
--- a/src/config/routes.rb
+++ b/src/config/routes.rb
@@ -189,6 +189,12 @@ Conductor::Application.routes.draw do
     end
   end
 
+  resources :notifications do
+    collection do
+      post 'filter'
+    end
+  end
+
   resources :config_servers do
     member do
       get 'test'
diff --git a/src/db/migrate/20120920130441_create_notifications.rb b/src/db/migrate/20120920130441_create_notifications.rb
new file mode 100644
index 0000000..ea5fd91
--- /dev/null
+++ b/src/db/migrate/20120920130441_create_notifications.rb
@@ -0,0 +1,28 @@
+class CreateNotifications < ActiveRecord::Migration
+  def self.up
+    # These are notifications
+    create_table :notifications do |t|
+      t.integer    :source_id, :null => false
+      t.string     :source_type, :null => false
+      t.string     :key, :null => false
+      t.text       :details
+      t.string     :level, :null => false
+      t.timestamps
+    end
+    create_table :user_notifications do |t|
+      t.references :user
+      t.references :notification
+      t.boolean    :viewed, :null => false, :default => false
+    end
+    add_index  :user_notifications, :user_id
+    add_index  :user_notifications, :notification_id
+  end
+
+  def self.down
+    remove_index :user_notifications, :column=>:user_id
+    remove_index :user_notifications, :column=>:notification_id
+    drop_table :user_notifications
+    drop_table :notifications
+  end
+
+end
diff --git a/src/lib/notifications/notifier.rb b/src/lib/notifications/notifier.rb
new file mode 100644
index 0000000..baab263
--- /dev/null
+++ b/src/lib/notifications/notifier.rb
@@ -0,0 +1,105 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+module Notifications
+
+  class Notifier
+    include Rails.application.routes.url_helpers
+
+    attr_reader :controller, :default_options
+
+    def initialize(controller = nil, options = { })
+      @controller      = controller
+      @default_options = { :level        => :success,
+                           :details      => nil,
+                           :user         => nil,
+                           :source       => nil}.merge(options)
+    end
+
+    # generates success-notice
+    def success(key, options = { })
+      notification key, options
+    end
+
+    # generates message-notice
+    def notice(key, options = { })
+      options = { :level => :notice }.merge options
+      notification key, options
+    end
+
+    # generates warning-notice, not persisted by default
+    def warning(key, options = { })
+      options = { :level => :warning }.merge options
+      notification key, options
+    end
+
+    # generates error-notice
+    def error(key, options = { })
+      options = { :level => :error }.merge options
+      notification key, options
+    end
+
+    # generates error-notice form exception, optionally adding a note
+    # @example
+    #   exception(an_exception)
+    #   exception("some note", an_exception)
+    def exception(key, *args)
+      options   = args.extract_options!
+      exception = args.pop
+
+      options = { :level   => :error,
+        :details => exception.backtrace.join("\n") }.merge options
+
+      notification key, options
+    end
+
+    protected
+
+    # Generate a notification:
+    #
+    # key:                  The notification dictionary key
+    # options:              Optional hash containing various optional parameters. This includes:
+    #
+    #   level:               The type of notice to be generated. Supported values include:
+    #                        :notice, :success (Default), :warning, :error
+    #
+    #   details:             String containing additional details. This would typically be to store
+    #                        information such as a stack trace that is in addition to the notice text.
+    #
+    #   user:                the user to send the notifcation to.  If not set, User.current is used
+    #
+    def notification(key, options = { })
+      options = process_options options
+
+      Notification.create! :key          => key,
+                           :details      => options[:details],
+                           :level        => options[:level],
+                           :source       => options[:source],
+                           :user_notifications =>
+        [UserNotification.new(:user => options[:user], :viewed => false)]
+    end
+
+    LEVELS          = [:notice, :success, :warning, :error]
+
+    def process_options(options)
+      options        = default_options.merge options
+      options[:user] ||= User.current
+
+      options.assert_valid_keys(*default_options.keys)
+
+      raise ArgumentError, "unknown notification level #{options[:level].inspect}" unless LEVELS.include? options[:level]
+
+      return options
+    end
+  end
+end
+
diff --git a/src/lib/notify.rb b/src/lib/notify.rb
new file mode 100644
index 0000000..504de9e
--- /dev/null
+++ b/src/lib/notify.rb
@@ -0,0 +1,13 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+Notify = Notifications::Notifier.new
-- 
1.7.6.5




More information about the aeolus-devel mailing list