summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorStefan Wintermeyer <stefan.wintermeyer@amooma.de>2012-12-17 12:05:14 +0100
committerStefan Wintermeyer <stefan.wintermeyer@amooma.de>2012-12-17 12:05:14 +0100
commiteaad37485fe59d0306c37cc038dda6d210052910 (patch)
tree072c4b0e33d442528555b82c415f5e7a1712b2b0 /app/models
parent3e706c2025ecc5523e81ad649639ef2ff75e7bac (diff)
parentb80bd744ad873f6fc43018bc4bfb90677de167bd (diff)
Merge branch 'develop'
Diffstat (limited to 'app/models')
-rw-r--r--app/models/.gitkeep0
-rw-r--r--app/models/ability.rb170
-rw-r--r--app/models/access_authorization.rb41
-rw-r--r--app/models/acd_agent.rb39
-rw-r--r--app/models/acd_caller.rb6
-rw-r--r--app/models/address.rb8
-rw-r--r--app/models/api.rb5
-rw-r--r--app/models/api/row.rb152
-rw-r--r--app/models/area_code.rb22
-rw-r--r--app/models/automatic_call_distributor.rb21
-rw-r--r--app/models/call.rb36
-rw-r--r--app/models/call_forward.rb262
-rw-r--r--app/models/call_forward_case.rb13
-rw-r--r--app/models/call_history.rb199
-rw-r--r--app/models/callthrough.rb60
-rw-r--r--app/models/conference.rb63
-rw-r--r--app/models/conference_invitee.rb39
-rw-r--r--app/models/country.rb21
-rw-r--r--app/models/dial_in_number_store.rb16
-rw-r--r--app/models/fax_account.rb77
-rw-r--r--app/models/fax_document.rb82
-rw-r--r--app/models/fax_resolution.rb15
-rw-r--r--app/models/fax_thumbnail.rb8
-rw-r--r--app/models/freeswitch_alias.rb23
-rw-r--r--app/models/freeswitch_call.rb24
-rw-r--r--app/models/freeswitch_cdr.rb4
-rw-r--r--app/models/freeswitch_channel.rb24
-rw-r--r--app/models/freeswitch_complete.rb23
-rw-r--r--app/models/freeswitch_fifo_bridge.rb23
-rw-r--r--app/models/freeswitch_fifo_caller.rb23
-rw-r--r--app/models/freeswitch_fifo_outbound.rb23
-rw-r--r--app/models/freeswitch_interface.rb23
-rw-r--r--app/models/freeswitch_nat.rb23
-rw-r--r--app/models/freeswitch_registration.rb23
-rw-r--r--app/models/freeswitch_task.rb23
-rw-r--r--app/models/freeswitch_voicemail_pref.rb23
-rw-r--r--app/models/gemeinschaft_setup.rb8
-rw-r--r--app/models/gs_cluster_sync_log_entry.rb99
-rw-r--r--app/models/gs_node.rb30
-rw-r--r--app/models/gui_function.rb40
-rw-r--r--app/models/gui_function_membership.rb7
-rw-r--r--app/models/hunt_group.rb43
-rw-r--r--app/models/hunt_group_member.rb67
-rw-r--r--app/models/language.rb11
-rw-r--r--app/models/manufacturer.rb46
-rw-r--r--app/models/oui.rb17
-rw-r--r--app/models/phone.rb240
-rw-r--r--app/models/phone_book.rb33
-rw-r--r--app/models/phone_book_entry.rb109
-rw-r--r--app/models/phone_model.rb56
-rw-r--r--app/models/phone_number.rb304
-rw-r--r--app/models/phone_number_range.rb16
-rw-r--r--app/models/phone_sip_account.rb17
-rw-r--r--app/models/remote_gs_node/gs_cluster_sync_log_entry.rb9
-rw-r--r--app/models/ringtone.rb15
-rw-r--r--app/models/sip_account.rb221
-rw-r--r--app/models/sip_domain.rb16
-rw-r--r--app/models/softkey.rb100
-rw-r--r--app/models/softkey_function.rb13
-rw-r--r--app/models/system_message.rb7
-rw-r--r--app/models/tenant.rb243
-rw-r--r--app/models/tenant_membership.rb25
-rw-r--r--app/models/user.rb208
-rw-r--r--app/models/user_group.rb33
-rw-r--r--app/models/user_group_membership.rb26
-rw-r--r--app/models/voicemail_message.rb52
-rw-r--r--app/models/voicemail_setting.rb12
-rw-r--r--app/models/whitelist.rb23
68 files changed, 3783 insertions, 0 deletions
diff --git a/app/models/.gitkeep b/app/models/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/models/.gitkeep
diff --git a/app/models/ability.rb b/app/models/ability.rb
new file mode 100644
index 0000000..d9ec74a
--- /dev/null
+++ b/app/models/ability.rb
@@ -0,0 +1,170 @@
+class Ability
+ include CanCan::Ability
+
+ def initialize( user )
+ # See the wiki for details: https://github.com/ryanb/cancan/wiki/Defining-Abilities
+ if user && user.current_tenant != nil
+ if GemeinschaftSetup.count == 1 && Tenant.count == 1 && User.count == 1 && UserGroup.count == 1
+ # This is a new installation with a Master-Tenant and a Super-Admin.
+ #
+ can [:read, :create], Tenant
+ else
+ tenant = user.current_tenant
+
+ if user.current_tenant.user_groups.where(:name => 'Admins').first \
+ && user.current_tenant.user_groups.where(:name => 'Admins').first.users.include?(user)
+ # ADMIN ABILITIES
+ # With great power comes great responsibility!
+ #
+ can :manage, :all
+
+ # Manufacturers and PhoneModels can not be changed
+ #
+ cannot [:create, :destroy, :edit, :update], Manufacturer
+ cannot [:create, :destroy, :edit, :update], PhoneModel
+
+ # Super-Tenant can not be destroyed or edited
+ #
+ cannot [:create, :destroy, :edit, :update], Tenant, :id => 1
+
+ cannot :manage, PhoneBook
+
+ # Phonebooks and PhoneBookEntries
+ #
+ can :manage, PhoneBook, :phone_bookable_type => 'Tenant', :phone_bookable_id => tenant.id
+ can :manage, PhoneBookEntry, :phone_book => { :phone_bookable_type => 'Tenant', :phone_bookable_id => tenant.id }
+
+ can :manage, PhoneBook, :phone_bookable_type => 'UserGroup', :phone_bookable_id => tenant.user_group_ids
+ tenant.user_groups.each do |user_group|
+ can :manage, PhoneBookEntry, :phone_book => { :id => user_group.phone_book_ids }
+ end
+
+ # Personal Phonebooks and PhoneBookEntries
+ #
+ can :manage, PhoneBook, :phone_bookable_type => 'User', :phone_bookable_id => user.id
+ can :manage, PhoneBookEntry, :phone_book => { :phone_bookable_type => 'User', :phone_bookable_id => user.id }
+
+ can :read, PhoneBook, :phone_bookable_type => 'Tenant', :phone_bookable_id => tenant.id
+ can :read, PhoneBookEntry, :phone_book => { :phone_bookable_type => 'Tenant', :phone_bookable_id => tenant.id }
+
+ can :read, PhoneBook, :phone_bookable_type => 'UserGroup', :phone_bookable_id => user.user_group_ids
+ user.user_groups.each do |user_group|
+ can :read, PhoneBookEntry, :phone_book => { :id => user_group.phone_book_ids }
+ end
+
+ # SystemMessages
+ #
+ cannot [:destroy, :edit, :update], SystemMessage
+
+ # A FacDocument can't be changed
+ #
+ cannot [:edit, :update], FaxDocument
+
+ # Can manage GsNodes
+ #
+ can :manage, GsNode
+
+ # Can't phones/1/phone_sip_accounts/1/edit
+ #
+ cannot :edit, PhoneSipAccount
+
+ # Dirty hack to disable PhoneNumberRange in the GUI
+ #
+ if STRICT_INTERNAL_EXTENSION_HANDLING == false
+ cannot :manage, PhoneNumberRange
+ end
+ else
+ # Any user can do the following stuff.
+ #
+
+ # Own Tenant and own User
+ #
+ can :read, Tenant, :id => user.current_tenant.id
+ can [ :read, :edit, :update ], User, :id => user.id
+
+ # Destroy his own avatar
+ #
+ can :destroy_avatar, User, :id => user.id
+
+ # Phonebooks and PhoneBookEntries
+ #
+ cannot :manage, PhoneBook
+
+ can :manage, PhoneBook, :phone_bookable_type => 'User', :phone_bookable_id => user.id
+ can :manage, PhoneBookEntry, :phone_book => { :phone_bookable_type => 'User', :phone_bookable_id => user.id }
+ can :manage, PhoneNumber, :phone_numberable_type => 'PhoneBookEntry', :phone_numberable_id => user.phone_books.map{ |phone_book| phone_book.phone_book_entry_ids}.flatten
+
+ can :read, PhoneBook, :phone_bookable_type => 'Tenant', :phone_bookable_id => tenant.id
+ can :read, PhoneBookEntry, :phone_book => { :phone_bookable_type => 'Tenant', :phone_bookable_id => tenant.id }
+
+ can :read, PhoneBook, :phone_bookable_type => 'UserGroup', :phone_bookable_id => user.user_group_ids
+ user.user_groups.each do |user_group|
+ can :read, PhoneBookEntry, :phone_book => { :id => user_group.phone_book_ids }
+ end
+
+ # UserGroups
+ #
+ can :read, UserGroupMembership, :user_id => user.id
+ can :read, UserGroup, :users => { :user_group_memberships => { :user_id => user.id }}
+
+ # SipAccounts and Phones
+ #
+ can :read, SipAccount, :sip_accountable_type => 'User', :sip_accountable_id => user.id
+ user.sip_accounts.each do |sip_account|
+ can :read, PhoneNumber, :id => sip_account.phone_number_ids
+ can :manage, CallForward, :phone_number_id => sip_account.phone_number_ids
+ can :manage, Ringtone, :ringtoneable_type => 'PhoneNumber', :ringtoneable_id => sip_account.phone_number_ids
+ can [:read, :destroy, :call] , CallHistory, :id => sip_account.call_history_ids
+ end
+ can :read, Phone, :phoneable_type => 'User', :phoneable_id => user.id
+
+ # Softkeys
+ #
+ can :manage, Softkey, :sip_account => { :id => user.sip_account_ids }
+
+ # Fax
+ #
+ can :read, FaxAccount, :fax_accountable_type => 'User', :fax_accountable_id => user.id
+ user.fax_accounts.each do |fax_account|
+ can :read, PhoneNumber, :id => fax_account.phone_number_ids
+ can [:read, :create, :delete], FaxDocument, :fax_account_id => fax_account.id
+ end
+
+ # Conferences
+ #
+ can [ :read, :edit, :update, :destroy ], Conference, :id => user.conference_ids
+ user.conferences.each do |conference|
+ can :read, PhoneNumber, :id => conference.phone_number_ids
+ can :manage, ConferenceInvitee, :conference_id => conference.id
+ end
+
+ # User can manage CallForwards of the PhoneNumbers of his
+ # own SipAccounts:
+ #
+ can :manage, CallForward, :phone_number_id => user.phone_number_ids
+
+ # SystemMessages
+ #
+ can :read, SystemMessage, :user_id => user.id
+
+ # SoftkeyFunctions
+ #
+ can :read, SoftkeyFunction
+
+ # Voicemail
+ #
+ can :manage, VoicemailMessage
+ can :manage, VoicemailSetting
+ end
+ end
+ else
+ if GemeinschaftSetup.count == 0 && Tenant.count == 0 && User.count == 0
+ # This is a fresh system.
+ #
+ can :create, GemeinschaftSetup
+ can :manage, SipDomain
+ end
+ end
+
+ end
+end
diff --git a/app/models/access_authorization.rb b/app/models/access_authorization.rb
new file mode 100644
index 0000000..ef33115
--- /dev/null
+++ b/app/models/access_authorization.rb
@@ -0,0 +1,41 @@
+class AccessAuthorization < ActiveRecord::Base
+ attr_accessible :name, :login, :pin, :phone_numbers_attributes, :sip_account_id
+
+ belongs_to :access_authorizationable, :polymorphic => true
+
+ validates_uniqueness_of :name, :scope => [ :access_authorizationable_type, :access_authorizationable_id ],
+ :allow_nil => true, :allow_blank => true
+
+ # The login is optional. But if set has to be done with digits only.
+ #
+ validates_format_of :login, :with => /\A([0-9]+)\Z/,
+ :allow_nil => true, :allow_blank => true,
+ :message => "must be numeric."
+
+ # The PIN is optional. But when set it has to be a proper PIN.
+ #
+ validates_format_of :pin, :with => /\A([0-9]+)\Z/,
+ :allow_nil => true, :allow_blank => true,
+ :message => "must be numeric."
+
+ validates_length_of :pin, :minimum => MINIMUM_PIN_LENGTH, :maximum => MAXIMUM_PIN_LENGTH,
+ :allow_nil => true, :allow_blank => true
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ accepts_nested_attributes_for :phone_numbers,
+ :reject_if => lambda { |phone_number| phone_number[:number].blank? },
+ :allow_destroy => true
+
+ # Optional SIP account.
+ #
+ belongs_to :sip_account
+
+ validates_presence_of :sip_account, :if => Proc.new{ |access_authorization| !access_authorization.sip_account_id.blank? },
+ :message => 'Given SIP account does not exist.'
+
+ acts_as_list :scope => [ :access_authorizationable_type, :access_authorizationable_id ]
+
+ def to_s
+ self.name || I18n.t('access_authorizations.name') + ' ID ' + self.id.to_s
+ end
+end
diff --git a/app/models/acd_agent.rb b/app/models/acd_agent.rb
new file mode 100644
index 0000000..a00ac4b
--- /dev/null
+++ b/app/models/acd_agent.rb
@@ -0,0 +1,39 @@
+class AcdAgent < ActiveRecord::Base
+ DESTINATION_TYPES = ['SipAccount']
+ STATUSES = ['active', 'inactive']
+
+ attr_accessible :uuid, :name, :status, :automatic_call_distributor_id, :last_call, :calls_answered, :destination_type, :destination_id
+
+ belongs_to :automatic_call_distributor
+
+ belongs_to :destination, :polymorphic => true
+
+ after_save :set_presence
+
+ def to_s
+ self.name || I18n.t('acd_agents.name') + ' ID ' + self.id.to_s
+ end
+
+ private
+ def set_presence
+ dialplan_function = nil
+
+ state = 'early'
+ if self.status == 'active'
+ state = 'confirmed'
+ elsif self.status == 'inactive'
+ state = 'terminated'
+ end
+
+ require 'freeswitch_event'
+ event = FreeswitchEvent.new("PRESENCE_IN")
+ event.add_header("proto", "sip")
+ event.add_header("from", "f-acdmtg-#{self.id}@#{SipDomain.first.host}")
+ event.add_header("event_type", "presence")
+ event.add_header("alt_event_type", "dialog")
+ event.add_header("presence-call-direction", "outbound")
+ event.add_header("answer-state", state)
+ event.add_header("unique-id", "acd_agent_#{self.id}")
+ return event.fire()
+ end
+end
diff --git a/app/models/acd_caller.rb b/app/models/acd_caller.rb
new file mode 100644
index 0000000..1be48b9
--- /dev/null
+++ b/app/models/acd_caller.rb
@@ -0,0 +1,6 @@
+class AcdCaller < ActiveRecord::Base
+ attr_accessible :channel_uuid, :automatic_call_distributor_id, :status, :enter_time, :agent_answer_time, :callback_number, :callback_attempts
+
+ has_one :channel, :class_name => 'FreeswitchChannel', :foreign_key => 'uuid', :primary_key => 'channel_uuid'
+ belongs_to :automatic_call_distributor
+end
diff --git a/app/models/address.rb b/app/models/address.rb
new file mode 100644
index 0000000..bcf3474
--- /dev/null
+++ b/app/models/address.rb
@@ -0,0 +1,8 @@
+class Address < ActiveRecord::Base
+ attr_accessible :phone_book_entry_id, :line1, :line2, :street, :zip_code, :city, :country_id, :position, :uuid
+
+ belongs_to :country
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+end
diff --git a/app/models/api.rb b/app/models/api.rb
new file mode 100644
index 0000000..557d875
--- /dev/null
+++ b/app/models/api.rb
@@ -0,0 +1,5 @@
+module Api
+ def self.table_name_prefix
+ 'api_'
+ end
+end
diff --git a/app/models/api/row.rb b/app/models/api/row.rb
new file mode 100644
index 0000000..ac35516
--- /dev/null
+++ b/app/models/api/row.rb
@@ -0,0 +1,152 @@
+class Api::Row < ActiveRecord::Base
+
+ # This is the place to do some basic mapping.
+ #
+ alias_attribute :UserName, :user_name
+ alias_attribute :LastName, :last_name
+ alias_attribute :FirstName, :first_name
+ alias_attribute :PhoneOffice, :office_phone_number
+ alias_attribute :VoipNr, :internal_extension
+ alias_attribute :CellPhone, :mobile_phone_number
+ alias_attribute :Fax, :fax_phone_number
+ alias_attribute :Email, :email
+ alias_attribute :PIN, :pin
+ alias_attribute :PIN_LastUpdate, :pin_updated_at
+ alias_attribute :Photo, :photo_file_name
+
+ belongs_to :user
+
+ # Validations
+ #
+ validates_presence_of :user_name
+ validates_uniqueness_of :user_name
+
+ after_destroy :destroy_user
+
+ def to_s
+ self.user_name
+ end
+
+ def create_a_new_gemeinschaft_user
+ tenant = Tenant.find(DEFAULT_API_TENANT_ID)
+
+ # Find or create the user
+ #
+ if tenant.users.where(:user_name => self.user_name).count > 0
+ user = tenant.users.where(:user_name => self.user_name).first
+ else
+ user = tenant.users.create(
+ :user_name => self.user_name,
+ :last_name => self.last_name,
+ :first_name => self.first_name,
+ :middle_name => self.middle_name,
+ :email => self.email,
+ :new_pin => self.pin,
+ :new_pin_confirmation => self.pin,
+ :password => self.pin,
+ :password_confirmation => self.pin,
+ :language_id => tenant.language_id,
+ )
+ end
+
+ self.update_attributes({:user_id => user.id})
+
+ # Find or create a sip_account
+ #
+ if user.sip_accounts.count > 0
+ sip_account = user.sip_accounts.first
+ else
+ sip_account = user.sip_accounts.create(
+ :caller_name => self.user.to_s,
+ :voicemail_pin => self.pin,
+ )
+ end
+
+ # Create phone_numbers to this sip_account (BTW: phone_numbers are unqiue)
+ #
+ sip_account.phone_numbers.create(:number => self.internal_extension)
+ sip_account.phone_numbers.create(:number => self.office_phone_number)
+
+
+ # Find or create a fax account
+ #
+ if user.fax_accounts.count > 0
+ fax_account = user.fax_accounts.first
+ else
+ fax_account = user.fax_accounts.create(
+ :name => 'Default Fax',
+ :station_id => user.to_s,
+ :email => self.email,
+ :days_till_auto_delete => 90,
+ :retries => 3,
+ )
+ end
+
+ # Create phone_numbers to this fax_account
+ #
+ fax_account.phone_numbers.create(:number => self.fax_phone_number)
+
+ end
+
+ def destroy_user
+ self.user.destroy
+ end
+
+ def update_user_data
+ user = self.user
+ user.update_attributes(
+ :user_name => self.user_name,
+ :last_name => self.last_name,
+ :first_name => self.first_name,
+ :middle_name => self.middle_name,
+ :email => self.email,
+ :new_pin => self.pin,
+ :new_pin_confirmation => self.pin,
+ :password => self.pin,
+ :password_confirmation => self.pin,
+ )
+
+ # Find or create a sip_account
+ #
+ if user.sip_accounts.count > 0
+ sip_account = user.sip_accounts.first
+ else
+ sip_account = user.sip_accounts.create(
+ :caller_name => self.user.to_s,
+ :voicemail_pin => self.pin,
+ )
+ end
+
+ # Delete old phone_numbers
+ #
+ sip_account.phone_numbers.destroy_all
+
+ # Create phone_numbers to this sip_account (BTW: phone_numbers are unqiue)
+ #
+ sip_account.phone_numbers.create(:number => self.internal_extension)
+ sip_account.phone_numbers.create(:number => self.office_phone_number)
+
+ # Find or create a fax account
+ #
+ if user.fax_accounts.count > 0
+ fax_account = user.fax_accounts.first
+ else
+ fax_account = user.fax_accounts.create(
+ :name => 'Default Fax',
+ :station_id => user.to_s,
+ :email => self.email,
+ :days_till_auto_delete => 90,
+ :retries => 3,
+ )
+ end
+
+ # Delete old phone_number
+ #
+ fax_account.phone_numbers.destroy_all
+
+ # Create phone_numbers to this fax_account
+ #
+ fax_account.phone_numbers.create(:number => self.fax_phone_number)
+ end
+
+end
diff --git a/app/models/area_code.rb b/app/models/area_code.rb
new file mode 100644
index 0000000..6a9d946
--- /dev/null
+++ b/app/models/area_code.rb
@@ -0,0 +1,22 @@
+class AreaCode < ActiveRecord::Base
+
+ # Associations:
+ #
+ belongs_to :country
+
+ # Validations:
+ #
+ validates_presence_of :country
+ validates_presence_of :name
+ validates_presence_of :area_code
+
+ validates_uniqueness_of :area_code, :scope => [ :country_id, :central_office_code ]
+
+
+ def to_s
+ "#{self.name} (#{self.area_code}" +
+ (self.central_office_code.blank? ? '' : "-#{self.central_office_code}") +
+ ')'
+ end
+
+end
diff --git a/app/models/automatic_call_distributor.rb b/app/models/automatic_call_distributor.rb
new file mode 100644
index 0000000..678e0eb
--- /dev/null
+++ b/app/models/automatic_call_distributor.rb
@@ -0,0 +1,21 @@
+class AutomaticCallDistributor < ActiveRecord::Base
+ attr_accessible :uuid, :name, :strategy, :automatic_call_distributorable_type, :automatic_call_distributorable_id, :max_callers, :agent_timeout, :retry_timeout, :join, :leave, :gs_node_id, :announce_position, :announce_call_agents, :greeting, :goodbye, :music
+
+ belongs_to :automatic_call_distributorable, :polymorphic => true
+
+ has_many :acd_agents, :dependent => :destroy
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ accepts_nested_attributes_for :phone_numbers,
+ :reject_if => lambda { |phone_number| phone_number[:number].blank? },
+ :allow_destroy => true
+
+ validates_presence_of :strategy
+
+ STRATEGIES = ['ring_all', 'round_robin']
+ JOIN_ON = ['agents_available', 'agents_active', 'always']
+ LEAVE_ON = ['no_agents_available_timeout', 'no_agents_active_timeout', 'no_agents_available', 'no_agents_active', 'timeout', 'never']
+
+ def to_s
+ self.name
+ end
+end
diff --git a/app/models/call.rb b/app/models/call.rb
new file mode 100644
index 0000000..57961ec
--- /dev/null
+++ b/app/models/call.rb
@@ -0,0 +1,36 @@
+class Call < ActiveRecord::Base
+ self.table_name = 'channels'
+ self.primary_key = 'uuid'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ def sip_account
+ auth_name = self.name.match('^.+[/:](.+)@.+$')
+ if auth_name && ! auth_name[1].blank?
+ return SipAccount.where(:auth_name => auth_name[1]).first
+ end
+ end
+
+ def kill
+ require 'freeswitch_event'
+ return FreeswitchAPI.execute('uuid_kill', self.uuid, true);
+ end
+end
diff --git a/app/models/call_forward.rb b/app/models/call_forward.rb
new file mode 100644
index 0000000..0018cfb
--- /dev/null
+++ b/app/models/call_forward.rb
@@ -0,0 +1,262 @@
+class CallForward < ActiveRecord::Base
+
+ attr_accessor :to_voicemail, :hunt_group_id
+
+ attr_accessible :phone_number_id, :call_forward_case_id, :timeout,
+ :destination, :source, :depth, :active, :to_voicemail,
+ :hunt_group_id,
+ :call_forwardable_type, :call_forwardable_id,
+ :call_forwarding_destination, :position, :uuid
+
+ belongs_to :phone_number
+ belongs_to :call_forwardable, :polymorphic => true
+ has_many :softkeys
+
+ acts_as_list :scope => [ :phone_number_id, :call_forward_case_id ]
+
+ validates_presence_of :phone_number
+ validates_presence_of :call_forward_case_id
+ validates_presence_of :destination, :if => Proc.new { |cf| cf.call_forwardable_type.to_s.downcase == 'phonenumber' || cf.call_forwardable_type.blank? }
+
+ validates_inclusion_of :destination,
+ :in => [ nil, '' ],
+ :if => Proc.new { |cf| cf.to_voicemail == true }
+
+ belongs_to :call_forward_case
+
+ validates_presence_of :depth
+ validates_numericality_of :depth,
+ :only_integer => true,
+ :greater_than_or_equal_to => 1,
+ :less_than_or_equal_to => MAX_CALL_FORWARD_DEPTH
+
+ before_validation {
+ self.timeout = nil if self.call_forward_case_id != 3
+ }
+
+ validates_numericality_of :timeout,
+ :if => Proc.new { |cf| cf.call_forward_case_id == 3 },
+ :only_integer => true,
+ :greater_than_or_equal_to => 1,
+ :less_than_or_equal_to => 120
+
+ validates_inclusion_of :timeout,
+ :in => [ nil ],
+ :if => Proc.new { |cf| cf.call_forward_case_id != 3 }
+
+ validate :validate_empty_hunt_group, :if => Proc.new { |cf| cf.active == true && cf.call_forwardable_type == 'HuntGroup' && cf.call_forward_case.value == 'assistant' }
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ # Make sure the call forward's parent can't be changed:
+ before_validation { |cfwd|
+ if cfwd.id \
+ && cfwd.phone_number_id != cfwd.phone_number_id_was
+ errors.add( :phone_number_id, "cannot be changed." )
+ end
+ }
+
+ #before_validation :set_call_forwardable
+ before_save :split_and_format_destination_numbers
+ after_save :set_presence
+ after_save :work_through_callforward_rules_act_per_sip_account
+ after_save :deactivate_concurring_entries, :if => Proc.new { |cf| cf.active == true }
+ before_destroy :check_if_other_callforward_rules_have_to_be_destroyed
+ before_destroy :deactivate_connected_softkeys
+
+ def case_string
+ return self.call_forward_case ? self.call_forward_case.value : nil
+ end
+
+ def to_s
+ if self.call_forwardable_type.blank?
+ self.call_forwardable_type = ''
+ else
+ call_forwardable_type = " #{self.call_forwardable_type}"
+ end
+ if self.call_forwardable
+ destination = "#{self.call_forwardable}#{call_forwardable_type}"
+ else
+ destination = "#{self.destination}#{call_forwardable_type}"
+ end
+ "#{self.phone_number} (#{I18n.t("call_forward_cases.#{self.call_forward_case}")}) -> #{destination}"
+ end
+
+ def set_this_callforward_rule_to_all_phone_numbers_of_the_parent_sip_account
+ # This is to make sure that no recursion kicks in.
+ #
+ if ! self.phone_number.phone_numberable.respond_to? :callforward_rules_act_per_sip_account
+ return false
+ end
+
+ old_value_of_callforward_rules_act_per_sip_account = self.phone_number.phone_numberable.callforward_rules_act_per_sip_account
+ self.phone_number.phone_numberable.update_attributes({:callforward_rules_act_per_sip_account => false})
+
+ attributes_of_this_call_forward = self.attributes.delete_if {|key, value| ['id','updated_at','created_at','phone_number_id','call_forward_case_id', 'uuid'].include?(key)}
+ phone_numbers = self.phone_number.phone_numberable.phone_numbers.where('id != ?', self.phone_number.id)
+
+ phone_numbers.each do |phone_number|
+ # Problem
+ call_forward = phone_number.call_forwards.find_or_create_by_call_forward_case_id_and_position(self.call_forward_case_id, self.position, attributes_of_this_call_forward)
+ call_forward.update_attributes(attributes_of_this_call_forward)
+ end
+
+ self.phone_number.phone_numberable.update_attributes({:callforward_rules_act_per_sip_account => old_value_of_callforward_rules_act_per_sip_account})
+ end
+
+ def destroy_all_similar_callforward_rules_of_the_parent_sip_account
+ # This is to make sure that no recursion kicks in.
+ #
+ if ! self.phone_number.phone_numberable.respond_to? :callforward_rules_act_per_sip_account
+ return false
+ end
+
+ old_value_of_callforward_rules_act_per_sip_account = self.phone_number.phone_numberable.callforward_rules_act_per_sip_account
+ self.phone_number.phone_numberable.update_attributes({:callforward_rules_act_per_sip_account => false})
+
+ phone_numbers_of_parent_sip_account = self.phone_number.phone_numberable.phone_numbers.where('id != ?', self.phone_number.id)
+
+ phone_numbers_of_parent_sip_account.each do |phone_number|
+ if self.call_forwardable_type != 'Voicemail'
+ phone_number.call_forwards.where(:call_forward_case_id => self.call_forward_case_id, :destination => self.destination).destroy_all
+ else
+ phone_number.call_forwards.where(:call_forward_case_id => self.call_forward_case_id, :call_forwardable_type => self.call_forwardable_type).destroy_all
+ end
+ end
+
+ self.phone_number.phone_numberable.update_attributes({:callforward_rules_act_per_sip_account => old_value_of_callforward_rules_act_per_sip_account})
+ end
+
+ def call_forwarding_destination
+ "#{self.call_forwardable_id}:#{self.call_forwardable_type}"
+ end
+
+ def call_forwarding_destination=(destination_record)
+ self.call_forwardable_id, delimeter, self.call_forwardable_type = destination_record.to_s.partition(':')
+ end
+
+ def toggle
+ self.active = ! self.active
+ return self.save
+ end
+
+ def deactivate_connected_softkeys
+ softkey_function_deactivated = SoftkeyFunction.find_by_name('deactivated')
+ self.softkeys.each do |softkey|
+ if softkey.softkey_function_id != softkey_function_deactivated.id
+ softkey.update_attributes(:call_forward_id => nil, :softkey_function_id => softkey_function_deactivated.id)
+ end
+ end
+ end
+
+ private
+ def split_and_format_destination_numbers
+ if !self.destination.blank?
+ destinations = self.destination.gsub(/[^+0-9\,]/,'').gsub(/[\,]+/,',').split(/\,/).delete_if{|x| x.blank?}
+ self.destination = nil
+ if destinations.count > 0
+ destinations.each do |single_destination|
+ self.destination = self.destination.to_s + ", #{PhoneNumber.parse_and_format(single_destination)}"
+ end
+ end
+ self.destination = self.destination.to_s.gsub(/[^+0-9\,]/,'').gsub(/[\,]+/,',').split(/\,/).sort.delete_if{|x| x.blank?}.join(', ')
+ end
+ end
+
+ def set_presence
+ state = 'terminated'
+
+ if self.active
+ if self.call_forwardable_type and self.call_forwardable_type.downcase() == 'voicemail'
+ state = 'early'
+ else
+ state = 'confirmed'
+ end
+ end
+
+ return send_presence_event(state)
+
+ #if self.call_forward_case_id_changed?
+ # call_forwarding_service = CallForwardCase.where(:id => self.call_forward_case_id_was).first
+ # if call_forwarding_service
+ # send_presence_event(call_forwarding_service.value, state)
+ # end
+ #end
+
+ #return send_presence_event(self.call_forward_case.value, state)
+ end
+
+ def set_call_forwardable
+ if @hunt_group_id && HuntGroup.where(:id => @hunt_group_id.to_i).count > 0
+ self.call_forwardable = HuntGroup.where(:id => @hunt_group_id.to_i).first
+ end
+
+ if @to_voicemail && @to_voicemail.first.downcase == 'true'
+ self.call_forwardable_type = 'Voicemail'
+ self.call_forwardable_id = nil
+ end
+ end
+
+ def work_through_callforward_rules_act_per_sip_account
+ if ! self.phone_number.phone_numberable.respond_to? :callforward_rules_act_per_sip_account
+ return false
+ end
+
+ if self.phone_number.phone_numberable.callforward_rules_act_per_sip_account == true
+ self.set_this_callforward_rule_to_all_phone_numbers_of_the_parent_sip_account
+ end
+ end
+
+ def check_if_other_callforward_rules_have_to_be_destroyed
+ if ! self.phone_number.phone_numberable.respond_to? :callforward_rules_act_per_sip_account
+ return false
+ end
+
+ if self.phone_number.phone_numberable.callforward_rules_act_per_sip_account == true
+ self.destroy_all_similar_callforward_rules_of_the_parent_sip_account
+ end
+ end
+
+ def send_presence_event(state, call_forwarding_service = nil)
+ dialplan_function = "cftg-#{self.id}"
+ unique_id = "call_forwarding_#{self.id}"
+
+ if call_forwarding_service == 'always'
+ dialplan_function = "cfutg-#{self.phone_number.id}"
+ unique_id = "call_forwarding_number_#{self.phone_number.id}"
+ elsif call_forwarding_service == 'assistant'
+ dialplan_function = "cfatg-#{self.phone_number.id}"
+ unique_id = "call_forwarding_number_#{self.phone_number.id}"
+ end
+
+ if dialplan_function
+ require 'freeswitch_event'
+ event = FreeswitchEvent.new("PRESENCE_IN")
+ event.add_header("proto", "sip")
+ event.add_header("from", "f-#{dialplan_function}@#{SipDomain.first.host}")
+ event.add_header("event_type", "presence")
+ event.add_header("alt_event_type", "dialog")
+ event.add_header("presence-call-direction", "outbound")
+ event.add_header("answer-state", state)
+ event.add_header("unique-id", unique_id)
+ return event.fire()
+ end
+ end
+
+ def deactivate_concurring_entries
+ CallForward.where(:phone_number_id => self.phone_number_id, :call_forward_case_id => self.call_forward_case_id, :active => true).each do |call_forwarding_entry|
+ if call_forwarding_entry.id != self.id
+ call_forwarding_entry.update_attributes(:active => false)
+ end
+ end
+ end
+
+ def validate_empty_hunt_group
+ hunt_group = self.call_forwardable
+ if hunt_group && hunt_group.hunt_group_members.where(:active => true).count == 0
+ errors.add(:call_forwarding_destination, 'HuntGroup has no active members')
+ end
+ end
+
+end
diff --git a/app/models/call_forward_case.rb b/app/models/call_forward_case.rb
new file mode 100644
index 0000000..a0b872b
--- /dev/null
+++ b/app/models/call_forward_case.rb
@@ -0,0 +1,13 @@
+class CallForwardCase < ActiveRecord::Base
+
+ attr_accessible :value
+
+ has_many :call_forwards
+
+ validates_presence_of :value
+
+ def to_s
+ self.value
+ end
+
+end
diff --git a/app/models/call_history.rb b/app/models/call_history.rb
new file mode 100644
index 0000000..4db056a
--- /dev/null
+++ b/app/models/call_history.rb
@@ -0,0 +1,199 @@
+class CallHistory < ActiveRecord::Base
+ belongs_to :call_historyable, :polymorphic => true
+ belongs_to :caller_account, :polymorphic => true
+ belongs_to :callee_account, :polymorphic => true
+ belongs_to :auth_account, :polymorphic => true
+
+ def display_number
+ if self.entry_type == 'dialed'
+ return self.destination_number.to_s
+ else
+ return self.caller_id_number.to_s
+ end
+ end
+
+ def display_name
+ if self.entry_type == 'dialed'
+ begin
+ account = self.callee_account
+ rescue
+ account = nil
+ end
+ name_str = self.callee_id_name
+ else
+ begin
+ account = self.caller_account
+ rescue
+ account = nil
+ end
+ name_str = self.caller_id_name
+ end
+
+ if name_str.blank?
+ if account.class == SipAccount
+ return account.caller_name.to_s
+ elsif account
+ return account.to_s
+ end
+ else
+ return name_str.to_s
+ end
+ end
+
+ def display_auth_account_name
+ begin
+ account = self.auth_account
+ rescue
+ return nil
+ end
+
+ if account.class == SipAccount
+ return account.caller_name.to_s
+ elsif account
+ return account.to_s
+ end
+ end
+
+ def display_image(image_size = :mini, phone_book_entry)
+ if phone_book_entry
+ image = phone_book_entry.image_url(image_size)
+ if ! image.blank?
+ return image
+ end
+ end
+
+ begin
+ if self.entry_type == 'dialed'
+ account = self.callee_account
+ else
+ account = self.caller_account
+ end
+ rescue
+ return nil
+ end
+
+ if account.class == SipAccount && account.sip_accountable.class == User
+ return account.sip_accountable.image_url(image_size).to_s
+ end
+ end
+
+ def display_call_date(date_format, date_today_format)
+ if self.start_stamp.strftime('%Y%m%d') == DateTime::now.strftime('%Y%m%d')
+ return self.start_stamp.strftime(date_today_format)
+ end
+ return self.start_stamp.strftime(date_format)
+ end
+
+ def display_duration
+ if self.duration.to_i > 0
+ minutes = (self.duration / 1.minutes).to_i
+ seconds = self.duration - minutes.minutes.seconds
+ return '%i:%02i' % [minutes, seconds]
+ end
+ end
+
+ def phone_book_entry_by_number(number)
+ begin
+ call_historyable = self.call_historyable
+ rescue
+ return nil
+ end
+
+ if ! call_historyable
+ return nil
+ end
+
+ if call_historyable.class == SipAccount
+ owner = call_historyable.sip_accountable
+ end
+
+ if owner.class == User
+ phone_books = owner.phone_books.all
+ phone_books.concat(owner.current_tenant.phone_books.all)
+ elsif owner.class == Tenant
+ phone_books = owner.phone_books.all
+ end
+
+ if ! phone_books
+ return nil
+ end
+
+ phone_books.each do |phone_book|
+ phone_book_entry = phone_book.find_entry_by_number(number)
+ if phone_book_entry
+ return phone_book_entry
+ end
+ end
+
+ return nil
+
+ end
+
+ def voicemail_message
+ begin
+ return self.call_historyable.voicemail_messages.where(:forwarded_by => self.caller_channel_uuid).first
+ rescue
+ return nil
+ end
+ end
+
+ def call_historyable_uuid
+ begin
+ return self.call_historyable.uuid
+ rescue
+ return nil
+ end
+ end
+
+ def call_historyable_uuid=(uuid)
+ begin
+ return self.call_historyable_id = self.call_historyable_type.constantize.where(:uuid => uuid).first.id
+ rescue
+ end
+ end
+
+ def caller_account_uuid
+ begin
+ return self.caller_account.uuid
+ rescue
+ return nil
+ end
+ end
+
+ def caller_account_uuid=(uuid)
+ begin
+ return self.caller_account_id = self.caller_account_type.constantize.where(:uuid => uuid).first.id
+ rescue
+ end
+ end
+
+ def callee_account_uuid
+ begin
+ return self.callee_account.uuid
+ rescue
+ return nil
+ end
+ end
+
+ def callee_account_uuid=(uuid)
+ begin
+ return self.callee_account_id = self.callee_account_type.constantize.where(:uuid => uuid).first.id
+ rescue
+ end
+ end
+
+ def auth_account_uuid
+ begin
+ return self.auth_account.uuid
+ rescue
+ return nil
+ end
+ end
+
+ def auth_account_uuid=(uuid)
+ begin
+ return self.auth_account_id = self.auth_account_type.constantize.where(:uuid => uuid).first.id
+ rescue
+ end
+ end
+end
diff --git a/app/models/callthrough.rb b/app/models/callthrough.rb
new file mode 100644
index 0000000..c057fa6
--- /dev/null
+++ b/app/models/callthrough.rb
@@ -0,0 +1,60 @@
+class Callthrough < ActiveRecord::Base
+ attr_accessible :name, :clip_no_screening,
+ :phone_numbers_attributes, :access_authorizations_attributes,
+ :whitelists_attributes
+
+ # Validations and Associations
+ #
+ belongs_to :tenant
+
+ validates_presence_of :tenant_id
+ validates_presence_of :tenant
+
+ # These are the phone_numbers for this callthrough.
+ # One has to dial this number to access the callthrough.
+ #
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+
+ accepts_nested_attributes_for :phone_numbers,
+ :reject_if => lambda { |phone_number| phone_number[:number].blank? },
+ :allow_destroy => true
+
+ validate :requires_at_least_one_phone_number
+
+ # These are the access authorizations for this callthrough.
+ # One has to be known by his phone number or by a login/pin or even both.
+ #
+ has_many :access_authorizations, :as => :access_authorizationable, :dependent => :destroy
+
+ accepts_nested_attributes_for :access_authorizations,
+ :reject_if => lambda { |access_authorization| access_authorization[:login].blank? && access_authorization[:pin].blank? && access_authorization[:phone_numbers_attributes]['0'][:number].blank? },
+ :allow_destroy => true
+
+ has_many :access_authorization_phone_numbers, :source => :phone_numbers,
+ :through => :access_authorizations, :readonly => true
+
+ # These are the whitelists of the phone numbers which can be called through this callthrough.
+ #
+ has_many :whitelists, :as => :whitelistable, :dependent => :destroy
+
+ accepts_nested_attributes_for :whitelists,
+ :reject_if => lambda { |whitelist| whitelist[:phone_numbers_attributes]['0']['number'].blank? },
+ :allow_destroy => true
+
+ has_many :whitelisted_phone_numbers, :source => :phone_numbers,
+ :through => :whitelists, :readonly => true
+
+ # Delegations:
+ #
+ delegate :sip_domain, :to => :tenant, :allow_nil => true
+
+ def to_s
+ self.name || I18n.t('callthroughs.name') + ' ID ' + self.id
+ end
+
+
+ private
+ def requires_at_least_one_phone_number
+ errors.add(:base, "You must provide at least one phone number") if !self.phone_numbers.map{|phone_number| phone_number.valid?}.include?(true)
+ end
+end
diff --git a/app/models/conference.rb b/app/models/conference.rb
new file mode 100644
index 0000000..8be9f21
--- /dev/null
+++ b/app/models/conference.rb
@@ -0,0 +1,63 @@
+class Conference < ActiveRecord::Base
+ attr_accessible :name, :start, :end, :description, :pin,
+ :open_for_anybody, :max_members, :announce_new_member_by_name,
+ :announce_left_member_by_name
+
+ belongs_to :conferenceable, :polymorphic => true
+ has_many :conference_invitees, :dependent => :destroy
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+
+ validates_presence_of :conferenceable_type, :conferenceable_id
+ validates_presence_of :conferenceable
+ validates_presence_of :name
+ validates_presence_of :start, :if => Proc.new { |conference| !conference.end.blank? }
+ validates_presence_of :end, :if => Proc.new { |conference| !conference.start.blank? }
+ validates_presence_of :max_members
+ validates_numericality_of :max_members, :only_integer => true,
+ :greater_than => 0,
+ :less_than => (MAXIMUM_NUMBER_OF_PEOPLE_IN_A_CONFERENCE + 1),
+ :allow_nil => false,
+ :allow_blank => false
+
+ validates_inclusion_of :open_for_anybody, :in => [true, false]
+
+ validates_numericality_of :pin, :only_integer => true,
+ :greater_than => 0,
+ :allow_nil => true,
+ :allow_blank => true
+ validates_length_of :pin, :minimum => MINIMUM_PIN_LENGTH,
+ :allow_nil => true,
+ :allow_blank => true
+
+ validate :start_and_end_dates_must_make_sense, :if => Proc.new { |conference| !conference.start.blank? && !conference.end.blank? }
+
+ after_save :send_pin_email_when_pin_has_changed
+
+ default_scope where(:state => 'active').order(:start)
+
+ # State Machine stuff
+ state_machine :initial => :active do
+ end
+
+ def sip_domain
+ self.conferenceable.try(:sip_domain)
+ end
+
+ def to_s
+ name
+ end
+
+ private
+
+ def start_and_end_dates_must_make_sense
+ errors.add(:start, 'must be in the future') if self.start < Time.now - 10.minutes
+ errors.add(:end, 'must be later than the start') if self.end < self.start
+ end
+
+ def send_pin_email_when_pin_has_changed
+ if self.conferenceable.class == User && self.pin_changed?
+ Notifications.new_pin(self).deliver
+ end
+ end
+
+end
diff --git a/app/models/conference_invitee.rb b/app/models/conference_invitee.rb
new file mode 100644
index 0000000..7de20de
--- /dev/null
+++ b/app/models/conference_invitee.rb
@@ -0,0 +1,39 @@
+class ConferenceInvitee < ActiveRecord::Base
+ attr_accessible :pin, :speaker, :moderator, :phone_number, :phone_number_attributes
+
+ belongs_to :conference
+ belongs_to :phone_book_entry
+ has_one :phone_number, :as => :phone_numberable, :dependent => :destroy
+ accepts_nested_attributes_for :phone_number
+
+ validates_presence_of :conference_id
+ validates_presence_of :conference
+ validates_presence_of :phone_number
+ validates_numericality_of :pin, :only_integer => true,
+ :greater_than => 0,
+ :allow_nil => true,
+ :allow_blank => true
+ validates_length_of :pin, :minimum => MINIMUM_PIN_LENGTH,
+ :allow_nil => true,
+ :allow_blank => true
+
+ validates_inclusion_of :speaker, :in => [true, false]
+ validates_inclusion_of :moderator, :in => [true, false]
+
+ validate :uniqueness_of_phone_number_in_the_parent_conference
+ validates_uniqueness_of :phone_book_entry_id, :scope => :conference_id, :allow_nil => true
+
+ def to_s
+ "ID #{self.id}"
+ end
+
+ private
+
+ def uniqueness_of_phone_number_in_the_parent_conference
+ if self.conference.conference_invitees.where('id != ?', self.id).count > 0 &&
+ self.conference.conference_invitees.where('id != ?', self.id).map{|x| x.phone_number.number}.
+ include?(self.phone_number.number)
+ errors.add(:base, 'Phone number is not unique within the conference.')
+ end
+ end
+end
diff --git a/app/models/country.rb b/app/models/country.rb
new file mode 100644
index 0000000..018e348
--- /dev/null
+++ b/app/models/country.rb
@@ -0,0 +1,21 @@
+class Country < ActiveRecord::Base
+
+ has_many :area_codes, :dependent => :destroy
+ has_many :tenants
+ has_many :phone_number_ranges, :as => :phone_number_rangeable, :dependent => :destroy
+
+ validates_presence_of :name
+ validates_presence_of :country_code
+ validates_presence_of :international_call_prefix
+
+ validates_numericality_of :country_code,
+ :only_integer => true
+
+ validates_uniqueness_of :name, :scope => [ :country_code ],
+ :case_sensitive => false
+
+ def to_s
+ self.name
+ end
+
+end
diff --git a/app/models/dial_in_number_store.rb b/app/models/dial_in_number_store.rb
new file mode 100644
index 0000000..17c2202
--- /dev/null
+++ b/app/models/dial_in_number_store.rb
@@ -0,0 +1,16 @@
+class DialInNumberStore < ActiveRecord::Base
+ # Associations and Validations
+ #
+ validates_presence_of :dial_in_number_storeable_type
+ validates_presence_of :dial_in_number_storeable_id
+
+ belongs_to :dial_in_number_storeable, :polymorphic => true
+
+ validates_presence_of :dial_in_number_storeable
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+
+ # Delegations:
+ #
+ delegate :tenant, :to => :dial_in_number_storeable, :allow_nil => true
+end
diff --git a/app/models/fax_account.rb b/app/models/fax_account.rb
new file mode 100644
index 0000000..683447a
--- /dev/null
+++ b/app/models/fax_account.rb
@@ -0,0 +1,77 @@
+# encoding: UTF-8
+
+class FaxAccount < ActiveRecord::Base
+ attr_accessible :name, :email, :station_id, :days_till_auto_delete, :phone_numbers_attributes, :retries
+
+ # Validations:
+ #
+ validates_presence_of :fax_accountable_type, :fax_accountable_id
+ validates_presence_of :fax_accountable
+ validates_presence_of :name
+ validates_presence_of :tenant_id
+ validates_presence_of :tenant
+
+ validates_numericality_of :days_till_auto_delete, :allow_nil => true
+ validates_numericality_of :retries, :only_integer => true, :greater_than_or_equal_to => 0
+
+ validates_uniqueness_of :name, :scope => [:fax_accountable_type, :fax_accountable_id]
+
+ # Associations:
+ #
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ has_many :fax_documents, :dependent => :destroy
+
+ belongs_to :fax_accountable, :polymorphic => true
+ belongs_to :tenant
+
+ accepts_nested_attributes_for :phone_numbers
+
+ # Hooks
+ #
+ before_validation :find_and_set_tenant_id
+ before_validation :convert_umlauts
+
+ def to_s
+ name
+ end
+
+ private
+ def require_at_least_one_phone_number
+ if self.phone_numbers.count < 1
+ errors.add(:base, 'needs at least one valid phone number')
+ end
+ end
+
+ def find_and_set_tenant_id
+ if self.new_record? and self.tenant_id != nil
+ return
+ else
+ tenant = case self.fax_accountable_type
+ when 'UserGroup' ; fax_accountable.tenant
+ when 'User' ; fax_accountable.current_tenant || fax_accountable.tenants.last
+ else nil
+ end
+ self.tenant_id = tenant.id if tenant != nil
+ end
+ end
+
+ def convert_umlauts
+ self.name = self.name.sub(/ä/,'ae').
+ sub(/Ä/,'Ae').
+ sub(/ü/,'ue').
+ sub(/Ü/,'Ue').
+ sub(/ö/,'oe').
+ sub(/Ö/,'Oe').
+ sub(/ß/,'ss')
+ self.name = self.name.gsub(/[^a-zA-Z0-9\-\,\:\.\+ ]/,'_')
+ self.station_id = self.station_id.sub(/ä/,'ae').
+ sub(/Ä/,'Ae').
+ sub(/ü/,'ue').
+ sub(/Ü/,'Ue').
+ sub(/ö/,'oe').
+ sub(/Ö/,'Oe').
+ sub(/ß/,'ss')
+ self.station_id = self.station_id.gsub(/[^a-zA-Z0-9\-\,\:\.\+ ]/,'_')
+ end
+
+end
diff --git a/app/models/fax_document.rb b/app/models/fax_document.rb
new file mode 100644
index 0000000..67bdea9
--- /dev/null
+++ b/app/models/fax_document.rb
@@ -0,0 +1,82 @@
+class FaxDocument < ActiveRecord::Base
+# attr_accessible :inbound, :transmission_time, :sent_at, :document_total_pages, :document_transferred_pages, :ecm_requested, :ecm_used, :image_resolution, :image_size, :local_station_id, :result_code, :result_text, :remote_station_id, :success, :transfer_rate, :t38_gateway_format, :t38_peer, :document
+
+ mount_uploader :document, DocumentUploader
+ mount_uploader :tiff, TiffUploader
+
+ validates_presence_of :document
+ validates_numericality_of :retry_counter, :only_integer => true, :greater_than_or_equal_to => 0
+
+ belongs_to :fax_account
+ belongs_to :fax_resolution
+
+ validates_presence_of :fax_resolution_id
+ validates_presence_of :fax_resolution
+
+ has_one :destination_phone_number, :class_name => 'PhoneNumber', :as => :phone_numberable, :dependent => :destroy
+ accepts_nested_attributes_for :destination_phone_number
+
+ has_many :fax_thumbnails, :order => :position, :dependent => :destroy
+
+ after_create :render_thumbnails
+ after_create :convert_pdf_to_tiff
+
+ # Scopes
+ scope :inbound, where(:state => 'inbound')
+ scope :outbound, where(:state => ['queued_for_sending','sending','successful','unsuccessful'])
+
+ # State Machine stuff
+ state_machine :initial => :new do
+ event :queue_for_sending do
+ transition [:new] => :queued_for_sending
+ end
+
+ event :send_now do
+ transition [:queued_for_sending] => :sending
+ end
+
+ event :cancel do
+ transition [:sending, :queued_for_sending] => :unsuccessful
+ end
+
+ event :successful_sent do
+ transition [:sending, :queued_for_sending] => :successful
+ end
+
+ event :mark_as_inbound do
+ transition [:new] => :inbound
+ end
+ end
+
+ def to_s
+ name
+ end
+
+ private
+ def render_thumbnails
+ directory = "/tmp/GS-#{GEMEINSCHAFT_VERSION}/fax_thumbnails/#{self.id}"
+ system('mkdir -p ' + directory)
+ system("cd #{directory} && convert #{Rails.root.to_s}/public#{self.document.to_s}[0-100] -colorspace Gray PNG:'fax_page.png'")
+ number_of_thumbnails = Dir["#{directory}/fax_page-*.png"].count
+ (0..(number_of_thumbnails-1)).each do |i|
+ fax_thumbnail = self.fax_thumbnails.build
+ fax_thumbnail.thumbnail = File.open("#{directory}/fax_page-#{i}.png")
+ fax_thumbnail.save!
+ end
+ system("rm -rf #{directory}")
+ self.update_attributes(:document_total_pages => number_of_thumbnails) if self.document_total_pages.nil?
+ end
+
+ def convert_pdf_to_tiff
+ page_size_a4 = '595 842'
+ page_size_command = "<< /Policies << /PageSize 3 >> /InputAttributes currentpagedevice /InputAttributes get dup { pop 1 index exch undef } forall dup 0 << /PageSize [ #{page_size_a4} ] >> put >> setpagedevice"
+ directory = "/tmp/GS-#{GEMEINSCHAFT_VERSION}/faxes/#{self.id}"
+ system('mkdir -p ' + directory)
+ tiff_file_name = File.basename(self.document.to_s.downcase, ".pdf") + '.tiff'
+ system "cd #{directory} && gs -q -r#{self.fax_resolution.resolution_value} -dNOPAUSE -dBATCH -dSAFER -sDEVICE=tiffg3 -sOutputFile=\"#{tiff_file_name}\" -c \"#{page_size_command}\" -- \"#{Rails.root.to_s}/public#{self.document.to_s}\""
+ self.tiff = File.open("#{directory}/#{tiff_file_name}")
+ self.save
+ system("rm -rf #{directory}")
+ end
+
+end
diff --git a/app/models/fax_resolution.rb b/app/models/fax_resolution.rb
new file mode 100644
index 0000000..c9093fb
--- /dev/null
+++ b/app/models/fax_resolution.rb
@@ -0,0 +1,15 @@
+class FaxResolution < ActiveRecord::Base
+ validates_presence_of :name
+ validates_presence_of :resolution_value
+
+ validates_uniqueness_of :name
+ validates_uniqueness_of :resolution_value
+
+ has_many :fax_documents, :dependent => :destroy
+
+ acts_as_list
+
+ def to_s
+ self.name
+ end
+end
diff --git a/app/models/fax_thumbnail.rb b/app/models/fax_thumbnail.rb
new file mode 100644
index 0000000..a29c9ad
--- /dev/null
+++ b/app/models/fax_thumbnail.rb
@@ -0,0 +1,8 @@
+class FaxThumbnail < ActiveRecord::Base
+ mount_uploader :thumbnail, ThumbnailUploader
+ validates_presence_of :thumbnail
+
+ belongs_to :fax_document
+
+ acts_as_list :scope => :fax_document
+end
diff --git a/app/models/freeswitch_alias.rb b/app/models/freeswitch_alias.rb
new file mode 100644
index 0000000..9953edb
--- /dev/null
+++ b/app/models/freeswitch_alias.rb
@@ -0,0 +1,23 @@
+class FreeswitchAlias < ActiveRecord::Base
+ self.table_name = 'aliases'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_call.rb b/app/models/freeswitch_call.rb
new file mode 100644
index 0000000..95b2cdd
--- /dev/null
+++ b/app/models/freeswitch_call.rb
@@ -0,0 +1,24 @@
+class FreeswitchCall < ActiveRecord::Base
+ self.table_name = 'calls'
+ self.primary_key = 'call_uuid'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_cdr.rb b/app/models/freeswitch_cdr.rb
new file mode 100644
index 0000000..fd0eb75
--- /dev/null
+++ b/app/models/freeswitch_cdr.rb
@@ -0,0 +1,4 @@
+class FreeswitchCdr < ActiveRecord::Base
+ self.table_name = 'cdrs'
+ self.primary_key = 'uuid'
+end
diff --git a/app/models/freeswitch_channel.rb b/app/models/freeswitch_channel.rb
new file mode 100644
index 0000000..489e17d
--- /dev/null
+++ b/app/models/freeswitch_channel.rb
@@ -0,0 +1,24 @@
+class FreeswitchChannel < ActiveRecord::Base
+ self.table_name = 'channels'
+ self.primary_key = 'uuid'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_complete.rb b/app/models/freeswitch_complete.rb
new file mode 100644
index 0000000..e7ff465
--- /dev/null
+++ b/app/models/freeswitch_complete.rb
@@ -0,0 +1,23 @@
+class FreeswitchComplete < ActiveRecord::Base
+ self.table_name = 'complete'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_fifo_bridge.rb b/app/models/freeswitch_fifo_bridge.rb
new file mode 100644
index 0000000..06167f3
--- /dev/null
+++ b/app/models/freeswitch_fifo_bridge.rb
@@ -0,0 +1,23 @@
+class FreeswitchFifoBridge < ActiveRecord::Base
+ self.table_name = 'fifo_bridge'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_fifo_caller.rb b/app/models/freeswitch_fifo_caller.rb
new file mode 100644
index 0000000..50c1fb5
--- /dev/null
+++ b/app/models/freeswitch_fifo_caller.rb
@@ -0,0 +1,23 @@
+class FreeswitchFifoCaller < ActiveRecord::Base
+ self.table_name = 'fifo_callers'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_fifo_outbound.rb b/app/models/freeswitch_fifo_outbound.rb
new file mode 100644
index 0000000..029c21d
--- /dev/null
+++ b/app/models/freeswitch_fifo_outbound.rb
@@ -0,0 +1,23 @@
+class FreeswitchFifoOutbound < ActiveRecord::Base
+ self.table_name = 'fifo_outbound'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_interface.rb b/app/models/freeswitch_interface.rb
new file mode 100644
index 0000000..1602d62
--- /dev/null
+++ b/app/models/freeswitch_interface.rb
@@ -0,0 +1,23 @@
+class FreeswitchInterface < ActiveRecord::Base
+ self.table_name = 'interfaces'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_nat.rb b/app/models/freeswitch_nat.rb
new file mode 100644
index 0000000..8baf2bf
--- /dev/null
+++ b/app/models/freeswitch_nat.rb
@@ -0,0 +1,23 @@
+class FreeswitchNat < ActiveRecord::Base
+ self.table_name = 'nat'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_registration.rb b/app/models/freeswitch_registration.rb
new file mode 100644
index 0000000..7e80815
--- /dev/null
+++ b/app/models/freeswitch_registration.rb
@@ -0,0 +1,23 @@
+class FreeswitchRegistration < ActiveRecord::Base
+ self.table_name = 'registrations'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_task.rb b/app/models/freeswitch_task.rb
new file mode 100644
index 0000000..6e964d2
--- /dev/null
+++ b/app/models/freeswitch_task.rb
@@ -0,0 +1,23 @@
+class FreeswitchTask < ActiveRecord::Base
+ self.table_name = 'tasks'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/freeswitch_voicemail_pref.rb b/app/models/freeswitch_voicemail_pref.rb
new file mode 100644
index 0000000..b2400e8
--- /dev/null
+++ b/app/models/freeswitch_voicemail_pref.rb
@@ -0,0 +1,23 @@
+class FreeswitchVoicemailPref < ActiveRecord::Base
+ self.table_name = 'voicemail_prefs'
+
+ # Makes sure that this is a readonly model.
+ def readonly?
+ return true
+ end
+
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def delete
+ raise ActiveRecord::ReadOnlyRecord
+ end
+end
diff --git a/app/models/gemeinschaft_setup.rb b/app/models/gemeinschaft_setup.rb
new file mode 100644
index 0000000..b445b21
--- /dev/null
+++ b/app/models/gemeinschaft_setup.rb
@@ -0,0 +1,8 @@
+class GemeinschaftSetup < ActiveRecord::Base
+ belongs_to :user
+ accepts_nested_attributes_for :user
+ belongs_to :sip_domain
+ accepts_nested_attributes_for :sip_domain
+ belongs_to :country
+ belongs_to :language
+end
diff --git a/app/models/gs_cluster_sync_log_entry.rb b/app/models/gs_cluster_sync_log_entry.rb
new file mode 100644
index 0000000..063ff23
--- /dev/null
+++ b/app/models/gs_cluster_sync_log_entry.rb
@@ -0,0 +1,99 @@
+class GsClusterSyncLogEntry < ActiveRecord::Base
+ attr_accessible :gs_node_id, :class_name, :action, :content, :status, :history,
+ :homebase_ip_address, :waiting_to_be_synced, :association_method,
+ :association_uuid
+
+ validates :class_name,
+ :presence => true
+
+ validates :action,
+ :presence => true
+
+ validates :content,
+ :presence => true
+
+ after_create :apply_to_local_database
+
+ def apply_to_local_database
+ if self.homebase_ip_address != HOMEBASE_IP_ADDRESS
+ if self.class_name.constantize.new.attribute_names.include?('is_native')
+ case self.action
+ when 'create'
+ new_local_copy = self.class_name.constantize.new(
+ JSON(self.content).
+ delete_if{|key, value| ['id','updated_at','created_at'].
+ include?(key) },
+ :without_protection => true)
+ new_local_copy.is_native = false
+ find_and_connect_to_an_association(new_local_copy)
+ if new_local_copy.save(:validate => false)
+ logger.info "Created local copy of #{self.class_name} with the ID #{new_local_copy.id}. #{new_local_copy.to_s}"
+ else
+ logger.error "Couldn't create a local copy of #{self.class_name} with the ID #{new_local_copy.id}. #{new_local_copy.errors.to_yaml}"
+ end
+
+ when 'update'
+ local_copy = find_local_copy
+ if local_copy
+ # Only update an object if the update it self is newer than the local object.
+ #
+ if local_copy.updated_at < JSON(self.content)['updated_at'].to_time
+ local_copy.update_attributes(JSON(self.content).delete_if{|key, value| ['id','updated_at','created_at'].include?(key) }, :without_protection => true)
+ find_and_connect_to_an_association(local_copy)
+ if local_copy.save(:validate => false)
+ logger.info "Updated local copy of #{self.class_name} with the ID #{local_copy.id}. #{local_copy.to_s}"
+ else
+ logger.error "Couldn't update local copy of #{self.class_name} with the ID #{local_copy.id}. #{local_copy.errors.to_yaml}"
+ end
+ else
+ logger.error "Didn't update local copy of #{self.class_name} with the ID #{local_copy.id} because of a race condition (the local version was newer than the update). Please check GsClusterSyncLogEntry ID #{self.id}."
+ end
+ else
+ logger.error "Couldn't find local copy of #{self.class_name}. #{self.content}"
+ end
+
+ when 'destroy'
+ local_copy = find_local_copy
+ if local_copy
+ local_copy.destroy
+ logger.info "Destroyed local copy of #{self.class_name} with the ID #{local_copy.id}. #{local_copy.to_s}"
+ else
+ logger.error "Couldn't find local copy of #{self.class_name}. #{self.content}"
+ end
+ end
+ else
+ logger.error "The class #{self.class_name} doesn't offer the attribute is_native. Can't synchronize without."
+ end
+ end
+ end
+
+ def find_local_copy
+ self.class_name.constantize.find_by_uuid(JSON(self.content)['uuid'])
+ end
+
+ # Connect to the association (e.g. User to a SipAccount)
+ #
+ def find_and_connect_to_an_association(local_copy)
+ if !(self.association_method.blank? || self.association_uuid.blank?) && (self.association_method_changed? || self.association_uuid_changed?)
+ name_of_the_association_type = local_copy.attribute_names.delete_if{|x| !x.include?('_type')}.first
+ association = local_copy.send(name_of_the_association_type).constantize.where(:uuid => self.association_uuid).first
+ if association
+ local_copy.send "#{association_method}=", association
+ end
+ end
+ end
+
+ def populate_other_cluster_nodes
+ if self.homebase_ip_address == HOMEBASE_IP_ADDRESS && self.waiting_to_be_synced == true
+ if GsNode.where(:push_updates_to => true).count > 0
+ GsNode.where(:push_updates_to => true).each do |gs_node|
+ RemoteGsNode::GsClusterSyncLogEntry.site = gs_node.site
+ remote_enty = RemoteGsNode::GsClusterSyncLogEntry.create(self.attributes.delete_if{|key, value| ['id','updated_at','created_at'].include?(key) })
+ self.update_attributes(:waiting_to_be_synced => false)
+ self.save
+ end
+ end
+ end
+ end
+
+end
diff --git a/app/models/gs_node.rb b/app/models/gs_node.rb
new file mode 100644
index 0000000..229ceb2
--- /dev/null
+++ b/app/models/gs_node.rb
@@ -0,0 +1,30 @@
+class GsNode < ActiveRecord::Base
+ attr_accessible :name, :ip_address, :site, :element_name, :push_updates_to, :accepts_updates_from
+
+ has_many :phone_numbers, :foreign_key => :gs_node_id, :dependent => :destroy
+ has_many :users, :foreign_key => :gs_node_id, :dependent => :destroy
+ has_many :sip_accounts, :foreign_key => :gs_node_id, :dependent => :destroy
+ has_many :hunt_groups, :foreign_key => :gs_node_id, :dependent => :destroy
+
+ validates :name,
+ :presence => true
+
+ validates :ip_address,
+ :presence => true
+
+ validates :site,
+ :presence => true
+
+ validates :element_name,
+ :presence => true
+
+ def to_s
+ name
+ end
+
+ def synced
+ self.last_sync = Time.now
+ return self.save
+ end
+
+end
diff --git a/app/models/gui_function.rb b/app/models/gui_function.rb
new file mode 100644
index 0000000..e27a8d2
--- /dev/null
+++ b/app/models/gui_function.rb
@@ -0,0 +1,40 @@
+class GuiFunction < ActiveRecord::Base
+ attr_accessible :category, :name, :description, :gui_function_memberships_attributes
+
+ has_many :gui_function_memberships, :dependent => :destroy
+ has_many :user_groups, :through => :gui_function_memberships
+
+ accepts_nested_attributes_for :gui_function_memberships
+
+ validates :name, :presence => true,
+ :format => { :with => /\A[a-z_0-9]+\z/, :message => "Only lower case letters allowed" },
+ :length => { :in => 3..255 },
+ :uniqueness => true
+
+ def to_s
+ self.name
+ end
+
+ def self.display?(function_name = nil, user)
+ if function_name.blank? || GemeinschaftSetup.count == 0
+ true
+ else
+ if !user || user.class != User || function_name.class != String
+ false
+ else
+ function_name = function_name.downcase
+
+ activated_gui_function_names = GuiFunctionMembership.where(:user_group_id => user.user_group_ids, :activated => true).map{|gui_function_membership| gui_function_membership.gui_function.name}.uniq
+ deactivated_gui_function_names = GuiFunctionMembership.where(:user_group_id => user.user_group_ids, :activated => false).map{|gui_function_membership| gui_function_membership.gui_function.name}.uniq
+
+ deactivated_gui_function_names = deactivated_gui_function_names - activated_gui_function_names
+
+ if deactivated_gui_function_names.include?(function_name)
+ false
+ else
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/gui_function_membership.rb b/app/models/gui_function_membership.rb
new file mode 100644
index 0000000..d2bc7cd
--- /dev/null
+++ b/app/models/gui_function_membership.rb
@@ -0,0 +1,7 @@
+class GuiFunctionMembership < ActiveRecord::Base
+ belongs_to :gui_function
+ belongs_to :user_group
+
+ validates_associated :gui_function
+ validates_associated :user_group
+end
diff --git a/app/models/hunt_group.rb b/app/models/hunt_group.rb
new file mode 100644
index 0000000..276ae53
--- /dev/null
+++ b/app/models/hunt_group.rb
@@ -0,0 +1,43 @@
+class HuntGroup < ActiveRecord::Base
+ attr_accessible :name, :strategy, :seconds_between_jumps, :phone_numbers_attributes
+
+ belongs_to :tenant
+ has_many :call_forwards, :as => :call_forwardable, :dependent => :destroy
+
+ validates_uniqueness_of :name, :scope => :tenant_id,
+ :allow_nil => true, :allow_blank => true
+
+ validates_presence_of :strategy
+ validates_inclusion_of :strategy, :in => HUNT_GROUP_STRATEGIES
+
+ validates_presence_of :seconds_between_jumps,
+ :if => Proc.new{ |hunt_group| hunt_group.strategy != 'ring_all' }
+ validates_numericality_of :seconds_between_jumps,
+ :only_integer => true,
+ :greater_than_or_equal_to => VALID_SECONDS_BETWEEN_JUMPS_VALUES.min,
+ :less_than_or_equal_to => VALID_SECONDS_BETWEEN_JUMPS_VALUES.max,
+ :if => Proc.new{ |hunt_group| hunt_group.strategy != 'ring_all' }
+ validates_inclusion_of :seconds_between_jumps,
+ :in => VALID_SECONDS_BETWEEN_JUMPS_VALUES,
+ :if => Proc.new{ |hunt_group| hunt_group.strategy != 'ring_all' }
+ validates_inclusion_of :seconds_between_jumps,
+ :in => [nil],
+ :if => Proc.new{ |hunt_group| hunt_group.strategy == 'ring_all' }
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ has_many :hunt_group_members, :dependent => :destroy, :order => :position
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ accepts_nested_attributes_for :phone_numbers,
+ :reject_if => lambda { |phone_number| phone_number[:number].blank? },
+ :allow_destroy => true
+
+ has_many :hunt_group_members, :dependent => :destroy
+
+ def to_s
+ self.name || I18n.t('hunt_groups.name') + ' ID ' + self.id.to_s
+ end
+
+end
diff --git a/app/models/hunt_group_member.rb b/app/models/hunt_group_member.rb
new file mode 100644
index 0000000..7d9d3e0
--- /dev/null
+++ b/app/models/hunt_group_member.rb
@@ -0,0 +1,67 @@
+class HuntGroupMember < ActiveRecord::Base
+ attr_accessible :name, :active, :can_switch_status_itself, :phone_numbers_attributes
+
+ belongs_to :hunt_group
+ validates_presence_of :hunt_group
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ accepts_nested_attributes_for :phone_numbers,
+ :reject_if => lambda { |phone_number| phone_number[:number].blank? },
+ :allow_destroy => true
+
+ acts_as_list :scope => :hunt_group
+
+ after_save :set_presence
+ after_save :trigger_connected_call_forward_if_necessary
+
+ def to_s
+ self.name || I18n.t('hunt_group_members.name') + ' ID ' + self.id.to_s
+ end
+
+ private
+ def set_presence
+ dialplan_function = nil
+ state = 'terminated'
+
+ if self.active
+ state = 'confirmed'
+ end
+
+ require 'freeswitch_event'
+ event = FreeswitchEvent.new("PRESENCE_IN")
+ event.add_header("proto", "sip")
+ event.add_header("from", "f-hgmtg-#{self.id}@#{SipDomain.first.host}")
+ event.add_header("event_type", "presence")
+ event.add_header("alt_event_type", "dialog")
+ event.add_header("presence-call-direction", "outbound")
+ event.add_header("answer-state", state)
+ event.add_header("unique-id", "hunt_group_member_#{self.id}")
+ return event.fire()
+ end
+
+ # Turn on/off a connected CallForward.
+ # The last member who leaves the hunt_group deactivates the CallForward and the
+ # first member actives it.
+ #
+ def trigger_connected_call_forward_if_necessary
+ if self.active_changed? && self.hunt_group.hunt_group_members.count > 0
+ # deactive CallForward
+ #
+ if self.hunt_group.hunt_group_members.where(:active => false).count == self.hunt_group.hunt_group_members.count
+ self.hunt_group.call_forwards.where(:active => true).each do |x|
+ x.update_attributes({:active => false})
+ end
+ end
+
+ # active CallForward
+ #
+ if self.hunt_group.hunt_group_members.where(:active => true).count > 0
+ self.hunt_group.call_forwards.where(:active => false).each do |x|
+ x.update_attributes({:active => true})
+ end
+ end
+ end
+ end
+
+
+end
diff --git a/app/models/language.rb b/app/models/language.rb
new file mode 100644
index 0000000..1b9c2c0
--- /dev/null
+++ b/app/models/language.rb
@@ -0,0 +1,11 @@
+class Language < ActiveRecord::Base
+ has_many :tenants
+ has_many :users
+
+ validates_presence_of :name
+ validates_presence_of :code
+
+ def to_s
+ name
+ end
+end
diff --git a/app/models/manufacturer.rb b/app/models/manufacturer.rb
new file mode 100644
index 0000000..03d2bb7
--- /dev/null
+++ b/app/models/manufacturer.rb
@@ -0,0 +1,46 @@
+class Manufacturer < ActiveRecord::Base
+ attr_accessible :name, :ieee_name, :homepage_url
+
+ # Associations:
+ #
+ has_many :ouis, :dependent => :destroy
+ has_many :phone_models, :order => :name, :dependent => :destroy
+
+
+ # Validations:
+ #
+ validates_presence_of :name
+ validates_presence_of :ieee_name
+
+ validates_uniqueness_of :name, :case_sensitive => false
+
+ validate :validate_homepage_url
+
+ # State Machine stuff
+ default_scope where(:state => 'active').order(:name)
+ state_machine :initial => :active do
+
+ event :deactivate do
+ transition [:active] => :deactivated
+ end
+
+ event :activate do
+ transition [:deactivated] => :active
+ end
+ end
+
+ def to_s
+ self.name
+ end
+
+ private
+
+ def validate_homepage_url
+ if ! self.homepage_url.blank?
+ if ! CustomValidators.validate_url( self.homepage_url )
+ errors.add( :homepage_url, "is invalid." )
+ end
+ end
+ end
+
+end
diff --git a/app/models/oui.rb b/app/models/oui.rb
new file mode 100644
index 0000000..9a5bb1f
--- /dev/null
+++ b/app/models/oui.rb
@@ -0,0 +1,17 @@
+class Oui < ActiveRecord::Base
+ attr_accessible :value
+
+ validates_presence_of :manufacturer
+ validates_presence_of :value
+
+ belongs_to :manufacturer
+
+ # State Machine stuff
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+ end
+
+ def to_s
+ value
+ end
+end
diff --git a/app/models/phone.rb b/app/models/phone.rb
new file mode 100644
index 0000000..89371eb
--- /dev/null
+++ b/app/models/phone.rb
@@ -0,0 +1,240 @@
+require 'scanf'
+
+class Phone < ActiveRecord::Base
+
+ attr_accessible :mac_address, :ip_address, :http_user, :http_password,
+ :phone_model_id, :hot_deskable, :nightly_reboot,
+ :provisioning_key, :provisioning_key_active
+
+ # Associations
+ #
+ belongs_to :phone_model
+ belongs_to :phoneable, :polymorphic => true
+
+ has_many :phone_sip_accounts, :dependent => :destroy, :uniq => true, :order => :position
+ has_many :sip_accounts, :through => :phone_sip_accounts
+
+ # Validations
+ #
+ before_validation :sanitize_mac_address
+
+ validates_presence_of :mac_address
+ validate_mac_address :mac_address
+ validates_uniqueness_of :mac_address
+
+ validates_uniqueness_of :ip_address,
+ :if => Proc.new { |me| ! me.ip_address.blank? }
+ validate_ip_address :ip_address,
+ :if => Proc.new { |me| ! me.ip_address.blank? }
+
+ validates_presence_of :phone_model
+ validates_presence_of :phoneable
+
+ before_save :save_last_ip_address
+ before_save :destroy_phones_sip_accounts_if_phoneable_changed
+ before_save :remove_ip_address_when_mac_address_was_changed
+
+ # State machine:
+ #
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+
+ event :deactivate do
+ transition [:active] => :deactivated
+ end
+
+ event :activate do
+ transition [:deactivated] => :active
+ end
+ end
+
+ def to_s
+ "%s %s %s" % [
+ pretty_mac_address,
+ "(#{self.phone_model})",
+ self.ip_address ? "(#{self.ip_address})" : "",
+ ]
+ end
+
+ def pretty_mac_address
+ return [].fill('%02X', 0, 6).join(':') % self.mac_address.scanf( '%2X' * 6 )
+ end
+
+
+ def resync(reboot = false, sip_account = nil)
+ if ! self.phone_model || ! self.phone_model.manufacturer
+ return false
+ end
+
+ if self.phone_model.manufacturer.ieee_name == 'SNOM Technology AG'
+ if !sip_account
+ self.sip_accounts.where(:sip_accountable_type => self.phoneable_type).each do |sip_account_associated|
+ if sip_account_associated.registration
+ sip_account = sip_account_associated
+ break
+ end
+ end
+ end
+
+ if ! sip_account or ! sip_account.registration
+ require 'open-uri'
+ begin
+ if open("http://#{self.ip_address}/advanced_update.htm?reboot=Reboot", :http_basic_authentication=>[self.http_user, self.http_password], :proxy => nil)
+ return true
+ end
+ rescue
+ return false
+ end
+ end
+
+ require 'freeswitch_event'
+ event = FreeswitchEvent.new("NOTIFY")
+ event.add_header("profile", "gemeinschaft")
+ event.add_header("event-string", "check-sync;reboot=#{reboot.to_s}")
+ event.add_header("user", sip_account.auth_name)
+ event.add_header("host", sip_account.sip_domain.host)
+ event.add_header("content-type", "application/simple-message-summary")
+ return event.fire()
+
+ elsif self.phone_model.manufacturer.ieee_name == 'Siemens Enterprise CommunicationsGmbH & Co. KG'
+ require 'open-uri'
+ begin
+ if open("http://#{self.ip_address}:8085/contact_dls.html/ContactDLS", :http_basic_authentication=>[self.http_user, self.http_password], :proxy => nil)
+ return true
+ end
+ rescue
+ return false
+ end
+ end
+
+ return false
+ end
+
+
+ # OPTIMIZE i18n translations
+ def user_login(user, sip_account = nil)
+ if ! self.hot_deskable
+ errors.add(:hot_deskable, "Phone not hot-deskable")
+ return false
+ end
+
+ phones_affected = Hash.new()
+ sip_accounts = Array.new(1, sip_account)
+
+ if !sip_account
+ sip_accounts = user.sip_accounts.where(:hotdeskable => true).all
+ end
+
+ if sip_accounts.blank?
+ errors.add(:sip_accounts, "No hot-deskable Sip Accounts available")
+ return false
+ end
+
+ sip_account_resync = self.sip_accounts.where(:sip_accountable_type => self.phoneable_type).first
+
+ phones_affected.each_pair do |id,phone|
+ if phone.id != self.id
+ phone.user_logout()
+ end
+ end
+
+ self.phoneable = user
+ sip_accounts.each do |sip_account|
+ if ! self.sip_accounts.where(:id => sip_account.id).first
+ self.sip_accounts.push(sip_account)
+ end
+ end
+
+ @not_destroy_phones_sip_accounts = true
+ if ! self.save
+ return false
+ end
+
+ sleep(0.5)
+
+ if ! self.resync(true, sip_account_resync)
+ errors.add(:resync, "Resync failed")
+ return false
+ end
+
+ return true
+ end
+
+
+ # OPTIMIZE i18n translations
+ def user_logout
+ if ! self.hot_deskable or self.phoneable_type == 'Tenant'
+ errors.add(:hot_deskable, "Phone not hot-deskable")
+ return false
+ end
+
+ sip_account = self.sip_accounts.where(:sip_accountable_type => self.phoneable_type).first
+
+ tenant_sip_account = self.sip_accounts.where(:sip_accountable_type => 'Tenant').first
+ if tenant_sip_account
+ tenant = tenant_sip_account.sip_accountable
+ end
+
+ sip_account_ids = Array.new()
+ self.sip_accounts.where(:sip_accountable_type => 'User', :hotdeskable => true).each do |sip_account|
+ sip_account_ids.push(sip_account.id)
+ end
+
+ if tenant
+ self.phoneable = tenant
+ @not_destroy_phones_sip_accounts = true
+ if ! self.save
+ errors.add(:phoneable, "Could not change owner")
+ return false
+ end
+ end
+
+ if ! PhoneSipAccount.destroy_all(:sip_account_id => sip_account_ids)
+ errors.add(:sip_accounts, "Could not delete sip_accounts")
+ return false
+ end
+
+ sleep(0.5)
+
+ if ! self.resync(true, sip_account)
+ errors.add(:resync, "Resync failed")
+ return false
+ end
+
+ return true
+ end
+
+ private
+
+ # Sanitize MAC address.
+ #
+ def sanitize_mac_address
+ self.mac_address = self.mac_address.to_s.upcase.gsub( /[^A-F0-9]/, '' )
+ end
+
+ # Saves the last IP address.
+ #
+ def save_last_ip_address
+ if self.ip_address_changed? \
+ && self.ip_address != self.ip_address_was
+ self.last_ip_address = self.ip_address_was
+ end
+ end
+
+ # When ever the parent of a phone changes all the SIP accounts associations
+ # are destroyed unless this is a user logout operation
+ #
+ def destroy_phones_sip_accounts_if_phoneable_changed
+ if (self.phoneable_type_changed? || self.phoneable_id_changed?) && ! @not_destroy_phones_sip_accounts
+ self.phone_sip_accounts.destroy_all
+ end
+ end
+
+ def remove_ip_address_when_mac_address_was_changed
+ if self.mac_address_changed?
+ self.ip_address = nil
+ self.last_ip_address = nil
+ end
+ end
+
+end
diff --git a/app/models/phone_book.rb b/app/models/phone_book.rb
new file mode 100644
index 0000000..3603eae
--- /dev/null
+++ b/app/models/phone_book.rb
@@ -0,0 +1,33 @@
+class PhoneBook < ActiveRecord::Base
+ attr_accessible :name, :description, :uuid
+
+ belongs_to :phone_bookable, :polymorphic => true
+ has_many :phone_book_entries, :dependent => :destroy
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => [ :phone_bookable_type, :phone_bookable_id ]
+
+ validates_length_of :name, :within => 1..50
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ # State Machine stuff
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+ end
+
+ def to_s
+ name
+ end
+
+ def find_entry_by_number(number)
+ phone_book_entries_ids = self.phone_book_entries.map{|phone_book_entry| phone_book_entry.id}
+
+ phone_number = PhoneNumber.where(:phone_numberable_id => phone_book_entries_ids, :phone_numberable_type => 'PhoneBookEntry', :number => number).first
+
+ if phone_number
+ return phone_number.phone_numberable
+ end
+ end
+end
diff --git a/app/models/phone_book_entry.rb b/app/models/phone_book_entry.rb
new file mode 100644
index 0000000..db2b44b
--- /dev/null
+++ b/app/models/phone_book_entry.rb
@@ -0,0 +1,109 @@
+# encoding: UTF-8
+
+class PhoneBookEntry < ActiveRecord::Base
+ before_save :run_phonetic_algorithm
+ before_save :save_value_of_to_s
+
+ attr_accessible :first_name, :middle_name, :last_name, :title, :nickname, :organization, :is_organization, :department, :job_title, :is_male, :birthday, :birth_name, :description, :homepage_personal, :homepage_organization, :twitter_account, :facebook_account, :google_plus_account, :xing_account, :linkedin_account, :mobileme_account, :image
+
+ belongs_to :phone_book
+ has_many :conference_invitees, :dependent => :destroy
+
+ acts_as_list :scope => :phone_book
+
+ validates_presence_of :phone_book
+
+ validates_presence_of :last_name,
+ :unless => Proc.new { |entry| entry.is_organization }
+
+ validates_presence_of :organization,
+ :if => Proc.new { |entry| entry.is_organization }
+
+ validates_inclusion_of :is_male, :in => [true, false, 1, '1', 'on'],
+ :unless => Proc.new { |entry| entry.is_organization }
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+
+ has_many :addresses, :dependent => :destroy
+
+ # Avatar like photo
+ mount_uploader :image, ImageUploader
+
+ # TODO Validate homepage URLs and social media accounts.
+
+
+ default_scope where(:state => 'active')
+
+ # State Machine stuff
+ state_machine :initial => :active do
+ end
+
+ def to_s
+ if self.is_organization
+ "#{self.organization}".strip
+ else
+ [self.last_name.strip, self.first_name.strip].join(', ')
+ end
+ end
+
+ def self.koelner_phonetik(input)
+ if input.blank?
+ nil
+ else
+ # TODO: koelner_phonetik() needs to be tested.
+
+ # Umwandeln in Grossbuchstaben
+ phonetik = input.upcase.gsub(/[^A-ZÜüÖöÄäß]/,'').strip
+
+ # Umwandeln anhand der Tabelle auf
+ # http://de.wikipedia.org/wiki/K%C3%B6lner_Verfahren
+ phonetik = phonetik.gsub(/([XKQ])X/, '\1'+'8')
+ phonetik = phonetik.gsub(/[DT]([CSZ])/, '8'+'\1')
+ phonetik = phonetik.gsub(/C([^AHKOQUX])/, '8'+'\1')
+ phonetik = phonetik.gsub(/^C([^AHKLOQRUX])/, '8'+'\1')
+ phonetik = phonetik.gsub(/([SZ])C/, '\1'+'8')
+ phonetik = phonetik.gsub(/[SZß]/, '8')
+ phonetik = phonetik.gsub(/R/, '7')
+ phonetik = phonetik.gsub(/[MN]/, '6')
+ phonetik = phonetik.gsub(/L/, '5')
+ phonetik = phonetik.gsub(/X/, '48')
+ phonetik = phonetik.gsub(/([^SZ])C([AHKOQUX])/, '\1'+'4'+'\2' )
+ phonetik = phonetik.gsub(/^C([AHKLOQRUX])/, '4'+'\1')
+ phonetik = phonetik.gsub(/[GKQ]/, '4')
+ phonetik = phonetik.gsub(/PH/, '3H')
+ phonetik = phonetik.gsub(/[FVW]/, '3')
+ phonetik = phonetik.gsub(/[DT]([^CSZ])/, '2'+'\1')
+ phonetik = phonetik.gsub(/[BP]/, '1')
+ phonetik = phonetik.gsub(/H/, '')
+ phonetik = phonetik.gsub(/[AEIJOUYÜüÖöÄä]/, '0')
+
+ # Regeln für Buchstaben am Ende des Wortes
+ phonetik = phonetik.gsub(/P/, '1')
+ phonetik = phonetik.gsub(/[DT]/, '2')
+ phonetik = phonetik.gsub(/C/, '8')
+
+ # Entfernen aller doppelten
+ phonetik = phonetik.gsub(/([0-9])\1+/, '\1')
+
+ # Entfernen aller Codes "0" außer am Anfang.
+ phonetik = phonetik.gsub(/^0/, 'X')
+ phonetik = phonetik.gsub(/0/, '')
+ phonetik = phonetik.gsub(/^X/, '0')
+
+ phonetik
+ end
+ end
+
+ private
+
+ def run_phonetic_algorithm
+ self.first_name_phonetic = PhoneBookEntry.koelner_phonetik(self.first_name) if self.first_name_changed?
+ self.last_name_phonetic = PhoneBookEntry.koelner_phonetik(self.last_name) if self.last_name_changed?
+ self.organization_phonetic = PhoneBookEntry.koelner_phonetik(self.organization) if self.organization_changed?
+ end
+
+ def save_value_of_to_s
+ self.value_of_to_s = self.to_s
+ end
+
+end
diff --git a/app/models/phone_model.rb b/app/models/phone_model.rb
new file mode 100644
index 0000000..e00e0e3
--- /dev/null
+++ b/app/models/phone_model.rb
@@ -0,0 +1,56 @@
+class PhoneModel < ActiveRecord::Base
+ attr_accessible :name, :product_manual_homepage_url, :product_homepage_url, :uuid
+
+ # Associations
+ #
+ belongs_to :manufacturer
+
+ has_many :phones, :dependent => :destroy
+
+ # Validations
+ #
+ validates_presence_of :name
+ validate :validate_product_manual_homepage_url
+ validate :validate_product_homepage_url
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ def to_s
+ self.name
+ end
+
+ # State machine:
+ #
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+
+ event :deactivate do
+ transition [:active] => :deactivated
+ end
+
+ event :activate do
+ transition [:deactivated] => :active
+ end
+ end
+
+
+ private
+
+ def validate_product_manual_homepage_url
+ if ! self.product_manual_homepage_url.blank?
+ if ! CustomValidators.validate_url( self.product_manual_homepage_url )
+ errors.add( :product_manual_homepage_url, "is invalid." )
+ end
+ end
+ end
+
+ def validate_product_homepage_url
+ if ! self.product_homepage_url.blank?
+ if ! CustomValidators.validate_url( self.product_homepage_url )
+ errors.add( :product_homepage_url, "is invalid." )
+ end
+ end
+ end
+
+end
diff --git a/app/models/phone_number.rb b/app/models/phone_number.rb
new file mode 100644
index 0000000..4c0cf46
--- /dev/null
+++ b/app/models/phone_number.rb
@@ -0,0 +1,304 @@
+class PhoneNumber < ActiveRecord::Base
+ NUMBER_TYPES_INBOUND = ['SipAccount', 'Conference', 'FaxAccount', 'Callthrough', 'HuntGroup']
+
+ attr_accessible :name, :number, :gs_node_id, :access_authorization_user_id
+
+ has_many :call_forwards, :dependent => :destroy
+
+ has_many :ringtones, :as => :ringtoneable, :dependent => :destroy
+
+ belongs_to :phone_numberable, :polymorphic => true
+
+ belongs_to :gs_node
+
+ validates_uniqueness_of :number, :scope => [:phone_numberable_type, :phone_numberable_id]
+
+ validate :validate_inbound_uniqueness
+
+ before_save :save_value_of_to_s
+ after_create :copy_existing_call_forwards_if_necessary
+ before_validation :'parse_and_split_number!'
+ validate :validate_number, :if => Proc.new { |phone_number| STRICT_INTERNAL_EXTENSION_HANDLING && STRICT_DID_HANDLING }
+ validate :check_if_number_is_available, :if => Proc.new { |phone_number| STRICT_INTERNAL_EXTENSION_HANDLING && STRICT_DID_HANDLING }
+
+ acts_as_list :scope => [:phone_numberable_id, :phone_numberable_type]
+
+ # Sync other nodes when this is a cluster.
+ #
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+ after_create { self.create_on_other_gs_nodes('phone_numberable', self.phone_numberable.try(:uuid)) }
+ after_destroy :destroy_on_other_gs_nodes
+ after_update { self.update_on_other_gs_nodes('phone_numberable', self.phone_numberable.try(:uuid)) }
+
+ # State machine:
+ #
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+
+ event :deactivate do
+ transition [:active] => :deactivated
+ end
+
+ event :activate do
+ transition [:deactivated] => :active
+ end
+ end
+
+
+ def to_s
+ parts = []
+ parts << "+#{self.country_code}" if self.country_code
+ parts << self.area_code if self.area_code
+ parts << self.central_office_code if self.central_office_code
+ parts << self.subscriber_number if self.subscriber_number
+
+ if parts.empty?
+ return self.number
+ end
+ return parts.join("-")
+ end
+
+ # Parse a number in a tenant's context (respect the tenant's country)
+ #
+ def self.parse( number, tenant=nil )
+ number = number.to_s.gsub( /[^0-9+]/, '' )
+
+ if tenant.class.name == 'Tenant'
+ country = tenant.country
+ else
+ tenant = nil
+ country = GemeinschaftSetup.first.try(:country)
+ country ||= Country.where(:name => "Germany").first
+ end
+
+ parts = {
+ :country_code => nil,
+ :area_code => nil,
+ :central_office_code => nil,
+ :subscriber_number => nil,
+ :extension => nil,
+ }
+
+ if country
+ if ! country.international_call_prefix.blank?
+ number = number.gsub( /^#{Regexp.escape( country.international_call_prefix )}/, '+' )
+ end
+ if ! country.trunk_prefix.blank?
+ number = number.gsub( /^#{Regexp.escape( country.trunk_prefix )}/, "+#{country.country_code}" )
+ end
+ end
+
+ if number.match( /^[+]/ )
+ parts = self.parse_international_number( number.gsub(/[^0-9]/,'') )
+ return nil if parts.nil?
+ else
+ # Check if the number is an internal extension.
+ if tenant
+ internal_extension_range = tenant.phone_number_ranges.where(:name => INTERNAL_EXTENSIONS).first
+ if internal_extension_range
+ if internal_extension_range.phone_numbers.where(:number => number).length > 0
+ parts[:extension] = number
+ end
+ end
+ end
+
+ # Otherwise assume the number is a special number such as an emergency number.
+ if ! parts[:extension]
+ parts[:subscriber_number] = number
+ end
+ end
+
+ # return nil if all parts are blank:
+ return nil if (
+ parts[:country_code].blank? &&
+ parts[:area_code].blank? &&
+ parts[:central_office_code].blank? &&
+ parts[:subscriber_number].blank? &&
+ parts[:extension].blank?
+ )
+ parts # return value
+ end
+
+ def self.parse_and_format( number, tenant=nil )
+ attributes = PhoneNumber.parse(number, tenant)
+ if attributes
+ formated_number = attributes.map{|key,value| value}.delete_if{|x| x.nil?}.join('-')
+ formated_number = "+#{formated_number}" if attributes[:country_code]
+ return formated_number
+ end
+ return number
+ end
+
+ # Parse an international number.
+ # Assumed format for +number+ is e.g. "49261200000"
+ #
+ def self.parse_international_number( number )
+ number = number.to_s.gsub( /[^0-9]/, '' )
+
+ parts = {
+ :country_code => nil,
+ :area_code => nil,
+ :central_office_code => nil,
+ :subscriber_number => nil,
+ :extension => nil,
+ }
+
+ # Find country by country code:
+ country = Country.where( :country_code => number[0, 3]).first
+ country ||= Country.where( :country_code => number[0, 2]).first
+ country ||= Country.where( :country_code => number[0, 1]).first
+
+ return nil if ! country # invalid number format
+
+ parts[:country_code] = country.country_code
+ remainder = number[ parts[:country_code].length, 999 ] # e.g. "261200000"
+
+ case parts[:country_code]
+
+ when '1'
+ # Assure an NANP number
+ return nil if ! remainder.match(/[2-9]{1}[0-9]{2}[2-9]{1}[0-9]{2}[0-9]{4}/)
+
+ # Shortcut for NANPA closed dialplan:
+ parts[:area_code ] = remainder[ 0, 3]
+ parts[:central_office_code ] = remainder[ 3, 3]
+ parts[:subscriber_number ] = remainder[ 6, 4]
+ else
+ # variable-length dialplan, e.g. Germany
+
+ # Find longest area_code for the country:
+ longest_area_code = country.area_codes.order( "LENGTH(area_code) DESC" ).first
+
+ # Find a matching area_code:
+ if longest_area_code
+ longest_area_code.area_code.length.downto(1) do |area_code_length|
+ area_code = country.area_codes.where( :area_code => remainder[ 0, area_code_length ] ).first
+ if area_code
+ parts[:area_code] = area_code.area_code
+ break
+ end
+ end
+
+ return nil if ! parts[:area_code] # No matching area_code for the country.
+
+ remainder = remainder.gsub( /^#{parts[:area_code]}/, '' )
+ #remainder = number[ parts[:area_code].length, 999 ] # e.g. "200000"
+ end
+ parts[:subscriber_number] = remainder
+ end
+
+ parts # return value
+ end
+
+ def parse_and_split_number!
+ if self.phone_numberable_type == 'PhoneNumberRange' && self.phone_numberable.name == INTERNAL_EXTENSIONS
+ # The parent is the PhoneNumberRange INTERNAL_EXTENSIONS. Therefor it must be an extensions.
+ #
+ self.country_code = nil
+ self.area_code = nil
+ self.subscriber_number = nil
+ self.central_office_code = nil
+ self.extension = self.number.to_s.strip
+ else
+ if self.tenant &&
+ self.tenant.phone_number_ranges.exists?(:name => INTERNAL_EXTENSIONS) &&
+ self.tenant.phone_number_ranges.where(:name => INTERNAL_EXTENSIONS).first.phone_numbers.exists?(:number => self.number)
+ self.country_code = nil
+ self.area_code = nil
+ self.subscriber_number = nil
+ self.central_office_code = nil
+ self.extension = self.number.to_s.strip
+ else
+ parsed_number = PhoneNumber.parse( self.number )
+ if parsed_number
+ self.country_code = parsed_number[:country_code]
+ self.area_code = parsed_number[:area_code]
+ self.subscriber_number = parsed_number[:subscriber_number]
+ self.extension = parsed_number[:extension]
+ self.central_office_code = parsed_number[:central_office_code]
+
+ self.number = self.to_s.gsub( /[^\+0-9]/, '' )
+ end
+ end
+ end
+ end
+
+ # Find the (grand-)parent tenant of this phone number:
+ #
+ def tenant
+ #OPTIMIZE Add a tenant_id to SipAccount
+ case self.phone_numberable
+ when SipAccount
+ self.phone_numberable.tenant
+ when Conference
+ case self.phone_numberable.conferenceable
+ when Tenant
+ self.phone_numberable.conferenceable
+ when User
+ self.phone_numberable.conferenceable.current_tenant #OPTIMIZE
+ when UserGroup
+ self.phone_numberable.conferenceable.tenant
+ end
+ end
+ end
+
+ def move_up?
+ return self.position.to_i > PhoneNumber.where(:phone_numberable_id => self.phone_numberable_id, :phone_numberable_type => self.phone_numberable_type ).order(:position).first.position.to_i
+ end
+
+ def move_down?
+ return self.position.to_i < PhoneNumber.where(:phone_numberable_id => self.phone_numberable_id, :phone_numberable_type => self.phone_numberable_type ).order(:position).last.position.to_i
+ end
+
+ private
+
+ def validate_number
+ if ! PhoneNumber.parse( self.number )
+ errors.add( :number, "is invalid." )
+ end
+ end
+
+ def check_if_number_is_available
+ if self.phone_numberable_type != 'PhoneBookEntry' && self.tenant
+
+ phone_number_ranges = self.tenant.phone_number_ranges.where(
+ :name => [INTERNAL_EXTENSIONS, DIRECT_INWARD_DIALING_NUMBERS]
+ )
+ if !phone_number_ranges.empty?
+ if !PhoneNumber.where(:phone_numberable_type => 'PhoneNumberRange').
+ where(:phone_numberable_id => phone_number_ranges).
+ exists?(:number => self.number)
+ errors.add(:number, "isn't defined as an extenation or DID for the tenant '#{self.tenant}'. #{phone_number_ranges.inspect}")
+ end
+ end
+ end
+ end
+
+ def validate_inbound_uniqueness
+ if NUMBER_TYPES_INBOUND.include?(self.phone_numberable_type)
+ numbering_scope = PhoneNumber.where(:state => 'active', :number => self.number, :phone_numberable_type => NUMBER_TYPES_INBOUND)
+ if numbering_scope.where(:id => self.id).count == 0 && numbering_scope.count > 0
+ errors.add(:number, 'not unique')
+ end
+ end
+ end
+
+ def save_value_of_to_s
+ self.value_of_to_s = self.to_s
+ end
+
+ def copy_existing_call_forwards_if_necessary
+ if self.phone_numberable.class == SipAccount && self.phone_numberable.callforward_rules_act_per_sip_account == true
+ sip_account = SipAccount.find(self.phone_numberable)
+ if sip_account.phone_numbers.where('id != ?', self.id).count > 0
+ if sip_account.phone_numbers.where('id != ?', self.id).order(:created_at).first.call_forwards.count > 0
+ sip_account.phone_numbers.where('id != ?', self.id).first.call_forwards.each do |call_forward|
+ call_forward.set_this_callforward_rule_to_all_phone_numbers_of_the_parent_sip_account
+ end
+ end
+ end
+ end
+ end
+
+end
diff --git a/app/models/phone_number_range.rb b/app/models/phone_number_range.rb
new file mode 100644
index 0000000..2fdd9b6
--- /dev/null
+++ b/app/models/phone_number_range.rb
@@ -0,0 +1,16 @@
+class PhoneNumberRange < ActiveRecord::Base
+ attr_accessible :name, :description
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ belongs_to :phone_number_rangeable, :polymorphic => true
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => [:phone_number_rangeable_id, :phone_number_rangeable_type]
+ validates_inclusion_of :name, :in => [INTERNAL_EXTENSIONS, DIRECT_INWARD_DIALING_NUMBERS, SERVICE_NUMBERS]
+ validates_presence_of :phone_number_rangeable_id
+ validates_presence_of :phone_number_rangeable
+
+ def to_s
+ name
+ end
+end
diff --git a/app/models/phone_sip_account.rb b/app/models/phone_sip_account.rb
new file mode 100644
index 0000000..449ba39
--- /dev/null
+++ b/app/models/phone_sip_account.rb
@@ -0,0 +1,17 @@
+class PhoneSipAccount < ActiveRecord::Base
+ attr_accessible :sip_account_id
+
+ belongs_to :phone
+ belongs_to :sip_account
+
+ validates_presence_of :phone
+ validates_presence_of :sip_account
+
+ validates_uniqueness_of :sip_account_id, :scope => :phone_id
+
+ acts_as_list :scope => :phone
+
+ def to_s
+ "Position #{self.position}"
+ end
+end
diff --git a/app/models/remote_gs_node/gs_cluster_sync_log_entry.rb b/app/models/remote_gs_node/gs_cluster_sync_log_entry.rb
new file mode 100644
index 0000000..843494e
--- /dev/null
+++ b/app/models/remote_gs_node/gs_cluster_sync_log_entry.rb
@@ -0,0 +1,9 @@
+# Find docu about ActiveResource at
+# http://ofps.oreilly.com/titles/9780596521424/activeresource_id59243.html
+# test = RemoteGSNode::GcLogEntry.first.attributes.delete_if{|key, value| ['id','updated_at','created_at'].include?(key) })
+
+module RemoteGsNode
+ class GsClusterSyncLogEntry < ActiveResource::Base
+ self.site = 'http://0.0.0.0:3000'
+ end
+end \ No newline at end of file
diff --git a/app/models/ringtone.rb b/app/models/ringtone.rb
new file mode 100644
index 0000000..36053c0
--- /dev/null
+++ b/app/models/ringtone.rb
@@ -0,0 +1,15 @@
+class Ringtone < ActiveRecord::Base
+ attr_accessible :audio, :bellcore_id
+
+ mount_uploader :audio, AudioUploader
+ validates_presence_of :audio, :if => Proc.new{ |ringtone| ringtone.bellcore_id.blank? }
+ validates_presence_of :ringtoneable_type
+ validates_presence_of :ringtoneable_id
+ validates_presence_of :ringtoneable
+
+ belongs_to :ringtoneable, :polymorphic => true
+
+ def to_s
+ self.bellcore_id.to_s
+ end
+end
diff --git a/app/models/sip_account.rb b/app/models/sip_account.rb
new file mode 100644
index 0000000..8459265
--- /dev/null
+++ b/app/models/sip_account.rb
@@ -0,0 +1,221 @@
+# encoding: UTF-8
+
+class SipAccount < ActiveRecord::Base
+ include ActionView::Helpers::TextHelper
+
+ attr_accessible :auth_name, :caller_name, :password, :voicemail_pin,
+ :tenant_id, :call_waiting, :clir, :clip_no_screening,
+ :clip, :description, :callforward_rules_act_per_sip_account,
+ :hotdeskable, :gs_node_id
+
+ # Associations:
+ #
+ belongs_to :sip_accountable, :polymorphic => true
+
+ has_many :phone_sip_accounts, :uniq => true
+ has_many :phones, :through => :phone_sip_accounts
+
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+ has_many :call_forwards, :through => :phone_numbers
+
+ belongs_to :tenant
+ belongs_to :sip_domain
+
+ has_many :softkeys, :dependent => :destroy
+
+ has_many :voicemail_messages, :foreign_key => 'username', :primary_key => 'auth_name'
+
+ has_many :call_histories, :as => :call_historyable, :dependent => :destroy
+
+ has_one :voicemail_setting, :class_name => "VoicemailSetting", :primary_key => 'auth_name', :foreign_key => 'username', :dependent => :destroy
+
+ belongs_to :gs_node
+
+ # Delegations:
+ #
+ delegate :host, :to => :sip_domain, :allow_nil => true
+ delegate :realm, :to => :sip_domain, :allow_nil => true
+
+ # Validations:
+ #
+ validates_presence_of :caller_name
+ validates_presence_of :sip_accountable
+ validates_presence_of :tenant
+ validates_presence_of :sip_domain
+
+ validate_sip_password :password
+
+ validates_format_of :voicemail_pin, :with => /[0-9]+/,
+ :allow_nil => true, :allow_blank => true
+
+ validates_uniqueness_of :auth_name, :scope => :sip_domain_id
+
+ # Before and after hooks:
+ #
+ before_save :save_value_of_to_s
+ after_save :create_voicemail_setting, :if => :'voicemail_setting == nil'
+ before_validation :find_and_set_tenant_id
+ before_validation :set_sip_domain_id
+ before_validation :convert_umlauts_in_caller_name
+ before_destroy :remove_sip_accounts_or_logout_phones
+
+ # Sync other nodes when this is a cluster.
+ #
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ after_create { self.create_on_other_gs_nodes('sip_accountable', self.sip_accountable.try(:uuid)) }
+ after_destroy :destroy_on_other_gs_nodes
+ after_update { self.update_on_other_gs_nodes('sip_accountable', self.sip_accountable.try(:uuid)) }
+
+ after_update :log_out_phone_if_not_local
+
+ def to_s
+ truncate((self.caller_name || "SipAccount ID #{self.id}"), :length => TO_S_MAX_CALLER_NAME_LENGTH) + " (#{truncate(self.auth_name, :length => TO_S_MAX_LENGTH_OF_AUTH_NAME)}@...#{self.host.split(/\./)[2,3].to_a.join('.') if self.host })"
+ end
+
+ def call_forwarding_toggle( call_forwarding_service, to_voicemail = nil )
+ if ! self.phone_numbers.first
+ errors.add(:base, "You must provide at least one phone number")
+ end
+
+ service_id = CallForwardCase.where(:value => call_forwarding_service).first.id
+
+ call_forwarding_master = self.phone_numbers.first.call_forwards.where(:call_forward_case_id => service_id).order(:active).all(:conditions => 'source IS NULL OR source = ""').first
+ if ! call_forwarding_master
+ errors.add(:base, "No call forwarding entries found that could be toggled")
+ return false
+ end
+
+ if call_forwarding_master.active
+ call_forwarding_master.active = false
+ else
+ if call_forwarding_service = 'assistant' && call_forwarding_master.call_forwardable_type == 'HuntGroup' && call_forwarding_master.call_forwardable
+ if call_forwarding_master.call_forwardable.hunt_group_members.where(:active => true).count > 0
+ call_forwarding_master.active = true
+ else
+ call_forwarding_master.active = false
+ end
+ end
+ end
+
+ self.phone_numbers.each do |phone_number|
+ call_forwarding = phone_number.call_forwards.where(:call_forward_case_id => service_id).order(:active).all(:conditions => 'source IS NULL OR source = ""').first
+ if ! call_forwarding
+ call_forwarding = CallForward.new()
+ call_forwarding.phone_number_id = phone_number.id
+ end
+
+ if to_voicemail == nil
+ to_voicemail = call_forwarding_master.to_voicemail
+ end
+
+ call_forwarding.call_forward_case_id = call_forwarding_master.call_forward_case_id
+ call_forwarding.timeout = call_forwarding_master.timeout
+ call_forwarding.destination = call_forwarding_master.destination
+ call_forwarding.source = call_forwarding_master.source
+ call_forwarding.depth = call_forwarding_master.depth
+ call_forwarding.active = call_forwarding_master.active
+ call_forwarding.to_voicemail = to_voicemail
+
+ if ! call_forwarding.save
+ call_forwarding.errors.messages.each_with_index do |(error_key, error_message), index|
+ errors.add(error_key, "number: #{phone_number}: #{error_message}")
+ end
+ end
+ end
+
+ if errors.empty?
+ return call_forwarding_master
+ end
+
+ return false
+ end
+
+ def registration
+ return FreeswitchRegistration.where(:reg_user => self.auth_name).first
+ end
+
+ def call( phone_number )
+ require 'freeswitch_event'
+ return FreeswitchAPI.execute(
+ 'originate',
+ "{origination_uuid=#{UUID.new.generate},origination_caller_id_number='#{phone_number}',origination_caller_id_name='Call'}user/#{self.auth_name} #{phone_number}",
+ true
+ );
+ end
+
+
+ private
+
+ def save_value_of_to_s
+ self.value_of_to_s = self.to_s
+ end
+
+ def find_and_set_tenant_id
+ if self.new_record? and self.tenant_id != nil
+ return
+ else
+ tenant = case self.sip_accountable_type
+ when 'Tenant' ; sip_accountable
+ when 'UserGroup' ; sip_accountable.tenant
+ when 'User' ; sip_accountable.try(:current_tenant) || sip_accountable.try(:tenants).try(:last)
+ else nil
+ end
+ self.tenant_id = tenant.id if tenant != nil
+ end
+ end
+
+ def set_sip_domain_id
+ self.sip_domain_id = self.tenant.try(:sip_domain_id)
+ end
+
+ def convert_umlauts_in_caller_name
+ if !self.caller_name.blank?
+ self.caller_name = self.caller_name.sub(/ä/,'ae').
+ sub(/Ä/,'Ae').
+ sub(/ü/,'ue').
+ sub(/Ü/,'Ue').
+ sub(/ö/,'oe').
+ sub(/Ö/,'Oe').
+ sub(/ß/,'ss')
+
+ self.caller_name = self.caller_name.gsub(/[^a-zA-Z0-9\-\,\:\. ]/,'_')
+ end
+ end
+
+ # Make sure that a tenant phone goes back to the tenant and doesn't
+ # get deleted with this user.
+ #
+ def remove_sip_accounts_or_logout_phones
+ self.phones.each do |phone|
+ if phone.sip_accounts.where(:sip_accountable_type => 'Tenant').count > 0
+ phone.user_logout
+ else
+ PhoneSipAccount.delete_all(:sip_account_id => self.id)
+ end
+ end
+ self.reload
+ end
+
+ # log out phone if sip_account is not on this node
+ def log_out_phone_if_not_local
+ if self.gs_node_id && ! GsNode.where(:ip_address => HOMEBASE_IP_ADDRESS, :id => self.gs_node_id).first
+ self.phones.each do |phone|
+ phone.user_logout;
+ end
+ end
+ end
+
+ def create_voicemail_setting
+ voicemail_setting = VoicemailSetting.new()
+ voicemail_setting.username = self.auth_name
+ voicemail_setting.domain = self.sip_domain.try(:host)
+ voicemail_setting.password = self.voicemail_pin
+ voicemail_setting.notify = true
+ voicemail_setting.attachment = true
+ voicemail_setting.mark_read = true
+ voicemail_setting.purge = false
+ voicemail_setting.save
+ end
+end
diff --git a/app/models/sip_domain.rb b/app/models/sip_domain.rb
new file mode 100644
index 0000000..252fe4a
--- /dev/null
+++ b/app/models/sip_domain.rb
@@ -0,0 +1,16 @@
+class SipDomain < ActiveRecord::Base
+ attr_accessible :host, :realm
+
+ has_many :tenants, :dependent => :restrict
+ has_many :sip_accounts, :dependent => :restrict
+
+ validates_presence_of :host
+ validates_uniqueness_of :host, :case_sensitive => false
+
+ validates_presence_of :realm
+ validates_uniqueness_of :realm
+
+ def to_s
+ self.host
+ end
+end
diff --git a/app/models/softkey.rb b/app/models/softkey.rb
new file mode 100644
index 0000000..a709036
--- /dev/null
+++ b/app/models/softkey.rb
@@ -0,0 +1,100 @@
+class Softkey < ActiveRecord::Base
+ attr_accessible :softkey_function_id, :number, :label, :call_forward_id, :uuid
+
+ belongs_to :sip_account
+ belongs_to :softkey_function
+ belongs_to :call_forward
+
+ # Any CallForward BLF must have a valid softkey_call_forward_id.
+ #
+ validates_presence_of :call_forward_id, :if => Proc.new{ |softkey| self.softkey_function_id != nil &&
+ self.softkey_function_id == SoftkeyFunction.find_by_name('call_forwarding').try(:id) }
+
+ # These functions need a number to act.
+ #
+ validates_presence_of :number, :if => Proc.new{ |softkey| self.softkey_function_id != nil &&
+ ['blf','speed_dial','dtmf','conference'].include?(softkey.softkey_function.name) }
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ acts_as_list :scope => :sip_account
+
+ before_validation :clean_up_and_leave_only_values_which_make_sense_for_the_current_softkey_function_id
+ after_validation :save_function_name_in_function, :if => Proc.new{ |softkey| self.call_forward_id.blank? }
+ after_save :resync_phone
+ after_destroy :resync_phone
+
+ def possible_blf_call_forwards
+ if self.sip_account.phone_numbers.count == 0
+ nil
+ else
+ if self.sip_account.callforward_rules_act_per_sip_account == true
+ # We pick one phone_number and display the rules of it.
+ #
+ phone_number = self.sip_account.phone_numbers.order(:number).first
+ call_forwards = self.sip_account.call_forwards.where(:phone_number_id => phone_number.id)
+ else
+ call_forwards = self.sip_account.call_forwards
+ end
+
+ phone_numbers_ids = self.sip_account.phone_number_ids
+ phone_numbers = PhoneNumber.where(:id => phone_numbers_ids).pluck(:number)
+
+ hunt_group_ids = PhoneNumber.where(:phone_numberable_type => 'HuntGroupMember', :number => phone_numbers).
+ map{ |phone_number| phone_number.phone_numberable.hunt_group.id }.
+ uniq
+
+ call_forwards + CallForward.where(:call_forwardable_type => 'HuntGroup', :call_forwardable_id => hunt_group_ids).
+ where('phone_number_id NOT IN (?)', phone_numbers_ids)
+ end
+ end
+
+ def to_s
+ if (['call_forwarding'].include?(self.softkey_function.name))
+ "#{self.call_forward}"
+ else
+ if ['log_out', 'log_in'].include?(self.softkey_function.name)
+ I18n.t("softkeys.functions.#{self.softkey_function.name}")
+ else
+ "#{self.softkey_function.name} : #{self.number.to_s}"
+ end
+ end
+ end
+
+ def resync_phone
+ phone_sip_account = PhoneSipAccount.find_by_sip_account_id(self.sip_account_id)
+ if phone_sip_account && phone_sip_account.phone
+ phone_sip_account.phone.resync()
+ end
+ end
+
+ def move_up?
+ return self.position.to_i > Softkey.where(:sip_account_id => self.sip_account_id ).order(:position).first.position.to_i
+ end
+
+ def move_down?
+ return self.position.to_i < Softkey.where(:sip_account_id => self.sip_account_id ).order(:position).last.position.to_i
+ end
+
+ private
+
+ def save_function_name_in_function
+ self.function = self.softkey_function.name
+ end
+
+ # Make sure that no number is set when there is no need for one.
+ # And make sure that there is no CallForward connected when not needed.
+ #
+ def clean_up_and_leave_only_values_which_make_sense_for_the_current_softkey_function_id
+ if self.softkey_function_id != nil
+ if ['blf','speed_dial','dtmf','conference'].include?(self.softkey_function.name)
+ self.call_forward_id = nil
+ end
+ if ['call_forwarding'].include?(self.softkey_function.name)
+ self.number = nil
+ end
+ end
+ end
+
+end
diff --git a/app/models/softkey_function.rb b/app/models/softkey_function.rb
new file mode 100644
index 0000000..976827d
--- /dev/null
+++ b/app/models/softkey_function.rb
@@ -0,0 +1,13 @@
+class SoftkeyFunction < ActiveRecord::Base
+ validates_presence_of :name
+
+ validates_uniqueness_of :name
+
+ acts_as_list
+
+ default_scope order(:position)
+
+ def to_s
+ self.name
+ end
+end
diff --git a/app/models/system_message.rb b/app/models/system_message.rb
new file mode 100644
index 0000000..0d9e862
--- /dev/null
+++ b/app/models/system_message.rb
@@ -0,0 +1,7 @@
+class SystemMessage < ActiveRecord::Base
+ attr_accessible :content
+
+ belongs_to :user
+
+ validates_presence_of :content
+end
diff --git a/app/models/tenant.rb b/app/models/tenant.rb
new file mode 100644
index 0000000..6f98603
--- /dev/null
+++ b/app/models/tenant.rb
@@ -0,0 +1,243 @@
+# encoding: UTF-8
+
+class Tenant < ActiveRecord::Base
+ attr_accessible :name, :description, :sip_domain_id, :country_id, :language_id, :from_field_pin_change_email, :from_field_voicemail_email
+
+ if STRICT_INTERNAL_EXTENSION_HANDLING == true
+ attr_accessible :internal_extension_ranges
+ end
+
+ if STRICT_DID_HANDLING == true
+ attr_accessible :did_list
+ end
+
+ # Associations:
+ #
+ has_many :tenant_memberships, :dependent => :destroy
+ has_many :users, :through => :tenant_memberships, :validate => true
+
+ has_many :user_groups, :dependent => :destroy
+
+ has_many :phone_books, :as => :phone_bookable, :dependent => :destroy
+ has_many :phone_book_entries, :through => :phone_books
+
+ has_many :phone_number_ranges, :as => :phone_number_rangeable, :dependent => :destroy
+
+ has_many :phones, :as => :phoneable, :dependent => :destroy
+ has_many :users_phones, :through => :users, :source => :phones, :readonly => true
+
+ has_many :callthroughs, :dependent => :destroy
+
+ has_many :fax_accounts, :dependent => :destroy # A tenant can't have a FaxAccount by itself!
+
+ belongs_to :country
+ belongs_to :language
+
+ belongs_to :sip_domain
+
+ has_many :sip_accounts, :as => :sip_accountable, :dependent => :destroy
+ has_many :users_sip_accounts, :through => :users, :source => :sip_accounts, :readonly => true
+
+ has_many :conferences, :as => :conferenceable, :dependent => :destroy
+
+ has_many :hunt_groups, :dependent => :destroy
+ has_many :hunt_group_members, :through => :hunt_groups
+
+ has_many :automatic_call_distributors, :as => :automatic_call_distributorable, :dependent => :destroy
+ has_many :acd_agents, :through => :automatic_call_distributors
+
+ # Phone numbers of the tenant.
+ #
+ has_many :phone_number_ranges_phone_numbers, :through => :phone_number_ranges, :source => :phone_numbers, :readonly => true
+ has_many :phone_numbers, :through => :sip_accounts
+ has_many :conferences_phone_numbers, :through => :conferences, :source => :phone_numbers, :readonly => true
+ has_many :callthroughs_phone_numbers, :through => :conferences, :source => :phone_numbers, :readonly => true
+ has_many :huntgroups_phone_numbers, :through => :conferences, :source => :phone_numbers, :readonly => true
+ has_many :fax_accounts_phone_numbers, :through => :fax_accounts, :source => :phone_numbers, :readonly => true
+
+ # Phone numbers of users of the tenant.
+ #
+ has_many :users_phone_numbers, :through => :users, :source => :phone_numbers, :readonly => true
+ has_many :user_groups_phone_numbers, :through => :users, :source => :phone_numbers, :readonly => true
+ has_many :users_conferences, :through => :users, :source => :conferences, :readonly => true
+ has_many :users_conferences_phone_numbers, :through => :users_conferences, :source => :phone_numbers, :readonly => true
+ has_many :users_fax_accounts, :through => :users, :source => :fax_accounts, :readonly => true
+ has_many :users_fax_accounts_phone_numbers, :through => :users_fax_accounts, :source => :phone_numbers, :readonly => true
+
+ # Validations:
+ #
+ validates_presence_of :name, :state, :country, :language
+ validates_length_of :name, :within => 1..255
+ validates_uniqueness_of :name
+
+ validates_length_of :name, :within => 1..100
+
+ # Before and after hooks:
+ #
+ after_create :create_a_default_phone_book
+
+ # State machine:
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+
+ event :deactivate do
+ transition [:active] => :deactivated
+ end
+
+ event :activate do
+ transition [:deactivated] => :active
+ end
+ end
+
+ def to_s
+ name
+ end
+
+ if STRICT_INTERNAL_EXTENSION_HANDLING == true
+ def array_of_internal_extension_numbers
+ ranges = self.internal_extension_ranges.gsub(/[^0-9\-,]/,'').gsub(/[\-]+/,'-').gsub(/[,]+/,',').split(/,/)
+ output = []
+ ranges.each do |range|
+ mini_range = range.split(/-/).map{|x| x.to_i}.sort
+ if mini_range.size == 1
+ output << mini_range[0]
+ else
+ output = output + (mini_range[0]..mini_range[1]).to_a
+ end
+ output = output.try(:flatten)
+ end
+ output.try(:sort).try(:uniq).map{|number| number.to_s }
+ end
+
+ # Generate the internal_extensions
+ #
+ def generate_internal_extensions
+ internal_extensions = self.phone_number_ranges.find_or_create_by_name(INTERNAL_EXTENSIONS, :description => 'A list of all available internal extensions.')
+
+ phone_number_list = Array.new
+
+ if self.array_of_internal_extension_numbers.size > 0
+ if self.country.phone_number_ranges.first.try(:phone_numbers) == nil
+ phone_number_list = self.array_of_internal_extension_numbers
+ elsif
+ # Don't create extensions like 911, 110 or 112 (at least by default)
+ #
+ phone_number_list = (self.array_of_internal_extension_numbers - self.country.phone_number_ranges.where(:name => SERVICE_NUMBERS).first.phone_numbers.map{|entry| entry.number})
+ end
+ end
+
+ phone_number_list.each do |number|
+ internal_extensions.phone_numbers.find_or_create_by_name_and_number('Extension', number)
+ end
+ end
+
+ end
+
+ if STRICT_DID_HANDLING == true
+ def array_of_dids_generated_from_did_list
+ numbers = self.did_list.downcase.gsub(/[^0-9,x\+]/,'').gsub(/[,]+/,',').split(/,/)
+ array_of_all_external_numbers = []
+ numbers.each do |number|
+ if number.include?('x')
+ self.array_of_internal_extension_numbers.each do |internal_extension|
+ array_of_all_external_numbers << number.gsub(/x/, "-#{internal_extension.to_s}")
+ end
+ else
+ array_of_all_external_numbers << number
+ end
+ end
+ array_of_all_external_numbers.try(:sort).try(:uniq).map{|number| number.to_s }
+ end
+
+ # Generate the external numbers (DIDs)
+ #
+ def generate_dids
+ dids = self.phone_number_ranges.find_or_create_by_name(DIRECT_INWARD_DIALING_NUMBERS, :description => 'A list of all available DIDs.')
+ self.array_of_dids_generated_from_did_list.each do |number|
+ dids.phone_numbers.find_or_create_by_name_and_number('DID', number)
+ end
+ end
+
+ end
+
+
+ # All phone_numbers which can be used
+ #
+ def internal_extensions_and_dids
+ @internal_extensions_and_dids ||= self.phone_number_ranges_phone_numbers.
+ where(:phone_numberable_type => 'PhoneNumberRange').
+ where(:phone_numberable_id => self.phone_number_ranges.
+ where(:name => [INTERNAL_EXTENSIONS, DIRECT_INWARD_DIALING_NUMBERS]).
+ map{|pnr| pnr.id })
+ end
+
+ def array_of_internal_extensions
+ @array_of_internal_extensions ||= self.phone_number_ranges_phone_numbers.
+ where(:phone_numberable_type => 'PhoneNumberRange').
+ where(:phone_numberable_id => self.phone_number_ranges.
+ where(:name => INTERNAL_EXTENSIONS).
+ map{|pnr| pnr.id }).
+ map{|phone_number| phone_number.number }.
+ sort.uniq
+ end
+
+ def array_of_dids
+ @array_of_dids ||= self.phone_number_ranges_phone_numbers.
+ where(:phone_numberable_type => 'PhoneNumberRange').
+ where(:phone_numberable_id => self.phone_number_ranges.where(:name => DIRECT_INWARD_DIALING_NUMBERS).map{|pnr| pnr.id }).
+ map{|phone_number| phone_number.to_s }.
+ sort.uniq
+ end
+
+ def array_of_assigned_phone_numbers
+ (self.phone_numbers + self.conferences_phone_numbers +
+ self.callthroughs_phone_numbers + self.huntgroups_phone_numbers +
+ self.fax_accounts_phone_numbers + self.users_phone_numbers +
+ self.user_groups_phone_numbers + self.users_conferences_phone_numbers +
+ self.users_fax_accounts_phone_numbers).
+ map{|phone_number| phone_number.number }.
+ sort.uniq
+ end
+
+ def array_of_available_internal_extensions
+ (self.array_of_internal_extensions - self.array_of_assigned_phone_numbers).sort.uniq
+ end
+
+ def array_of_available_dids
+ (self.array_of_dids - self.array_of_assigned_phone_numbers).sort.uniq
+ end
+
+ def array_of_available_internal_extensions_and_dids
+ self.array_of_available_internal_extensions + self.array_of_available_dids
+ end
+
+ private
+
+ # Create a public phone book for this tenant
+ def create_a_default_phone_book
+ if self.name != SUPER_TENANT_NAME
+ general_phone_book = self.phone_books.find_or_create_by_name_and_description(
+ I18n.t('phone_books.general_phone_book.name'),
+ I18n.t('phone_books.general_phone_book.description', :resource => self.to_s)
+ )
+ amooma = general_phone_book.phone_book_entries.create(
+ :organization => 'AMOOMA GmbH',
+ :is_organization => true,
+ :description => "Hersteller von Gemeinschaft. Rufen Sie uns an, falls Sie kommerziellen Support oder Consulting für Gemeinschaft benötigen.",
+ :homepage_organization => 'http://www.amooma.de',
+ :twitter_account => 'amooma_de',
+ :facebook_account => 'https://www.facebook.com/AMOOMA.GmbH',
+ )
+ amooma.phone_numbers.create(
+ :name => 'Office',
+ :number => '+492622706480'
+ )
+ amooma.addresses.create(
+ :street => 'Bachstr. 124',
+ :zip_code => '56566',
+ :city => 'Neuwied',
+ :country_id => Country.where(:country_code => 49).first.try(:id),
+ )
+ end
+ end
+end
diff --git a/app/models/tenant_membership.rb b/app/models/tenant_membership.rb
new file mode 100644
index 0000000..122f702
--- /dev/null
+++ b/app/models/tenant_membership.rb
@@ -0,0 +1,25 @@
+class TenantMembership < ActiveRecord::Base
+ belongs_to :tenant
+ belongs_to :user
+
+ validates_presence_of :tenant
+ validates_presence_of :user
+
+ after_create :set_current_tenant_if_necessary
+
+ # State Machine stuff
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+ end
+
+ private
+ # The first TenantMembership becomes the current_tenant by default.
+ #
+ def set_current_tenant_if_necessary
+ if !self.user.current_tenant
+ self.user.current_tenant = self.tenant
+ self.user.save
+ end
+ end
+
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..2d0256f
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,208 @@
+require 'digest/sha2'
+
+class User < ActiveRecord::Base
+ after_create :create_a_default_phone_book, :if => :'is_native != false'
+
+ # Sync other nodes when this is a cluster.
+ #
+ after_create :create_on_other_gs_nodes
+ after_destroy :destroy_on_other_gs_nodes
+ after_update :update_on_other_gs_nodes
+
+ attr_accessible :user_name, :email, :password, :password_confirmation,
+ :first_name, :middle_name, :last_name, :male,
+ :image, :current_tenant_id, :language_id,
+ :new_pin, :new_pin_confirmation, :send_voicemail_as_email_attachment,
+ :importer_checksum, :gs_node_id
+
+ attr_accessor :new_pin, :new_pin_confirmation
+
+ before_validation {
+ # If the PIN and PIN confirmation are left blank in the GUI
+ # then the user/admin does not want to change the PIN.
+ if self.new_pin.blank? && self.new_pin_confirmation.blank?
+ self.new_pin = nil
+ self.new_pin_confirmation = nil
+ end
+ }
+
+ validates_length_of [:new_pin, :new_pin_confirmation],
+ :minimum => MINIMUM_PIN_LENGTH, :maximum => MAXIMUM_PIN_LENGTH,
+ :allow_blank => true, :allow_nil => true
+ validates_format_of [:new_pin, :new_pin_confirmation],
+ :with => /^[0-9]+$/,
+ :allow_blank => true, :allow_nil => true,
+ :message => "must be numeric."
+
+ validates_confirmation_of :new_pin, :if => :'pin_changed?'
+ before_save :hash_new_pin, :if => :'pin_changed?'
+
+ has_secure_password
+
+ validates_presence_of :password, :password_confirmation, :on => :create, :if => :'password_digest.blank?'
+ validates_presence_of :email
+ validates_presence_of :last_name
+ validates_presence_of :first_name
+ validates_presence_of :user_name
+
+ validates_uniqueness_of :user_name, :case_sensitive => false
+ validates_uniqueness_of :email, :allow_nil => true, :case_sensitive => false
+
+ validates_length_of :user_name, :within => 0..50
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ # Associations:
+ #
+ has_many :tenant_memberships, :dependent => :destroy
+ has_many :tenants, :through => :tenant_memberships
+
+ has_many :user_group_memberships, :dependent => :destroy, :uniq => true
+ has_many :user_groups, :through => :user_group_memberships
+
+ has_many :phone_books, :as => :phone_bookable, :dependent => :destroy
+ has_many :phone_book_entries, :through => :phone_books
+
+ has_many :phones, :as => :phoneable
+ has_many :sip_accounts, :as => :sip_accountable, :dependent => :destroy
+ has_many :phone_numbers, :through => :sip_accounts
+
+ has_many :conferences, :as => :conferenceable, :dependent => :destroy
+
+ has_many :fax_accounts, :as => :fax_accountable, :dependent => :destroy
+
+ has_many :system_messages, :dependent => :destroy
+
+ has_many :auto_destroy_access_authorization_phone_numbers, :class_name => 'PhoneNumber', :foreign_key => 'access_authorization_user_id', :dependent => :destroy
+
+ belongs_to :current_tenant, :class_name => 'Tenant'
+ validates_presence_of :current_tenant, :if => Proc.new{ |user| user.current_tenant_id }
+
+ belongs_to :language
+ validates_presence_of :language_id
+ validates_presence_of :language
+
+ validate :current_tenant_is_included_in_tenants, :if => Proc.new{ |user| user.current_tenant_id }
+
+ belongs_to :gs_node
+
+ # Avatar like photo
+ mount_uploader :image, ImageUploader
+
+ before_save :format_email_and_user_name
+
+ before_destroy :destroy_or_logout_phones
+
+ def destroy
+ clean_whitelist_entries
+ super
+ end
+
+ def pin_changed?
+ ! @new_pin.blank?
+ end
+
+ def sip_domain
+ if self.current_tenant
+ return self.current_tenant.sip_domain
+ end
+ return nil
+ end
+
+ def to_s
+ max_first_name_length = 10
+ max_last_name_length = 20
+ if self.first_name.blank?
+ self.last_name.strip
+ else
+ "#{self.first_name.strip} #{self.last_name.strip}"
+ end
+ end
+
+ def self.find_user_by_phone_number( number, tenant )
+ tenant = Tenant.where( :id => tenant.id ).first
+ if tenant
+ if tenant.sip_domain
+ user = tenant.sip_domain.sip_accounts.
+ joins(:phone_numbers).
+ where(:phone_numbers => { :number => number }).
+ first.
+ try(:sip_accountable)
+ if user.class.name == 'User'
+ return user
+ end
+ end
+ end
+ return nil
+ end
+
+ def authenticate_by_pin?( entered_pin )
+ self.pin_hash == Digest::SHA2.hexdigest( "#{self.pin_salt}#{entered_pin}" )
+ end
+
+
+ private
+
+ def hash_new_pin
+ if @new_pin \
+ && @new_pin_confirmation \
+ && @new_pin_confirmation == @new_pin
+ self.pin_salt = SecureRandom.base64(8)
+ self.pin_hash = Digest::SHA2.hexdigest(self.pin_salt + @new_pin)
+ end
+ end
+
+ def format_email_and_user_name
+ self.email = self.email.downcase.strip if !self.email.blank?
+ self.user_name = self.user_name.downcase.strip if !self.user_name.blank?
+ end
+
+ # Create a personal phone book for this user:
+ def create_a_default_phone_book
+ private_phone_book = self.phone_books.find_or_create_by_name_and_description(
+ I18n.t('phone_books.private_phone_book.name', :resource => self.to_s),
+ I18n.t('phone_books.private_phone_book.description')
+ )
+ end
+
+ # Check if a current_tenant_id is possible tenant_membership wise.
+ def current_tenant_is_included_in_tenants
+ if !self.tenants.include?(Tenant.find(self.current_tenant_id))
+ errors.add(:current_tenant_id, "is not possible (no TenantMembership)")
+ end
+ end
+
+ # Make sure that there are no whitelist entries with phone_numbers of
+ # a just destroyed user.
+ #
+ def clean_whitelist_entries
+ phone_numbers = PhoneNumber.where( :phone_numberable_type => 'Whitelist').
+ where( :number => self.phone_numbers.map{ |x| x.number } )
+ phone_numbers.each do |phone_number|
+ if phone_number.phone_numberable.whitelistable.class == Callthrough
+ whitelist = Whitelist.find(phone_number.phone_numberable)
+ phone_number.destroy
+ if whitelist.phone_numbers.count == 0
+ # Very lickly that this Whitelist doesn't make sense any more.
+ #
+ whitelist.destroy
+ end
+ end
+ end
+ end
+
+ # Make sure that a tenant phone goes back to the tenant and doesn't
+ # get deleted with this user.
+ #
+ def destroy_or_logout_phones
+ self.phones.each do |phone|
+ if phone.sip_accounts.where(:sip_accountable_type => 'Tenant').count > 0
+ phone.user_logout
+ else
+ phone.destroy
+ end
+ end
+ end
+
+end
diff --git a/app/models/user_group.rb b/app/models/user_group.rb
new file mode 100644
index 0000000..44f2fd8
--- /dev/null
+++ b/app/models/user_group.rb
@@ -0,0 +1,33 @@
+class UserGroup < ActiveRecord::Base
+ attr_accessible :name, :description
+
+ belongs_to :tenant
+
+ validates_presence_of :name
+ validates_uniqueness_of :name, :scope => :tenant_id
+
+ validates_presence_of :tenant
+
+ validates_length_of :name, :within => 1..50
+
+ has_many :user_group_memberships, :dependent => :destroy, :uniq => true
+ has_many :users, :through => :user_group_memberships
+
+ has_many :gui_function_memberships, :dependent => :destroy
+ has_many :gui_functions, :through => :gui_function_memberships
+
+ has_many :phone_books, :as => :phone_bookable, :dependent => :destroy
+ has_many :phone_book_entries, :through => :phone_books
+
+ has_many :sip_accounts, :as => :sip_accountable, :dependent => :destroy
+
+ has_many :conferences, :as => :conferenceable, :dependent => :destroy
+
+ has_many :fax_accounts, :as => :fax_accountable, :dependent => :destroy
+
+ acts_as_list :scope => :tenant_id
+
+ def to_s
+ name
+ end
+end
diff --git a/app/models/user_group_membership.rb b/app/models/user_group_membership.rb
new file mode 100644
index 0000000..18a8d48
--- /dev/null
+++ b/app/models/user_group_membership.rb
@@ -0,0 +1,26 @@
+class UserGroupMembership < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :user_group
+
+ validates_uniqueness_of :user_id, :scope => :user_group_id
+ validates_presence_of :user
+ validates_presence_of :user_group
+
+ validate :user_belongs_to_the_tenant_of_the_user_group
+
+ # State Machine stuff
+ default_scope where(:state => 'active')
+ state_machine :initial => :active do
+ end
+
+ def to_s
+ "#{self.user} / #{self.user_group}"
+ end
+
+ private
+ def user_belongs_to_the_tenant_of_the_user_group
+ if !self.user_group.tenant.users.include?(self.user)
+ errors.add(:user_id, "not a member of the tenant which this group belongs to")
+ end
+ end
+end
diff --git a/app/models/voicemail_message.rb b/app/models/voicemail_message.rb
new file mode 100644
index 0000000..91ba457
--- /dev/null
+++ b/app/models/voicemail_message.rb
@@ -0,0 +1,52 @@
+class VoicemailMessage < ActiveRecord::Base
+ self.table_name = 'voicemail_msgs'
+ self.primary_key = 'uuid'
+
+# belongs_to :sip_account, :foreign_key => 'username', :primary_key => 'auth_name', :readonly => true
+ # Prevent objects from being destroyed
+ def before_destroy
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Prevent objects from being deleted
+ def self.delete_all
+ raise ActiveRecord::ReadOnlyRecord
+ end
+
+ # Delete Message on FreeSWITCH over EventAPI
+ def delete
+ require 'freeswitch_event'
+ result = FreeswitchAPI.execute('vm_delete', "#{self.username}@#{self.domain} #{self.uuid}");
+ end
+
+ # Alias for delete
+ def destroy
+ self.delete
+ end
+
+ # Mark Message read
+ def mark_read(mark_read_or_unread = true)
+ read_status = mark_read_or_unread ? 'read' : 'unread'
+ require 'freeswitch_event'
+ result = FreeswitchAPI.execute('vm_read', "#{self.username}@#{self.domain} #{read_status} #{self.uuid}");
+ end
+
+ def format_date(epoch, date_format = '%m/%d/%Y %H:%M', date_today_format = '%H:%M')
+ if epoch && epoch > 0
+ time = Time.at(epoch)
+ if time.strftime('%Y%m%d') == Time.now.strftime('%Y%m%d')
+ return time.in_time_zone.strftime(date_today_format)
+ end
+ return time.in_time_zone.strftime(date_format)
+ end
+ end
+
+ def display_duration
+ if self.message_len.to_i > 0
+ minutes = (self.message_len / 1.minutes).to_i
+ seconds = self.message_len - minutes.minutes.seconds
+ return '%i:%02i' % [minutes, seconds]
+ end
+ end
+
+end
diff --git a/app/models/voicemail_setting.rb b/app/models/voicemail_setting.rb
new file mode 100644
index 0000000..a8bb304
--- /dev/null
+++ b/app/models/voicemail_setting.rb
@@ -0,0 +1,12 @@
+class VoicemailSetting < ActiveRecord::Base
+ self.table_name = 'voicemail_prefs'
+ self.primary_key = 'username'
+
+ attr_accessible :username, :domain, :name_path, :greeting_path, :password, :notify, :attachment, :mark_read, :purge, :sip_account
+
+ has_one :sip_account, :foreign_key => 'auth_name'
+
+ validates_presence_of :username
+ validates_presence_of :domain
+ validates :username, :uniqueness => {:scope => :domain}
+end
diff --git a/app/models/whitelist.rb b/app/models/whitelist.rb
new file mode 100644
index 0000000..8303728
--- /dev/null
+++ b/app/models/whitelist.rb
@@ -0,0 +1,23 @@
+class Whitelist < ActiveRecord::Base
+ attr_accessible :name, :phone_numbers_attributes, :uuid
+
+ belongs_to :whitelistable, :polymorphic => true
+
+ # These are the phone_numbers for this whitelist.
+ #
+ has_many :phone_numbers, :as => :phone_numberable, :dependent => :destroy
+
+ accepts_nested_attributes_for :phone_numbers,
+ :reject_if => lambda { |phone_number| phone_number[:number].blank? },
+ :allow_destroy => true
+
+ acts_as_list :scope => [ :whitelistable_type, :whitelistable_id ]
+
+ validates_presence_of :uuid
+ validates_uniqueness_of :uuid
+
+ def to_s
+ self.name || I18n.t('whitelists.name') + ' ID ' + self.id.to_s
+ end
+
+end