added ui for course exports

refs #3396

Change-Id: I07c30535e042dada7c7d4174a7bb8148b7f6b1fb
Reviewed-on: https://gerrit.instructure.com/3039
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Bracken Mosbacker 2011-04-06 09:13:00 -06:00
parent 53fc924ced
commit fc4cc9ff46
20 changed files with 530 additions and 31 deletions

View File

@ -237,6 +237,8 @@ class ApplicationController < ActionController::Base
elsif request.path.match(/\A\/profile/) || request.path == '/' || request.path.match(/\A\/dashboard\/files/) || request.path.match(/\A\/calendar/) || request.path.match(/\A\/assignments/) || request.path.match(/\A\/files/)
@context = @current_user
@context_membership = @context
elsif params[:content_export_id]
@context = ContentExport.find(params[:content_export_id])
end
if @context.try_rescue(:only_wiki_is_public) && params[:controller].match(/wiki/) && !@current_user && (!@context.is_a?(Course) || session[:enrollment_uuid_course_id] != @context.id)
@show_left_side = false

View File

@ -0,0 +1,95 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class ContentExportsController < ApplicationController
before_filter :require_context
before_filter { |c| c.active_tab = "settings" }
def index
if authorized_action(@context, @current_user, :manage)
@exports = @context.content_exports.active
@current_export_id = nil
if export = @context.content_exports.running.first
@current_export_id = export.id
end
end
end
def show
if authorized_action(@context, @current_user, :manage)
if export = @context.content_exports.find_by_id(params[:id])
render_export(export)
else
render :json => {:errors => {:base => "Export does not exist"}}.to_json, :status => :bad_request
end
end
end
def create
if authorized_action(@context, @current_user, :manage)
if @context.content_exports.running.count == 0
export = ContentExport.new
export.course = @context
export.user = @current_user
export.workflow_state = 'created'
export.export_type = 'common_cartridge'
export.progress = 0
if export.save
export.export_course
render_export(export)
else
render :json => {:error_message => "Couldn't create course export."}.to_json
end
else
# an export is already running, just return it
export = @context.content_exports.running.first
render_export(export)
end
end
end
def destroy
if authorized_action(@context, @current_user, :manage)
if export = @context.content_exports.find_by_id(params[:id])
export.destroy
render :json => {:success=>'true'}.to_json
else
render :json => {:errors => {:base => "Export does not exist"}}.to_json, :status => :bad_request
end
end
end
def xml_schema
file = nil
if params[:version]
file = Rails.root + "lib/canvas/cc/xsd/#{params[:version]}.xsd"
end
if File.exists?(file)
send_file(file, :type => 'text/xml', :disposition => 'inline')
else
render :template => 'shared/errors/404_message', :status => :bad_request
end
end
private
def render_export(export)
render :json => export.to_json(:only => [:id, :progress, :workflow_state],:methods => [:download_url, :error_message])
end
end

View File

@ -1,3 +1,6 @@
You need to register any new notifications in the DB. To do this edit the rake
task db:load_notifications in lib/tasks/db_load_data.rake
Since messages are parsed in a different thread than the request, they don't
know the time_zone of the user by default. If you want to display formatted
versions of a date or time, you could call force_zone from within the template

View File

@ -0,0 +1,12 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(asset.context) %>/<%= asset.context.class.to_s.downcase.pluralize %>/<%= asset.context_id %>/content_exports
<% end %>
<% define_content :subject do %>
Course Export Failed: <%= asset.context.name %>
<% end %>
There was a problem exporting the course "<%= asset.context.name %>".
Please notify your system administrator, and give them the following export identifier: "ContentExport:<%= asset.id %>".
<%= content :link %>

View File

@ -0,0 +1,11 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(asset.context) %>/<%= asset.context.class.to_s.downcase.pluralize %>/<%= asset.context_id %>/content_exports
<% end %>
<% define_content :subject do %>
Course Export Finished: <%= asset.context.name %>
<% end %>
Your course export for "<%= asset.context.name %>" has finished.
<%= content :link %>

View File

@ -0,0 +1,111 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class ContentExport < ActiveRecord::Base
include Workflow
belongs_to :course
belongs_to :user
belongs_to :attachment
has_many :attachments, :as => :context, :dependent => :destroy
has_a_broadcast_policy
serialize :settings
workflow do
state :created
state :exporting
state :exported
state :failed
state :deleted
end
set_broadcast_policy do |p|
p.dispatch :content_export_finished
p.to { [user] }
p.whenever {|record|
record.changed_state(:exported)
}
p.dispatch :content_export_failed
p.to { [user] }
p.whenever {|record|
record.changed_state(:failed)
}
end
def export_course
self.workflow_state = 'exporting'
self.save
begin
if Canvas::CC::CCExporter.export(self)
self.workflow_state = 'exported'
else
self.workflow_state = 'failed'
end
rescue
message = $!.to_s
stack = "#{$!}: #{$!.backtrace.join("\n")}"
add_error(message, stack)
self.workflow_state = 'failed'
ensure
self.save
end
end
handle_asynchronously :export_course, :priority => Delayed::LOW_PRIORITY
def download_url
self.attachment ? self.attachment.authenticated_s3_url : nil
end
def error_message
self.settings[:last_error]
end
def add_error(message, stack)
self.settings[:error_messages] ||= []
self.settings[:error_messages] << [message, stack]
self.settings[:last_error] = message
end
def root_account
self.course.root_account
end
def running?
['created', 'exporting'].member? self.workflow_state
end
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
self.attachment.destroy! if self.attachment
save!
end
def settings
read_attribute(:settings) || write_attribute(:settings,{}.with_indifferent_access)
end
def fast_update_progress(val)
self.progress = val
ContentExport.update_all({:progress=>val}, "id=#{self.id}")
end
named_scope :active, {:conditions => ['workflow_state != ?', 'deleted']}
named_scope :running, {:conditions => ['workflow_state IN (?)', ['created', 'exporting']]}
end

View File

@ -107,6 +107,7 @@ class Course < ActiveRecord::Base
has_many :media_objects, :as => :context
has_many :page_views, :as => :context
has_many :role_overrides, :as => :context
has_many :content_exports
attr_accessor :import_source
before_save :assign_uuid

View File

@ -0,0 +1,50 @@
<% add_crumb "Course Exports" %>
<% content_for :page_title do %>Course Exports<% end %>
<% content_for :page_header do %>
<h1>Course Exports</h1>
<% end %>
<div>
<h2>Course Exports</h2>
<div class="export_messages" style="<%= hidden %>">
<div class="ui-state-error error_message"></div>
</div>
<div id="exports">
<% @exports.each do |export| %>
<% unless export.running? %>
<% if export.workflow_state == 'exported' #&& export.attachment %>
<p>
Course Export from <span class="created_at time_ago_date"><%= datetime_string export.created_at %></span>:
<a href="<%= export.download_url %>">Click here to download</a>
</p>
<% end %>
<% end %>
<% end %>
</div>
<div class="form" style="<%= hidden if @current_export_id %>">
<% form_tag course_content_exports_path(@context.id), :id => "exporter_form" do %>
<div style="float: left; margin-left: 5px;">
<button type="submit" class="submit_button button big-button">Create a Course Export</button>
</div>
<div class="clear"></div>
<% end %>
</div>
<div class="progress_bar_holder" style="<%= hidden unless @current_export_id %> margin-top: 10px;">
<div class="export_progress"></div>
<div class="progress_message">
The export process has started. This can take awhile for large courses.
<em>You can leave the page</em> and you'll get an email when the export is complete.
</div>
</div>
<% if @current_export_id %>
<div id="current_export_id" style="<%= hidden %>"><%= @current_export_id %></div>
<% end %>
</div>
<% jammit_js :content_exports %>

View File

@ -33,6 +33,7 @@
<a href="<%= context_url(@context, :context_confirm_action_url, :event => 'conclude') %>" class="delete_course_link button button-sidebar-wide"><%= image_tag "delete.png" %>End this Course</a>
<a href="<%= context_url(@context, :context_start_copy_url) %>" class="button button-sidebar-wide"><%= image_tag "file_multiple.png" %>Copy this Course</a>
<a href="<%= context_url(@context, :context_imports_url) %>" class="button button-sidebar-wide"><%= image_tag "file_multiple.png" %>Import Content into this Course</a>
<a href="<%= context_url(@context, :course_content_exports_url) %>" class="button button-sidebar-wide"><%= image_tag "file_multiple.png" %>Export this Course</a>
<% end %>
<table class="summary" style="margin-top: 20px;">
<thead>

View File

@ -182,6 +182,8 @@ javascripts:
- public/javascripts/link_enrollment.js
plugins:
- public/javascripts/plugins.js
content_exports:
- public/javascripts/content_exports.js
stylesheets:
common:

View File

@ -219,6 +219,7 @@ ActionController::Routing::Routes.draw do |map|
m.last_redirect 'items/last', :controller => 'context_modules', :action => 'module_redirect', :last => 1
m.first_redirect 'items/first', :controller => 'context_modules', :action => 'module_redirect', :first => 1
end
course.resources :content_exports, :only => %w(create index destroy show)
course.context_modules_assignment_info 'modules/items/assignment_info', :controller => 'context_modules', :action => 'content_tag_assignment_data', :conditions => {:method => :get}
course.context_modules_item_redirect 'modules/items/:id', :controller => 'context_modules', :action => 'item_redirect', :conditions => {:method => :get}
course.context_modules_item_details 'modules/items/sequence/:id', :controller => 'context_modules', :action => 'item_details', :conditions => {:method => :get}
@ -600,6 +601,14 @@ ActionController::Routing::Routes.draw do |map|
map.saml_logout "saml_logout", :controller => "pseudonym_sessions", :action => "saml_logout"
map.saml_meta_data "saml_meta_data", :controller => 'accounts', :action => 'saml_meta_data'
# Routes for course exports
map.connect 'xsd/:version.xsd', :controller => 'content_exports', :action => 'xml_schema'
map.resources :content_exports do |ce|
ce.resources :files do |file|
file.download 'download', :controller => 'files', :action => 'show', :download => '1'
end
end
Jammit::Routes.draw(map)
# API routes

View File

@ -0,0 +1,20 @@
class AddContentExport < ActiveRecord::Migration
def self.up
create_table :content_exports do |t|
t.integer :user_id, :limit => 8
t.integer :course_id, :limit => 8
t.integer :attachment_id, :limit => 8
t.string :export_type
t.text :settings
t.float :progress
t.string :workflow_state
t.timestamps
end
add_index :content_exports, [:course_id]
add_index :content_exports, [:user_id]
end
def self.down
drop_table :content_exports
end
end

View File

@ -20,17 +20,20 @@ module Canvas::CC
attr_accessor :course, :user, :export_dir, :manifest, :zip_file
def initialize(course, user, opts={})
@course = course
@user = user
def initialize(content_export, opts={})
@content_export = content_export
@course = opts[:course] || @content_export.course
@user = opts[:user] || @content_export.user
@export_dir = nil
@manifest = nil
@zip_file = nil
@zip_name = nil
@logger = Rails.logger
@migration_config = Setting.from_config('external_migration')
end
def self.export(course, user, opts={})
exporter = CCExporter.new(course, user, opts)
def self.export(content_export, opts={})
exporter = CCExporter.new(content_export, opts)
exporter.export
end
@ -41,25 +44,53 @@ module Canvas::CC
@manifest = Manifest.new(self)
@manifest.create_document
@manifest.close
#copy all folder contents into zip file
#create attachment from zip file
#delete directory
rescue => e
#todo error handling
@logger.error e
#delete directory?
copy_all_to_zip
@zip_file.close
if @content_export && File.exists?(@zip_path)
att = Attachment.new
att.context = @content_export
att.user = @content_export.user
up_data = ActionController::TestUploadedFile.new(@zip_path, Attachment.mimetype(@zip_path))
att.uploaded_data = up_data
if att.save
@content_export.attachment = att
@content_export.save
end
end
rescue
message = $!.to_s
stack = "#{$!}: #{$!.backtrace.join("\n")}"
@content_export.add_error(message, stack) if @content_export
@logger.error $!
return false
ensure
@zip_file.close if @zip_file
if !@migration_config[:keep_after_complete] && File.directory?(@export_dir)
FileUtils::rm_rf(@export_dir)
end
end
true
end
def set_progress(progress)
@content_export.fast_update_progress(progress) if @content_export
end
private
def copy_all_to_zip
Dir["#{@export_dir}/**/**"].each do |file|
next if File.basename(file) == @zip_name
file_path = file.sub(@export_dir+'/', '')
@zip_file.add(file_path, file)
end
end
def create_export_dir
slug = "common_cartridge_#{@course.id}_user_#{@user.id}"
config = Setting.from_config('external_migration')
if config && config[:data_folder]
folder = config[:data_folder]
if @migration_config && @migration_config[:data_folder]
folder = @migration_config[:data_folder]
else
folder = Dir.tmpdir
end
@ -76,8 +107,9 @@ module Canvas::CC
end
def create_zip_file
path = File.join(@export_dir, "#{@course.name.to_url}-export.#{CCHelper::CC_EXTENSION}")
@zip_file = Zip::ZipFile.new(path, Zip::ZipFile::CREATE)
@zip_name = "#{@course.name.to_url}-export.#{CCHelper::CC_EXTENSION}"
@zip_path = File.join(@export_dir, @zip_name)
@zip_file = Zip::ZipFile.new(@zip_path, Zip::ZipFile::CREATE)
end
end

View File

@ -18,8 +18,8 @@
module Canvas::CC
module CCHelper
CANVAS_NAMESPACE = 'http://www.instructure.com/xsd/cccv0p1'
XSD_URI = 'cccv0p1.xsd'
CANVAS_NAMESPACE = 'http://canvas.instructure.com/xsd/cccv1p0'
XSD_URI = 'http://canvas.instructure.com/xsd/cccv1p0.xsd'
# IMS formats/types
# The IMS documentation for Common Cartridge has conflicting values

View File

@ -62,8 +62,10 @@ module Canvas::CC
manifest_node.metadata do |md|
create_metadata(md)
end
set_progress(5)
Organization.create_organizations(self, manifest_node)
set_progress(10)
Resource.create_resources(self, manifest_node)
@ -96,5 +98,9 @@ module Canvas::CC
end
end
end
def set_progress(progress)
@exporter.set_progress(progress)
end
end
end

View File

@ -44,15 +44,26 @@ module Canvas::CC
@manifest_node.resources do |resources|
@resources = resources
add_canvas_non_cc_data
set_progress(15)
add_wiki_pages
set_progress(30)
add_assignments
set_progress(35)
add_topics
add_web_links
set_progress(40)
add_course_files
set_progress(70)
QTI::QTIGenerator.generate_qti(@manifest, resources)
set_progress(90)
create_basic_lti_links
#todo download kaltura videos?
end
end
def set_progress(progress)
@manifest.set_progress(progress)
end
end
end

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://www.instructure.com/xsd/cccv0p1"
xmlns="http://www.instructure.com/xsd/cccv0p1"
<xs:schema targetNamespace="http://canvas.instructure.com/xsd/cccv1p0"
xmlns="http://canvas.instructure.com/xsd/cccv1p0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<!-- todo: useful comments as documentation ;) -->
@ -74,10 +74,10 @@
<xs:element name="unlock_at" type="xs:dateTime" minOccurs="0"/>
<xs:element name="peer_reviews_due_at" type="xs:dateTime" minOccurs="0"/>
<xs:element name="all_day_date" type="xs:date" minOccurs="0"/>
<xs:element name="assignment_group_identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="grading_standard_identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="rubric_identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="quiz_identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="assignment_group_identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="grading_standard_identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="rubric_identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="quiz_identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="allowed_extensions" type="xs:string" minOccurs="0"/>
<xs:element name="points_possible" type="xs:float" minOccurs="0"/>
<xs:element name="min_score" type="xs:float" minOccurs="0"/>
@ -140,7 +140,7 @@
<xs:complexType>
<xs:all minOccurs="0">
<xs:element name="title" type="xs:string" minOccurs="0"/>
<xs:element name="identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="identifierref" type="xs:string" minOccurs="0"/>
</xs:all>
<xs:attribute name="type" type="xs:string" use="required"/>
</xs:complexType>
@ -156,7 +156,7 @@
<xs:complexType>
<xs:all minOccurs="0">
<xs:element name="content_type" type="xs:string" minOccurs="1"/>
<xs:element name="identifierref" type="xs:IDREFS" minOccurs="0"/>
<xs:element name="identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="url" type="xs:string" minOccurs="0"/>
<xs:element name="position" type="xs:integer" minOccurs="0"/>
<xs:element name="indent" type="xs:integer" minOccurs="0"/>
@ -174,7 +174,7 @@
<xs:element name="completionRequirement">
<xs:complexType>
<xs:all minOccurs="0">
<xs:element name="identifierref" type="xs:IDREF" minOccurs="1"/>
<xs:element name="identifierref" type="xs:string" minOccurs="1"/>
<xs:element name="min_score" type="xs:integer" minOccurs="0"/>
<xs:element name="max_score" type="xs:integer" minOccurs="0"/>
</xs:all>
@ -258,7 +258,7 @@
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="drop_count" type="xs:integer" minOccurs="0"/>
</xs:all>
</xs:complexType>
@ -364,7 +364,7 @@
<xs:element name="points" type="xs:float" minOccurs="0"/>
<xs:element name="mastery_points" type="xs:float" minOccurs="0"/>
<xs:element name="ignore_for_scoring" type="xs:boolean" minOccurs="0"/>
<xs:element name="learning_outcome_identifierref" type="xs:IDREF" minOccurs="0"/>
<xs:element name="learning_outcome_identifierref" type="xs:string" minOccurs="0"/>
<xs:element name="ratings" type="ratingsType" minOccurs="0"/>
</xs:all>
</xs:complexType>

View File

@ -188,6 +188,28 @@ namespace :db do
}, %{
There was a problem importing <%= asset.migration_settings[:course_name] %> into <%= asset.context.name %>. Please notify your system administrator, and give them the following error code: "ContentMigration:<%= asset.id %>:<%= asset.progress %>".
}
create_notification 'ContentExport', 'Message', 0,
'http://<%= HostUrl.default_host %>', %{
Content Export Finished
Course Export Finished: <%= asset.context.name %>
Your course export for "<%= asset.context.name %>" has finished.
}, %{
Your course export for "<%= asset.context.name %>" has finished.
}
create_notification 'ContentExport', 'Message', 0,
'http://<%= HostUrl.default_host %>', %{
Content Export Failed
Course Export failed: <%= asset.context.name %>
There was a problem exporting the course "<%= asset.context.name %>".
}, %{
There was a problem exporting the course "<%= asset.context.name %>".
}
create_notification 'User', 'Other', 0,
'http://<%= HostUrl.default_host %>', %{

View File

@ -0,0 +1,111 @@
/**
* Copyright (C) 2011 Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
$(document).ready(function(event) {
var state = 'nothing';
var current_id = null;
function startPoll() {
$("#exporter_form").html("Processing<div style='font-size: 0.8em;'>this may take a bit...</div>")
.attr('disabled', true);
$(".instruction").hide();
$(".progress_bar_holder").slideDown();
$(".export_progress").progressbar();
state = "nothing";
var fakeTickCount = 0;
var tick = function() {
if(state == "nothing") {
fakeTickCount++;
var progress = ($(".export_progress").progressbar('option', 'value') || 0) + 0.25;
if(fakeTickCount < 10) {
$(".export_progress").progressbar('option', 'value', progress);
}
setTimeout(tick, 2000);
} else {
state = "nothing";
fakeTickCount = 0;
setTimeout(tick, 10000);
}
};
var checkup = function() {
var lastProgress = null;
var waitTime = 1500;
$.ajaxJSON(location.href + "/" + current_id, 'GET', {}, function(data) {
state = "updating";
var content_export = data.content_export;
var progress = 0;
if(content_export) {
progress = Math.max($(".export_progress").progressbar('option', 'value') || 0, content_export.progress);
$(".export_progress").progressbar('option', 'value', progress);
}
if(content_export.workflow_state == 'exported') {
$("#exporter_form").hide();
$(".export_progress").progressbar('option', 'value', 100);
$(".progress_message").html("The course has been exported.");
$("#exports").append('<p>New Course Export: <a href="' + content_export.download_url + '">Click here to download</a> </p>')
} else if(content_export.workflow_state == 'failed') {
code = "content_export_" + content_export.id;
$(".progress_bar_holder").hide();
$("#exporter_form").hide();
var message = "There was an error exporting your course. Please notify your system administrator and give them the following export identifier: \"" + code + "\"";
$(".export_messages .error_message").html(message);
$(".export_messages").show();
} else {
if(progress == lastProgress) {
waitTime = Math.max(waitTime + 500, 30000);
} else {
waitTime = 1500;
}
lastProgress = progress;
setTimeout(checkup, 1500);
}
}, function() {
setTimeout(checkup, 3000);
});
};
setTimeout(checkup, 2000);
setTimeout(tick, 1000)
}
$("#exporter_form").formSubmit({
success: function(data) {
if(data && data.content_export) {
current_id = data.content_export.id
startPoll();
} else {
//show error message
$(".export_messages .error_message").text(data.error_message);
$(".export_messages").show();
}
},
error: function(data) {
$(this).find(".submit_button").attr('disabled', false).text("Process Data");
}
});
function check_if_exporting() {
//state = "checking";
if( $('#current_export_id').size() ){
//state = "nothing";
current_id = $('#current_export_id').text()
startPoll();
}
}
check_if_exporting();
});

View File

@ -238,7 +238,7 @@ module Instructure #:nodoc:
def broadcast_notifications
return if @broadcasted
@broadcasted = true
raise ArgumentError, "Block not supplied for #{self.class.to_s}" unless self.class.broadcast_policy_block
raise ArgumentError, "Broadcast Policy block not supplied for #{self.class.to_s}" unless self.class.broadcast_policy_block
self.instance_eval &self.class.broadcast_policy_block
self.broadcast_policy_list.each {|p| p.broadcast(self) }
end