diff options
Diffstat (limited to 'app/models')
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 |