Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • ActionMetaData
  • ActionText
  • Admin
  • AmorphousModel
  • ApiHook
  • APIModel
  • ChartSetting
  • ContactForm
  • ContactList
  • Credentials
  • Criteria
  • Dropdowns
  • Events
  • EventsData
  • Fields
  • FormLayout
  • Imports
  • InlineEmail
  • LeadRouting
  • Locations
  • LoginForm
  • Maps
  • Modules
  • Notes
  • Notification
  • PhoneNumber
  • Profile
  • Record
  • Relationships
  • Roles
  • RoleToPermission
  • RoleToUser
  • RoleToWorkflow
  • Rules
  • Session
  • SessionLog
  • Social
  • Tags
  • TempFile
  • Tips
  • Tours
  • TrackEmail
  • TriggerLog
  • URL
  • ViewLog
  • Widgets
  • X2List
  • X2ListCriterion
  • X2ListItem
  • X2Model
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: 
   3: /*****************************************************************************************
   4:  * X2Engine Open Source Edition is a customer relationship management program developed by
   5:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
   6:  * 
   7:  * This program is free software; you can redistribute it and/or modify it under
   8:  * the terms of the GNU Affero General Public License version 3 as published by the
   9:  * Free Software Foundation with the addition of the following permission added
  10:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  11:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
  12:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  13:  * 
  14:  * This program is distributed in the hope that it will be useful, but WITHOUT
  15:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  16:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
  17:  * details.
  18:  * 
  19:  * You should have received a copy of the GNU Affero General Public License along with
  20:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
  21:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  22:  * 02110-1301 USA.
  23:  * 
  24:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
  25:  * California 95067, USA. or at email address contact@x2engine.com.
  26:  * 
  27:  * The interactive user interfaces in modified source and object code versions
  28:  * of this program must display Appropriate Legal Notices, as required under
  29:  * Section 5 of the GNU Affero General Public License version 3.
  30:  * 
  31:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  32:  * these Appropriate Legal Notices must retain the display of the "Powered by
  33:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
  34:  * technical reasons, the Appropriate Legal Notices must display the words
  35:  * "Powered by X2Engine".
  36:  *****************************************************************************************/
  37: 
  38: 
  39: Yii::import('application.components.X2LinkableBehavior');
  40: Yii::import('application.components.X2ChangeLogBehavior');
  41: Yii::import('application.components.x2flow.X2FlowTriggerBehavior');
  42: Yii::import('application.components.X2TimestampBehavior');
  43: Yii::import('application.components.TagBehavior');
  44: 
  45: Yii::import('application.modules.actions.models.Actions');
  46: Yii::import('application.modules.users.models.*');
  47: Yii::import('application.models.X2Flow');
  48: 
  49: /**
  50:  * General model class that uses dynamic fields
  51:  *
  52:  * @property array $fieldPermissions Associative array of field names to
  53:  *  permissions: 0 for no access, 1 for read access, and 2 for read/write
  54:  * @property string $myModelName (read-only) Model name of the instance.
  55:  * @property array $relatedX2Models (read-only) Models associated via the
  56:  *  associations table
  57:  * @property array $readableAttributeNames (read-only) Names of attributes that
  58:  *  can be accessed, per the field-level security settings, by the current user.
  59:  * @property boolean $isExemptFromFieldLevelPermissions True if the user is
  60:  *  admin or has no roles (in which case field-level permissions do not apply)
  61:  * @package application.models
  62:  */
  63: abstract class X2Model extends X2ActiveRecord {
  64: 
  65:     public $supportsFieldLevelPermissions = true;
  66: 
  67:     /**
  68:      * @var true if this model can have workflows associated with it, false otherwise 
  69:      */
  70:     public $supportsWorkflow = true;
  71: 
  72:     /**
  73:      * @var (optional) string Used in the search scenario to uniquely identify this model. Allows 
  74:      *  filters to be saved separately for each grid view.
  75:      */
  76:     public $uid = null;
  77: 
  78:     /**
  79:      * @var bool If true, grid views displaying models of this type will have their filter and
  80:      *  sort settings saved in the database instead of in the session
  81:      */
  82:     public $dbPersistentGridSettings = false;
  83: 
  84:     /**
  85:      * @var bool $disablePersistentGridSettings If true, grid settings will not be saved to or 
  86:      *  retrieved from the session/db
  87:      */
  88:     public $disablePersistentGridSettings = false;
  89: 
  90:     /**
  91:      * Temporary hack to allow importer to skip certain validation rules. This is used in place of
  92:      * scenario because the scenario property isn't used correctly in many places throughout the
  93:      * codebase. Scenario is meant to be used to filter validation rules 
  94:      * (http://www.yiiframework.com/doc/api/1.1/CModel#scenario-detail), and not otherwise. So,
  95:      * for now, changing the scenario can mean introducing unintended side-effects not related
  96:      * to validation. Eventually, all non-validation uses of scenario should be refactored.
  97:      * @var string $subScenario  
  98:      */
  99:     public $subScenario = '';
 100: 
 101:     protected $_oldAttributes = array();
 102: 
 103:     /**
 104:      * A flag for disabling the automatic setting of fields in events like find,
 105:      * update, validate (etc) to reduce overhead during queries.
 106:      * @var type
 107:      */
 108:     public static $autoPopulateFields = true;
 109: 
 110:     /**
 111:      * List of mapping between module names/associationType values and model class names
 112:      */
 113:     public static $associationModels = array(
 114:         'bugreports' => 'BugReports',
 115:         'media' => 'Media',
 116:         'actions' => 'Actions',
 117:         'calendar' => 'X2Calendar',
 118:         'contacts' => 'Contacts',
 119:         'accounts' => 'Accounts',
 120:         'product' => 'Product',
 121:         'products' => 'Product',
 122:         'Campaign' => 'Campaign',
 123:         'x2Leads' => 'X2Leads',
 124:         'marketing' => 'Campaign',
 125:         'quote' => 'Quote',
 126:         'quotes' => 'Quote',
 127:         'opportunities' => 'Opportunity',
 128:         'social' => 'Social',
 129:         'services' => 'Services',
 130:         'users' => 'User',
 131:         
 132:         '' => ''
 133:     );
 134: 
 135:     /**
 136:      * 1-1 mapping between model names and the names of the modules they belong to  
 137:      */
 138:     public static $modelNameToModuleName = array(
 139:         'Accounts' => 'Accounts',
 140:         'Actions' => 'Actions',
 141:         'BugReports' => 'BugReports',
 142:         'Campaign' => 'Marketing', 
 143:         'Contacts' => 'Contacts',
 144:         'X2List' => 'Contacts',
 145:         'Groups' => 'Groups',
 146:         'Product' => 'Products',
 147:         'Media' => 'Media',
 148:         'Opportunity' => 'Opportunities',
 149:         'Quote' => 'Quotes', 
 150:         'Services' => 'Services',
 151:         'User' => 'Users',
 152:         'WebForm' => 'Marketing',
 153:         'Workflow' => 'Workflow',
 154:         'X2Calendar' => 'Calendar',
 155:         'X2Leads' => 'X2Leads',
 156:     );
 157: 
 158: //    public static $modelTitles = array(
 159: //        'X2Leads' => 'Leads',
 160: //        'X2List' => 'Contact Lists',
 161: //        'BugReports' => 'Bug Reports',
 162: //        'Accounts' => 'Accounts',
 163: //        'Actions' => 'Actions',
 164: //        'Campaign' => 'Campaigns',
 165: //        'Contacts' => 'Contacts',
 166: //        'Groups' => 'Groups',
 167: //        'Product' => 'Products',
 168: //        'Media' => 'Media',
 169: //        'Opportunity' => 'Opportunities',
 170: //        'Quote' => 'Quotes', 
 171: //        'Services' => 'Services',
 172: //        'User' => 'Users',
 173: //        'WebForm' => 'Web Forms',
 174: //        'Workflow' => 'Processes',
 175: //        'X2Calendar' => 'Calendars', 
 176: //    );
 177: 
 178:     public static $translatedModelTitles = array();
 179: 
 180:     protected static $_editableFieldNames = array();
 181: 
 182:     /**
 183:      * Stores one copy of fields for all instances of this model
 184:      * @var type
 185:      */
 186:     protected static $_fields;
 187: 
 188:     /**
 189:      * Stores, for the current user, the permissions of the fields (1 for read,
 190:      * 2 for read/write, 0 for no access)
 191:      * 
 192:      * @var type
 193:      */
 194:     protected static $_fieldPermissions = array();
 195: 
 196:     /**
 197:      * Stores possible references to models via lookup fields. The structure of
 198:      * this array is:
 199:      *
 200:      * 1st level (array):
 201:      * [model class key] => [array value]
 202:      *
 203:      * 2nd level (array):
 204:      * [table name key] => [array value]
 205:      *
 206:      * So for each model name, there is an array of corresponding tables (and
 207:      * for each table, a list of columns) that need to be updated if the nameId
 208:      * attribute changes.
 209:      * @var type
 210:      */
 211:     protected static $_nameIdRefs;
 212:     // cache for models loaded for link field attributes (used by automation system)
 213:     protected static $_linkedModels;
 214:     protected $_runAfterCreate;   // run afterCreate before afterSave, but only for new records
 215:     protected $fieldFormatterClass = 'FieldFormatter';
 216:     private static $_modelNames;
 217:     private static $_attributeLabels;
 218: 
 219:     /**
 220:      * Mapping from model name to record name of module associated with that model
 221:      */
 222:     private static $recordNames = array(
 223:         'Actions' => 'action',
 224:         'Contacts' => 'contact',
 225:         'Accounts' => 'account',
 226:         'Product' => 'product',
 227:         'Campaign' => 'campaign',
 228:         'Quote' => 'quote',
 229:         'Opportunity' => 'opportunity',
 230:         'Services' => 'case',
 231:         'Groups' => 'group',
 232:         'Docs' => 'doc',
 233:         'X2Leads' => 'lead',
 234:         'X2List' => 'list item',
 235:     );
 236: 
 237:     /**
 238:      * Initialize the model.
 239:      *
 240:      * Calls {@link queryFields()} before CActiveRecord::__constructo() is
 241:      * called, and populates the model with default values, if any.
 242:      */
 243: 
 244:     public function __construct(
 245:         $scenario = 'insert', $uid = null, $dbPersistentGridSettings = false, 
 246:         $disablePersistentGridSettings = false) {
 247: 
 248:         $this->uid = $uid;
 249:         $this->dbPersistentGridSettings = $dbPersistentGridSettings;
 250:         $this->disablePersistentGridSettings = $disablePersistentGridSettings;
 251:         $this->queryFields();
 252:         parent::__construct($scenario);
 253:         if ($this->getIsNewRecord() && $scenario == 'insert') {
 254:             foreach ($this->getFields() as $field) {
 255:                 if ($field->defaultValue != null && !$field->readOnly) {
 256:                     $this->{$field->fieldName} = $field->defaultValue;
 257:                 }
 258:             }
 259:         }
 260:     }
 261: 
 262:     public static function model($className = 'CActiveRecord') {
 263:         $modelName = self::getModelName($className);
 264:         if (class_exists($modelName)) {
 265:             return parent::model($modelName);
 266:         } else {
 267:             throw new CHttpException(500, 'Class: ' . $className . " not found.");
 268:         }
 269:     }
 270: 
 271:     /**
 272:      * Like {@link model()} except without the exception thrown in case of bad model name 
 273:      */
 274:     public static function model2 ($className = 'CActiveRecord') {
 275:         $modelName = self::getModelName($className);
 276:         if (class_exists($modelName)) {
 277:             return parent::model($modelName);
 278:         } else {
 279:             return false;
 280:         }
 281:     }
 282: 
 283:     /**
 284:      * Runs specified function without the specified behavior
 285:      * @param string $behaviorName 
 286:      * @param function $fn 
 287:      */
 288:     public function runWithoutBehavior($behaviorName, $fn) {
 289:         $this->disableBehavior($behaviorName);
 290:         $fn();
 291:         $this->enableBehavior($behaviorName);
 292:     }
 293: 
 294:     /**
 295:      * Returns name of records associated with model type or $type if none could be found
 296:      * @param string $type class name of subclass of X2Model
 297:      * @param bool $plural if true, the record name will be pluralized
 298:      * @return string 
 299:      */
 300:     public static function getRecordName($type, $plural = false) {
 301:         if (isset(self::$recordNames[$type])) {
 302:             $recordName = self::$recordNames[$type];
 303:             if ($plural) {
 304:                 if (preg_match("/y$/", $recordName)) {
 305:                     $recordName = preg_replace("/y$/", 'ies', $recordName);
 306:                 } else {
 307:                     $recordName .= 's';
 308:                 }
 309:             }
 310:             return $recordName;
 311:         } else {
 312:             return $type;
 313:         }
 314:     }
 315: 
 316:     public static function getAllRecordNames() {
 317:         return self::$recordNames;
 318:     }
 319: 
 320:     /**
 321:      * Get association type corresponding to model 
 322:      * @return string
 323:      */
 324:     public static function getAssociationType($modelName) {
 325:         $modelsToTypes = array_flip(X2Model::$associationModels);
 326:         if (isset($modelsToTypes[$modelName])) {
 327:             return $modelsToTypes[$modelName];
 328:         } else {
 329:             return strtolower($modelName);
 330:         }
 331:     }
 332: 
 333:     /**
 334:      * Gets name of model corresponding to module or association type
 335:      * @return string
 336:      */
 337:     public static function getModelName($typeOrModuleName, $strict = false) {
 338:         if (array_key_exists(strtolower($typeOrModuleName), X2Model::$associationModels)) {
 339:             return X2Model::$associationModels[strtolower($typeOrModuleName)];
 340:         } else if (!$strict) {
 341:             if (class_exists(ucfirst($typeOrModuleName))) {
 342:                 return ucfirst($typeOrModuleName);
 343:             } elseif (class_exists($typeOrModuleName)) {
 344:                 return $typeOrModuleName;
 345:             } else {
 346:                 return false;
 347:             }
 348:         }
 349:     }
 350: 
 351:     /**
 352:      * @param array $modelNames names of models
 353:      * @return array models with given names
 354:      */
 355:     public static function getModelsFromNames (array $modelNames) {
 356:         return array_map (function ($name) {
 357:             return new $name ();
 358:         }, $modelNames);
 359:     }
 360: 
 361:     /**
 362:      * @param array $models models
 363:      * @return array names of tables associated with given models
 364:      */
 365:     public static function getTableNames (array $models) {
 366:         return array_map (function ($model) {
 367:             return $model->tableName();
 368:         }, $models);
 369:     }
 370: 
 371:     /**
 372:      * Retrieves a list of model names.
 373:      *
 374:      * Obtains model names as an associative array with model names as the keys
 375:      * and human-readable model names as their values. This is used in place of
 376:      * {@link getDisplayedModelNamesList()} (formerly Admin::getModelList) where
 377:      * specifying values for {@link modelName}, because the value of that should
 378:      * ALWAYS be the name of the actual class, and {@link X2Model::getModelName()}
 379:      * is guaranteed to return a class name (or false, if the class does not
 380:      * exist).
 381:      *
 382:      * @param null|CDbCriteria $criteria if not null, will be used to query modules table. 
 383:      *  Specifying a CDbCriteria will bypass caching.
 384:      * @return array module titles indexed by associated model class names
 385:      */
 386:     public static function getModelNames($criteria=null) {
 387:         if ($criteria !== null || !isset(self::$_modelNames)) {
 388:             $modelNames = array ();
 389:             if ($criteria === null) {
 390:                 $modules = self::getModules ();
 391:             } else {
 392:                 $criteria->addColumnCondition (
 393:                     array('visible' => 1, 'editable' => true), 'AND', 'OR');
 394:                 $modules = X2Model::model('Modules')->findAll ($criteria);
 395:             }
 396:             foreach ($modules as $module) {
 397:                 if ($modelName = X2Model::getModelName($module->name)) {
 398:                     $modelNames[$modelName] = Yii::t('app', $module->title);
 399:                 } else { // Shouldn't happen since getModelName uses class_exists
 400:                     $modelNames[ucfirst($module->name)] = Yii::t('app', $module->title);
 401:                 }
 402:             }
 403:             asort ($modelNames);
 404:             if ($criteria !== null) {
 405:                 return $modelNames;
 406:             } else {
 407:                 self::$_modelNames = $modelNames;
 408:             }
 409:         }
 410:         return self::$_modelNames;
 411:     }
 412: 
 413:     /**
 414:      * Tests whether or not model name is the name of a visible, editable module's primary model
 415:      * @param string $modelName 
 416:      * @return true
 417:      */
 418:     public static function isModuleModelName ($modelName) {
 419:         $moduleModelsByName = array_flip (self::getModuleModelNames ());
 420:         return isset ($moduleModelsByName[$modelName]);
 421:     }
 422: 
 423:     /**
 424:      * magic getter for names of visible, editable modules
 425:      */
 426:     private static $_moduleModelNames;
 427:     public static function getModuleModelNames () {
 428:         if (!isset (self::$_moduleModelNames)) {
 429:             $modules = self::getModules ();
 430:             $modelNames = array ();
 431:             foreach ($modules as $module) {
 432:                 if ($modelName = X2Model::getModelName($module->name))
 433:                     $modelNames [] = $modelName;
 434:                 else // Shouldn't happen since getModelName uses class_exists
 435:                     $modelNames [] = ucfirst($module->name);
 436:             }
 437:             self::$_moduleModelNames = $modelNames;
 438:         }
 439:         return self::$_moduleModelNames;
 440:     }
 441: 
 442:     /**
 443:      * magic getter for visible, editable modules 
 444:      */
 445:     private static $_modules;
 446:     public static function getModules () {
 447:         if (!isset (self::$_modules)) {
 448:             self::$_modules = X2Model::model('Modules')
 449:                 ->findAllByAttributes(array('visible' => 1, 'editable' => true));
 450:         }
 451:         return self::$_modules;
 452:     }
 453: 
 454:     /**
 455:      * @var array $_moduleModelsByName 
 456:      */
 457:     private static $_moduleModelsByName; 
 458:     public static function getModuleModelsByName () {
 459:         if (!isset (self::$_moduleModelsByName)) {
 460:             $modules = self::getModules ();
 461:             $modelNames = array ();
 462:             foreach ($modules as $module) {
 463:                 if ($modelName = X2Model::getModelName($module->name))
 464:                     $modelNames[$modelName] = X2Model::model ($modelName);
 465:                 else // Custom module most likely
 466:                     $modelNames[ucfirst($module->name)] = X2Model::model ($modelName);
 467:             }
 468:             self::$_moduleModelsByName = $modelNames;
 469:         }
 470:         return self::$_moduleModelsByName;
 471:     }
 472: 
 473:     /**
 474:      * Returns the translated module titles indexed by association type
 475:      * @return array 
 476:      */
 477:     public static function getAssociationTypeOptions() {
 478:         $modelNames = array_keys(self::getModelNames());
 479:         
 480: 
 481:         $associationTypes = array();
 482:         foreach ($modelNames as $modelName) {
 483:             $associationTypes[self::getAssociationType($modelName)] = self::getModelTitle(
 484:                 $modelName);
 485:         }
 486:         return $associationTypes;
 487:     }
 488: 
 489:     public function getDisplayName ($plural=true) {
 490:         $moduleName = X2Model::getModuleName (get_class ($this));
 491:         return Modules::displayName ($plural, $moduleName);
 492:     }
 493: 
 494:     /**
 495:      * Returns the title of the model to display in the UI
 496:      */
 497:     public static function getModelTitle($modelClass, $singular = false) {
 498:         if ($modelClass == 'Calendar') // model name is prefixed with X2
 499:             $modelClass = 'X2Calendar';
 500:         if (!isset(self::$translatedModelTitles[$singular][$modelClass])) {
 501:             if (!isset (self::$translatedModelTitles)) {
 502:                 self::$translatedModelTitles = array ();
 503:             }
 504:             self::$translatedModelTitles[$singular] = array ();
 505:             try {
 506:                 $model = self::model ($modelClass);
 507:             } catch (Exception $e) {
 508:                 $model = null;
 509:             }
 510:             if ($model) {
 511:                 $title = $model->getDisplayName (!$singular);
 512:             } else {
 513:                 $title = $modelClass;
 514:             }
 515:             self::$translatedModelTitles[$singular][$modelClass] = Yii::t(
 516:                 ($model && isset(self::model($modelClass)->module)) ? 
 517:                     self::model($modelClass)->module : 'app', $title);
 518:         }
 519:         return self::$translatedModelTitles[$singular][$modelClass];
 520:     }
 521: 
 522:     public static function getTranslatedModelTitles($singular = false) {
 523:         $modelTitles = array();
 524:         foreach (self::$modelNameToModuleName  as $model => $module) {
 525:             $modelTitles[$model] = self::getModelTitle($model, $singular);
 526:         }
 527:         return $modelTitles;
 528:     }
 529: 
 530:     /**
 531:      * Returns module name for given model name, or $modelName if none could be found 
 532:      * @param string $modelName the name of a model
 533:      * @return string the name of the module associated with the model
 534:      */
 535:     public static function getModuleName($modelName) {
 536:         if (isset(self::$modelNameToModuleName[$modelName])) {
 537:             return self::$modelNameToModuleName[$modelName];
 538:         } else {
 539:             return strtolower($modelName);
 540:         }
 541:     }
 542: 
 543:     /**
 544:      * Returns model name of module associated with current controller
 545:      * Precondition: model is an instance of X2Model
 546:      * @return string model name
 547:      */
 548:     public static function getModuleModelName() {
 549:         return X2Model::getModelName(Yii::app()->controller->module->name);
 550:     }
 551: 
 552:     /**
 553:      * Returns model of module associated with current controller
 554:      * Precondition: model is an instance of X2Model
 555:      * @return object model
 556:      */
 557:     public static function getModuleModel() {
 558:         return X2Model::model(X2Model::getModuleModelName());
 559:     }
 560: 
 561:     /**
 562:      * Updates action timer sum fields in X2Model.
 563:      * 
 564:      * @todo write a proper unit test for this method
 565:      */
 566:     public static function updateTimerTotals($modelId, $modelName = null) {
 567:         Yii::import('application.modules.actions.models.*');
 568:         $modelName = empty($modelName) ? get_called_class() : $modelName;
 569:         $model = self::model($modelName)->findByPk($modelId);
 570:         if (empty($model) || $model->asa('X2LinkableBehavior') == null)
 571:             return;
 572:         // All fields of type "timerSum":
 573:         $fields = array_filter($model->fields, function($f) {
 574:             return $f->type == 'timerSum';
 575:         });
 576:         foreach ($fields as $field) {
 577:             if ($field->linkType == null) {
 578:                 // "all types" specified, so take the shortcut of summing over
 579:                 // timeSpent field of all actions, which already itself 
 580:                 // contain sums over timer records, all of which are also
 581:                 // associated with the current model:
 582:                 $model->{$field->fieldName} = Yii::app()->db->createCommand()
 583:                         ->select("SUM(timeSpent)")
 584:                         ->from(Actions::model()->tableName())
 585:                         ->where("associationId=:id 
 586:                              AND associationType=:module
 587:                              AND type IN ('call','time')")
 588:                         ->queryScalar(array(
 589:                     ':module' => $model->module,
 590:                     ':id' => $modelId
 591:                 ));
 592:             } else {
 593:                 // Sum over all *published* timer records of the given type:
 594:                 $model->{$field->fieldName} = Yii::app()->db->createCommand()
 595:                         ->select("SUM(endtime-timestamp)")
 596:                         ->from(ActionTimer::model()->tableName())
 597:                         ->where("associationId=:id
 598:                         AND associationType=:modelName
 599:                         AND actionId IS NOT NULL
 600:                         AND type=:type")
 601:                         ->queryScalar(array(
 602:                     ':id' => $modelId,
 603:                     ':modelName' => $modelName,
 604:                     ':type' => $field->linkType
 605:                 ));
 606:             }
 607:         }
 608:         if (count($fields) > 0)
 609:             $model->update(array_map(function($f) {
 610:                 return $f->fieldName;
 611:             }, $fields));
 612:     }
 613: 
 614:     /**
 615:      * Hides record from all but admin users who have "Show Hidden" turned on
 616:      */
 617:     public function hide () {
 618:         $visibilityAttr = $this->getVisibilityAttr ();
 619:         $assignmentAttr = $this->getAssignmentATtr ();
 620:         $this->$visibilityAttr = X2PermissionsBehavior::VISIBILITY_PRIVATE;
 621:         $this->$assignmentAttr = 'Anyone';
 622:     }
 623: 
 624:     /**
 625:      * Use all email addresses of the model for finding a record
 626:      * @param type $email
 627:      */
 628:     public function findByEmail($email) {
 629:         $criteria = new CDbCriteria;
 630:         $paramCount = 0;
 631:         foreach ($this->getFields() as $field) {
 632:             if ($field->type == 'email') {
 633:                 $paramCount++;
 634:                 $params[$param = ":email$paramCount"] = $email;
 635:                 $criteria->addCondition("`{$field->fieldName}`=$param", 'OR');
 636:             }
 637:         }
 638:         $criteria->params = $params;
 639:         if($this->asa('X2DuplicateBehavior')){
 640:             $criteria->addCondition($this->getHiddenCondition(), 'AND');
 641:         }
 642:         return self::model(get_class($this))->find($criteria);
 643:     }
 644: 
 645:     /**
 646:      * Finds a model via a nameId reference
 647:      * @param type $nameId
 648:      * @return type
 649:      */
 650:     public function findByNameId($nameId) {
 651:         return self::model()->findByAttributes(compact('nameId'));
 652:     }
 653:     
 654:     public function findByAttributes($attributes, $condition = '', $params = array()) {
 655:         if ($this->asa('X2DuplicateBehavior')) {
 656:             $hiddenCondition = $this->getHiddenCondition();
 657:             if (empty($condition)) {
 658:                 $condition = $hiddenCondition;
 659:             } else {
 660:                 if (is_array($condition)) {
 661:                     if (isset($condition['condition'])) {
 662:                         $condition['condition'] .= ' AND ' . $hiddenCondition;
 663:                     } else {
 664:                         $condition['condition'] = $hiddenCondition;
 665:                     }
 666:                 } else {
 667:                     $condition .= ' AND ' . $hiddenCondition;
 668:                 }
 669:             }
 670:         }
 671:         return parent::findByAttributes($attributes, $condition, $params);
 672:     }
 673: 
 674:     /**
 675:      * Magic getter for {@link myModelName}
 676:      * @return string
 677:      */
 678:     public function getMyModelName() {
 679:         return self::getModelName(get_class($this));
 680:     }
 681: 
 682:     
 683: 
 684: 
 685:     
 686:     public function resetFieldsPropertyCache () {
 687:         $key = $this->tableName();
 688:         self::$_fields[$key] = null;
 689:         $this->queryFields();
 690:     }
 691: 
 692:     /**
 693:      * Queries and caches Fields objects for the model.
 694:      *
 695:      * This method obtains the fields defined for the model in
 696:      * <tt>x2_fields</tt> and makes them available for later usage to ensure
 697:      * that the query does not need to be performed again. The fields are stored
 698:      * as both static attributes of the model and and as Yii cache objects.
 699:      */
 700:     protected function queryFields() {
 701:         $key = $this->tableName();
 702: 
 703:         // only look up fields if they haven't already been looked up
 704:         if (!isset(self::$_fields[$key])) { 
 705: 
 706:             // check the app cache for the data
 707:             self::$_fields[$key] = Yii::app()->cache->get('fields_' . $key); 
 708:             if (self::$_fields[$key] === false) { // if the cache is empty, look up the fields
 709:                 $fieldList = CActiveRecord::model('Fields')->findAllByAttributes(
 710:                     array('modelName' => get_class($this), 'isVirtual' => 0));
 711:                 if (!empty($fieldList)) {
 712:                     self::$_fields[$key] = $fieldList;
 713: 
 714:                     // cache the data
 715:                     Yii::app()->cache->set('fields_' . $key, self::$_fields[$key], 0); 
 716:                 } else {
 717:                     self::$_fields[$key] = $this->attributeLabels();
 718:                 }
 719:             }
 720:         }
 721:     }
 722: 
 723:     public function relations() {
 724:         $relations = array();
 725:         $myClass = get_class($this);
 726: 
 727:         // Generate relations from link-type fields.
 728:         foreach (self::$_fields[$this->tableName()] as &$_field) {
 729:             if ($_field->type === 'link' && class_exists($_field->linkType)) {
 730:                 $relations[$alias = $_field->fieldName . 'Model'] = array(
 731:                     self::BELONGS_TO,
 732:                     $_field->linkType,
 733:                     array($_field->fieldName => 'nameId'),
 734:                 );
 735:             }
 736:         }
 737:         if (Yii::app()->contEd('pro')) {
 738:             $relations['gallery'] = array(
 739:                 self::HAS_ONE, 'GalleryToModel',
 740:                 'modelId',
 741:                 'condition' => 'modelName="' . $myClass . '"');
 742:         }
 743:         return $relations;
 744:     }
 745: 
 746:     /**
 747:      * Returns a list of behaviors that this model should behave as.
 748:      * @return array the behavior configurations (behavior name=>behavior configuration)
 749:      */
 750:     public function behaviors() {
 751:         $behaviors = array(
 752:             'X2LinkableBehavior' => array('class' => 'X2LinkableBehavior'),
 753:             'X2TimestampBehavior' => array('class' => 'X2TimestampBehavior'),
 754:             'X2FlowTriggerBehavior' => array('class' => 'X2FlowTriggerBehavior'),
 755:             'TagBehavior' => array('class' => 'TagBehavior'),
 756:             'changelog' => array('class' => 'X2ChangeLogBehavior'),
 757:             'permissions' => array('class' => Yii::app()->params->modelPermissions),
 758:             'X2MergeableBehavior' => array('class' => 'X2MergeableBehavior'),
 759:             'relationships' => array('class' => 'RelationshipsBehavior'),
 760:         );
 761:         if (Yii::app()->contEd('pro')) {
 762:             $behaviors['galleryBehavior'] = array(
 763:                 'class' => 'application.extensions.gallerymanager.GalleryBehavior',
 764:                 'idAttribute' => 'galleryId',
 765:                 'versions' => array(
 766:                     'small' => array(
 767:                         'centeredpreview' => array(98, 98),
 768:                     ),
 769:                 ),
 770:                 'name' => true,
 771:                 'description' => true,
 772:             );
 773:         }
 774:         return $behaviors;
 775:     }
 776: 
 777:     /**
 778:      * Saves attributes on initial model lookup
 779:      */
 780:     public function afterFind() {
 781:         $this->_oldAttributes = $this->getAttributes();
 782:         parent::afterFind();
 783:     }
 784: 
 785:     /**
 786:      * Remembers if this was a new record before saving.
 787:      * @returns the answer from {@link CActiveRecord::beforeSave()}
 788:      */
 789:     public function beforeSave() {
 790:         if ($this->asa ('ContactsNameBehavior')) {
 791:             $this->asa ('ContactsNameBehavior')->setName ();
 792:         }
 793: 
 794:         $this->_runAfterCreate = $this->getIsNewRecord();
 795:         if (!$this->_runAfterCreate) {
 796:             $this->updateNameId();
 797:         } else {
 798:             // Safeguard against duplicate entries (violating unique constraint
 799:             // on the nameId column): set uniqueId before submitting to
 800:             // some unique value, and let it be updated to a proper uniqueId
 801:             // value after saving. This is just in case the nameId update after
 802:             // insertion fails, which is easily corrected.
 803:             if ($this->hasAttribute('nameId')) {
 804:                 $this->nameId = uniqid();
 805:             }
 806:         }
 807:         return parent::beforeSave();
 808:     }
 809: 
 810:     public function onAfterCreate($event) {
 811:         $this->raiseEvent('onAfterCreate', $event);
 812:     }
 813: 
 814:     public function afterCreate() {
 815:         $this->_runAfterCreate = false;
 816:         if ($this->hasEventHandler('onAfterCreate'))
 817:             $this->onAfterCreate(new CEvent($this));
 818:     }
 819: 
 820:     public function onAfterInsert($event) {
 821:         $this->raiseEvent('onAfterInsert', $event);
 822:     }
 823: 
 824:     public function onAfterUpdate($event) {
 825:         $this->raiseEvent('onAfterUpdate', $event);
 826:     }
 827: 
 828:     public function afterUpdate() {
 829: 
 830:         // Update, as necessary, references to this record via the nameId field.
 831:         /* x2tempstart */
 832:         $this->updateNameIdRefs();
 833:         /* x2tempend */
 834: 
 835:         if ($this->hasEventHandler('onAfterUpdate')) {
 836:             $this->onAfterUpdate(new CEvent($this));
 837:         }
 838:     }
 839: 
 840:     /**
 841:      * Runs when a model is deleted.
 842:      * Clears any entries in <tt>x2_phone_numbers</tt>.
 843:      * Fires onAfterDelete event.
 844:      */
 845:     public function afterDelete() {
 846:         // Clear out old tags:
 847:         $class = get_class($this);
 848:         Tags::model()->deleteAllByAttributes(array(
 849:             'type' => $class,
 850:             'itemId' => $this->id
 851:         ));
 852: 
 853:         // Clear out old phone numbers
 854:         X2Model::model('PhoneNumber')->deleteAllByAttributes(array(
 855:             'modelId' => $this->id,
 856:             'modelType' => $class
 857:         ));
 858: 
 859:         RecordAliases::model ()->deleteAllByAttributes (array (
 860:             'recordId' => $this->id,
 861:             'recordType' => $class,
 862:         ));
 863: 
 864:         // Change all references to this record so that they retain the name but
 865:         // exclude the ID:
 866:         if ($this->hasAttribute('nameId') && $this->hasAttribute('name')) {
 867:             $this->_oldAttributes = $this->getAttributes();
 868:             $this->nameId = $this->name;
 869:             $this->updateNameIdRefs();
 870:         }
 871: 
 872:         // clear out associated actions
 873:         Actions::model()->deleteAllByAttributes(
 874:                 array(
 875:                     'associationType' => strtolower(self::getAssociationType(get_class($this))),
 876:                     'associationId' => $this->id
 877:         ));
 878: 
 879:         if ($this->hasEventHandler('onAfterDelete'))
 880:             $this->onAfterDelete(new CEvent($this));
 881:     }
 882: 
 883:     /**
 884:      * Modified to enable/disable X2Flow record update trigger.
 885:      * This method is Copyright (c) 2008-2014 by Yii Software LLC
 886:      * http://www.yiiframework.com/license/
 887:      */
 888:     public function save($runValidation = true, $attributes = null) {
 889:         if (!$runValidation || $this->validate($attributes)) {
 890:             /* x2modstart */
 891:             if ($this->asa('X2FlowTriggerBehavior') &&
 892:                     $this->asa('X2FlowTriggerBehavior')->enabled) {
 893:                 $this->enableUpdateTrigger();
 894:             }
 895:             $retVal = $this->getIsNewRecord() ?
 896:                     $this->insert($attributes) : $this->update($attributes);
 897:             if ($this->asa('X2FlowTriggerBehavior') &&
 898:                     $this->asa('X2FlowTriggerBehavior')->enabled) {
 899: 
 900:                 $this->disableUpdateTrigger();
 901:             }
 902:             /* x2modend */
 903:             return $retVal;
 904:         } else {
 905:             return false;
 906:         }
 907:     }
 908: 
 909:     /**
 910:      * Runs when a model is saved.
 911:      * Scans attributes for phone numbers and index them in <tt>x2_phone_numbers</tt>.
 912:      * Updates <tt>x2_relationships</tt> table based on link type fields.
 913:      * Fires onAfterSave event.
 914:      */
 915:     public function afterSave() {
 916:         if ($this->_runAfterCreate)
 917:             $this->afterCreate();
 918:         else
 919:             $this->afterUpdate();
 920: 
 921:         $phoneFields = array();
 922:         $linkFields = array();
 923: 
 924:         // look through fields for phone numbers and relationships
 925:         foreach (self::$_fields[$this->tableName()] as &$_field) {
 926:             if ($_field->type === 'phone') {
 927:                 $phoneFields[$_field->fieldName] = $this->getAttribute($_field->fieldName);
 928:             } elseif ($_field->type === 'link') {
 929:                 $nameAndId = Fields::nameAndId($this->getAttribute($_field->fieldName));
 930:                 $linkFields[$_field->fieldName] = array(
 931:                     'id' => $nameAndId[1],
 932:                     'type' => $_field->linkType
 933:                 );
 934:             }
 935:         }
 936: 
 937:         // deal with phone numbers
 938:         if (count($phoneFields)) {
 939:             // clear out old phone numbers
 940:             X2Model::model('PhoneNumber')->deleteAllByAttributes(
 941:                 array('modelId' => $this->id, 'modelType' => get_class($this))); 
 942:         }
 943: 
 944:         // create new entries in x2_phone_numbers
 945:         foreach ($phoneFields as $field => &$number) {  
 946:             if (!empty($number)) {
 947:                 $num = new PhoneNumber;
 948:                 // eliminate everything other than digits
 949:                 $num->number = preg_replace('/\D/', '', $number); 
 950:                 $num->modelId = $this->id;
 951:                 $num->modelType = get_class($this);
 952:                 $num->fieldName = $field;
 953:                 $num->save();
 954:             }
 955:         }
 956: 
 957:         parent::afterSave(); // raise onAfterSave event for behaviors, such as X2ChangeLogBehavior
 958:     }
 959: 
 960:     /**
 961:      * Generates validation rules for custom fields
 962:      * @return array validation rules for model attributes.
 963:      */
 964:     public function rules() {
 965:         return array_merge (
 966:             parent::rules (), 
 967:             self::modelRules(self::$_fields[$this->tableName()], $this));
 968:     }
 969: 
 970:     public static function modelRules(&$fields, $model) {
 971:         $fieldTypes = array(
 972:             'required',
 973:             'email',
 974:             'unique',
 975:             'int',
 976:             'numerical',
 977:             //'date',
 978:             //'float',
 979:             'boolean',
 980:             'safe',
 981:             'search',
 982:             'link',
 983:             'foreignKey',
 984:             'uniqueIndex',
 985:         );
 986:         $fieldRules = array_fill_keys($fieldTypes, array());
 987:         $validators = Fields::getFieldTypes('validator');
 988: 
 989:         foreach ($fields as &$_field) {
 990: 
 991:             $fieldRules['search'][] = $_field->fieldName;
 992:             if (isset($validators[$_field->type]) && $_field->safe) {
 993:                 $fieldRules[$validators[$_field->type]][] = $_field->fieldName;
 994:             }
 995: 
 996:             if ($_field->required) {
 997:                 $fieldRules['required'][] = $_field->fieldName;
 998:             }
 999:             /* x2tempstart */  
1000:             // see note above subScenario property
1001:             if (!(property_exists ($model, 'subScenario') &&
1002:                   $model->subScenario === 'importOverwrite' && $_field->fieldName === 'id') &&
1003:                 $_field->uniqueConstraint) {
1004:             /* x2tempend */ 
1005:                 $fieldRules['unique'][] = $_field->fieldName;
1006:             }
1007: 
1008:             if ($_field->type == 'link' && $_field->required)
1009:                 $fieldRules['link'][] = $_field->fieldName;
1010: 
1011:             if ($_field->keyType === 'FOR') {
1012:                 $fieldRules['foreignKey'][] = $_field->fieldName;
1013:             }
1014:             if ($_field->keyType === 'UNI') {
1015:                 $fieldRules['uniqueIndex'][] = $_field->fieldName;
1016:             }
1017:         }
1018: 
1019:         $rules =  array(
1020:             array(implode(',', $fieldRules['foreignKey']),
1021:                 'application.components.validators.X2ModelForeignKeyValidator'),
1022:             array(implode(',', $fieldRules['uniqueIndex']),
1023:                 'application.components.validators.X2ModelUniqueIndexValidator'),
1024:             array(implode(',', $fieldRules['required']), 'required'),
1025:             array(implode(',', $fieldRules['unique']), 'unique'),
1026:             array(implode(',', $fieldRules['numerical']), 'numerical'),
1027:             array(implode(',', $fieldRules['email']), 'email'),
1028:             array(implode(',', $fieldRules['int']), 'numerical', 'integerOnly' => true),
1029:             array(implode(',', $fieldRules['boolean']), 'boolean'),
1030:             array(implode(',', $fieldRules['link']), 'application.components.ValidLinkValidator'),
1031:             array(implode(',', $fieldRules['safe']), 'safe'),
1032:             array(implode(',', $fieldRules['search']), 'safe', 'on' => 'search'),
1033:         );
1034: 
1035:         return $rules;
1036:     }
1037: 
1038:     /**
1039:      * Returns the named attribute value.
1040:      * Recognizes linked attributes and looks them up with {@link getLinkedAttribute()}
1041:      * @param string $name the attribute name
1042:      * @param bool $renderFlag
1043:      * @param bool $makeLinks If the render flag is set, determines whether to render attributes
1044:      *  as links
1045:      * @return mixed the attribute value. Null if the attribute is not set or does not exist.
1046:      * @see hasAttribute
1047:      */
1048:     public function getAttribute($name, $renderFlag = false, $makeLinks = false) {
1049:         // check for a linked attribute (eg. "account.assignedTo")
1050:         $nameParts = explode('.', $name); 
1051: 
1052:         if (count($nameParts) > 1) { 
1053:             // We have a complicated link like "account.primaryContact.email"
1054: 
1055:             $linkField = array_shift($nameParts); // Remove the current model
1056:             $linkModel = $this->getLinkedModel($linkField);
1057: 
1058:             // Put the name back together e.g. primaryContact.email
1059:             $name = implode('.', $nameParts); 
1060: 
1061:             if (isset($linkModel)) {
1062:                 return $linkModel->getAttribute($name, $renderFlag);
1063:             } else {
1064:                 // If it's an assignment field, check the Profile model
1065:                 $fieldInfo = $this->getField($linkField); 
1066:                 if ($fieldInfo instanceof Fields && $fieldInfo->type == 'assignment') {
1067:                     $profRecord = X2Model::model('Profile')
1068:                         ->findByAttributes(array('username' => $this->$linkField));
1069: 
1070:                     if (isset($profRecord)) {
1071:                         return $profRecord->getAttribute($name, false);
1072:                     }
1073:                 }
1074:             }
1075:         } else {
1076:             if ($renderFlag) {
1077:                 return $this->renderAttribute($name, $makeLinks);
1078:             } else {
1079:                 return parent::getAttribute($name);
1080:             }
1081:         }
1082:         return null;
1083:     }
1084: 
1085:     /**
1086:      * Looks up a linked attribute by loading the linked model and calling getAttribute() on it.
1087:      * @param string $linkField the attribute of $this linking to the external model
1088:      * @param string $attribute the attribute of the external model
1089:      * @return mixed the attribute value. Null if the attribute is not set or does not exist.
1090:      */
1091:     public function getLinkedAttribute($linkField, $attribute) {
1092:         if (null !== $model = $this->getLinkedModel($linkField))
1093:             return $model->getAttribute($attribute);
1094:         return null;
1095:     }
1096: 
1097:     /**
1098:      * Looks up a linked attribute by loading the linked model and calling renderAttribute() on it.
1099:      * @param string $linkField the attribute of $this linking to the external model
1100:      * @param string $attribute the attribute of the external model
1101:      * @return mixed the properly formatted attribute value. Null if the attribute is not set or 
1102:      *  does not exist.
1103:      * @see getLinkedAttribute
1104:      */
1105:     public function renderLinkedAttribute($linkField, $attribute) {
1106:         if (null !== $model = $this->getLinkedModel($linkField))
1107:             return $model->renderAttribute($attribute);
1108:         return null;
1109:     }
1110: 
1111:     /**
1112:      * Looks up an external model referenced in a link field.
1113:      * Caches loaded models in X2Model::$_linkedModels
1114:      * @param string $linkField the attribute of $this linking to the external model
1115:      * @param bool $lookup Actually look up the model; otherwise (if false) use
1116:      *  the name/ID to populate a dummy model that can be used for just
1117:      *  generating a link.
1118:      * @return mixed the active record. Null if the attribute is not set or does not exist.
1119:      */
1120:     public function getLinkedModel($linkField, $lookup = true) {
1121:         $nameId = $this->getAttribute($linkField);
1122:         list($name, $id) = Fields::nameAndId($nameId);
1123: 
1124:         if (ctype_digit((string) $id)) {
1125:             $field = $this->getField($linkField);
1126: 
1127:             if ($field !== null && $field->type === 'link') {
1128:                 $modelClass = $field->linkType;
1129: 
1130:                 if (!$lookup) {
1131:                     return self::getLinkedModelMock($modelClass, $name, $id);
1132:                 }
1133: 
1134:                 // try to look up the linked model
1135:                 if (!isset(self::$_linkedModels[$modelClass][$id])) {
1136:                     self::$_linkedModels[$modelClass][$id] = X2Model::model($modelClass)->findByPk($id);
1137:                     if (self::$_linkedModels[$modelClass][$id] === null)  // if it doesn't exist, set it to false in the cache
1138:                         self::$_linkedModels[$modelClass][$id] = false;  // so isset() returns false and we can skip this next time
1139:                 }
1140: 
1141:                 if (self::$_linkedModels[$modelClass][$id] !== false)
1142:                     return self::$_linkedModels[$modelClass][$id];  // success!
1143:             }
1144:         }
1145:         return null;
1146:     }
1147: 
1148:     /**
1149:      * Creates a mock-up of a linked model with the minimum requirements for
1150:      * generating a link in a view of another model.
1151:      * 
1152:      * @param string $modelClass
1153:      * @param string $name
1154:      * @param integer $id
1155:      * @param bool $allowEmpty Return the model even if $name/$id are empty.
1156:      * @return \modelClass|string
1157:      */
1158:     public static function getLinkedModelMock($modelClass, $name, $id, $allowEmpty = false) {
1159:         if ($id !== null || $allowEmpty) {
1160:             // Take the shortcut for link generation:
1161:             $model = X2Model::model($modelClass);
1162:             if (!$model instanceof X2Model) {
1163:                 throw new CException("Error: model $modelClass does not refer to an existing child class of X2Model.");
1164:             }
1165:             if ($model->hasAttribute('id') && $model->hasAttribute('name')) {
1166:                 $model->id = $id;
1167:                 $model->name = $name;
1168:                 return $model;
1169:             } else {
1170:                 return null;
1171:             }
1172:         } else {
1173:             return null;
1174:         }
1175:     }
1176: 
1177:     /**
1178:      * Wrapper method for generating a link to the view for a model record.
1179:      *
1180:      * @param int $id the route to this model's AutoComplete data source
1181:      * @param string $class the model class
1182:      * @return string a link to the model, or $id if the model is invalid
1183:      */
1184:     public static function getModelLink($id, $class, $requireAbsoluteUrl = false) {
1185:         try {
1186:             $model = X2Model::model($class)->findByPk($id);
1187:         } catch (CHttpException $e) {
1188:             $model = null;
1189:         }
1190:         if (isset($model) && !is_null($model->asa('X2LinkableBehavior'))) {
1191:             if (isset(Yii::app()->controller) && method_exists(Yii::app()->controller, 'checkPermissions')) {
1192:                 if (Yii::app()->controller->checkPermissions($model, 'view')) {
1193:                     if ($requireAbsoluteUrl) {
1194:                         return $model->getUrlLink();
1195:                     } else {
1196:                         return $model->getLink();
1197:                     }
1198:                 } else {
1199:                     return $model->renderAttribute('name');
1200:                 }
1201:             } else {
1202:                 if ($requireAbsoluteUrl) {
1203:                     return $model->getUrlLink();
1204:                 } else {
1205:                     return $model->getLink();
1206:                 }
1207:             }
1208:             // return CHtml::link($model->name,array($model->getDefaultRoute().'/'.$model->id));
1209:         } elseif (is_numeric($id)) {
1210:             return '';
1211:         } else {
1212:             return $id;
1213:         }
1214:     }
1215: 
1216:     /**
1217:      * @return array static linked models indexed by link field name
1218:      */
1219:     public function getStaticLinkedModels () {
1220:         $linkFields = array_filter ($this->fields, function ($field) {
1221:                 return $field->type === 'link';
1222:             });
1223:         $linkedModels = array ();
1224:         foreach ($linkFields as $field) {
1225:             $linkedModels[$field->fieldName] = new $field->linkType ();
1226:         }
1227:         return $linkedModels;
1228:     }
1229: 
1230:     /**
1231:      * Link generation shortcut.
1232:      * @param type $modelClass
1233:      * @param type $nameId
1234:      * @param array $htmlOptions options to be applied to the link element
1235:      * @return type
1236:      */
1237:     public static function getModelLinkMock($modelClass, $nameId, $htmlOptions = array()) {
1238:         list($name, $id) = Fields::nameAndId($nameId);
1239:         $model = self::getLinkedModelMock($modelClass, $name, $id);
1240:         if ($model instanceof X2Model && !is_null($model->asa('X2LinkableBehavior'))) {
1241:             return $model->getLink($htmlOptions);
1242:         } else {
1243:             return CHtml::encode($name);
1244:         }
1245:     }
1246: 
1247:     /**
1248:      * Returns all possible models, either as a regular array or associative
1249:      * (key and value are the same)
1250:      * @param boolean $assoc
1251:      * @return array
1252:      */
1253:     public static function getModelTypes($assoc = false, $filter=null) {
1254:         $modelTypes = Yii::app()->db->createCommand()
1255:             ->selectDistinct('modelName')
1256:             ->from('x2_fields')
1257:             ->where('modelName!="Calendar"')
1258:             ->order('modelName ASC')
1259:             ->queryColumn();
1260:         if ($filter) {
1261:             $modelTypes = array_filter ($modelTypes, $filter);
1262:         }
1263: 
1264:         if ($assoc === true) {
1265:             $modelTypes = array_combine($modelTypes, array_map(function($type) {
1266:                 return X2Model::model ($type)->getDisplayName (true, false);
1267:             }, $modelTypes));
1268:             asort ($modelTypes);
1269:             return $modelTypes;
1270:         }
1271:         $modelTypes = array_map(function($term) {
1272:             return Yii::t('app', $term);
1273:         }, $modelTypes);
1274:         return $modelTypes;
1275:     }
1276: 
1277:     /**
1278:      * Like getModelTypes () except that only types of models whic support relationships are 
1279:      * returned. 
1280:      * 
1281:      * if $assoc is true, the return array will appear as so:
1282:      *        array (
1283:      *            <Model Name> => <Translated Model Name>, ...
1284:      *        )
1285:      * if $assoc is false:
1286:      *        array (
1287:      *            <index> => <Model Name>, ...
1288:      *        )
1289:      * 
1290:      * @param boolean $assoc to return as an associative array or not
1291:      * @return array of model names as specified above
1292:      */
1293:     private static $_modelsWhichSupportRelationships;
1294:     public static function getModelTypesWhichSupportRelationships($assoc = false, $refresh = false) {
1295:         if (!isset (self::$_modelsWhichSupportRelationships[$assoc]) || $refresh) {
1296:             $modelTypes = self::getModelTypes(true);
1297:             $filteredTypes = array ();
1298:             foreach ($modelTypes as $type => $title) {
1299:                 if (X2Model::Model ($type)->asa('relationships')) {
1300:                     if ($assoc) {
1301:                         $filteredTypes[$type] = $title; 
1302:                     } else {
1303:                         $filteredTypes[] = $type;
1304:                     }
1305:                 }
1306:             }
1307:             self::$_modelsWhichSupportRelationships[$assoc] = $filteredTypes;
1308:         }
1309: 
1310:         return self::$_modelsWhichSupportRelationships[$assoc];
1311:     }
1312: 
1313:     /**
1314:      * Like getModelTypes () except that only types of models which support workflow are 
1315:      * returned
1316:      * @param boolean $assoc
1317:      * @return array 
1318:      */
1319:     public static function getModelTypesWhichSupportWorkflow($assoc = false, $associationTypes = false) {
1320:         $modelTypes = self::getModelTypes($assoc);
1321:         $tmp = $assoc ? array_flip($modelTypes) : $modelTypes;
1322:         $tmp = array_filter($tmp, function ($a) use ($assoc) {
1323:             return X2Model::Model($a)->supportsWorkflow;
1324:         });
1325:         $tmp = $assoc ? array_flip($tmp) : $tmp;
1326:         $tmp = array_intersect($modelTypes, $tmp);
1327:         if($associationTypes){
1328:             $arr = array();
1329:             foreach ($tmp as $k => $v) {
1330:                 if ($assoc) {
1331:                     $arr[X2Model::getAssociationType($k)] = $v;
1332:                 } else {
1333:                     $arr[] = X2Model::getAssociationType($v);
1334:                 }
1335:             }
1336:             $tmp = $arr;
1337:         }
1338:         return $tmp;
1339:     }
1340: 
1341:     /**
1342:      * Returns a translated label using "module" defined in 
1343:      * @param type $label
1344:      * @return type
1345:      */
1346:     public function translatedAttributeLabel($label) {
1347:         return Yii::t((bool) $this->asa('X2LinkableBehavior') ? 
1348:             (empty($this->module) ? 'app' : $this->module) : 'app', $label);
1349:     }
1350: 
1351:     /**
1352:      * Returns custom attribute values defined in x2_fields
1353:      * @return array customized attribute labels (name=>label)
1354:      * @see generateAttributeLabel
1355:      */
1356:     public function getAttributeLabels () {
1357:         $tableName = $this->tableName ();
1358:         if (!isset (self::$_attributeLabels[$tableName])) {
1359:             $labels = array();
1360: 
1361:             foreach (self::$_fields[$tableName] as &$_field) {
1362:                 $labels[$_field->fieldName] = 
1363:                     $this->translatedAttributeLabel($_field->attributeLabel);
1364:             }
1365: 
1366:             self::$_attributeLabels[$tableName] = $labels;
1367:         }
1368:         return self::$_attributeLabels[$tableName];
1369:     }
1370: 
1371:     /**
1372:      * Returns custom attribute values defined in x2_fields
1373:      * @return array customized attribute labels (name=>label)
1374:      * @see generateAttributeLabel
1375:      */
1376:     public function attributeLabels() {
1377:         $labels = array();
1378: 
1379:         foreach (self::$_fields[$this->tableName()] as &$_field) {
1380:             $labels[$_field->fieldName] = $this->translatedAttributeLabel($_field->attributeLabel);
1381:         }
1382: 
1383:         return $labels;
1384:     }
1385: 
1386:     /**
1387:      * Returns the text label for the specified attribute.
1388:      * This method overrides the parent implementation by supporting
1389:      * returning the label defined in relational object.
1390:      * In particular, if the attribute name is in the form of "post.author.name",
1391:      * then this method will derive the label from the "author" relation's "name" attribute.
1392:      * @param string $attribute the attribute name
1393:      * @return string the attribute label
1394:      * @see generateAttributeLabel
1395:      * @since 1.1.4
1396:      */
1397:     public function getAttributeLabel($attribute) {
1398:         $attributeLabels = $this->getAttributeLabels ();
1399:         if (isset ($attributeLabels[$attribute])) return $attributeLabels[$attribute];
1400:         
1401:         if (isset(self::$_fields[$this->tableName()][$attribute])) {
1402:             return self::$_fields[$this->tableName()][$attribute];
1403:         }
1404:         // original Yii code
1405:         if (strpos($attribute, '.') !== false) {
1406:             $segs = explode('.', $attribute);
1407:             $name = array_pop($segs);
1408:             $model = $this;
1409:             foreach ($segs as $seg) {
1410:                 $relations = $model->getMetaData()->relations;
1411:                 if (isset($relations[$seg]))
1412:                     $model = X2Model::model($relations[$seg]->className);
1413:                 else
1414:                     break;
1415:             }
1416:             return $model->getAttributeLabel($name);
1417:         } else
1418:             return $this->generateAttributeLabel($attribute);
1419:     }
1420: 
1421:     public function getOldAttributes() {
1422:         return $this->_oldAttributes;
1423:     }
1424: 
1425:     /**
1426:      * Returns all attributes of the current model that the user has permission
1427:      * to view.
1428:      * 
1429:      * @param type $names
1430:      */
1431:     public function getReadableAttributeNames() {
1432:         return array_keys(array_filter($this->getFieldPermissions(), function($p) {
1433:             return $p >= Fields::READ_PERMISSION;
1434:         }));
1435:     }
1436: 
1437:     public function getEditableAttributeNames () {
1438:         return array_keys(array_filter($this->getFieldPermissions(), function($p) {
1439:             return $p >= Fields::WRITE_PERMISSION;
1440:         }));
1441:     }
1442: 
1443:     /**
1444:      * Filters attributes to those for which the current user has view permission
1445:      * @return array attribute values indexed by name 
1446:      */
1447:     public function getVisibleAttributes () {
1448:         if (!Yii::app()->params->isAdmin && !empty(Yii::app()->params->roles)) {
1449:             $fieldPermissions = $this->getFieldPermissions();
1450:         } else { // bypass permissions
1451:             return $this->getAttributes ();
1452:         }
1453: 
1454:         $visibleAttributeNames = array ();
1455:         foreach ($fieldPermissions as $fieldName => $permission) {
1456:             if ($permission >= Fields::READ_PERMISSION) { 
1457:                 $visibleAttributeNames[] = $fieldName;
1458:             }
1459:         }
1460:         return $this->getAttributes ($visibleAttributeNames);
1461:     }
1462: 
1463:     /**
1464:      * @param bool $assoc If true, fields in returned array will be indexed by field name
1465:      * @param null|function $filterFn 
1466:      * @param int $requiredPermission Used to filter fields by field-level permissions
1467:      * @return array 
1468:      */
1469:     public function getFields($assoc = false, $filterFn=null,
1470:         $requiredPermission=Fields::NO_PERMISSION){
1471: 
1472:         if($assoc){
1473:             $fields = array();
1474:             foreach(self::$_fields[$this->tableName()] as &$field) {
1475:                 if ($filterFn !== null) {
1476:                     if ($filterFn ($field)) {
1477:                         $fields[$field->fieldName] = $field;
1478:                     }
1479:                 } else {
1480:                     $fields[$field->fieldName] = $field;
1481:                 }
1482:             }
1483:             return $fields;
1484:         }else{
1485:             if ($filterFn !== null) {
1486:                 $fields = array();
1487:                 foreach(self::$_fields[$this->tableName()] as &$field) {
1488:                     if ($filterFn ($field)) {
1489:                         $fields[] = $field;
1490:                     }
1491:                 }
1492:                 return $fields;
1493:             } else {
1494:                 return self::$_fields[$this->tableName()];
1495:             }
1496:         }
1497: 
1498:         // remove all fields for which the user lacks sufficient permission
1499:         if ($requiredPermission > Fields::NO_PERMISSION) {
1500:             $permissions = $this->getFieldPermissions ();
1501:             foreach ($permissions as $name => $permissionLevel) {
1502:                 if ($permissionLevel > $requiredPermission) {
1503:                     unset ($fields[$name]);
1504:                 }
1505:             }
1506:         }
1507:     }
1508: 
1509:     /**
1510:      * @return array all standard comparison operators
1511:      */
1512:     public static function getFieldComparisonOptions () {
1513:         return array(
1514:             '=' => Yii::t('app','equals'),
1515:             '>' => Yii::t('app','greater than'),
1516:             '<' => Yii::t('app','less than'),
1517:             '>=' => Yii::t('app','greater than or equal to'),
1518:             '<=' => Yii::t('app','less than or equal to'),
1519:             '<>' => Yii::t('app','not equal to'),
1520:             'list' => Yii::t('app','in list'),
1521:             'notList' => Yii::t('app','not in list'),
1522:             'empty' => Yii::t('app','empty'),
1523:             'notEmpty' => Yii::t('app','not empty'),
1524:             'contains' => Yii::t('app','contains'),
1525:             'noContains' => Yii::t('app','does not contain'),
1526:             'before' => Yii::t('app','before'),
1527:             'after' => Yii::t('app','after'),
1528:         );
1529:     }
1530: 
1531:     /**
1532:      * @param bool $includeFieldsOfLinkedRecords if true, add field options for related models
1533:      * @param bool $condList 
1534:      * @param function|null $filterFn if set, will be used to filter results
1535:      * @param string $separator used to separate parent attribute from field name 
1536:      * @return array  
1537:      */
1538:     public function getFieldsForDropdown (
1539:         $includeFieldsOfLinkedRecords=false, $condList=true, $filterFn=null, $separator='.') {
1540: 
1541:         if ($includeFieldsOfLinkedRecords) {
1542:             $linkedModels = $this->getStaticLinkedModels ();
1543:             $fieldsForDropdown = array ();
1544:             $fieldsForDropdown[''] = $this->_getFieldsForDropdown (
1545:                 null, $condList, true, $filterFn, $separator);
1546:             foreach ($linkedModels as $fieldName => $linkedModel) {
1547:                 if ($this->getField ($fieldName)) {
1548:                     $optGroupHeader = $this->getAttributeLabel ($fieldName);
1549:                 } else if (self::isModuleModelName ($fieldName)) {
1550:                     $optGroupHeader = self::getModelTitle ($fieldName);
1551:                 } else {
1552:                     throw new CException ('invalid field name');
1553:                 }
1554:                 $fieldsForDropdown[$optGroupHeader] = $linkedModel->_getFieldsForDropdown (
1555:                         $fieldName, $condList, true, $filterFn, $separator);
1556:             }
1557:             return $fieldsForDropdown;
1558:         } else {
1559:             return $this->_getFieldsForDropdown (null, $condList, true, $filterFn, $separator);
1560:         }
1561:     }
1562: 
1563:     /**
1564:      * @return null|Fields Fields instance if found, null otherwise
1565:      */
1566:     public function getField($fieldName) {
1567:         foreach (self::$_fields[$this->tableName()] as &$_field) {
1568:             if ($_field->fieldName == $fieldName)
1569:                 return $_field;
1570:         }
1571:         return null;
1572:     }
1573: 
1574:     /**
1575:      * Whether to skip applying field level permissions
1576:      *
1577:      * Returns false if the user has any roles and isn't administrator; returns
1578:      * true (meaning, no arbitrary restrictions on field access/editability)
1579:      * 
1580:      * @return boolean
1581:      */
1582:     public function getIsExemptFromFieldLevelPermissions() {
1583:         return Yii::app()->params->isAdmin || empty(Yii::app()->params->roles);
1584:     }
1585: 
1586:     public function insert($attributes = null) {
1587:         $succeeded = parent::insert($attributes);
1588:         // Alter and save the nameId field:
1589:         if ($succeeded && self::$autoPopulateFields) {
1590:             $this->updateNameId(true);
1591: 
1592:             if ($this->hasEventHandler('onAfterInsert'))
1593:                 $this->onAfterInsert(new CEvent($this));
1594:         }
1595:         return $succeeded;
1596:     }
1597: 
1598:     /**
1599:      * @param string $field the name of the field
1600:      * @param string $class the name of the class with which the field is associated
1601:      * @param int $id
1602:      * @param bool $encode Whether to html encode the number
1603:      * @param bool $makeLink Whether return a phone link
1604:      * @param string $default What to use in case phone number lookup failed;
1605:      *  circumvents the need to re-query the model if used.
1606:      */
1607:     public static function getPhoneNumber(
1608:         $field, $class, $id, $encode = false, $makeLink = false, $default = null) {
1609: 
1610:         $phoneCheck = CActiveRecord::model('PhoneNumber')
1611:             ->findByAttributes(
1612:                 array('modelId' => $id, 'modelType' => $class, 'fieldName' => $field));
1613:         if ($phoneCheck instanceof PhoneNumber && strlen($phoneCheck->number) == 10 &&
1614:                 strpos($phoneCheck->number, '0') !== 0 && strpos($phoneCheck->number, '1') !== 0) {
1615: 
1616:             $number = (string) $phoneCheck->number;
1617:             $fmtNumber = "(" . substr($number, 0, 3) . ") " . substr($number, 3, 3) . "-" . 
1618:                 substr($number, 6, 4);
1619:         } elseif ($default != null) {
1620:             $number = (string) $default;
1621:             $fmtNumber = $default;
1622:         } else {
1623:             $record = X2Model::model($class)->findByPk($id);
1624:             if (isset($record) && $record->hasAttribute($field)) {
1625:                 $number = (string) $record->$field;
1626:                 $fmtNumber = $number;
1627:             }
1628:         }
1629:         if ($encode && isset($fmtNumber)) {
1630:             $fmtNumber = CHtml::encode($fmtNumber);
1631:         }
1632:         if (isset($fmtNumber) && $makeLink && !Yii::app()->params->profile->disablePhoneLinks) {
1633:             return '<a href="tel:' . $number . '">' . $fmtNumber . '</a>';
1634:         }
1635:         return isset($fmtNumber) ? $fmtNumber : '';
1636:     }
1637: 
1638:     public static function renderModelInput(CModel $model, $field, $htmlOptions = array()) {
1639:         if (!$field->asa ('CommonFieldsBehavior')) {
1640:             throw new Exception ('$field must have CommonFieldsBehavior');
1641:         }
1642:         if ($field->required) {
1643:             if (isset($htmlOptions['class'])) {
1644:                 $htmlOptions['class'] .= ' x2-required';
1645:             } else {
1646:                 $htmlOptions = array_merge(
1647:                         array(
1648:                     'class' => 'x2-required'
1649:                         ), $htmlOptions
1650:                 );
1651:             }
1652:         }
1653:         $fieldName = $field->fieldName;
1654:         if (!isset($field))
1655:             return null;
1656:         switch ($field->type) {
1657:             case 'text':
1658:                 return CHtml::activeTextArea($model, $field->fieldName,
1659:                                 array_merge(
1660:                                         array('title' => $field->attributeLabel),
1661:                                         array_merge(array('encode' => false),
1662:                                                 $htmlOptions)));
1663: 
1664:             case 'date':
1665:                 $oldDateVal = $model->$fieldName;
1666:                 $model->$fieldName = Formatter::formatDate($model->$fieldName, 'medium');
1667:                 Yii::import('application.extensions.CJuiDateTimePicker.CJuiDateTimePicker');
1668:                 $pickerOptions = array(// jquery options
1669:                     'dateFormat' => Formatter::formatDatePicker(),
1670:                     'changeMonth' => false,
1671:                     'changeYear' => true,
1672:                 );
1673:                 if (Yii::app()->getLanguage() === 'fr')
1674:                     $pickerOptions['monthNamesShort'] = Formatter::getPlainAbbrMonthNames();
1675:                 $input = Yii::app()->controller->widget('CJuiDateTimePicker', array(
1676:                     'model' => $model, //Model object
1677:                     'attribute' => $fieldName, //attribute name
1678:                     'mode' => 'date', //use "time","date" or "datetime" (default)
1679:                     'options' => $pickerOptions,
1680:                     'htmlOptions' => array_merge(array(
1681:                         'title' => $field->attributeLabel,
1682:                             ), $htmlOptions),
1683:                     'language' => (Yii::app()->language == 'en') ? '' : Yii::app()->getLanguage(),
1684:                         ), true);
1685:                 $model->$fieldName = $oldDateVal;
1686:                 return $input;
1687:             case 'dateTime':
1688:                 $oldDateTimeVal = $model->$fieldName;
1689:                 $pickerOptions = array(// jquery options
1690:                     'dateFormat' => Formatter::formatDatePicker('medium'),
1691:                     'timeFormat' => Formatter::formatTimePicker(),
1692:                     'ampm' => Formatter::formatAMPM(),
1693:                     'changeMonth' => true,
1694:                     'changeYear' => true,
1695:                 );
1696:                 if (Yii::app()->getLanguage() === 'fr')
1697:                     $pickerOptions['monthNamesShort'] = Formatter::getPlainAbbrMonthNames();
1698:                 $model->$fieldName = Formatter::formatDateTime($model->$fieldName);
1699:                 Yii::import('application.extensions.CJuiDateTimePicker.CJuiDateTimePicker');
1700:                 $input = Yii::app()->controller->widget('CJuiDateTimePicker', array(
1701:                     'model' => $model, //Model object
1702:                     'attribute' => $fieldName, //attribute name
1703:                     'mode' => 'datetime', //use "time","date" or "datetime" (default)
1704:                     'options' => $pickerOptions,
1705:                     'htmlOptions' => array_merge(array(
1706:                         'title' => $field->attributeLabel,
1707:                             ), $htmlOptions),
1708:                     'language' => (Yii::app()->language == 'en') ? '' : Yii::app()->getLanguage(),
1709:                         ), true);
1710:                 $model->$fieldName = $oldDateTimeVal;
1711:                 return $input;
1712:             case 'dropdown':
1713:                 // Note: if desired to translate dropdown options, change the seecond argument to 
1714:                 // $model->module
1715:                 $om = $field->getDropdownOptions ();
1716:                 $multi = (bool) $om['multi'];
1717:                 $dropdowns = $om['options'];
1718:                 $curVal = $multi ? CJSON::decode($model->{$field->fieldName}) : $model->{$field->fieldName};
1719: 
1720:                 $ajaxArray = array();
1721:                 if ($field instanceof Fields) {
1722:                     $dependencyCount = X2Model::model('Dropdowns')
1723:                         ->countByAttributes(array('parent' => $field->linkType));
1724:                     $fieldDependencyCount = X2Model::model('Fields')
1725:                         ->countByAttributes(array(
1726:                             'modelName' => $field->modelName, 
1727:                             'type' => 'dependentDropdown', 
1728:                             'linkType' => $field->linkType));
1729:                     if ($dependencyCount > 0 && $fieldDependencyCount > 0) {
1730:                         $ajaxArray = array('ajax' => array(
1731:                             'type' => 'GET', //request type
1732:                             'url' => Yii::app()->controller->createUrl('/site/dynamicDropdown'),
1733:                             'data' => 'js:{
1734:                                 "val":$(this).val(),
1735:                                 "dropdownId":"' . $field->linkType . '",
1736:                                 "field":true, "module":"' . $field->modelName . '"
1737:                             }',
1738:                             'success' => '
1739:                                 function(data){
1740:                                     if(data){
1741:                                         data=JSON.parse(data);
1742:                                         if(data[0] && data[1]){
1743:                                             $("#' . $field->modelName . '_"+data[0]).html(data[1]);
1744:                                         }
1745:                                     }
1746:                                 }',
1747:                         ));
1748:                     }
1749:                 }
1750:                 $htmlOptions = array_merge(
1751:                     $htmlOptions, $ajaxArray, array('title' => $field->attributeLabel));
1752:                 if ($multi) {
1753:                     $multiSelectOptions = array();
1754:                     if (!is_array($curVal))
1755:                         $curVal = array();
1756:                     foreach ($curVal as $option)
1757:                         $multiSelectOptions[$option] = array('selected' => 'selected');
1758:                     $htmlOptions = array_merge(
1759:                             $htmlOptions, array(
1760:                         'options' => $multiSelectOptions,
1761:                         'multiple' => 'multiple'
1762:                     ));
1763:                 } elseif ($field->includeEmpty) {
1764:                     $htmlOptions = array_merge(
1765:                         $htmlOptions, array('empty' => Yii::t('app', "Select an option")));
1766:                 }
1767:                 return CHtml::activeDropDownList($model, $field->fieldName, $dropdowns, $htmlOptions);
1768: 
1769:             case 'dependentDropdown':
1770:                 return CHtml::activeDropDownList($model, $field->fieldName, array('' => '-'), array_merge(
1771:                                         array(
1772:                             'title' => $field->attributeLabel,
1773:                                         ), $htmlOptions
1774:                 ));
1775:             case 'link':
1776:                 $linkSource = null;
1777:                 $linkId = '';
1778:                 $name = '';
1779: 
1780:                 if (class_exists($field->linkType)) {
1781:                     // Create a model for autocompletion:
1782:                     if (!empty($model->$fieldName)) {
1783:                         list($name, $linkId) = Fields::nameAndId($model->$fieldName);
1784:                         $linkModel = X2Model::getLinkedModelMock($field->linkType, $name, $linkId, true);
1785:                     } else {
1786:                         $linkModel = X2Model::model($field->linkType);
1787:                     }
1788:                     if ($linkModel instanceof X2Model && $linkModel->asa('X2LinkableBehavior') instanceof X2LinkableBehavior) {
1789:                         $linkSource = Yii::app()->controller->createUrl($linkModel->autoCompleteSource);
1790:                         $linkId = $linkModel->id;
1791:                         $oldLinkFieldVal = $model->$fieldName; 
1792:                         $model->$fieldName = $name;
1793:                     }
1794:                 }
1795: 
1796:                 static $linkInputCounter = 0;
1797:                 $hiddenInputId = $field->modelName . '_' . $fieldName . "_id".$linkInputCounter++;
1798:                 $input = CHtml::hiddenField(
1799:                     $field->modelName . '[' . $fieldName . '_id]', $linkId, 
1800:                     array('id' => $hiddenInputId))
1801:                         .Yii::app()->controller->widget('zii.widgets.jui.CJuiAutoComplete', array(
1802:                             'model' => $model,
1803:                             'attribute' => $fieldName,
1804:                             // 'name'=>'autoselect_'.$fieldName,
1805:                             'source' => $linkSource,
1806:                             'value' => $name,
1807:                             'options' => array(
1808:                                 'minLength' => '1',
1809:                                 'select' => 'js:function( event, ui ) {
1810:                                     $("#'.$hiddenInputId.'").
1811:                                         val(ui.item.id);
1812:                                     $(this).val(ui.item.value);
1813:                                     return false;
1814:                             }',
1815:                             'create' => $field->linkType == 'Contacts' ? 
1816:                                 'js:function(event, ui) {
1817:                                     $(this).data( "uiAutocomplete" )._renderItem = 
1818:                                         function(ul,item) {
1819:                                             return $("<li>").data("item.autocomplete",item).
1820:                                                 append(x2.forms.renderContactLookup(item)).
1821:                                                 appendTo(ul);
1822:                                         };
1823:                             }' : ($field->linkType == 'BugReports' ? 'js:function(event, ui) {
1824:                                 $(this).data( "uiAutocomplete" )._renderItem = 
1825:                                     function( ul, item ) {
1826: 
1827:                                     var label = "<a style=\"line-height: 1;\">" + item.label;
1828: 
1829:                                     label += "<span style=\"font-size: 0.6em;\">";
1830: 
1831:                                     // add email if defined
1832:                                     if(item.subject) {
1833:                                         label += "<br>";
1834:                                         label += item.subject;
1835:                                     }
1836: 
1837:                                     label += "</span>";
1838:                                     label += "</a>";
1839: 
1840:                                     return $( "<li>" )
1841:                                         .data( "item.autocomplete", item )
1842:                                         .append( label )
1843:                                         .appendTo( ul );
1844:                                 };
1845:                             }' : ''),
1846:                             ),
1847:                             'htmlOptions' => array_merge(array(
1848:                                 'title' => $field->attributeLabel,
1849:                                     ), $htmlOptions)
1850:                                 ), true);
1851:                 if (isset ($oldLinkFieldVal)) $model->$fieldName = $oldLinkFieldVal;
1852:                 return $input;
1853:             case 'rating':
1854:                 return Yii::app()->controller->widget('X2StarRating', array(
1855:                     'model' => $model,
1856:                     'attribute' => $field->fieldName,
1857:                     'readOnly' => isset($htmlOptions['disabled']) && $htmlOptions['disabled'],
1858:                     'minRating' => Fields::RATING_MIN, //minimal value
1859:                     'maxRating' => Fields::RATING_MAX, //max value
1860:                     'starCount' => Fields::RATING_MAX - Fields::RATING_MIN + 1, //number of stars
1861:                     'cssFile' => Yii::app()->theme->getBaseUrl() . '/css/rating/jquery.rating.css',
1862:                     'htmlOptions' => $htmlOptions,
1863:                     'callback'=>'function(value, link){
1864:                         if (typeof x2 !== "undefined" &&
1865:                             typeof x2.InlineEditor !== "undefined" &&
1866:                             typeof x2.InlineEditor.ratingFields !== "undefined") {
1867: 
1868:                             x2.InlineEditor.ratingFields["'.
1869:                                 $field->modelName.'['.$field->fieldName.']"] = value;
1870:                         }
1871:                     }',), true);
1872: 
1873:             case 'boolean':
1874:                 $checkbox = CHtml::openTag ('div', X2Html::mergeHtmlOptions (
1875:                     $htmlOptions, array (
1876:                         'class' => 'checkboxWrapper'
1877:                     )
1878:                 ));
1879:                 $checkbox .= CHtml::activeCheckBox($model, $field->fieldName, array_merge(array(
1880:                     'unchecked' => 0,
1881:                     'title' => $field->attributeLabel,
1882:                 ), $htmlOptions)); 
1883:                 $checkbox .= CHtml::closeTag ('div');
1884:                 return $checkbox;
1885:             case 'assignment':
1886:                 $oldAssignmentVal = $model->$fieldName;
1887:                 $model->$fieldName = !empty($model->$fieldName) ? 
1888:                     ($field->linkType == 'multiple' && !is_array($model->$fieldName) ? 
1889:                         explode(', ', $model->$fieldName) : $model->$fieldName) : 
1890:                     X2Model::getDefaultAssignment();
1891:                 $dropdownList = CHtml::activeDropDownList (
1892:                     $model, $fieldName, X2Model::getAssignmentOptions (true, true), 
1893:                     array_merge (array (
1894:                         // 'tabindex'=>isset($item['tabindex'])? $item['tabindex'] : null,
1895:                         // 'disabled'=>$item['readOnly']? 'disabled' : null,
1896:                         'title' => $field->attributeLabel,
1897:                         'id' => $field->modelName . '_' . $fieldName . '_assignedToDropdown',
1898:                         'multiple' =>
1899:                         ($field->linkType == 'multiple' ? 'multiple' : null),
1900:                     ), $htmlOptions)
1901:                 );
1902:                 $model->$fieldName = $oldAssignmentVal;
1903:                 return $dropdownList;
1904:             case 'optionalAssignment': // optional assignment for users (can be left blank)
1905: 
1906:                 $users = User::getNames();
1907:                 unset($users['Anyone']);
1908: 
1909:                 return CHtml::activeDropDownList($model, $fieldName, $users, array_merge(array(
1910:                             // 'tabindex'=>isset($item['tabindex'])? $item['tabindex'] : null,
1911:                             // 'disabled'=>$item['readOnly']? 'disabled' : null,
1912:                             'title' => $field->attributeLabel,
1913:                             'empty' => '',
1914:                                         ), $htmlOptions));
1915: 
1916:             case 'visibility':
1917:                 $permissionsBehavior = Yii::app()->params->modelPermissions;
1918:                 return CHtml::activeDropDownList($model, $field->fieldName, $permissionsBehavior::getVisibilityOptions(), array_merge(array(
1919:                             'title' => $field->attributeLabel,
1920:                             'id' => $field->modelName . "_visibility",
1921:                                         ), $htmlOptions));
1922: 
1923:             // 'varchar', 'email', 'url', 'int', 'float', 'currency', 'phone'
1924:             // case 'int':
1925:             // return CHtml::activeNumberField($model, $field->fieldNamearray_merge(array(
1926:             // 'title' => $field->attributeLabel,
1927:             // ), $htmlOptions));
1928: 
1929:             case 'percentage':
1930:                 $htmlOptions['class'] = empty($htmlOptions['class']) ? 'input-percentage' : $htmlOptions['class'] . ' input-percentage';
1931:                 return CHtml::activeTextField($model, $field->fieldName, array_merge(array(
1932:                             'title' => $field->attributeLabel,
1933:                                         ), $htmlOptions));
1934: 
1935:             case 'currency':
1936:                 $fieldName = $field->fieldName;
1937:                 $elementId = isset($htmlOptions['id']) ? 
1938:                     '#' . $htmlOptions['id'] : 
1939:                     '#' . $field->modelName . '_' . $field->fieldName;
1940:                 Yii::app()->controller->widget('application.extensions.moneymask.MMask', array(
1941:                     'element' => $elementId,
1942:                     'currency' => Yii::app()->params['currency'],
1943:                     'config' => array(
1944:                         //'showSymbol' => true,
1945:                         'affixStay' => true,
1946:                         'decimal' => Yii::app()->locale->getNumberSymbol('decimal'),
1947:                         'thousands' => Yii::app()->locale->getNumberSymbol('group'),
1948:                     )
1949:                 ));
1950: 
1951:                 return CHtml::activeTextField($model, $field->fieldName, array_merge(array(
1952:                             'title' => $field->attributeLabel,
1953:                             'class' => 'currency-field',
1954:                                         ), $htmlOptions));
1955:             case 'credentials':
1956:                 $typeAlias = explode(':', $field->linkType);
1957:                 $type = $typeAlias[0];
1958:                 if (count($typeAlias) > 1) {
1959:                     $uid = Credentials::$sysUseId[$typeAlias[1]];
1960:                 } else {
1961:                     $uid = Yii::app()->user->id;
1962:                 }
1963:                 return Credentials::selectorField($model, $field->fieldName, $type, $uid);
1964: 
1965:             case 'timerSum':
1966:                 // Sorry, no-can-do. This is field derives its value from a sum over timer records.
1967:                 return $model->renderAttribute($field->fieldName);
1968:             case 'float':
1969:             case 'int':
1970:                 if (isset($model->$fieldName)) {
1971:                     $oldNumVal = $model->$fieldName;
1972:                     $model->$fieldName = Yii::app()->locale->numberFormatter->formatDecimal($model->$fieldName);
1973:                 }
1974:                 $input = CHtml::activeTextField($model, $field->fieldName, array_merge(array(
1975:                             'title' => $field->attributeLabel,
1976:                                         ), $htmlOptions));
1977:                 if (isset ($oldNumVal)) {
1978:                     $model->$fieldName = $oldNumVal;
1979:                 }
1980:                 return $input;
1981:             default:
1982:                 return CHtml::activeTextField($model, $field->fieldName, array_merge(array(
1983:                             'title' => $field->attributeLabel,
1984:                                         ), $htmlOptions));
1985: 
1986:             // array(
1987:             // 'tabindex'=>isset($item['tabindex'])? $item['tabindex'] : null,
1988:             // 'disabled'=>$item['readOnly']? 'disabled' : null,
1989:             // 'title'=>$field->attributeLabel,
1990:             // 'style'=>$default?'color:#aaa;':null,
1991:             // ));
1992:         }
1993:     }
1994: 
1995:     public static function renderMergeInput($modelType, $idArray, $field) {
1996:         $options = array();
1997:         $equalFlag = true;
1998:         $selected = $idArray[0];
1999:         $lastValue = null;
2000:         foreach ($idArray as $id) {
2001:             $tmpModel = X2Model::model($modelType)->findByPk($id);
2002:             $equalFlag = $equalFlag && (is_null($lastValue) || $lastValue == $tmpModel->{$field->fieldName});
2003:             $lastValue = is_null($tmpModel->{$field->fieldName}) ? "" : $tmpModel->{$field->fieldName};
2004:             $options[$id] = strlen($tmpModel->{$field->fieldName}) == 0 ? '' : $tmpModel->renderAttribute($field->fieldName, false, true, false, false);
2005:         }
2006:         if ($field->type !== 'text') {
2007:             $valid = array_filter($options, 'strlen');
2008:             if (key($valid) != $selected) {
2009:                 $selected = key($valid);
2010:             }
2011:             foreach ($options as &$option) {
2012:                 if (strlen($option) == 0) {
2013:                     $option = '-';
2014:                 }
2015:             }
2016:             return CHtml::dropDownList(X2Model::getModelName($modelType) . '[' . $field->fieldName . ']', $selected, $options, array(
2017:                         'disabled' => $equalFlag ? 'disabled' : '',
2018:                     )) . ($equalFlag?CHtml::hiddenField(X2Model::getModelName($modelType) . '[' . $field->fieldName . ']', $selected):"");
2019:         } else {
2020:             $str = str_replace("<br />", "\n", implode("\n--\n", $options));
2021:             return CHtml::textArea(X2Model::getModelName($modelType) . '[' . $field->fieldName . ']', $str, array(
2022:                         'style' => 'height:100px;',
2023:             ));
2024:         }
2025:     }
2026: 
2027:     /**
2028:      * Renders an attribute of the model based on its field type
2029:      * @param string $fieldName the name of the attribute to be rendered
2030:      * @param array $htmlOptions htmlOptions to be used on the input
2031:      * @return string the HTML or text for the formatted attribute
2032:      */
2033:     public function renderInput($fieldName, $htmlOptions = array()) {
2034:         $field = $this->getField($fieldName);
2035:         
2036:         if (!$field) {
2037:             return $this->renderErroneousField();
2038:         }
2039: 
2040:         if ($this->inputRenderer) {
2041:             // check if there's a renderer for this field type
2042:             if ($input = $this->inputRenderer->renderInput ($field, $htmlOptions)) {
2043:                 return $input;
2044:             }
2045:         }
2046: 
2047:         return self::renderModelInput($this, $field, $htmlOptions);
2048:     }
2049: 
2050:     /**
2051:      * Renders an error when a field cannot be found
2052:      */
2053:     public function renderErroneousField() {
2054:         $html  = '<span class="erroneous-field">';
2055:         $html .=  Yii::t('app', 'Field could not be found');
2056:         $html .= '</span>';
2057:         return $html;
2058:     }
2059: 
2060:     /**
2061:      * Sets attributes using X2Fields
2062:      * @param array &$data array of attributes to be set (eg. $_POST['Contacts'])
2063:      * @param bool $filter encode all HTML special characters in input
2064:      * @param bool $bypassPermissions (optional)
2065:      */
2066:     public function setX2Fields(&$data, $filter = false, $bypassPermissions=false) {
2067:         $editableFieldsFieldNames = $this->getEditableFieldNames();
2068: 
2069:         // loop through fields to deal with special types
2070:         foreach (self::$_fields[$this->tableName()] as &$field) {
2071:             $fieldName = $field->fieldName;
2072: 
2073:             // skip fields that are read-only or haven't been set
2074:             if (!isset($data[$fieldName]) ||
2075:                 (!$bypassPermissions && !in_array($fieldName, $editableFieldsFieldNames))) {
2076: 
2077:                 if (isset($data[$fieldName]) &&
2078:                     !in_array($fieldName, $editableFieldsFieldNames)) {
2079: 
2080:                     //if (YII_DEBUG)
2081:                         //printR('setX2Fields: Warning: ' . $fieldName . ' set');
2082:                 }
2083:                 continue;
2084:             }
2085: 
2086:             // eliminate placeholder values
2087:             if ($data[$fieldName] === $this->getAttributeLabel($fieldName) &&
2088:                 $field->type !== 'dropdown') {
2089: 
2090:                 $data[$fieldName] = null;
2091:             }
2092: 
2093:             if ($field->type === 'currency') {
2094:                 $defaultCurrency = Yii::app()->settings->currency;
2095:                 $curSym = Yii::app()->locale->getCurrencySymbol($defaultCurrency);
2096:                 if (is_null($curSym))
2097:                     $curSym = $defaultCurrency;
2098:                 $data[$fieldName] = Fields::strToNumeric($data[$fieldName], 'currency', $curSym);
2099:             }
2100: 
2101:             if ($field->type === 'link') {
2102:                 // Do a preliminary lookup for linkId in case there are
2103:                 // duplicates (similar name) and the user selects one of them,
2104:                 // in which case there is an ID from the form that was populated
2105:                 // via the auto-complete input widget:
2106:                 $linkId = null;
2107:                 if (isset($data[$fieldName . '_id'])) {
2108:                     // get the linked model's ID from the hidden autocomplete field
2109:                     $linkId = $data[$fieldName . '_id'];
2110:                 }
2111: 
2112:                 if (ctype_digit((string) $linkId)) {
2113:                     $link = Yii::app()->db->createCommand()
2114:                             ->select('name,nameId')
2115:                             ->from(X2Model::model($field->linkType)->tableName())
2116:                             ->where('id=?', array($linkId))
2117:                             ->queryRow(true);
2118:                     // Make sure the linked model exists and that the name matches:
2119:                     if (isset($link['name']) && $link['name'] === $data[$fieldName]) {
2120:                         $data[$fieldName] = $link['nameId'];
2121:                     }
2122:                 }
2123:             }
2124:             $this->$fieldName = $field->parseValue($data[$fieldName], $filter);
2125:         }
2126: 
2127:         // Set default values.
2128:         //
2129:         // This should only happen in the case that the field was not included
2130:         // in the form submission data (with the exception of assignment fields), and the field is 
2131:         // empty, and the record is new.
2132:         if ($this->getIsNewRecord() && $this->scenario == 'insert') {
2133:             // Set default values
2134:             foreach ($this->getFields(true) as $fieldName => $field) {
2135:                 if (!isset($data[$fieldName]) && $this->$fieldName == '' &&
2136:                         $field->defaultValue != null && !$field->readOnly) {
2137: 
2138:                     $this->$fieldName = $field->defaultValue;
2139:                 } else if ($this->$fieldName === null && $field->defaultValue === null &&
2140:                         $field->type === 'assignment') {
2141: 
2142:                     $this->$fieldName = self::getDefaultAssignment();
2143:                 }
2144:             }
2145:         }
2146:     }
2147: 
2148:     /**
2149:      * Base search function, includes Retrieves a list of models based on the current 
2150:      *  search/filter conditions.
2151:      * @param CDbCriteria $criteria the attribute name
2152:      * @param integer $pageSize If set, will override property of profile model
2153:      * @return CActiveDataProvider the data provider that can return the models based on the 
2154:      *  search/filter conditions.
2155:      */
2156:     public function searchBase($criteria, $pageSize = null, $showHidden = false) {
2157:         if(isset($_GET['showHidden']) && $_GET['showHidden'] && 
2158:            Yii::app()->user->checkAccess(self::getModuleName(get_class($this)).'Admin')){
2159: 
2160:             $showHidden = true;
2161:         }
2162: 
2163:         if ($criteria === null){
2164:             $criteria = $this->getAccessCriteria(
2165:                 't', Yii::app()->params->modelPermissions, $showHidden);
2166:         }else{
2167:             $criteria->mergeWith(
2168:                 $this->getAccessCriteria('t', Yii::app()->params->modelPermissions, $showHidden));
2169:         }
2170: 
2171:         $filterCriteria = new CDbCriteria;
2172:         $this->compareAttributes($filterCriteria);
2173:         $criteria->mergeWith ($filterCriteria);
2174:         $criteria->with = array(); // No joins necessary!
2175:         $sort = new SmartSort(
2176:                 get_class($this), isset($this->uid) ? $this->uid : get_class($this));
2177:         $sort->multiSort = false;
2178:         $sort->attributes = $this->getSort();
2179:         $sort->defaultOrder = 't.lastUpdated DESC, t.id DESC';
2180: 
2181:         if ($pageSize === null) {
2182:             if (!Yii::app()->user->isGuest) {
2183:                 $pageSize = Profile::getResultsPerPage();
2184:             } else {
2185:                 $pageSize = 20;
2186:             }
2187:         }
2188: 
2189:         $dataProvider = new SmartActiveDataProvider(get_class($this), array(
2190:             'sort' => $sort,
2191:             'pagination' => array(
2192:                 'pageSize' => $pageSize,
2193:             ),
2194:             'criteria' => $criteria,
2195:             'uid' => $this->uid,
2196:             'dbPersistentGridSettings' => $this->dbPersistentGridSettings,
2197:             'disablePersistentGridSettings' => $this->disablePersistentGridSettings,
2198:         ));
2199:         $sort->applyOrder($criteria);
2200:         return $dataProvider;
2201:     }
2202: 
2203:     public function getSort() {
2204:         $attributes = array();
2205:         foreach (self::$_fields[$this->tableName()] as &$field) {
2206:             $fieldName = $field->fieldName;
2207:             switch ($field->type) {
2208:                 default:
2209:                     $attributes[$fieldName] = array(
2210:                         'asc' => 't.' . $fieldName . ' ASC',
2211:                         'desc' => 't.' . $fieldName . ' DESC',
2212:                     );
2213:             }
2214:         }
2215:         return $attributes;
2216:     }
2217: 
2218:     /**
2219:      * Unshifts valid operators of the front of the string.
2220:      * @return array (<operator>, <remaining string>)
2221:      */
2222:     public function unshiftOperator($string) {
2223:         $retArr = array('', $string);
2224:         if (strlen($string) > 1) {
2225:             if (strlen($string) > 2 && preg_match("/^(<>|>=).*/", $string)) {
2226:                 $retArr = array(substr($string, 0, 2), substr($string, 2));
2227:             } else if (preg_match("/^(<|>|=).*/", $string)) {
2228:                 $retArr = array($string[0], substr($string, 1));
2229:             }
2230:         }
2231: 
2232:         return $retArr;
2233:     }
2234: 
2235:     /**
2236:      * Helper method for compareAttributes 
2237:      */
2238:     protected function compareAttribute(&$criteria, $field) {
2239:         $fieldName = $field->fieldName;
2240:         switch ($field->type) {
2241:             case 'boolean':
2242:                 $criteria->compare(
2243:                         't.' . $fieldName, $this->compareBoolean($this->$fieldName), true);
2244:                 break;
2245:             case 'assignment':
2246:                 $assignmentCriteria = new CDbCriteria;
2247:                 $assignmentVal = $this->compareAssignment($this->$fieldName);
2248: 
2249:                 if ($field->linkType === 'multiple' && $this->$fieldName) {
2250:                     if (!is_array ($assignmentVal)) $assignmentVal = array ();
2251:                     $assignmentVal = array_map (function ($val) {
2252:                         return preg_quote ($val);
2253:                     }, $assignmentVal);
2254:                     if (strlen ($this->$fieldName) && strncmp (
2255:                         "Anyone", ucfirst ($this->$fieldName), strlen ($this->$fieldName)) === 0) {
2256: 
2257:                         $assignmentVal[] = 'Anyone';
2258:                     }
2259:                     $assignmentRegex = 
2260:                         '(^|, )('.implode ('|', $assignmentVal).')'.
2261:                         (in_array ('Anyone', $assignmentVal) ? '?' : '').'(, |$)';
2262: 
2263:                     $assignmentParamName = CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount;
2264:                     $criteria->params[$assignmentParamName] = $assignmentRegex;
2265:                     CDbCriteria::$paramCount++;
2266:                     $criteria->addCondition(
2267:                         't.' . $fieldName .' REGEXP BINARY '.$assignmentParamName);
2268:                 } else {
2269:                     $assignmentCriteria->compare(
2270:                         't.' . $fieldName, $assignmentVal, true);
2271:                     if (strlen ($this->$fieldName) && strncmp (
2272:                         "Anyone", ucfirst ($this->$fieldName), strlen ($this->$fieldName)) === 0) {
2273: 
2274:                         $assignmentCriteria->compare('t.' . $fieldName, 'Anyone', false, 'OR');
2275:                         $assignmentCriteria->addCondition('t.' . $fieldName . ' = ""', 'OR');
2276:                     }
2277:                 }
2278:                 $criteria->mergeWith ($assignmentCriteria);
2279:                 break;
2280:             case 'dropdown':
2281:                 $dropdownVal = $this->compareDropdown($field->linkType, $this->$fieldName);
2282:                 if (is_array ($dropdownVal)) {
2283:                     foreach ($dropdownVal as $val) {
2284:                         $dropdownRegex = 
2285:                             '(^|((\\[|,)"))'.preg_quote ($val).'(("(,|\\]))|$)';
2286:                         $dropdownParamName = CDbCriteria::PARAM_PREFIX.CDbCriteria::$paramCount;
2287:                         $criteria->params[$dropdownParamName] = $dropdownRegex;
2288:                         CDbCriteria::$paramCount++;
2289:                         $criteria->addCondition(
2290:                             't.' . $fieldName .' REGEXP BINARY '.$dropdownParamName);
2291:                     }
2292:                 } else {
2293:                     $criteria->compare('t.' . $fieldName, $dropdownVal, false);
2294:                 }
2295:                 break;
2296:             case 'date':
2297:             case 'dateTime':
2298:                 if (!empty($this->$fieldName)) {
2299:                     // get operator and convert date string to timestamp
2300:                     $retArr = $this->unshiftOperator($this->$fieldName);
2301: 
2302:                     $operator = $retArr[0];
2303:                     $timestamp = Formatter::parseDate($retArr[1]);
2304:                     if (!$timestamp) {
2305:                         // if date string couldn't be parsed, it's better to display no results
2306:                         // than non-empty incorrect results (which could result in bad mass updates
2307:                         // or deletes)
2308:                         $criteria->addCondition('FALSE');
2309:                     } else if ($operator === '=' || $operator === '') {
2310:                         $criteria->addBetweenCondition(
2311:                             't.' . $fieldName, $timestamp, $timestamp + 60 * 60 * 24);
2312:                     } else {
2313:                         $value = $operator . $timestamp;
2314:                         $criteria->compare('t.' . $fieldName, $value);
2315:                     }
2316:                 }
2317:                 break;
2318:             case 'phone':
2319:             // $criteria->join .= ' RIGHT JOIN x2_phone_numbers ON (x2_phone_numbers.itemId=t.id AND x2_tags.type="Contacts" AND ('.$tagConditions.'))';
2320:             default:
2321:                 $criteria->compare('t.' . $fieldName, $this->$fieldName, true);
2322:         }
2323:     }
2324: 
2325:     public function compareAttributes(&$criteria) {
2326:         if ($this->asa ('TagBehavior') && $this->asa ('TagBehavior')->getEnabled () && 
2327:             $this->tags) {
2328: 
2329:             $tagCriteria = new CDbCriteria;
2330:             $this->compareTags ($tagCriteria);
2331:             $criteria->mergeWith ($tagCriteria);
2332:         }
2333: 
2334:         foreach (self::$_fields[$this->tableName()] as &$field) {
2335:             $this->compareAttribute($criteria, $field);
2336:         }
2337:     }
2338: 
2339:     protected function compareBoolean($data) {
2340:         if (is_null($data) || $data == '')
2341:             return null;
2342: 
2343:         // default to true unless recognized as false
2344:         return in_array(
2345:             mb_strtolower(
2346:                 trim($data)), array(0, 'f', 'false', Yii::t('actions', 'No')), true) ? 0 : 1;
2347:     }
2348: 
2349:     public function compareAssignment($data) {
2350:         if (is_null($data) || $data == '')
2351:             return null;
2352:         $userNames = Yii::app()->db->createCommand()
2353:                 ->select('username')
2354:                 ->from('x2_users')
2355:                 ->where(array('like', 'CONCAT(firstName," ",lastName)', "%$data%"))
2356:                 ->queryColumn();
2357:         $groupIds = Yii::app()->db->createCommand()
2358:                 ->select('id')
2359:                 ->from('x2_groups')
2360:                 ->where(array('like', 'name', "%$data%"))
2361:                 ->queryColumn();
2362: 
2363:         return (count($groupIds) + count($userNames) == 0) ? -1 : $userNames + $groupIds;
2364:     }
2365: 
2366:     protected function compareDropdown($ddId, $value) {
2367:         if (is_null($value) || $value == '') {
2368:             return null;
2369:         }
2370:         $dropdown = X2Model::model('Dropdowns')->findByPk($ddId);
2371:         $multi = $dropdown->multi;
2372:         if (isset($dropdown)) {
2373:             $index = $dropdown->getDropdownIndex($ddId, $value, $multi);
2374:             if (!is_null($index)) {
2375:                 return $index;
2376:             }else {
2377:                 return -1;
2378:             }
2379:         }
2380:         return -1;
2381:     }
2382: 
2383:     /**
2384:      * Attempts to load the model with the given ID, if the current
2385:      * user passes authentication checks. Throws an exception if not.
2386:      * @param Integer $d The ID of the model to load
2387:      * @return mixed The model object
2388:      */
2389:     /*     public static function load($modelName,$id) {
2390:       $model = X2Model::model($modelName)->findByPk($id);
2391:       if($model === null)
2392:       throw new CHttpException(404, Yii::t('app', 'Sorry, this record doesn\'t seem to exist.'));
2393: 
2394: 
2395: 
2396:       $authItem = ucfirst(Yii::app()->controller->id).ucfirst(Yii::app()->controller->action->id);
2397: 
2398:       // $authItem = ucfirst(Yii::app()->controller->id).'ViewPrivate';
2399: 
2400:       $result = Yii::app()->user->checkAccess($authItem);
2401: 
2402:       if($model->hasAttribute('visibility') && $model->hasAttribute('assignedTo')) {
2403:       throw new CHttpException(403, 'You are not authorized to perform this action.');
2404:       }
2405: 
2406:       return $model;
2407:       } */
2408: 
2409:     /**
2410:      * Returns a model of the appropriate type with a particular record loaded.
2411:      *
2412:      * @param String $type The type of the model to load
2413:      * @param Integer $id The id of the record to load
2414:      * @return CActiveRecord A database record with the requested type and id
2415:      */
2416:     public static function getAssociationModel($type, $id) {
2417:         if ($id != 0 && $modelName = X2Model::getModelName($type))
2418:             return X2Model::model($modelName)->findByPk($id);
2419:         else
2420:             return null;
2421:     }
2422: 
2423:     /**
2424:      * Picks the primary key attribute out of an associative aray and finds the record
2425:      * @param array $params
2426:      * @return type
2427:      */
2428:     public function findByPkInArray(array $params) {
2429:         $pkc = $this->tableSchema->primaryKey;
2430:         $pk = null;
2431:         if (is_array($pkc)) { // Composite primary key
2432:             $pk = array();
2433:             foreach ($pkc as $colName) {
2434:                 if (array_key_exists($colName, $params))
2435:                     $pk[$colName] = $params[$colName];
2436:                 else // Primary key column missing
2437:                     return null;
2438:             }
2439:         } elseif (array_key_exists($pkc, $params)) { // Single-column primary key
2440:             $pk = $params[$pkc];
2441:         } else { // Can't do anything; primary key not found in array.
2442:             return null;
2443:         }
2444:         return $this->findByPk($pk);
2445:     }
2446: 
2447:     /**
2448:      * Sets the nameId field appropriately.
2449:      *
2450:      * The model must be inserted already, so that its primary key can
2451:      * be used.
2452:      * @param bool $save If true, update the model when done.
2453:      */
2454:     public function updateNameId($save = false) {
2455:         if (!$this->hasAttribute('nameId')) {
2456:             return;
2457:         }
2458:         $this->_oldAttributes['nameId'] = $this->nameId;
2459:         $this->nameId = Fields::nameId($this->name, $this->id);
2460:         if ($save) {
2461:             $that = $this;
2462:             $this->runWithoutBehavior('X2FlowTriggerBehavior', function () use ($that) {
2463:                 $that->updateByPk($that->id, array('nameId' => $that->nameId));
2464:             });
2465:         }
2466:     }
2467: 
2468:     /**
2469:      * Populates the nameId field in multiple records (or all) with one query.
2470:      *
2471:      * Note, if the method {@link Fields::nameId()} is ever dramatically changed,
2472:      * this method too will need to be changed accordingly. The unit test,
2473:      * {X2ModelTest::testMassUpdateNameId()}, is designed to fail when this
2474:      * happens in order to draw attention to it.
2475:      *
2476:      * @param string $modelName
2477:      * @param mixed $ids
2478:      */
2479:     public static function massUpdateNameId($modelName, $ids = array()) {
2480:         $param = array();
2481:         if ($modelName === 'Actions') {
2482:             // Actions don't have a nameId; instead set the associationName
2483:             $idField = 'associationId';
2484:             $nameField = 'associationName';
2485:             $nameIdField = 'associationName';
2486:         } else {
2487:             $idField = 'id';
2488:             $nameField = 'name';
2489:             $nameIdField = 'nameId';
2490:         }
2491:         $sql = "UPDATE `" . self::model($modelName)->tableName() . "` "
2492:                 . "SET `".$nameIdField."`=CONCAT(`".$nameField."`,:delim,`".$idField."`)";
2493:         if (is_array($ids) && count($ids) > 1) {
2494:             // Multiple records with IDs specified by $ids parameters
2495:             $count = 0;
2496:             foreach ($ids as $id) {
2497:                 $param[":id" . $count] = $id;
2498:                 $count++;
2499:             }
2500:             $sql .= ' WHERE `id` IN (' . implode(',', array_keys($param)) . ')';
2501:         } else if (!empty($ids)) {
2502:             // One ID specified:
2503:             $param[':id'] = is_array($ids) ? reset($ids) : $ids;
2504:             $sql .= ' WHERE `id`=:id';
2505:         } else {
2506:             // All records to be udpated:
2507:             $sql .= ' WHERE 1';
2508:         }
2509:         $param[':delim'] = Fields::NAMEID_DELIM;
2510:         $result = Yii::app()->db->createCommand($sql)->execute($param);
2511:         if ($modelName === 'Actions') {
2512:             // clean up association nameIds where the model could not be found
2513:             $sql = 'UPDATE x2_actions '.
2514:                 'SET associationName = LEFT(`associationName`, INSTR(`associationName`, "_0")-1)'.
2515:                 ' WHERE associationName like "%_0"';
2516:             Yii::app()->db->createCommand($sql)->execute();
2517:         }
2518:         return $result;
2519:     }
2520: 
2521:     /**
2522:      * Updates references to this model record in other tables.
2523:      *
2524:      * This is a temporary solution to be used until a future version, wherein
2525:      * all tables have been migrated to InnoDB and foreign key constraints can
2526:      * be used to maintain referential integrity. However, this function should
2527:      * still be kept in place for handling the special case of deletion, which
2528:      * cannot be adequately handled by foreign key constraints.
2529:      *
2530:      * It may be a while before we can safely migrate everything to InnoDB,
2531:      * because one or more of our big-ticket customers have FULLTEXT indexes on
2532:      * their contacts table to make searching faster, and FULLTEXT indexes are
2533:      * not supported in InnoDB.
2534:      *
2535:      * @todo Also in said future version would be a suite of unit-tested
2536:      *  methods in Fields (or this model) for creating, deleting and altering
2537:      *  indexes and foreign key constraints, so that new custom fields can
2538:      *  be created properly.
2539:      */
2540:     public function updateNameIdRefs() {
2541: 
2542:         // Update all references to this model via the nameId field.
2543:         //
2544:         // The name field and thus nameId field may have changed. Update all
2545:         // references to it so that searching/sorting works properly.
2546:         if (!$this->hasAttribute('nameId')) {
2547:             return;
2548:         }
2549:         $nameId = $this->nameId;
2550:         $oldNameId = isset($this->_oldAttributes['nameId']) ? 
2551:             $this->_oldAttributes['nameId'] : null;
2552:         if ($nameId !== $oldNameId) {
2553:             // First, however, we need to get references:
2554:             $modelName = get_class($this);
2555:             if (!isset(self::$_nameIdRefs[$modelName])) {
2556:                 // Attempt to get cached values:
2557:                 $cacheIndex = 'nameIdRefs_' . $modelName;
2558:                 self::$_nameIdRefs[$modelName] = Yii::app()->cache->get($cacheIndex);
2559:                 // Generate and store an index of references if not available:
2560:                 if (self::$_nameIdRefs[$modelName] == false) {
2561:                     $fields = Fields::model()->findAllByAttributes(array(
2562:                         'type' => 'link',
2563:                         'linkType' => $modelName
2564:                     ));
2565:                     self::$_nameIdRefs[$modelName] = array();
2566:                     foreach ($fields as $field) {
2567:                         $table = X2Model::model($field->modelName)->tableName();
2568:                         self::$_nameIdRefs[$modelName][$table][] = $field->fieldName;
2569:                     }
2570:                     // cache the data
2571:                     Yii::app()->cache->set($cacheIndex, self::$_nameIdRefs[$modelName], 0); 
2572:                 }
2573:             }
2574: 
2575:             // Go through references and run updates:
2576:             foreach (self::$_nameIdRefs[$modelName] as $table => $columns) {
2577:                 foreach ($columns as $column) {
2578:                     Yii::app()->db->createCommand()->update(
2579:                         $table, array($column => $nameId), 
2580:                         "$column=:nid", array(':nid' => $oldNameId)
2581:                     );
2582:                 }
2583:             }
2584:         }
2585:     }
2586: 
2587:     /**
2588:      * @return bool true if attribute changed after being saved, false otherwise 
2589:      */
2590:     public function attributeChanged ($attr) {
2591:         $oldAttributes = $this->getOldAttributes();
2592:         return (!isset($oldAttributes[$attr]) && $this->isNewRecord) || 
2593:             (in_array ($attr, array_keys ($oldAttributes)) && 
2594:              $this->getAttribute($attr) != $oldAttributes[$attr]);
2595:     }
2596: 
2597:     /**
2598:      * Helper method for renderModelInput () and setX2Fields () used to retrieve the application
2599:      * default for the assignment field. This gets superceded by user define default values.
2600:      */
2601:     public static function getDefaultAssignment() {
2602:         return Yii::app()->user->isGuest ? 'Anyone' : Yii::app()->user->getName();
2603:     }
2604: 
2605:     /**
2606:      * Returns assignment selection options
2607:      * @param type $anyone
2608:      * @param type $showGroups
2609:      * @param type $showSeparator
2610:      * @return type
2611:      */
2612:     public static function getAssignmentOptions(
2613:         $anyone = true, $showGroups = true, $showSeparator = true) {
2614: 
2615:         $users = User::getNames();
2616:         if ($anyone !== true)
2617:             unset($users['Anyone']);
2618: 
2619:         if ($showGroups === true) {
2620:             $groups = Groups::getNames();
2621:             if (count($groups) > 0) {
2622:                 if ($showSeparator)
2623:                     $users = $users + array('' => '--------------------') + $groups;
2624:                 else
2625:                     $users = $users + $groups;
2626:             }
2627:         }
2628:         return $users;
2629:     }
2630: 
2631:     /**
2632:      * Returns an array of field names that the user has permission to edit
2633:      * @param boolean if false, get attribute labels as well as field names
2634:      * @return mixed if $suppressAttributeLabels is true, an array of field names is returned,
2635:      *    otherwise an associative array is returned (fieldName => attributeLabel)
2636:      */
2637:     public function getEditableFieldNames($suppressAttributeLabels = true) {
2638:         $class = get_class($this);
2639:         if (!isset(self::$_editableFieldNames[$class])) {
2640:             $editableFields = array_keys(
2641:                     array_filter($this->fieldPermissions, function($p) {
2642:                         return $p >= 2;
2643:                     }));
2644:             if (sizeof($editableFields)) {
2645:                 $params = AuxLib::bindArray($editableFields);
2646:                 $in = AuxLib::arrToStrList(array_keys($params));
2647:                 self::$_editableFieldNames[$class] = Yii::app()->db->createCommand()
2648:                     ->select('fieldName, attributeLabel')
2649:                     ->from('x2_fields')
2650:                     ->where('readOnly!=1 AND modelName="' . get_class($this) . '" '
2651:                             . 'AND fieldName IN ' . $in, $params)
2652:                     ->queryAll();
2653:             } else {
2654:                 self::$_editableFieldNames[$class] = array();
2655:             }
2656:         }
2657: 
2658:         $editableFieldNames = array();
2659:         if (!$suppressAttributeLabels) {
2660:             foreach (self::$_editableFieldNames[$class] as $fieldInfo) {
2661:                 $editableFieldNames[$fieldInfo['fieldName']] = $fieldInfo['attributeLabel'];
2662:             }
2663:         } else {
2664:             foreach (self::$_editableFieldNames[$class] as $fieldInfo) {
2665:                 $editableFieldNames[] = $fieldInfo['fieldName'];
2666:             }
2667:         }
2668: 
2669:         return $editableFieldNames;
2670:     }
2671: 
2672:     /**
2673:      * Getter for {@link fieldPermissions}
2674:      * @return type
2675:      */
2676:     public function getFieldPermissions() {
2677:         $class = get_class($this);
2678: 
2679:         if (!isset(self::$_fieldPermissions[$class])) {
2680:             $roles = Roles::getUserRoles(Yii::app()->getSuId());
2681:             if (!$this->isExemptFromFieldLevelPermissions) {
2682:                 $permRecords = Yii::app()->db->createCommand()
2683:                     ->select("f.fieldName,MAX(rtp.permission),f.readOnly")
2684:                     ->from(RoleToPermission::model()->tableName() . ' rtp')
2685:                     ->join(Fields::model()->tableName() . ' f', 'rtp.fieldId=f.id '
2686:                             . 'AND rtp.roleId IN ' . AuxLib::arrToStrList($roles) . ' '
2687:                             . 'AND f.modelName=:class', array(':class' => $class))
2688:                     ->group('f.fieldName')
2689:                     ->queryAll(false);
2690:             } else {
2691:                 $permRecords = Yii::app()->db->createCommand()
2692:                     ->select("fieldName,CAST(2 AS UNSIGNED INTEGER),readOnly")
2693:                     ->from(Fields::model()->tableName() . ' f')
2694:                     ->where('modelName=:class', array(':class' => $class))
2695:                     ->queryAll(false);
2696:             }
2697:             $fieldPerms = array();
2698:             foreach ($permRecords as $record) {
2699:                 // If the permissions of the user on the field are "2" (write),
2700:                 // subtract the readOnly field
2701:                 $fieldPerms[$record[0]] = $record[1] - 
2702:                     (integer) ((integer) $record[1] === 2 ? $record[2] : 0);
2703:             }
2704:             self::$_fieldPermissions[$class] = $fieldPerms;
2705:         }
2706:         return self::$_fieldPermissions[$class];
2707:     }
2708: 
2709:     /**
2710:      * Build a json-encoded form layout for models whose Forms are not editable or
2711:      * for custom modules that do not yet have a user-created form.
2712:      * @param string $modelname The model for which to build a default layout.
2713:      * @return json The default layout for the selected model.
2714:      */
2715:     public static function getDefaultFormLayout($modelName) {
2716:         $model = X2Model::model($modelName);
2717:         $fields = Fields::model()->findAllByAttributes(
2718:             array('modelName' => $modelName),
2719:             new CDbCriteria(array('order' => 'attributeLabel ASC'))
2720:         );
2721:         $layout = array('sections' => array(array(
2722:             'collapsible' => false,
2723:             'title' => ucfirst($modelName) . ' Info',
2724:             'rows' => array(array(
2725:                     'cols' => array(array(
2726:                             'items' => array(),
2727:                         )),
2728:                 )),
2729:         )));
2730: 
2731:         foreach ($fields as $field) {
2732:             if ($field->readOnly)
2733:                 continue;
2734: 
2735:             // hide associationType, it will be set in the dialog by associationName
2736:             if ($field->fieldName == 'associationType')
2737:                 continue;
2738: 
2739:             $newField = array(
2740:                 'name' => 'formItem_' . $field->fieldName,
2741:                 'labelType' => 'left',
2742:                 'readOnly' => $field->readOnly,
2743:                 'height' => 30,
2744:                 'width' => 155,
2745:             );
2746:             $layout['sections'][0]['rows'][0]['cols'][0]['items'][] = $newField;
2747:         }
2748: 
2749:         return json_encode($layout);
2750:     }
2751: 
2752:     /**
2753:      * Should be used before inserting user-generated input into SQL string in cases
2754:      * where parameter binding cannot be used (e.g. for SQL object names). 
2755:      * @param array|string $attribute Name of attribute(s)
2756:      * @throws CException If attribute does not exist
2757:      */
2758:     public static function checkThrowAttrError ($attribute) {
2759:         if (is_array ($attribute)) {
2760:             foreach ($attribute as $name) {
2761:                 self::checkThrowAttrError ($name);
2762:             }
2763:             return;
2764:         }
2765: 
2766:         // prevent SQL injection by validating attribute name
2767:         if (!self::model(get_called_class())->hasAttribute($attribute)) {
2768:             throw new CException(
2769:             Yii::t('app', '{attribute} is not an {modelClass} field.', array(
2770:                 '{attribute}' => $attribute,
2771:                 '{modelClass}' => get_called_class())));
2772:         }
2773:     }
2774: 
2775:     /**
2776:      * Retrieves model of a specified type with a specified id 
2777:      * @param bool $isAssocType If true, $type will be treated as an association type. Otherwise,
2778:      *  $type will be treated as a model class name.
2779:      * @return mixed object or null
2780:      */
2781:     public static function getModelOfTypeWithId($type, $id, $isAssocType = false) {
2782:         if ($isAssocType) {
2783:             if (!(empty($type) || empty($id)) &&
2784:                     X2Model::getModelName($type)) { // both ID and type must be set
2785:                 return X2Model::model(X2Model::getModelName($type))->findByPk($id);
2786:             }
2787:         } else {
2788:             if (!(empty($type) || empty($id)) &&
2789:                 is_subclass_of($type, 'CActiveRecord')) { // both ID and type must be set
2790:                 return X2Model::model($type)->findByPk($id);
2791:             }
2792:         }
2793:         return null; // invalid type or invalid id 
2794:     }
2795: 
2796:     public static function getModelOfTypeWithName($type, $name) {
2797:         $modelName = X2Model::getModelName($type); // both ID and type must be set
2798:         if (!(empty($type) || empty($name)) && $modelName) { // both ID and type must be set
2799:             $model = X2Model::model($modelName);
2800:             if ($model->hasAttribute('name')) {
2801:                 return $model->findByAttributes(array(
2802:                             'name' => $name
2803:                 ));
2804:             }
2805:         }
2806:         return null; // invalid type or invalid name 
2807:     }
2808: 
2809:     /**
2810:      * @param string $modelClass the model class for which the autocomplete should be rendered
2811:      * @param bool $ajax if true, registered scripts are processed with ajaxRender
2812:      */
2813:     public static function renderModelAutocomplete(
2814:         $modelClass, $ajax = false, $htmlOptions = array(), $value=null) {
2815: 
2816:         $modelClass = self::getModelName ($modelClass);
2817: 
2818:         if (!class_exists($modelClass) || !$modelClass::model()->asa('X2LinkableBehavior')) {
2819:             if ($ajax) {
2820:                 echo 'failure';
2821:                 return;
2822:             } else {
2823:                 return 'failure';
2824:             }
2825:             /* throw new CException (
2826:               Yii::t('app',
2827:               'Error: renderModelAutocomplete: {modelClass} does not have '.
2828:               'X2LinkableBehavior', array ('{modelClass}' => $modelClass))); */
2829:         }
2830: 
2831:         if ($ajax)
2832:             Yii::app()->clientScript->scriptMap['*.css'] = false;
2833: 
2834:         $renderWidget = function () use ($modelClass, $htmlOptions, $value) {
2835:             Yii::app()->controller->widget('zii.widgets.jui.CJuiAutoComplete', array(
2836:                 'name' => (isset($htmlOptions['name']) ? $htmlOptions['name'] : 'recordName'),
2837:                 'source' => Yii::app()->controller->createUrl(
2838:                         X2Model::model($modelClass)->autoCompleteSource),
2839:                 'value' => $value ? $value : Yii::t('app', 'Start typing to suggest...'),
2840:                 'options' => array(
2841:                     'minLength' => '1',
2842:                     'create' =>
2843:                     'js:function (event, ui) {
2844:                         // check for callback in parent form
2845:                         if ($(this).closest ("form").data ("afterAutocompleteCreated")) {
2846:                             $(this).closest ("form").data ("afterAutocompleteCreated") ();
2847:                         }
2848:                     }',
2849:                     'select' =>
2850:                     'js:function (event, ui) {
2851:                         $(this).val(ui.item.value);
2852:                         // expects next input to be a hidden input which will contain the
2853:                         // record id
2854:                         $(this).nextAll ("input").val(ui.item.id);
2855:                         return false;
2856:                     }'
2857:                 ),
2858:                 'htmlOptions' => array_merge(array(
2859:                     'class' => 'record-name-autocomplete x2-default-field',
2860:                     'data-default-text' => Yii::t('app', 'Start typing to suggest...'),
2861:                     'style' => $value ? '' : 'color:#aaa',
2862:                 ), $htmlOptions),
2863:             ));
2864:             Yii::app()->clientScript->registerScript('renderModelAutocomplete', "
2865:                 x2.forms.enableDefaultText ($('.record-name-autocomplete'));
2866:             ", CClientScript::POS_READY);
2867:         };
2868: 
2869:         if ($ajax) {
2870:             X2Widget::ajaxRender($renderWidget);
2871:         } else {
2872:             $renderWidget();
2873:         }
2874:     }
2875: 
2876:     /**
2877:      * Returns list of insertable attribute tokens which can be used for this model 
2878:      * @param int $_depth (private) 
2879:      * @return array of strings
2880:      */
2881:     private static $getInsertableAttributeTokensDepth = 0; // limits recursive depth
2882:     public function getInsertableAttributeTokens() {
2883:         X2Model::$getInsertableAttributeTokensDepth++;
2884:         $tokens = array();
2885:         if (X2Model::$getInsertableAttributeTokensDepth > 2)
2886:             return $tokens;
2887: 
2888:         // simple tokens
2889:         $tokens = array_merge(
2890:                 $tokens, array_map(function ($elem) {
2891:                     return '{' . $elem . '}';
2892:                 }, $this->attributeNames()));
2893: 
2894:         // assignment tokens
2895:         if (X2Model::$getInsertableAttributeTokensDepth < 2) {
2896:             $assignmentFields = array_filter(
2897:                     $this->fields, function ($elem) {
2898:                 return $elem->type === 'assignment';
2899:             });
2900:             foreach ($assignmentFields as $field) {
2901:                 $assignmentModel = X2Model::model('Profile');
2902:                 $tokens = array_merge(
2903:                         $tokens, array_map(function ($elem) use ($field) {
2904:                             return '{' . $field->fieldName . '.' . $elem . '}';
2905:                         }, $assignmentModel->attributeNames())
2906:                 );
2907:             }
2908:         }
2909: 
2910:         // link tokens
2911:         if (X2Model::$getInsertableAttributeTokensDepth < 2) {
2912:             $linkFields = array_filter($this->fields, function ($elem) {
2913:                 return $elem->type === 'link';
2914:             });
2915:             foreach ($linkFields as $field) {
2916:                 $linkModelName = $field->linkType;
2917:                 $linkModel = $linkModelName::model();
2918:                 $tokens = array_merge(
2919:                     $tokens, array_map(function ($elem) use ($field) {
2920:                         return '{' . $field->fieldName . '.' . 
2921:                             preg_replace('/\{|\}/', '', $elem) . '}';
2922:                     }, $linkModel->getInsertableAttributeTokens())
2923:                 );
2924:             }
2925:         }
2926: 
2927:         X2Model::$getInsertableAttributeTokensDepth--;
2928:         return $tokens;
2929:     }
2930: 
2931:     /**
2932:      * Finds all active records satisfying the specified condition.
2933:      * See {@link find()} for detailed explanation about $condition and $params.
2934:      * @param mixed $condition query condition or criteria.
2935:      * @param array $params parameters to be bound to an SQL statement.
2936:      * @param bool $getCommand If true, command is returned instead of populating records
2937:      * @return CActiveRecord[] list of active records satisfying the specified condition. An 
2938:      *  empty array is returned if none is found.
2939:      *
2940:      * This method is Copyright (c) 2008-2014 by Yii Software LLC
2941:      * http://www.yiiframework.com/license/
2942:      */
2943:     public function findAll(
2944:         $condition = '', $params = array()/* x2modstart */, $getCommand = false/* x2modend */) {
2945: 
2946:         Yii::trace(get_class($this) . '.findAll()', 'system.db.ar.CActiveRecord');
2947:         $criteria = $this->getCommandBuilder()->createCriteria($condition, $params);
2948:         return $this->query($criteria, true/* x2modstart */, $getCommand/* x2modend */);
2949:     }
2950: 
2951:     public function duplicateFields() {
2952:         return array(
2953:             'name',
2954:         );
2955:     }
2956: 
2957:     /**
2958:      * Performs the actual DB query and populates the AR objects with the query result.
2959:      * This method is mainly internally used by other AR query methods.
2960:      * @param CDbCriteria $criteria the query criteria
2961:      * @param boolean $all whether to return all data
2962:      * @param bool $getCommand If true, command is returned instead of populating records
2963:      * @return mixed the AR objects populated with the query result
2964:      * @since 1.1.7
2965:      *
2966:      * This method is Copyright (c) 2008-2014 by Yii Software LLC
2967:      * http://www.yiiframework.com/license/
2968:      */
2969:     protected function query(
2970:         $criteria, $all = false/* x2modstart */, $getCommand = false/* x2modend */) {
2971: 
2972:         $this->beforeFind();
2973:         $this->applyScopes($criteria);
2974: 
2975:         if (empty($criteria->with)) {
2976:             if (!$all)
2977:                 $criteria->limit = 1;
2978:             $command = $this->getCommandBuilder()
2979:                     ->createFindCommand($this->getTableSchema(), $criteria, $this->getTableAlias());
2980:             /* x2modstart */
2981:             if ($getCommand)
2982:                 return $command;
2983:             /* x2modend */
2984:             return $all ? $this->populateRecords($command->queryAll(), true, $criteria->index) :
2985:                     $this->populateRecord($command->queryRow());
2986:         }
2987:         else {
2988:             /* x2modstart */
2989:             if ($getCommand) {
2990:                 return null;
2991:             }
2992:             /* x2modend */ 
2993:             $finder=$this->getActiveFinder($criteria->with);
2994:             return $finder->query($criteria,$all);
2995:         }
2996:     }
2997: 
2998:     /**
2999:      * Helper method for {@link getFieldsForDropdown}
3000:      * @param string|null $parentAttribute Can be used to prefix names of attributes
3001:      * @param bool $condList If true, returned array's values will include, in addition to the
3002:      *  field label, field data required by the X2ConditionList widget.
3003:      * @param bool $sorted if true, results will be sorted by field name 
3004:      * @param function|null $filterFn if set, will be used to filter results
3005:      * @param string $separator used to separate parent attribute from field name 
3006:      * @return array 
3007:      */
3008:     private function _getFieldsForDropdown (
3009:         $parentAttribute=null, $condList=false, $sorted=true, $filterFn, $separator='.') {
3010: 
3011:         $fieldModels = $this->getFields(false, $filterFn, Fields::READ_PERMISSION);
3012:         $permissions = $this->getFieldPermissions ();
3013:         $fields = array();
3014: 
3015:         foreach($fieldModels as &$field) {
3016:             if($field->isVirtual)
3017:                 continue;
3018: 
3019:             $fieldName = $field->fieldName;
3020:             if ($this instanceof Actions && $fieldName === 'actionDescription') {
3021:                 $fieldName = 'ActionText.text';
3022:             }
3023:             $attributes = $field->getAttributes ();
3024:             if ($parentAttribute !== null) {
3025:                 $fieldName = $parentAttribute.$separator.$fieldName;
3026:             }
3027:             if ($field->type === 'date' || $field->type === 'dateTime') {
3028:                 $dateFns = array ('year', 'month', 'day');
3029:                 if ($field->type === 'dateTime') {
3030:                     $dateFns = array_merge ($dateFns, array ('hour', 'minute', 'second'));
3031:                 }
3032:                 foreach ($dateFns as $fn) {
3033:                     $name = $fn.'('.$fieldName.')';
3034:                     $label = $this->getAttributeLabel($fieldName).' (' . Yii::t('app', $fn) . ')';
3035:                     if ($condList) {
3036:                         $fields[] = X2ConditionList::listOption (
3037:                             array_merge ($attributes, array (
3038:                                 'attributeLabel' => $label,
3039:                                 'type' => 'varchar',
3040:                            )), $name);
3041:                     } else {
3042:                         $fields[$name] = $label;
3043:                     }
3044:                 }
3045:             } 
3046:             if ($condList) {
3047:                 $fields[] = X2ConditionList::listOption ($attributes, $fieldName);
3048:             } else {
3049:                 $fields[$fieldName] = $this->getAttributeLabel($fieldName);
3050:             }
3051:         }
3052:         if ($sorted) {
3053:             if ($condList) {
3054:                 usort ($fields, function ($a, $b) {
3055:                     return strcasecmp ($a['label'], $b['label']);
3056:                 });
3057:             } else {
3058:                 $fields = ArrayUtil::asorti ($fields);
3059:             }
3060:         }
3061:         return $fields;
3062:     }
3063: 
3064:     public function getSummaryFields () {
3065:         $summaryFields = array ();
3066:         if (isset ($this->name)) {
3067:             $summaryFields[] = 'name';
3068:         }
3069:         if (isset ($this->email)) {
3070:             $summaryFields[] = 'email';
3071:         }
3072:         if (isset ($this->phone)) {
3073:             $summaryFields[] = 'phone';
3074:         }
3075:         if ($this->getField ('assignedTo')) {
3076:             $summaryFields[] = 'assignedTo';
3077:         }
3078:         return $summaryFields;
3079:     }
3080: 
3081: }
3082: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0