2011-02-01 09:57:29 +08:00
#
# 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 WebConference < ActiveRecord :: Base
include SendToStream
2011-04-19 06:26:23 +08:00
include TextHelper
2012-04-25 05:31:03 +08:00
attr_accessible :title , :duration , :description , :conference_type , :user , :user_settings , :context
2011-02-01 09:57:29 +08:00
attr_readonly :context_id , :context_type
belongs_to :context , :polymorphic = > true
has_many :web_conference_participants
has_many :users , :through = > :web_conference_participants
has_many :invitees , :through = > :web_conference_participants , :source = > :user , :conditions = > [ 'web_conference_participants.participation_type = ?' , 'invitee' ]
has_many :attendees , :through = > :web_conference_participants , :source = > :user , :conditions = > [ 'web_conference_participants.participation_type = ?' , 'attendee' ]
belongs_to :user
validates_length_of :description , :maximum = > maximum_text_length , :allow_nil = > true , :allow_blank = > true
2013-08-08 06:19:48 +08:00
validates_presence_of :conference_type , :title , :context_id , :context_type , :user_id
2011-02-01 09:57:29 +08:00
2011-02-18 08:45:55 +08:00
before_validation :infer_conference_details
2011-02-01 09:57:29 +08:00
before_create :assign_uuid
after_save :touch_context
has_a_broadcast_policy
2013-03-21 03:38:19 +08:00
scope :for_context_codes , lambda { | context_codes | where ( :context_code = > context_codes ) }
2011-02-01 09:57:29 +08:00
2011-04-06 07:33:58 +08:00
serialize :settings
def settings
2011-04-19 06:26:23 +08:00
read_attribute ( :settings ) || write_attribute ( :settings , default_settings )
end
# whether they replace the whole hash or just update some values, make sure
# we save those changes (after we sanitize it)
before_save :merge_user_settings
def merge_user_settings
unless user_settings . empty?
2013-05-21 04:20:08 +08:00
( type ? type . constantize : self ) . user_setting_fields . each do | name , field_data |
next if field_data [ :restricted_to ] && ! field_data [ :restricted_to ] . call ( self )
settings [ name ] = cast_setting ( user_settings [ name ] , field_data [ :type ] )
2011-04-19 06:26:23 +08:00
end
@user_settings = nil
end
end
def user_settings = ( new_settings )
@user_settings = new_settings . symbolize_keys
end
def user_settings
@user_settings || =
2013-05-21 04:20:08 +08:00
self . class . user_setting_fields . keys . inject ( { } ) { | hash , key |
2011-04-19 06:26:23 +08:00
hash [ key ] = settings [ key ]
hash
}
end
2011-05-07 02:44:34 +08:00
def external_urls_name ( key )
external_urls [ key ] [ :name ] . call
end
def external_urls_link_text ( key )
external_urls [ key ] [ :link_text ] . call
end
2011-04-19 06:26:23 +08:00
def cast_setting ( value , type )
case type
2011-09-27 12:53:08 +08:00
when :boolean
[ '1' , 'on' , 'true' ] . include? ( value . to_s )
2011-04-19 06:26:23 +08:00
else value
end
end
def friendly_setting ( value )
case value
2011-09-27 12:53:08 +08:00
when true
t ( '#web_conference.settings.boolean.true' , " On " )
when false
t ( '#web_conference.settings.boolean.false' , " Off " )
2011-04-19 06:26:23 +08:00
else value . to_s
end
end
def default_settings
@default_settings || =
2013-05-21 04:20:08 +08:00
self . class . user_setting_fields . inject ( { } ) { | hash , ( name , data ) |
2011-04-19 06:26:23 +08:00
hash [ name ] = data [ :default ] if data [ :default ]
hash
}
end
2013-05-21 04:20:08 +08:00
def self . user_setting_field ( name , options )
user_setting_fields [ name ] = options
2011-04-19 06:26:23 +08:00
end
2013-10-29 05:21:23 +08:00
if CANVAS_RAILS2
def self . user_setting_fields
read_inheritable_attribute ( :user_setting_fields ) || write_inheritable_attribute ( :user_setting_fields , { } )
end
else
class_attribute :user_setting_fields
self . user_setting_fields = { }
2013-05-21 04:20:08 +08:00
end
def self . user_setting_field_name ( key )
user_setting_fields [ key ] [ :name ] . call
end
def self . user_setting_field_description ( key )
user_setting_fields [ key ] [ :description ] . call
2011-04-19 06:26:23 +08:00
end
def external_urls
@external_urls || = self . class . external_urls . dup . delete_if { | key , info | info [ :restricted_to ] && ! info [ :restricted_to ] . call ( self ) }
end
# #{key}_external_url should return an array of hashes with url information (:name, :id, and :url).
# if there is just one, we will redirect, otherwise we'll present links to all of them (possibly
# redirecting through here again in case the url has a short-lived token and needs to be
# regenerated)
def external_url_for ( key , user , url_id = nil )
external_urls [ key . to_sym ] &&
respond_to? ( " #{ key } _external_url " ) &&
send ( " #{ key } _external_url " , user , url_id ) || [ ]
end
2013-10-29 05:21:23 +08:00
if CANVAS_RAILS2
def self . external_urls
read_inheritable_attribute ( :external_urls ) || write_inheritable_attribute ( :external_urls , { } )
end
else
class_attribute :external_urls
self . external_urls = { }
2011-04-19 06:26:23 +08:00
end
def self . external_url ( name , options )
external_urls [ name ] = options
2011-04-06 07:33:58 +08:00
end
2011-02-01 09:57:29 +08:00
def assign_uuid
2011-04-15 06:09:37 +08:00
self . uuid || = AutoHandle . generate_securish_uuid
2011-02-01 09:57:29 +08:00
end
protected :assign_uuid
set_broadcast_policy do | p |
p . dispatch :web_conference_invitation
2012-04-25 05:31:03 +08:00
p . to { @new_participants . select { | p | context . membership_for_user ( p ) . active? } }
2011-02-01 09:57:29 +08:00
p . whenever { | record |
@new_participants && ! @new_participants . empty?
}
end
on_create_send_to_streams do
[ self . user_id ] + self . web_conference_participants . map ( & :user_id )
end
def add_user ( user , type )
return unless user
2011-05-14 00:49:23 +08:00
p = self . web_conference_participants . find_by_web_conference_id_and_user_id ( self . id , user . id )
p || = self . web_conference_participants . build ( :web_conference = > self , :user = > user )
2011-02-01 09:57:29 +08:00
p . participation_type = type unless type == 'attendee' && p . participation_type == 'initiator'
2011-05-20 04:40:55 +08:00
( @new_participants || = [ ] ) << user if p . new_record?
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
# Once anyone starts attending the conference, mark it as started.
2011-02-01 09:57:29 +08:00
if type == 'attendee'
self . started_at || = Time . now
self . save
end
p . save
end
def added_users
attendees
end
def add_initiator ( user )
add_user ( user , 'initiator' )
end
def add_invitee ( user )
add_user ( user , 'invitee' )
end
def add_attendee ( user )
add_user ( user , 'attendee' )
end
def context_code
read_attribute ( :context_code ) || " #{ self . context_type . underscore } _ #{ self . context_id } " rescue nil
end
def infer_conference_settings
end
def conference_type = ( val )
2011-02-18 08:45:55 +08:00
conf_type = WebConference . conference_types . detect { | t | t [ :conference_type ] == val }
2011-02-01 09:57:29 +08:00
if conf_type
2011-02-18 08:45:55 +08:00
write_attribute ( :conference_type , conf_type [ :conference_type ] )
write_attribute ( :type , conf_type [ :class_name ] )
conf_type [ :conference_type ]
2011-02-01 09:57:29 +08:00
else
nil
end
end
def infer_conference_details
infer_conference_settings
2011-02-18 08:45:55 +08:00
self . conference_type || = config && config [ :conference_type ]
2011-02-01 09:57:29 +08:00
self . context_code = " #{ self . context_type . underscore } _ #{ self . context_id } " rescue nil
self . user_ids || = ( self . user_id || " " ) . to_s
self . added_user_ids || = " "
2011-06-16 04:39:32 +08:00
self . title || = self . context . is_a? ( Course ) ? t ( '#web_conference.default_name_for_courses' , " Course Web Conference " ) : t ( '#web_conference.default_name_for_groups' , " Group Web Conference " )
2011-02-01 09:57:29 +08:00
self . start_at || = self . started_at
self . end_at || = self . ended_at
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
self . end_at || = self . start_at + self . duration . minutes if self . start_at && self . duration
2011-02-01 09:57:29 +08:00
if self . started_at && self . ended_at && self . ended_at < self . started_at
self . ended_at = self . started_at
end
end
def initiator
self . user
end
def available?
! self . started_at
end
def finished?
self . started_at && ! self . active?
end
def restartable?
2011-05-24 02:34:24 +08:00
end_at && Time . now < = end_at && ! long_running?
2011-02-01 09:57:29 +08:00
end
2011-05-24 02:34:24 +08:00
def long_running?
duration . nil?
end
2013-05-23 03:53:24 +08:00
def long_running
long_running? ? 1 : 0
end
2011-05-24 02:34:24 +08:00
DEFAULT_DURATION = 60
2011-02-01 09:57:29 +08:00
def duration_in_seconds
2011-05-24 02:34:24 +08:00
duration ? duration * 60 : nil
2011-02-01 09:57:29 +08:00
end
def running_time
2013-02-14 01:39:53 +08:00
if ended_at . present? && started_at . present?
[ ended_at - started_at , 60 ] . max
else
0
end
2011-02-01 09:57:29 +08:00
end
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
def restart
self . start_at || = Time . now
2011-05-24 02:34:24 +08:00
self . end_at || = self . start_at + self . duration_in_seconds if self . duration
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
self . started_at || = self . start_at
self . ended_at = nil
self . save
end
2011-02-01 09:57:29 +08:00
def active? ( force_check = false )
if ! force_check
2011-05-24 02:34:24 +08:00
return true if self . start_at && ( self . end_at . nil? || self . end_at && Time . now > self . start_at && Time . now < self . end_at )
2011-02-01 09:57:29 +08:00
return true if self . ended_at && Time . now < self . ended_at
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
return false if self . ended_at && Time . now > self . ended_at
2011-02-01 09:57:29 +08:00
return @conference_active if @conference_active
end
@conference_active = ( conference_status == :active )
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
# If somehow the end_at didn't get set, set the end date
# based on the start time and duration
2011-05-24 02:34:24 +08:00
if @conference_active && ! self . end_at && ! long_running?
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
self . start_at || = Time . now
self . end_at = [ self . start_at , Time . now ] . compact . min + self . duration_in_seconds
2011-02-01 09:57:29 +08:00
self . save
revising conferences open/closed logic
this logic didn't make sense to me, and it was obviously
broken, so I rewrote it. These are the new assumptions
for web conferences:
- when the first participant joins a conference, we set
started_at, start_at and end_at
- once nobody is using the conference anymore and end_at
has passed, or it is more than 15 minutes past
end_at, we set ended_at
- if ended_at is set and has passed, the conference is
considered closed
- if ended_at is not set and a user tries to join an
inactive conference, don't let them unless they're
authorized to resume the conference
- conferences are resumable by those with permission, but
only until the end_at date, after that it's all over
fixes #3827
Change-Id: I3f7474c314a99f0fd5a2b7f9222216d2fc3168a1
Reviewed-on: https://gerrit.instructure.com/2304
Reviewed-by: Zach Wily <zach@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-02-16 04:21:16 +08:00
# If the conference is still active but it's been more than fifteen minutes
# since it was supposed to end, just go ahead and end it
elsif @conference_active && self . end_at && self . end_at < 15 . minutes . ago && ! self . ended_at
self . ended_at = Time . now
self . start_at || = self . started_at
self . end_at || = self . ended_at
@conference_active = false
self . save
# If the conference is no longer in use and its end_at has passed,
# consider it ended
elsif @conference_active == false && self . started_at && self . end_at && self . end_at < Time . now && ! self . ended_at
2011-05-24 02:34:24 +08:00
close
2011-02-01 09:57:29 +08:00
end
@conference_active
2013-05-23 03:53:24 +08:00
rescue Errno :: ECONNREFUSED = > ex
# Account credentials changed, server unreachable/down, bad stuff happened.
@conference_active = false
@conference_active
2011-02-01 09:57:29 +08:00
end
2011-05-24 02:34:24 +08:00
def close
self . ended_at = Time . now
self . start_at || = started_at
self . end_at || = ended_at
save
end
2011-02-01 09:57:29 +08:00
def presenter_key
@presenter_key || = " instructure_ " + Digest :: MD5 . hexdigest ( [ user_id , self . uuid ] . join ( " , " ) )
end
def attendee_key
@attendee_key || = self . conference_key
end
2013-05-21 04:20:08 +08:00
# Default implementaiton since not every conference type requires initiation
2011-02-01 09:57:29 +08:00
def initiate_conference
2011-02-18 08:45:55 +08:00
true
2011-02-01 09:57:29 +08:00
end
2013-05-23 03:53:24 +08:00
# Default implementation since most implementations don't support recording yet
2013-05-21 04:20:08 +08:00
def recordings
[ ]
end
2011-02-01 09:57:29 +08:00
def craft_url ( user = nil , session = nil , return_to = " http://www.instructure.com " )
user || = self . user
2011-06-02 00:12:00 +08:00
initiate_conference and touch or return nil
2013-10-24 08:08:19 +08:00
if user == self . user || self . grants_right? ( user , session , :initiate )
2011-02-01 09:57:29 +08:00
admin_join_url ( user , return_to )
else
participant_join_url ( user , return_to )
end
end
2011-08-26 00:54:00 +08:00
def has_advanced_settings?
respond_to? ( :admin_settings_url )
end
def has_advanced_settings
has_advanced_settings? ? 1 : 0
end
2013-03-21 03:38:19 +08:00
scope :after , lambda { | date | where ( " web_conferences.start_at IS NULL OR web_conferences.start_at>? " , date ) }
2011-02-01 09:57:29 +08:00
set_policy do
given { | user , session | self . users . include? ( user ) && self . cached_context_grants_right? ( user , session , :read ) }
2011-07-14 00:24:17 +08:00
can :read and can :join
2011-02-01 09:57:29 +08:00
2012-03-15 09:02:26 +08:00
given { | user , session | self . users . include? ( user ) && self . cached_context_grants_right? ( user , session , :read ) && long_running? && active? }
can :resume
2011-02-01 09:57:29 +08:00
given { | user , session | ( self . is_public rescue false ) }
2011-07-14 00:24:17 +08:00
can :read and can :join
2011-02-01 09:57:29 +08:00
given { | user , session | self . cached_context_grants_right? ( user , session , :create_conferences ) }
2011-07-14 00:24:17 +08:00
can :create
2011-02-01 09:57:29 +08:00
2011-02-12 05:27:23 +08:00
given { | user , session | user && user . id == self . user_id && self . cached_context_grants_right? ( user , session , :create_conferences ) }
2011-07-14 00:24:17 +08:00
can :initiate
2011-02-12 05:27:23 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | self . cached_context_grants_right? ( user , session , :manage_content ) }
2011-07-14 00:24:17 +08:00
can :read and can :join and can :initiate and can :create and can :delete
2011-05-24 02:34:24 +08:00
given { | user , session | cached_context_grants_right? ( user , session , :manage_content ) && ! finished? }
2011-07-14 00:24:17 +08:00
can :update
2011-05-24 02:34:24 +08:00
given { | user , session | cached_context_grants_right? ( user , session , :manage_content ) && long_running? && active? }
2011-07-14 00:24:17 +08:00
can :close
2011-02-01 09:57:29 +08:00
end
def config
2011-02-18 08:45:55 +08:00
@config || = WebConference . config ( self . class . to_s )
2011-02-01 09:57:29 +08:00
end
def valid_config?
if ! config
false
else
2011-02-18 08:45:55 +08:00
config [ :class_name ] == self . class . to_s
2011-02-01 09:57:29 +08:00
end
end
2013-03-21 03:38:19 +08:00
scope :active , scoped
2011-02-18 08:45:55 +08:00
2011-08-26 00:54:00 +08:00
def as_json ( options = { } )
2013-05-23 03:53:24 +08:00
url = options . delete ( :url )
join_url = options . delete ( :join_url )
options . reverse_merge! ( :only = > %w( id title description conference_type duration started_at ended_at user_ids context_id context_type context_code ) )
result = super ( options . merge ( :include_root = > false , :methods = > [ :has_advanced_settings , :long_running , :user_settings , :recordings ] ) )
result [ 'url' ] = url
result [ 'join_url' ] = join_url
result
2011-08-26 00:54:00 +08:00
end
2011-02-18 08:45:55 +08:00
def self . plugins
Canvas :: Plugin . all_for_tag ( :web_conferencing )
end
2011-02-01 09:57:29 +08:00
def self . conference_types
2011-04-19 06:26:23 +08:00
plugins . map { | plugin |
2011-08-02 23:39:53 +08:00
next unless plugin . enabled? &&
2011-04-19 06:26:23 +08:00
( klass = ( plugin . base || " #{ plugin . id . classify } Conference " ) . constantize rescue nil ) &&
klass < self . base_ar_class
2011-02-18 08:45:55 +08:00
plugin . settings . merge (
:conference_type = > plugin . id . classify ,
2011-04-19 06:26:23 +08:00
:class_name = > ( plugin . base || " #{ plugin . id . classify } Conference " ) ,
2013-05-21 04:20:08 +08:00
:user_setting_fields = > klass . user_setting_fields ,
2011-04-19 06:26:23 +08:00
:plugin = > plugin
2011-03-19 22:08:37 +08:00
) . with_indifferent_access
2011-04-19 06:26:23 +08:00
} . compact
2011-02-01 09:57:29 +08:00
end
2011-02-18 08:45:55 +08:00
def self . config ( class_name = nil )
if class_name
conference_types . detect { | c | c [ :class_name ] == class_name }
2011-02-01 09:57:29 +08:00
else
2011-02-18 08:45:55 +08:00
conference_types . first
2011-02-01 09:57:29 +08:00
end
end
2011-02-18 08:45:55 +08:00
2011-02-01 09:57:29 +08:00
def self . serialization_excludes ; [ :uuid ] ; end
end