diff --git a/CRM/Admin/Form/AdhocChargesItem.php b/CRM/Admin/Form/AdhocChargesItem.php index 6bf7122e..34dcfe3e 100644 --- a/CRM/Admin/Form/AdhocChargesItem.php +++ b/CRM/Admin/Form/AdhocChargesItem.php @@ -38,7 +38,6 @@ * */ class CRM_Admin_Form_AdhocChargesItem extends CRM_Admin_Form { - protected $_id = NULL; function preProcess() { parent::preProcess(); diff --git a/CRM/Admin/Form/Resource.php b/CRM/Admin/Form/Resource.php index 5e4d5277..1d558b2c 100755 --- a/CRM/Admin/Form/Resource.php +++ b/CRM/Admin/Form/Resource.php @@ -38,7 +38,6 @@ * */ class CRM_Admin_Form_Resource extends CRM_Admin_Form { - protected $_id = NULL; function preProcess() { parent::preProcess(); diff --git a/CRM/Admin/Form/ResourceConfigOption.php b/CRM/Admin/Form/ResourceConfigOption.php index 6c1ce72c..fa6e4f28 100644 --- a/CRM/Admin/Form/ResourceConfigOption.php +++ b/CRM/Admin/Form/ResourceConfigOption.php @@ -38,7 +38,6 @@ * */ class CRM_Admin_Form_ResourceConfigOption extends CRM_Admin_Form { - protected $_id = NULL; protected $_sid = NULL; function preProcess() { diff --git a/CRM/Admin/Form/ResourceConfigSet.php b/CRM/Admin/Form/ResourceConfigSet.php index faa6d5c0..311ca8d4 100644 --- a/CRM/Admin/Form/ResourceConfigSet.php +++ b/CRM/Admin/Form/ResourceConfigSet.php @@ -38,7 +38,6 @@ * */ class CRM_Admin_Form_ResourceConfigSet extends CRM_Admin_Form { - protected $_id = NULL; function preProcess() { parent::preProcess(); diff --git a/CRM/Booking/DAO/AdhocCharges.php b/CRM/Booking/DAO/AdhocCharges.php index 3652bdb9..5acf9756 100644 --- a/CRM/Booking/DAO/AdhocCharges.php +++ b/CRM/Booking/DAO/AdhocCharges.php @@ -1,299 +1,278 @@ __table = 'civicrm_booking_adhoc_charges'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @param bool $plural + * Whether to return the plural version of the title. */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'booking_id', 'civicrm_booking', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'item_id', 'civicrm_booking_adhoc_charges_item', 'id') , - ); + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Adhoc Chargeses') : E::ts('Adhoc Charges'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'booking_id', 'civicrm_booking', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'item_id', 'civicrm_booking_adhoc_charges_item', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'booking_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_adhoc_charges.id', + 'table_name' => 'civicrm_booking_adhoc_charges', + 'entity' => 'AdhocCharges', + 'bao' => 'CRM_Booking_DAO_AdhocCharges', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'booking_id' => [ 'name' => 'booking_id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, + 'description' => E::ts('FK to Booking ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_adhoc_charges.booking_id', + 'table_name' => 'civicrm_booking_adhoc_charges', + 'entity' => 'AdhocCharges', + 'bao' => 'CRM_Booking_DAO_AdhocCharges', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Booking', - ) , - 'item_id' => array( + 'add' => NULL, + ], + 'item_id' => [ 'name' => 'item_id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, + 'description' => E::ts('FK to Item ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_adhoc_charges.item_id', + 'table_name' => 'civicrm_booking_adhoc_charges', + 'entity' => 'AdhocCharges', + 'bao' => 'CRM_Booking_DAO_AdhocCharges', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_AdhocChargesItem', - ) , - 'quantity' => array( + 'add' => NULL, + ], + 'quantity' => [ 'name' => 'quantity', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Quantity') , - 'required' => true, - ) , - 'is_cancelled' => array( + 'title' => E::ts('Quantity'), + 'required' => TRUE, + 'where' => 'civicrm_booking_adhoc_charges.quantity', + 'table_name' => 'civicrm_booking_adhoc_charges', + 'entity' => 'AdhocCharges', + 'bao' => 'CRM_Booking_DAO_AdhocCharges', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_cancelled' => [ 'name' => 'is_cancelled', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is cancelled') , - 'import' => true, + 'title' => E::ts('Slot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_adhoc_charges.is_cancelled', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - 'is_deleted' => array( + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_adhoc_charges', + 'entity' => 'AdhocCharges', + 'bao' => 'CRM_Booking_DAO_AdhocCharges', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is in the Trash') , - 'import' => true, + 'title' => E::ts('Slot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_adhoc_charges.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_adhoc_charges', + 'entity' => 'AdhocCharges', + 'bao' => 'CRM_Booking_DAO_AdhocCharges', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'booking_id' => 'booking_id', - 'item_id' => 'item_id', - 'quantity' => 'quantity', - 'is_cancelled' => 'is_cancelled', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_adhoc_charges', $prefix, []); + return $r; + } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_adhoc_charges'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_adhoc_charges', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of indices + * + * @param bool $localize * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_adhoc_charges'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_cancelled' => [ + 'name' => 'index_is_cancelled', + 'field' => [ + 0 => 'is_cancelled', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_adhoc_charges::0::is_cancelled', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_adhoc_charges::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/AdhocChargesItem.php b/CRM/Booking/DAO/AdhocChargesItem.php index 7f7873c3..4e751939 100644 --- a/CRM/Booking/DAO/AdhocChargesItem.php +++ b/CRM/Booking/DAO/AdhocChargesItem.php @@ -1,298 +1,282 @@ __table = 'civicrm_booking_adhoc_charges_item'; parent::__construct(); } + /** - * returns all the column names of this table + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Adhoc Charges Items') : E::ts('Adhoc Charges Item'); + } + + /** + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('ID') , - 'required' => true, - ) , - 'name' => array( + 'title' => E::ts('ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_adhoc_charges_item.id', + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'name' => [ 'name' => 'name', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Name') , - 'required' => true, + 'title' => E::ts('Name'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'label' => array( + 'where' => 'civicrm_booking_adhoc_charges_item.name', + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'add' => NULL, + ], + 'label' => [ 'name' => 'label', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Label') , - 'required' => true, + 'title' => E::ts('Label'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'price' => array( + 'where' => 'civicrm_booking_adhoc_charges_item.label', + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'add' => NULL, + ], + 'price' => [ 'name' => 'price', 'type' => CRM_Utils_Type::T_MONEY, - 'title' => E::ts('Price') , - 'required' => true, - ) , - 'weight' => array( + 'title' => E::ts('Price'), + 'required' => TRUE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_booking_adhoc_charges_item.price', + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'add' => NULL, + ], + 'weight' => [ 'name' => 'weight', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Weight') , - 'required' => true, - ) , - 'is_active' => array( + 'title' => E::ts('Weight'), + 'required' => TRUE, + 'where' => 'civicrm_booking_adhoc_charges_item.weight', + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_active' => [ 'name' => 'is_active', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is cancelled') , - 'import' => true, + 'title' => E::ts('Slot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_adhoc_charges_item.is_active', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, + 'export' => TRUE, 'default' => '1', - ) , - 'is_deleted' => array( + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is in the Trash') , - 'import' => true, + 'title' => E::ts('Slot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_adhoc_charges_item.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_adhoc_charges_item', + 'entity' => 'AdhocChargesItem', + 'bao' => 'CRM_Booking_DAO_AdhocChargesItem', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'name' => 'name', - 'label' => 'label', - 'price' => 'price', - 'weight' => 'weight', - 'is_active' => 'is_active', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_adhoc_charges_item'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_adhoc_charges_item', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_adhoc_charges_item'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_adhoc_charges_item', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_active' => [ + 'name' => 'index_is_active', + 'field' => [ + 0 => 'is_active', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_adhoc_charges_item::0::is_active', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_adhoc_charges_item::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/Booking.php b/CRM/Booking/DAO/Booking.php index 28aa1408..fc94341a 100644 --- a/CRM/Booking/DAO/Booking.php +++ b/CRM/Booking/DAO/Booking.php @@ -1,525 +1,563 @@ __table = 'civicrm_booking'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference - */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'primary_contact_id', 'civicrm_contact', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'secondary_contact_id', 'civicrm_contact', 'id') , - ); + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Bookings') : E::ts('Booking'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'primary_contact_id', 'civicrm_contact', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'secondary_contact_id', 'civicrm_contact', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'primary_contact_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking.id', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'primary_contact_id' => [ 'name' => 'primary_contact_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Primary Contact ID') , - 'required' => true, - 'import' => true, + 'title' => E::ts('Primary Contact ID'), + 'description' => E::ts('FK to Contact ID'), + 'required' => TRUE, + 'import' => TRUE, 'where' => 'civicrm_booking.primary_contact_id', 'headerPattern' => '/contact(.?id)?/i', 'dataPattern' => '/^\d+$/', - 'export' => true, + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, 'FKClassName' => 'CRM_Contact_DAO_Contact', - ) , - 'secondary_contact_id' => array( + 'add' => '4.4', + ], + 'secondary_contact_id' => [ 'name' => 'secondary_contact_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Secondary Contact ID') , - 'required' => false, - 'import' => true, + 'title' => E::ts('Secondary Contact ID'), + 'description' => E::ts('FK to Contact ID'), + 'required' => FALSE, + 'import' => TRUE, 'where' => 'civicrm_booking.secondary_contact_id', 'headerPattern' => '/contact(.?id)?/i', 'dataPattern' => '/^\d+$/', - 'export' => true, + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, 'FKClassName' => 'CRM_Contact_DAO_Contact', - ) , - 'booking_title' => array( + 'add' => '4.4', + ], + 'booking_title' => [ 'name' => 'title', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Title') , - 'required' => true, + 'title' => E::ts('Title'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - 'export' => true, 'where' => 'civicrm_booking.title', - 'headerPattern' => '', - 'dataPattern' => '', - ) , - 'booking_status_id' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'booking_status_id' => [ 'name' => 'status_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Status ID') , - 'required' => true, - 'export' => true, + 'title' => E::ts('Status ID'), + 'description' => E::ts('The status associated with this booking. Implicit FK to option_value row in booking status option_group.'), + 'required' => TRUE, 'where' => 'civicrm_booking.status_id', - 'headerPattern' => '', - 'dataPattern' => '', - 'pseudoconstant' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'pseudoconstant' => [ 'optionGroupName' => 'booking_status', - ) - ) , - 'booking_date' => array( + 'optionEditPath' => 'civicrm/admin/options/booking_status', + ], + 'add' => '4.4', + ], + 'booking_date' => [ 'name' => 'booking_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Booking Date') , - 'required' => true, - 'export' => true, + 'title' => E::ts('Booking Date'), + 'required' => TRUE, 'where' => 'civicrm_booking.booking_date', - 'headerPattern' => '', - 'dataPattern' => '', - ) , - 'booking_start_date' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'booking_start_date' => [ 'name' => 'start_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Start Date') , - 'required' => true, - 'export' => true, + 'title' => E::ts('Start Date'), + 'required' => TRUE, 'where' => 'civicrm_booking.start_date', - 'headerPattern' => '', - 'dataPattern' => '', - ) , - 'booking_end_date' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'booking_end_date' => [ 'name' => 'end_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('End Date') , - 'required' => true, - 'export' => true, + 'title' => E::ts('End Date'), + 'required' => TRUE, 'where' => 'civicrm_booking.end_date', - 'headerPattern' => '', - 'dataPattern' => '', - ) , - 'booking_po_number' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'booking_po_number' => [ 'name' => 'po_number', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('PO Number') , - 'required' => true, + 'title' => E::ts('PO Number'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - 'export' => true, 'where' => 'civicrm_booking.po_number', - 'headerPattern' => '', - 'dataPattern' => '', - ) , - 'booking_total_amount' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'total_amount' => [ 'name' => 'total_amount', 'type' => CRM_Utils_Type::T_MONEY, - 'title' => E::ts('Total amount') , - 'required' => true, - 'import' => true, + 'title' => E::ts('Total amount'), + 'description' => E::ts('Total amount of this booking calculated from slots,sub slots, ad-hoc charges and discount amount'), + 'required' => TRUE, + 'precision' => [ + 20, + 2, + ], + 'import' => TRUE, 'where' => 'civicrm_booking.total_amount', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - 'description' => array( + 'export' => TRUE, + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'description' => [ 'name' => 'description', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Description') , + 'title' => E::ts('Description'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'note' => array( + 'where' => 'civicrm_booking.description', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'note' => [ 'name' => 'note', 'type' => CRM_Utils_Type::T_TEXT, - 'title' => E::ts('Note') , - ) , - 'adhoc_charges_note' => array( + 'title' => E::ts('Note'), + 'where' => 'civicrm_booking.note', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'adhoc_charges_note' => [ 'name' => 'adhoc_charges_note', 'type' => CRM_Utils_Type::T_TEXT, - 'title' => E::ts('Adhoc Charges Note') , - ) , - 'participants_estimate' => array( + 'title' => E::ts('Adhoc Charges Note'), + 'where' => 'civicrm_booking.adhoc_charges_note', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'participants_estimate' => [ 'name' => 'participants_estimate', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Participants Estimate') , + 'title' => E::ts('Participants Estimate'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'participants_actual' => array( + 'where' => 'civicrm_booking.participants_estimate', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'participants_actual' => [ 'name' => 'participants_actual', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Participants Actual') , + 'title' => E::ts('Participants Actual'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'discount_amount' => array( + 'where' => 'civicrm_booking.participants_actual', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'discount_amount' => [ 'name' => 'discount_amount', 'type' => CRM_Utils_Type::T_MONEY, - 'title' => E::ts('Discount Amount') , - ) , - 'booking_is_deleted' => array( + 'title' => E::ts('Discount Amount'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_booking.discount_amount', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'booking_is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Booking is in the Trash') , - 'import' => true, + 'title' => E::ts('Booking is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - 'created_by' => array( + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => '4.4', + ], + 'created_by' => [ 'name' => 'created_by', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Created By') , - 'required' => true, - ) , - 'created_date' => array( + 'title' => E::ts('Created By'), + 'required' => TRUE, + 'where' => 'civicrm_booking.created_by', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'created_date' => [ 'name' => 'created_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Created Date') , - 'required' => true, - ) , - 'updated_by' => array( + 'title' => E::ts('Created Date'), + 'required' => TRUE, + 'where' => 'civicrm_booking.created_date', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'updated_by' => [ 'name' => 'updated_by', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Updated By') , - 'required' => true, - ) , - 'updated_date' => array( + 'title' => E::ts('Updated By'), + 'required' => TRUE, + 'where' => 'civicrm_booking.updated_by', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + 'updated_date' => [ 'name' => 'updated_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Updated Date') , - 'required' => true, - ) , - ); + 'title' => E::ts('Updated Date'), + 'required' => TRUE, + 'where' => 'civicrm_booking.updated_date', + 'table_name' => 'civicrm_booking', + 'entity' => 'Booking', + 'bao' => 'CRM_Booking_DAO_Booking', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'primary_contact_id' => 'primary_contact_id', - 'secondary_contact_id' => 'secondary_contact_id', - 'title' => 'booking_title', - 'status_id' => 'booking_status_id', - 'booking_date' => 'booking_date', - 'start_date' => 'booking_start_date', - 'end_date' => 'booking_end_date', - 'po_number' => 'booking_po_number', - 'total_amount' => 'total_amount', - 'description' => 'description', - 'note' => 'note', - 'adhoc_charges_note' => 'adhoc_charges_note', - 'participants_estimate' => 'participants_estimate', - 'participants_actual' => 'participants_actual', - 'discount_amount' => 'discount_amount', - 'is_deleted' => 'booking_is_deleted', - 'created_by' => 'created_by', - 'created_date' => 'created_date', - 'updated_by' => 'updated_by', - 'updated_date' => 'updated_date', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported * - * @access public - * return array - * @static - */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported * - * @access public - * return array - * @static - */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/BookingConfig.php b/CRM/Booking/DAO/BookingConfig.php index bb3e1e94..5a0e095d 100644 --- a/CRM/Booking/DAO/BookingConfig.php +++ b/CRM/Booking/DAO/BookingConfig.php @@ -1,375 +1,384 @@ __table = 'civicrm_booking_config'; parent::__construct(); } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Booking Configs') : E::ts('Booking Config'); + } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'domain_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_config.id', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'domain_id' => [ 'name' => 'domain_id', 'type' => CRM_Utils_Type::T_INT, - ) , - 'day_start_at' => array( + 'where' => 'civicrm_booking_config.domain_id', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'day_start_at' => [ 'name' => 'day_start_at', 'type' => CRM_Utils_Type::T_TIME, - 'title' => E::ts('Day Start At') , - 'required' => true, - ) , - 'day_end_at' => array( + 'title' => E::ts('Day Start At'), + 'required' => TRUE, + 'where' => 'civicrm_booking_config.day_start_at', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'day_end_at' => [ 'name' => 'day_end_at', 'type' => CRM_Utils_Type::T_TIME, - 'title' => E::ts('Day End At') , - 'required' => true, - ) , - 'time_period' => array( + 'title' => E::ts('Day End At'), + 'required' => TRUE, + 'where' => 'civicrm_booking_config.day_end_at', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'time_period' => [ 'name' => 'time_period', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Time Period') , - 'required' => true, - ) , - 'log_confirmation_email' => array( + 'title' => E::ts('Time Period'), + 'required' => TRUE, + 'where' => 'civicrm_booking_config.time_period', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'log_confirmation_email' => [ 'name' => 'log_confirmation_email', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Log Confirmation Email') , - 'required' => true, - ) , - 'unlimited_resource_time_config' => array( + 'title' => E::ts('Log Confirmation Email'), + 'description' => E::ts('Create an activity record againt contact for conformation emails'), + 'required' => TRUE, + 'where' => 'civicrm_booking_config.log_confirmation_email', + 'default' => '0', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'unlimited_resource_time_config' => [ 'name' => 'unlimited_resource_time_config', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Unlimited Resource Time Config') , - 'required' => true, - ) , - 'cc_email_address' => array( + 'title' => E::ts('Unlimited Resource Time Config'), + 'description' => E::ts('Only allow unlimited resources to be booked within time span of the parent limited resource booking'), + 'required' => TRUE, + 'where' => 'civicrm_booking_config.unlimited_resource_time_config', + 'default' => '1', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'cc_email_address' => [ 'name' => 'cc_email_address', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Cc Email Address') , + 'title' => E::ts('Cc Email Address'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'bcc_email_address' => array( + 'where' => 'civicrm_booking_config.cc_email_address', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'bcc_email_address' => [ 'name' => 'bcc_email_address', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Bcc Email Address') , + 'title' => E::ts('Bcc Email Address'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'slot_new_colour' => array( + 'where' => 'civicrm_booking_config.bcc_email_address', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'slot_new_colour' => [ 'name' => 'slot_new_colour', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Slot New Colour') , + 'title' => E::ts('Slot New Colour'), 'maxlength' => 10, 'size' => CRM_Utils_Type::TWELVE, - ) , - 'slot_being_edited_colour' => array( + 'where' => 'civicrm_booking_config.slot_new_colour', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'slot_being_edited_colour' => [ 'name' => 'slot_being_edited_colour', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Slot Being Edited Colour') , + 'title' => E::ts('Slot Being Edited Colour'), 'maxlength' => 10, 'size' => CRM_Utils_Type::TWELVE, - ) , - 'slot_booked_colour' => array( + 'where' => 'civicrm_booking_config.slot_being_edited_colour', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'slot_booked_colour' => [ 'name' => 'slot_booked_colour', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Slot Booked Colour') , + 'title' => E::ts('Slot Booked Colour'), 'maxlength' => 10, 'size' => CRM_Utils_Type::TWELVE, - ) , - 'slot_provisional_colour' => array( + 'where' => 'civicrm_booking_config.slot_booked_colour', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'slot_provisional_colour' => [ 'name' => 'slot_provisional_colour', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Slot Provisional Colour') , + 'title' => E::ts('Slot Provisional Colour'), 'maxlength' => 10, 'size' => CRM_Utils_Type::TWELVE, - ) , - 'slot_unavailable_colour' => array( + 'where' => 'civicrm_booking_config.slot_provisional_colour', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + 'slot_unavailable_colour' => [ 'name' => 'slot_unavailable_colour', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Slot Unavailable Colour') , + 'title' => E::ts('Slot Unavailable Colour'), 'maxlength' => 10, 'size' => CRM_Utils_Type::TWELVE, - ) , - ); + 'where' => 'civicrm_booking_config.slot_unavailable_colour', + 'table_name' => 'civicrm_booking_config', + 'entity' => 'BookingConfig', + 'bao' => 'CRM_Booking_DAO_BookingConfig', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'domain_id' => 'domain_id', - 'day_start_at' => 'day_start_at', - 'day_end_at' => 'day_end_at', - 'time_period' => 'time_period', - 'log_confirmation_email' => 'log_confirmation_email', - 'unlimited_resource_time_config' => 'unlimited_resource_time_config', - 'cc_email_address' => 'cc_email_address', - 'bcc_email_address' => 'bcc_email_address', - 'slot_new_colour' => 'slot_new_colour', - 'slot_being_edited_colour' => 'slot_being_edited_colour', - 'slot_booked_colour' => 'slot_booked_colour', - 'slot_provisional_colour' => 'slot_provisional_colour', - 'slot_unavailable_colour' => 'slot_unavailable_colour', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_config'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_config', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_config'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_config', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/Cancellation.php b/CRM/Booking/DAO/Cancellation.php index ca6c2e34..c3ca0dce 100644 --- a/CRM/Booking/DAO/Cancellation.php +++ b/CRM/Booking/DAO/Cancellation.php @@ -1,286 +1,258 @@ __table = 'civicrm_booking_cancellation'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @param bool $plural + * Whether to return the plural version of the title. */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'booking_id', 'civicrm_booking', 'id') , - ); + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Cancellations') : E::ts('Cancellation'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'booking_id', 'civicrm_booking', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'booking_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_cancellation.id', + 'table_name' => 'civicrm_booking_cancellation', + 'entity' => 'Cancellation', + 'bao' => 'CRM_Booking_DAO_Cancellation', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'booking_id' => [ 'name' => 'booking_id', 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Booking'), + 'where' => 'civicrm_booking_cancellation.booking_id', + 'table_name' => 'civicrm_booking_cancellation', + 'entity' => 'Cancellation', + 'bao' => 'CRM_Booking_DAO_Cancellation', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Booking', - ) , - 'cancellation_date' => array( + 'add' => NULL, + ], + 'cancellation_date' => [ 'name' => 'cancellation_date', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Cancellation Date') , - 'required' => true, - ) , - 'cancellation_fee' => array( + 'title' => E::ts('Cancellation Date'), + 'required' => TRUE, + 'where' => 'civicrm_booking_cancellation.cancellation_date', + 'table_name' => 'civicrm_booking_cancellation', + 'entity' => 'Cancellation', + 'bao' => 'CRM_Booking_DAO_Cancellation', + 'localizable' => 0, + 'add' => NULL, + ], + 'cancellation_fee' => [ 'name' => 'cancellation_fee', 'type' => CRM_Utils_Type::T_MONEY, - 'title' => E::ts('Cancellation charge') , - 'required' => true, - ) , - 'additional_fee' => array( + 'title' => E::ts('Cancellation charge'), + 'required' => TRUE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_booking_cancellation.cancellation_fee', + 'table_name' => 'civicrm_booking_cancellation', + 'entity' => 'Cancellation', + 'bao' => 'CRM_Booking_DAO_Cancellation', + 'localizable' => 0, + 'add' => NULL, + ], + 'additional_fee' => [ 'name' => 'additional_fee', 'type' => CRM_Utils_Type::T_MONEY, - 'title' => E::ts('Additional charge') , - ) , - 'comment' => array( + 'title' => E::ts('Additional charge'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_booking_cancellation.additional_fee', + 'table_name' => 'civicrm_booking_cancellation', + 'entity' => 'Cancellation', + 'bao' => 'CRM_Booking_DAO_Cancellation', + 'localizable' => 0, + 'add' => NULL, + ], + 'comment' => [ 'name' => 'comment', 'type' => CRM_Utils_Type::T_TEXT, - 'title' => E::ts('Comment') , - ) , - ); + 'title' => E::ts('Comment'), + 'where' => 'civicrm_booking_cancellation.comment', + 'table_name' => 'civicrm_booking_cancellation', + 'entity' => 'Cancellation', + 'bao' => 'CRM_Booking_DAO_Cancellation', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'booking_id' => 'booking_id', - 'cancellation_date' => 'cancellation_date', - 'cancellation_fee' => 'cancellation_fee', - 'additional_fee' => 'additional_fee', - 'comment' => 'comment', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_cancellation'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_cancellation', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_cancellation'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_cancellation', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/Payment.php b/CRM/Booking/DAO/Payment.php index 9faa5388..607da76c 100644 --- a/CRM/Booking/DAO/Payment.php +++ b/CRM/Booking/DAO/Payment.php @@ -1,263 +1,211 @@ __table = 'civicrm_booking_payment'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @param bool $plural + * Whether to return the plural version of the title. */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'booking_id', 'civicrm_booking', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'contribution_id', 'civicrm_contribution', 'id') , - ); + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Payments') : E::ts('Payment'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'booking_id', 'civicrm_booking', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contribution_id', 'civicrm_contribution', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('ID') , - 'required' => true, - ) , - 'booking_id' => array( + 'title' => E::ts('ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_payment.id', + 'table_name' => 'civicrm_booking_payment', + 'entity' => 'Payment', + 'bao' => 'CRM_Booking_DAO_Payment', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'booking_id' => [ 'name' => 'booking_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Booking ID') , - 'required' => true, + 'title' => E::ts('Booking ID'), + 'description' => E::ts('Foreign key to the booking id for this payment.'), + 'required' => TRUE, + 'where' => 'civicrm_booking_payment.booking_id', + 'table_name' => 'civicrm_booking_payment', + 'entity' => 'Payment', + 'bao' => 'CRM_Booking_DAO_Payment', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Booking', - ) , - 'contribution_id' => array( + 'add' => '2.0', + ], + 'contribution_id' => [ 'name' => 'contribution_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Contribution ID') , - 'required' => true, - 'import' => true, + 'title' => E::ts('Contribution ID'), + 'description' => E::ts('Foreign key to the contribution for this payment.'), + 'required' => TRUE, + 'import' => TRUE, 'where' => 'civicrm_booking_payment.contribution_id', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, + 'export' => TRUE, + 'table_name' => 'civicrm_booking_payment', + 'entity' => 'Payment', + 'bao' => 'CRM_Booking_DAO_Payment', + 'localizable' => 0, 'FKClassName' => 'CRM_Contribute_DAO_Contribution', - ) , - ); + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'booking_id' => 'booking_id', - 'contribution_id' => 'contribution_id', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_payment'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_payment', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_payment'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_payment', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/Resource.php b/CRM/Booking/DAO/Resource.php index d321d79a..7aa14f33 100644 --- a/CRM/Booking/DAO/Resource.php +++ b/CRM/Booking/DAO/Resource.php @@ -1,358 +1,369 @@ __table = 'civicrm_booking_resource'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @param bool $plural + * Whether to return the plural version of the title. */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'set_id', 'civicrm_booking_resource_config_set', 'id') , - ); + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Resources') : E::ts('Resource'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'set_id', 'civicrm_booking_resource_config_set', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'set_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_resource.id', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'set_id' => [ 'name' => 'set_id', 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to resource configuration option set'), + 'where' => 'civicrm_booking_resource.set_id', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_ResourceConfigSet', - ) , - 'booking_resource_label' => array( + 'add' => NULL, + ], + 'booking_resource_label' => [ 'name' => 'label', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Label') , - 'required' => true, + 'title' => E::ts('Label'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'booking_resource_description' => array( + 'where' => 'civicrm_booking_resource.label', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'add' => NULL, + ], + 'booking_resource_description' => [ 'name' => 'description', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Label') , + 'title' => E::ts('Label'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'type_id' => array( + 'where' => 'civicrm_booking_resource.description', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'add' => NULL, + ], + 'type_id' => [ 'name' => 'type_id', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Type ID') , - 'required' => true, + 'title' => E::ts('Type ID'), + 'description' => E::ts('The type associated with this resource. Implicit FK to option_value row in booking_resource_type option_group.'), + 'required' => TRUE, 'maxlength' => 512, 'size' => CRM_Utils_Type::HUGE, - 'pseudoconstant' => array( + 'where' => 'civicrm_booking_resource.type_id', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'pseudoconstant' => [ 'optionGroupName' => 'booking_resource_type', - ) - ) , - 'location_id' => array( + 'optionEditPath' => 'civicrm/admin/options/booking_resource_type', + ], + 'add' => '4.4', + ], + 'location_id' => [ 'name' => 'location_id', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Location ID') , + 'title' => E::ts('Location ID'), + 'description' => E::ts('The location associated with this resource. Implicit FK to option_value row in booking_resource_location option_group.'), 'maxlength' => 512, 'size' => CRM_Utils_Type::HUGE, - 'pseudoconstant' => array( + 'where' => 'civicrm_booking_resource.location_id', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'pseudoconstant' => [ 'optionGroupName' => 'booking_resource_location', - ) - ) , - 'weight' => array( + 'optionEditPath' => 'civicrm/admin/options/booking_resource_location', + ], + 'add' => '4.4', + ], + 'weight' => [ 'name' => 'weight', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Weight') , - 'required' => true, - ) , - 'is_unlimited' => array( + 'title' => E::ts('Weight'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource.weight', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_unlimited' => [ 'name' => 'is_unlimited', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'required' => true, - ) , - 'is_active' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_resource.is_unlimited', + 'default' => '0', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_active' => [ 'name' => 'is_active', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is cancelled') , - 'import' => true, + 'title' => E::ts('Slot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_resource.is_active', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, + 'export' => TRUE, 'default' => '1', - ) , - 'is_deleted' => array( + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is in the Trash') , - 'import' => true, + 'title' => E::ts('Slot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_resource.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_resource', + 'entity' => 'Resource', + 'bao' => 'CRM_Booking_DAO_Resource', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'set_id' => 'set_id', - 'label' => 'booking_resource_label', - 'description' => 'booking_resource_description', - 'type_id' => 'type_id', - 'location_id' => 'location_id', - 'weight' => 'weight', - 'is_unlimited' => 'is_unlimited', - 'is_active' => 'is_active', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_resource'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_resource', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_resource'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_resource', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_unlimited' => [ + 'name' => 'index_is_unlimited', + 'field' => [ + 0 => 'is_unlimited', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource::0::is_unlimited', + ], + 'index_is_active' => [ + 'name' => 'index_is_active', + 'field' => [ + 0 => 'is_active', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource::0::is_active', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/ResourceConfigOption.php b/CRM/Booking/DAO/ResourceConfigOption.php index 305b3148..e90291af 100644 --- a/CRM/Booking/DAO/ResourceConfigOption.php +++ b/CRM/Booking/DAO/ResourceConfigOption.php @@ -1,344 +1,342 @@ __table = 'civicrm_booking_resource_config_option'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @param bool $plural + * Whether to return the plural version of the title. */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'set_id', 'civicrm_booking_resource_config_set', 'id') , - ); + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Resource Config Options') : E::ts('Resource Config Option'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'set_id', 'civicrm_booking_resource_config_set', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('ID') , - 'required' => true, - ) , - 'set_id' => array( + 'title' => E::ts('ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_config_option.id', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'set_id' => [ 'name' => 'set_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Set ID') , - 'required' => true, + 'title' => E::ts('Set ID'), + 'description' => E::ts('Foreign key to the resource set for this option.'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_config_option.set_id', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_ResourceConfigSet', - ) , - 'label' => array( + 'add' => NULL, + ], + 'label' => [ 'name' => 'label', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Label') , - 'required' => true, + 'title' => E::ts('Label'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'price' => array( + 'where' => 'civicrm_booking_resource_config_option.label', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'add' => NULL, + ], + 'price' => [ 'name' => 'price', 'type' => CRM_Utils_Type::T_MONEY, - 'title' => E::ts('Price') , - 'required' => true, - ) , - 'max_size' => array( + 'title' => E::ts('Price'), + 'required' => TRUE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_booking_resource_config_option.price', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'add' => NULL, + ], + 'max_size' => [ 'name' => 'max_size', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Max Size') , + 'title' => E::ts('Max Size'), 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'unit_id' => array( + 'where' => 'civicrm_booking_resource_config_option.max_size', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'add' => NULL, + ], + 'unit_id' => [ 'name' => 'unit_id', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Unit ID') , + 'title' => E::ts('Unit ID'), + 'description' => E::ts('The unit associated with this config option. Implicit FK to option_value row in booking_size_unit option_group.'), 'maxlength' => 512, 'size' => CRM_Utils_Type::HUGE, - 'pseudoconstant' => array( + 'where' => 'civicrm_booking_resource_config_option.unit_id', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'pseudoconstant' => [ 'optionGroupName' => 'booking_size_unit', - ) - ) , - 'weight' => array( + 'optionEditPath' => 'civicrm/admin/options/booking_size_unit', + ], + 'add' => '4.4', + ], + 'weight' => [ 'name' => 'weight', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Weight') , - 'required' => true, - ) , - 'is_active' => array( + 'title' => E::ts('Weight'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_config_option.weight', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_active' => [ 'name' => 'is_active', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is cancelled') , - 'import' => true, + 'title' => E::ts('Slot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_resource_config_option.is_active', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, + 'export' => TRUE, 'default' => '1', - ) , - 'is_deleted' => array( + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is in the Trash') , - 'import' => true, + 'title' => E::ts('Slot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_resource_config_option.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_resource_config_option', + 'entity' => 'ResourceConfigOption', + 'bao' => 'CRM_Booking_DAO_ResourceConfigOption', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'set_id' => 'set_id', - 'label' => 'label', - 'price' => 'price', - 'max_size' => 'max_size', - 'unit_id' => 'unit_id', - 'weight' => 'weight', - 'is_active' => 'is_active', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_resource_config_option', $prefix, []); + return $r; + } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_resource_config_option'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_resource_config_option', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of indices + * + * @param bool $localize * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_resource_config_option'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_active' => [ + 'name' => 'index_is_active', + 'field' => [ + 0 => 'is_active', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource_config_option::0::is_active', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource_config_option::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/ResourceConfigSet.php b/CRM/Booking/DAO/ResourceConfigSet.php index 1816e176..385b7141 100644 --- a/CRM/Booking/DAO/ResourceConfigSet.php +++ b/CRM/Booking/DAO/ResourceConfigSet.php @@ -1,272 +1,242 @@ __table = 'civicrm_booking_resource_config_set'; parent::__construct(); } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Resource Config Sets') : E::ts('Resource Config Set'); + } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('ID') , - 'required' => true, - ) , - 'title' => array( + 'title' => E::ts('ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_config_set.id', + 'table_name' => 'civicrm_booking_resource_config_set', + 'entity' => 'ResourceConfigSet', + 'bao' => 'CRM_Booking_DAO_ResourceConfigSet', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'title' => [ 'name' => 'title', 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Title') , - 'required' => true, + 'title' => E::ts('Title'), + 'required' => TRUE, 'maxlength' => 255, 'size' => CRM_Utils_Type::HUGE, - ) , - 'weight' => array( + 'where' => 'civicrm_booking_resource_config_set.title', + 'table_name' => 'civicrm_booking_resource_config_set', + 'entity' => 'ResourceConfigSet', + 'bao' => 'CRM_Booking_DAO_ResourceConfigSet', + 'localizable' => 0, + 'add' => NULL, + ], + 'weight' => [ 'name' => 'weight', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Weight') , - 'required' => true, - ) , - 'is_active' => array( + 'title' => E::ts('Weight'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_config_set.weight', + 'table_name' => 'civicrm_booking_resource_config_set', + 'entity' => 'ResourceConfigSet', + 'bao' => 'CRM_Booking_DAO_ResourceConfigSet', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_active' => [ 'name' => 'is_active', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is cancelled') , - 'import' => true, + 'title' => E::ts('Slot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_resource_config_set.is_active', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, + 'export' => TRUE, 'default' => '1', - ) , - 'is_deleted' => array( + 'table_name' => 'civicrm_booking_resource_config_set', + 'entity' => 'ResourceConfigSet', + 'bao' => 'CRM_Booking_DAO_ResourceConfigSet', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is in the Trash') , - 'import' => true, + 'title' => E::ts('Slot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_resource_config_set.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_resource_config_set', + 'entity' => 'ResourceConfigSet', + 'bao' => 'CRM_Booking_DAO_ResourceConfigSet', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'title' => 'title', - 'weight' => 'weight', - 'is_active' => 'is_active', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_resource_config_set', $prefix, []); + return $r; + } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_resource_config_set'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_resource_config_set', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of indices + * + * @param bool $localize * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_resource_config_set'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_active' => [ + 'name' => 'index_is_active', + 'field' => [ + 0 => 'is_active', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource_config_set::0::is_active', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_resource_config_set::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/ResourceCriteria.php b/CRM/Booking/DAO/ResourceCriteria.php index 76473a85..ea67da22 100644 --- a/CRM/Booking/DAO/ResourceCriteria.php +++ b/CRM/Booking/DAO/ResourceCriteria.php @@ -1,257 +1,208 @@ __table = 'civicrm_booking_resource_criteria'; parent::__construct(); } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Resource Criterias') : E::ts('Resource Criteria'); + } + /** - * return foreign keys and entity references + * Returns foreign keys and entity references. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @return array + * [CRM_Core_Reference_Interface] */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'resource_id', 'civicrm_booking_resource', 'id') , - ); + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'resource_id', 'civicrm_booking_resource', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('ID') , - 'required' => true, - ) , - 'resource_id' => array( + 'title' => E::ts('ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_criteria.id', + 'table_name' => 'civicrm_booking_resource_criteria', + 'entity' => 'ResourceCriteria', + 'bao' => 'CRM_Booking_DAO_ResourceCriteria', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'resource_id' => [ 'name' => 'resource_id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Resource ID') , - 'required' => true, + 'title' => E::ts('Resource ID'), + 'description' => E::ts('Foreign key to the resoure for this resource criteria.'), + 'required' => TRUE, + 'where' => 'civicrm_booking_resource_criteria.resource_id', + 'table_name' => 'civicrm_booking_resource_criteria', + 'entity' => 'ResourceCriteria', + 'bao' => 'CRM_Booking_DAO_ResourceCriteria', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Resource', - ) , - 'criteria_id' => array( + 'add' => NULL, + ], + 'criteria_id' => [ 'name' => 'criteria_id', 'type' => CRM_Utils_Type::T_STRING, - 'required' => true, + 'description' => E::ts('Foreign key to the resource criteria option group.'), + 'required' => TRUE, 'maxlength' => 512, 'size' => CRM_Utils_Type::HUGE, - ) , - ); + 'where' => 'civicrm_booking_resource_criteria.criteria_id', + 'table_name' => 'civicrm_booking_resource_criteria', + 'entity' => 'ResourceCriteria', + 'bao' => 'CRM_Booking_DAO_ResourceCriteria', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'resource_id' => 'resource_id', - 'criteria_id' => 'criteria_id', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_resource_criteria'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_resource_criteria', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_resource_criteria'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_resource_criteria', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/Slot.php b/CRM/Booking/DAO/Slot.php index 2e390566..8698334d 100644 --- a/CRM/Booking/DAO/Slot.php +++ b/CRM/Booking/DAO/Slot.php @@ -1,347 +1,348 @@ __table = 'civicrm_booking_slot'; parent::__construct(); } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Slots') : E::ts('Slot'); + } + /** - * return foreign keys and entity references + * Returns foreign keys and entity references. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @return array + * [CRM_Core_Reference_Interface] */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'booking_id', 'civicrm_booking', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'resource_id', 'civicrm_booking_resource', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'config_id', 'civicrm_booking_resource_config_option', 'id') , - ); + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'booking_id', 'civicrm_booking', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'resource_id', 'civicrm_booking_resource', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'config_id', 'civicrm_booking_resource_config_option', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'booking_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_slot.id', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'booking_id' => [ 'name' => 'booking_id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, + 'description' => E::ts('FK to Booking ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_slot.booking_id', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Booking', - ) , - 'resource_id' => array( + 'add' => NULL, + ], + 'resource_id' => [ 'name' => 'resource_id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, + 'description' => E::ts('FK to resource ID'), + 'required' => TRUE, + 'where' => 'civicrm_booking_slot.resource_id', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Resource', - ) , - 'config_id' => array( + 'add' => NULL, + ], + 'config_id' => [ 'name' => 'config_id', 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to resource configuration option ID'), + 'where' => 'civicrm_booking_slot.config_id', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_ResourceConfigOption', - ) , - 'start' => array( + 'add' => NULL, + ], + 'start' => [ 'name' => 'start', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Start') , - 'required' => true, - ) , - 'end' => array( + 'title' => E::ts('Start'), + 'required' => TRUE, + 'where' => 'civicrm_booking_slot.start', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'add' => NULL, + ], + 'end' => [ 'name' => 'end', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('End') , - 'required' => true, - ) , - 'quantity' => array( + 'title' => E::ts('End'), + 'required' => TRUE, + 'where' => 'civicrm_booking_slot.end', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'add' => NULL, + ], + 'quantity' => [ 'name' => 'quantity', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Quantity') , - 'required' => true, - ) , - 'note' => array( + 'title' => E::ts('Quantity'), + 'required' => TRUE, + 'where' => 'civicrm_booking_slot.quantity', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'add' => NULL, + ], + 'note' => [ 'name' => 'note', 'type' => CRM_Utils_Type::T_TEXT, - 'title' => E::ts('Note') , - ) , - 'is_cancelled' => array( + 'title' => E::ts('Note'), + 'where' => 'civicrm_booking_slot.note', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_cancelled' => [ 'name' => 'is_cancelled', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is cancelled') , - 'import' => true, + 'title' => E::ts('Slot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_slot.is_cancelled', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - 'is_deleted' => array( + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('Slot is in the Trash') , - 'import' => true, + 'title' => E::ts('Slot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_slot.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_slot', + 'entity' => 'Slot', + 'bao' => 'CRM_Booking_DAO_Slot', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'booking_id' => 'booking_id', - 'resource_id' => 'resource_id', - 'config_id' => 'config_id', - 'start' => 'start', - 'end' => 'end', - 'quantity' => 'quantity', - 'note' => 'note', - 'is_cancelled' => 'is_cancelled', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_slot'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_slot', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported * - * @access public - * return array - * @static + * @param bool $prefix + * + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_slot'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_slot', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_cancelled' => [ + 'name' => 'index_is_cancelled', + 'field' => [ + 0 => 'is_cancelled', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_slot::0::is_cancelled', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_slot::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/DAO/SubSlot.php b/CRM/Booking/DAO/SubSlot.php index 6612e059..d15f79e9 100644 --- a/CRM/Booking/DAO/SubSlot.php +++ b/CRM/Booking/DAO/SubSlot.php @@ -1,333 +1,329 @@ __table = 'civicrm_booking_sub_slot'; parent::__construct(); } + /** - * return foreign keys and entity references + * Returns localized title of this entity. * - * @static - * @access public - * @return array of CRM_Core_EntityReference + * @param bool $plural + * Whether to return the plural version of the title. */ - static function getReferenceColumns() - { - if (!self::$_links) { - self::$_links = array( - new CRM_Core_EntityReference(self::getTableName() , 'slot_id', 'civicrm_booking_slot', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'resource_id', 'civicrm_booking_resource', 'id') , - new CRM_Core_EntityReference(self::getTableName() , 'config_id', 'civicrm_booking_resource_config_option', 'id') , - ); + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Sub Slots') : E::ts('Sub Slot'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'slot_id', 'civicrm_booking_slot', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'resource_id', 'civicrm_booking_resource', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'config_id', 'civicrm_booking_resource_config_option', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } - return self::$_links; + return Civi::$statics[__CLASS__]['links']; } + /** - * returns all the column names of this table + * Returns all the column names of this table * - * @access public * @return array */ - static function &fields() - { - if (!(self::$_fields)) { - self::$_fields = array( - 'id' => array( + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'required' => true, - ) , - 'slot_id' => array( + 'required' => TRUE, + 'where' => 'civicrm_booking_sub_slot.id', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, + ], + 'slot_id' => [ 'name' => 'slot_id', 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Slot ID'), + 'where' => 'civicrm_booking_sub_slot.slot_id', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Slot', - ) , - 'resource_id' => array( + 'add' => NULL, + ], + 'resource_id' => [ 'name' => 'resource_id', 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to resource ID'), + 'where' => 'civicrm_booking_sub_slot.resource_id', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_Resource', - ) , - 'config_id' => array( + 'add' => NULL, + ], + 'config_id' => [ 'name' => 'config_id', 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to resource configuration option ID'), + 'where' => 'civicrm_booking_sub_slot.config_id', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, 'FKClassName' => 'CRM_Booking_DAO_ResourceConfigOption', - ) , - 'time_required' => array( + 'add' => NULL, + ], + 'time_required' => [ 'name' => 'time_required', 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME, - 'title' => E::ts('Time Required') , - 'required' => true, - ) , - 'quantity' => array( + 'title' => E::ts('Time Required'), + 'required' => TRUE, + 'where' => 'civicrm_booking_sub_slot.time_required', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, + 'add' => NULL, + ], + 'quantity' => [ 'name' => 'quantity', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Quantity') , - 'required' => true, - ) , - 'note' => array( + 'title' => E::ts('Quantity'), + 'required' => TRUE, + 'where' => 'civicrm_booking_sub_slot.quantity', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, + 'add' => NULL, + ], + 'note' => [ 'name' => 'note', 'type' => CRM_Utils_Type::T_TEXT, - 'title' => E::ts('Note') , - ) , - 'is_cancelled' => array( + 'title' => E::ts('Note'), + 'where' => 'civicrm_booking_sub_slot.note', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_cancelled' => [ 'name' => 'is_cancelled', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('SubSlot is cancelled') , - 'import' => true, + 'title' => E::ts('SubSlot is cancelled'), + 'import' => TRUE, 'where' => 'civicrm_booking_sub_slot.is_cancelled', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - 'is_deleted' => array( + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, + 'add' => '4.4', + ], + 'is_deleted' => [ 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, - 'title' => E::ts('SubSlot is in the Trash') , - 'import' => true, + 'title' => E::ts('SubSlot is in the Trash'), + 'import' => TRUE, 'where' => 'civicrm_booking_sub_slot.is_deleted', - 'headerPattern' => '', - 'dataPattern' => '', - 'export' => true, - ) , - ); + 'export' => TRUE, + 'default' => '0', + 'table_name' => 'civicrm_booking_sub_slot', + 'entity' => 'SubSlot', + 'bao' => 'CRM_Booking_DAO_SubSlot', + 'localizable' => 0, + 'add' => '4.4', + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); } - return self::$_fields; + return Civi::$statics[__CLASS__]['fields']; } + /** - * Returns an array containing, for each field, the arary key used for that - * field in self::$_fields. + * Return a mapping from field-name to the corresponding key (as used in fields()). * - * @access public * @return array + * Array(string $name => string $uniqueName). */ - static function &fieldKeys() - { - if (!(self::$_fieldKeys)) { - self::$_fieldKeys = array( - 'id' => 'id', - 'slot_id' => 'slot_id', - 'resource_id' => 'resource_id', - 'config_id' => 'config_id', - 'time_required' => 'time_required', - 'quantity' => 'quantity', - 'note' => 'note', - 'is_cancelled' => 'is_cancelled', - 'is_deleted' => 'is_deleted', - ); + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); } - return self::$_fieldKeys; + return Civi::$statics[__CLASS__]['fieldKeys']; } + /** - * returns the names of this table + * Returns the names of this table * - * @access public - * @static * @return string */ - static function getTableName() - { + public static function getTableName() { return self::$_tableName; } + /** - * returns if this table needs to be logged + * Returns if this table needs to be logged * - * @access public - * @return boolean + * @return bool */ - function getLog() - { + public function getLog() { return self::$_log; } + /** - * returns the list of fields that can be imported + * Returns the list of fields that can be imported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &import($prefix = false) - { - if (!(self::$_import)) { - self::$_import = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('import', $field)) { - if ($prefix) { - self::$_import['booking_sub_slot'] = & $fields[$name]; - } else { - self::$_import[$name] = & $fields[$name]; - } - } - } - } - return self::$_import; + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'booking_sub_slot', $prefix, []); + return $r; } + /** - * returns the list of fields that can be exported + * Returns the list of fields that can be exported + * + * @param bool $prefix * - * @access public - * return array - * @static + * @return array */ - static function &export($prefix = false) - { - if (!(self::$_export)) { - self::$_export = array(); - $fields = self::fields(); - foreach($fields as $name => $field) { - if (CRM_Utils_Array::value('export', $field)) { - if ($prefix) { - self::$_export['booking_sub_slot'] = & $fields[$name]; - } else { - self::$_export[$name] = & $fields[$name]; - } - } - } - } - return self::$_export; + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'booking_sub_slot', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = [ + 'index_is_cancelled' => [ + 'name' => 'index_is_cancelled', + 'field' => [ + 0 => 'is_cancelled', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_sub_slot::0::is_cancelled', + ], + 'index_is_deleted' => [ + 'name' => 'index_is_deleted', + 'field' => [ + 0 => 'is_deleted', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_booking_sub_slot::0::is_deleted', + ], + ]; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; } + } diff --git a/CRM/Booking/Form/AddSubResource.php b/CRM/Booking/Form/AddSubResource.php index 8043072b..484be280 100644 --- a/CRM/Booking/Form/AddSubResource.php +++ b/CRM/Booking/Form/AddSubResource.php @@ -281,24 +281,22 @@ static function registerScripts() { ->addStyleFile('uk.co.compucorp.civicrm.booking', 'css/booking.css', 92, 'page-header') - ->addScriptFile('civicrm', 'packages/backbone/json2.js', 100, 'html-header', FALSE) - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'packages/underscore.js', 110, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.js', 120, 'html-header') - ->addScriptFile('civicrm', 'packages/backbone/backbone.marionette.js', 125, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.modelbinder.js', 125, 'html-header', FALSE) - ->addScriptFile('civicrm', 'js/crm.backbone.js', 130, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/json2.js', 100, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/underscore-umd.js', 110, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.js', 120, 'html-header') + ->addScriptFile(E::LONG_NAME, 'packages/backbone.marionette.js', 125, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.modelbinder.js', 125, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'js/vendor/crm.backbone.js', 130, 'html-header', FALSE) ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/vendor/moment.min.js', 120, 'html-header', FALSE) ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/civicrm-moment-strftime.js', 140, 'html-header', FALSE) - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/app.js', 150, 'html-header') - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/common/views.js', 151, 'html-header', FALSE) - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/utils.js', 151, 'html-header', FALSE) - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/entities.js', 160, 'html-header') - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/view.js', 170, 'html-header'); + ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/entities.js', 145, 'html-header') + ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/view.js', 145, 'html-header') + ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/app.js', 150, 'html-header'); $templateDir = CRM_Extension_System::singleton()->getMapper()->keyToBasePath('uk.co.compucorp.civicrm.booking') . '/templates/'; - $region = CRM_Core_Region::instance('page-header'); + $region = CRM_Core_Region::instance('html-header'); foreach (glob($templateDir . 'CRM/Booking/tpl/add-sub-resource/*.tpl') as $file) { $fileName = substr($file, strlen($templateDir)); $region->add(array( diff --git a/CRM/Booking/Form/Booking/Base.php b/CRM/Booking/Form/Booking/Base.php index 8b23a28e..5a8d2603 100644 --- a/CRM/Booking/Form/Booking/Base.php +++ b/CRM/Booking/Form/Booking/Base.php @@ -40,8 +40,6 @@ */ abstract class CRM_Booking_Form_Booking_Base extends CRM_Core_Form { - protected $_id; - protected $_cid; protected $_values; diff --git a/CRM/Booking/Form/Booking/Info.php b/CRM/Booking/Form/Booking/Info.php index 61a7f7ed..097d8874 100644 --- a/CRM/Booking/Form/Booking/Info.php +++ b/CRM/Booking/Form/Booking/Info.php @@ -399,15 +399,15 @@ static function registerScripts() { CRM_Core_Resources::singleton() ->addStyleFile('uk.co.compucorp.civicrm.booking', 'css/booking.css', 92, 'page-header') - ->addScriptFile('civicrm', 'packages/backbone/json2.js', 100, 'html-header', FALSE) - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'packages/underscore.js', 110, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.js', 120, 'html-header') - ->addScriptFile('civicrm', 'packages/backbone/backbone.marionette.js', 125, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.modelbinder.js', 125, 'html-header', FALSE) - ->addScriptFile('civicrm', 'js/crm.backbone.js', 130, 'html-header', FALSE) - - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/booking-info/app.js', 150, 'html-header') - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/booking-info/view.js', 170, 'html-header'); + ->addScriptFile(E::LONG_NAME, 'packages/json2.js', 100, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/underscore-umd.js', 110, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.js', 120, 'html-header') + ->addScriptFile(E::LONG_NAME, 'packages/backbone.marionette.js', 125, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.modelbinder.js', 125, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'js/vendor/crm.backbone.js', 130, 'html-header', FALSE) + + ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/booking-info/view.js', 150, 'html-header') + ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/booking-info/app.js', 170, 'html-header'); $templateDir = CRM_Extension_System::singleton()->getMapper()->keyToBasePath('uk.co.compucorp.civicrm.booking') . '/templates/'; $region = CRM_Core_Region::instance('page-header'); diff --git a/CRM/Booking/Form/DayView.php b/CRM/Booking/Form/DayView.php index b6ee6220..143c5206 100644 --- a/CRM/Booking/Form/DayView.php +++ b/CRM/Booking/Form/DayView.php @@ -47,6 +47,7 @@ function postProcess() { $resources = CRM_Booking_BAO_Slot::getSlotDetailsOrderByResourceBetweenDate($from, $to); //put resources result to values, being ready to display. $values['resources'] = $resources; + $values['resultDisplay'] = true; if(empty($resources)){ //check empty result //Convert date to show on no match found view diff --git a/CRM/Booking/Form/SelectResource.php b/CRM/Booking/Form/SelectResource.php index dd177186..4bc0d752 100644 --- a/CRM/Booking/Form/SelectResource.php +++ b/CRM/Booking/Form/SelectResource.php @@ -9,13 +9,6 @@ * @see http://wiki.civicrm.org/confluence/display/CRMDOC43/QuickForm+Reference */ class CRM_Booking_Form_SelectResource extends CRM_Core_Form { - /** - * the booking ID of the booking if we are editing a booking - * - * @var integer - */ - //protected $_id; - private $configOptions; @@ -212,15 +205,15 @@ static function registerScripts() { ->addStyleFile('uk.co.compucorp.civicrm.booking', 'js/vendor/dhtmlxScheduler/sources/dhtmlxscheduler.css', 92, 'page-header') ->addStyleFile('uk.co.compucorp.civicrm.booking', 'css/booking.css', 92, 'page-header') - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'packages/underscore.js', 110, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.js', 120, 'html-header') + ->addScriptFile(E::LONG_NAME, 'packages/underscore-umd.js', 110, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.js', 120, 'html-header') ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/vendor/moment.min.js', 120, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.marionette.js', 125, 'html-header', FALSE) - ->addScriptFile('civicrm', 'packages/backbone/backbone.modelbinder.js', 125, 'html-header', FALSE) - ->addScriptFile('civicrm', 'js/crm.backbone.js', 130, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.marionette.js', 125, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'packages/backbone.modelbinder.js', 125, 'html-header', FALSE) + ->addScriptFile(E::LONG_NAME, 'js/vendor/crm.backbone.js', 130, 'html-header', FALSE) + ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/entities.js', 135, 'html-header') ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/add-sub-resource/app.js', 140, 'html-header') - ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/utils.js', 141, 'html-header', FALSE) ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/booking/civicrm-moment-strftime.js', 142, 'html-header', FALSE) ->addScriptFile('uk.co.compucorp.civicrm.booking', 'js/vendor/dhtmlxScheduler/sources/dhtmlxscheduler.js', 132, 'html-header') diff --git a/CRM/Booking/Upgrader.php b/CRM/Booking/Upgrader.php index 1bb5a1b9..dc30408b 100755 --- a/CRM/Booking/Upgrader.php +++ b/CRM/Booking/Upgrader.php @@ -1,5 +1,5 @@ 3, + $params = [ 'sequential' => 1, 'label' => 'Booking', 'name' => CRM_Booking_Utils_Constants::ACTIVITY_TYPE, - ); - //chcck if it exist in case of re-installation - $optionValue = civicrm_api3('OptionValue', 'get',$params); - if($optionValue['count'] == 0){ + ]; + //check if it exist in case of re-installation + $optionValue = civicrm_api3('OptionValue', 'get', $params); + if ($optionValue['count'] == 0) { $params['weight'] = 1; $params['is_reserved'] = 1; $params['is_active'] = 1; - $result = civicrm_api('ActivityType', 'create', $params); + civicrm_api3('ActivityType', 'create', $params); } //create new activity type for sending email confirmation :CVB-95 - $params = array( - 'version' => 3, + $params = [ 'sequential' => 1, 'label' => 'Send booking confirmation', 'name' => CRM_Booking_Utils_Constants::ACTIVITY_TYPE_SEND_EMAIL, - ); - //chcck if it exist in case of re-installation - $optionValue = civicrm_api3('OptionValue', 'get',$params); - if($optionValue['count'] == 0){ + ]; + //check if it exist in case of re-installation + $optionValue = civicrm_api3('OptionValue', 'get', $params); + if ($optionValue['count'] == 0) { $params['weight'] = 1; $params['is_reserved'] = 1; $params['is_active'] = 1; - $result = civicrm_api('ActivityType', 'create', $params); + civicrm_api3('ActivityType', 'create', $params); } - $result = civicrm_api('OptionGroup', 'getsingle', array( - 'version' => 3, - 'sequential' => 1, - 'name' => 'msg_tpl_workflow_booking') + $result = civicrm_api3('OptionGroup', 'getsingle', [ + 'sequential' => 1, + 'name' => 'msg_tpl_workflow_booking', + ] ); - if(isset($result['id'])){ - $params = array( - 'version' => 3, + if (isset($result['id'])) { + $params = [ 'sequential' => 1, 'option_group_id' => $result['id'], - ); - $opvResult = civicrm_api('OptionValue', 'get', $params); - if(isset($opvResult['values']) && !empty($opvResult['values'])){ - foreach ($opvResult['values'] as $value) { + ]; + $opvResult = civicrm_api3('OptionValue', 'get', $params); + if (isset($opvResult['values']) && !empty($opvResult['values'])) { + foreach ($opvResult['values'] as $value) { switch ($value['name']) { case 'booking_offline_receipt': $html = file_get_contents($this->extensionDir . '/msg_tpl/booking_offline_receipt.html', FILE_USE_INCLUDE_PATH); @@ -63,139 +60,99 @@ public function install() { $title = E::ts("Booking - Confirmation and Receipt (off-line)"); break; } - if(isset($title)){ - $params = array( - 'version' => 3, - 'sequential' => 1, + if (isset($title)) { + $params = [ 'msg_title' => $title, - 'msg_subject' => E::ts("Booking - Confirmation Receipt").' - '.ts("Booking Status:").'{$booking_status}', + 'msg_subject' => E::ts("Booking - Confirmation Receipt") . ' - ' . E::ts("Booking Status:") . '{$booking_status}', 'msg_text' => $text, 'msg_html' => $html, 'is_active' => 1, 'workflow_id' => $value['id'], 'is_default' => 1, 'is_reserved' => 0, - ); - $result = civicrm_api('MessageTemplate', 'create', $params); + ]; + civicrm_api3('MessageTemplate', 'create', $params); $params['is_default'] = 0; $params['is_reserved'] = 1; //re-created another template - $result = civicrm_api('MessageTemplate', 'create', $params); + civicrm_api3('MessageTemplate', 'create', $params); } } } } $this->executeSqlFile('sql/civibooking_default.sql'); - $this->upgrade_1101(); + // Enable booking custom data + $this->upgrade_1102(); } /** * Example: Run an external SQL script when the module is uninstalled */ public function uninstall() { - $this->removeNavigationMenus(); - } - - /** - * Removes menu items added by this extension. - */ - private function removeNavigationMenus() { - $menuItems = $this->buildBookingSubMenusParameters(); - - foreach ($menuItems as $item) { - $this->removeNav($item['name']); - } - - CRM_Core_BAO_Navigation::resetNavigation(); } /** * Example: Run a simple query when a module is enabled * -*/ + */ public function enable() { - $this->executeSqlFile('sql/civibooking_enable.sql'); - $this->toggleIsActiveMenuItems(true); + $this->executeSqlFile('sql/civibooking_enable.sql'); } /** * Example: Run a simple query when a module is disabled * - */ + */ public function disable() { //TODO:: Disable the message template $this->executeSqlFile('sql/civibooking_disable.sql'); - $this->toggleIsActiveMenuItems(false); - } - - /** - * Sets is_active parameter for menu items created by this extension. - * - * @param $isActive - */ - private function toggleIsActiveMenuItems($isActive) { - $sortedItems = array(); - $menuItems = $this->buildBookingSubMenusParameters(); - $isActive = (int) $isActive; - foreach ($menuItems as $item) { - $parent = CRM_Utils_Array::value('parent_name', $item, '_noparent_'); - $sortedItems[$parent][] = $item['name']; - } - - foreach ($sortedItems as $parent => $items) { - $params = array( - 'sequential' => 1, - 'name' => array('IN' => $items), - 'parent_id' => $parent, - 'api.Navigation.create' => array('id' => '$value.id', 'is_active' => $isActive), - ); - - if ($parent === '_noparent_') { - $params['parent_id'] = array('IS NULL' => 1); - } - - $versionNum = $this->versionSwitcher(); - if ($versionNum >= 470){ - civicrm_api3('Navigation', 'get', $params); - } - else { - $items = implode("','", $items); - $query = "UPDATE civicrm_navigation SET is_active = {$isActive} WHERE name IN ('{$items}')"; - CRM_Core_DAO::executeQuery($query); - } - } - - CRM_Core_BAO_Navigation::resetNavigation(); } // By convention, functions that look like "function upgrade_NNNN()" are // upgrade tasks. They are executed in order (like Drupal's hook_update_N). public function upgrade_1100() { - $this->ctx->log->info('Applying update 1100'); - $this->executeSqlFile('sql/update_1100.sql'); + Civi::log()->info('Applying update 1100'); + if (!CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_booking_config', 'unlimited_resource_time_config')) { + $this->executeSqlFile('sql/update_1100.sql'); + } return TRUE; } /** + * Deprecated. The navigation menu building process is now handled with a hook. * This upgrade builds navigation menus for the extension. */ public function upgrade_1101() { - $administerMenuId = $this->getAdministerMenuID(); + return TRUE; + } - // skip adding menu if there is no administer menu - if ($administerMenuId) { - $bookingSubMenus = $this->buildBookingSubMenusParameters(); + public function upgrade_1102() { + Civi::log()->info('Enabling Booking custom data'); + self::enableBookingCustomData(); + return TRUE; + } - foreach ($bookingSubMenus as $menuItem) { - $this->addNav($menuItem); - } + public static function enableBookingCustomData() { + // Enable Booking custom data + $optionValue = [ + 'name' => 'civicrm_booking', + 'label' => 'Booking', + 'value' => 'Booking', + ]; + $optionValues = civicrm_api3('OptionValue', 'get', [ + 'option_group_id' => 'cg_extend_objects', + 'name' => $optionValue['name'], + ]); + if (!$optionValues['count']) { + civicrm_api3('OptionValue', 'create', [ + 'option_group_id' => 'cg_extend_objects', + 'name' => $optionValue['name'], + 'label' => $optionValue['label'], + 'value' => $optionValue['value'], + ]); } - - CRM_Core_BAO_Navigation::resetNavigation(); - - return TRUE; } /** @@ -203,15 +160,15 @@ public function upgrade_1101() { * * @return array */ - private function buildBookingSubMenusParameters() { - $bookingStatusGid = $this->getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_BOOKING_STATUS); - $resourceTypeGid = $this->getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_RESOURCE_TYPE); - $resourceLocationGId = $this->getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_RESOURCE_LOCATION); - $sizeUnitGid = $this->getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_SIZE_UNIT); - $cancellationChargesGid = $this->getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_CANCELLATION_CHARGES); - - $menus = array( - array( + public static function getMenuItems() { + $bookingStatusGid = self::getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_BOOKING_STATUS); + $resourceTypeGid = self::getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_RESOURCE_TYPE); + $resourceLocationGId = self::getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_RESOURCE_LOCATION); + $sizeUnitGid = self::getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_SIZE_UNIT); + $cancellationChargesGid = self::getOptionGroupID(CRM_Booking_Utils_Constants::OPTION_CANCELLATION_CHARGES); + + $menus = [ + [ 'label' => ts('CiviBooking'), 'name' => 'admin_booking', 'url' => '#', @@ -219,105 +176,105 @@ private function buildBookingSubMenusParameters() { 'operator' => null, 'separator' => 1, 'parent_name' => 'Administer', - ), - array( + ], + [ 'label' => ts('Resource Configuration Set'), 'name' => 'resource_config_set', 'url' => CRM_Utils_System::url('civicrm/admin/resource/config_set', "reset=1", TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Manage Resources'), 'name' => 'manage_resources', 'url' => CRM_Utils_system::url('civicrm/admin/resource', "reset=1", TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Additional Charges Item'), 'name' => 'adhoc_charges_item', 'url' => CRM_Utils_system::url('civicrm/admin/adhoc_charges_item', "reset=1", TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Booking Status'), 'name' => 'booking_status', - 'url' => CRM_Utils_system::url('civicrm/admin/options', array('gid' => $bookingStatusGid, 'reset' => 1), TRUE), + 'url' => CRM_Utils_system::url('civicrm/admin/options', ['gid' => $bookingStatusGid, 'reset' => 1], TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Resource Type'), 'name' => 'resource_type', - 'url' => CRM_Utils_system::url('civicrm/admin/options', array('gid' => $resourceTypeGid, 'reset' => 1), TRUE), + 'url' => CRM_Utils_system::url('civicrm/admin/options', ['gid' => $resourceTypeGid, 'reset' => 1], TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Size Unit'), 'name' => 'size_unit', - 'url' => CRM_Utils_system::url('civicrm/admin/options', array('gid' => $sizeUnitGid, 'reset' => 1), TRUE), + 'url' => CRM_Utils_system::url('civicrm/admin/options', ['gid' => $sizeUnitGid, 'reset' => 1], TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Cancellation Charges'), 'name' => 'cancellation_charges', - 'url' => CRM_Utils_system::url('civicrm/admin/options', array('gid' => $cancellationChargesGid,'reset' => 1), TRUE), + 'url' => CRM_Utils_system::url('civicrm/admin/options', ['gid' => $cancellationChargesGid,'reset' => 1], TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Booking Component Settings'), 'name' => 'booking_component_settings', 'url' => CRM_Utils_system::url('civicrm/admin/setting/preferences/booking', "reset=1", TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Resource Location'), 'name' => 'resource_location', - 'url' => CRM_Utils_system::url('civicrm/admin/options', array('gid' => $resourceLocationGId,'reset' => 1), TRUE), + 'url' => CRM_Utils_system::url('civicrm/admin/options', ['gid' => $resourceLocationGId,'reset' => 1], TRUE), 'permission' => null, 'operator' => null, 'separator' => 0, - 'parent_name' => 'admin_booking', - ), - array( + 'parent_name' => 'Administer/admin_booking', + ], + [ 'label' => ts('Find Bookings'), 'name' => 'find_booking', 'url' => CRM_Utils_system::url('civicrm/booking/search', "reset=1", TRUE), 'permission' => 'administer CiviBooking,create and update bookings,view all bookings', 'operator' => null, 'separator' => 0, - ), - array( + ], + [ 'label' => ts('Booking'), 'name' => 'booking', 'url' => null, 'permission' => 'administer CiviBooking,create and update bookings,view all bookings', 'operator' => null, 'separator' => null, - ), - array( + ], + [ 'label' => ts('New Booking'), 'name' => 'new_booking', 'url' => CRM_Utils_system::url('civicrm/booking/add', "reset=1", TRUE), @@ -325,8 +282,8 @@ private function buildBookingSubMenusParameters() { 'operator' => null, 'separator' => 0, 'parent_name' => 'booking', - ), - array( + ], + [ 'label' => ts('Day View'), 'name' => 'day_view', 'url' => CRM_Utils_system::url('civicrm/booking/day-view', "reset=1", TRUE), @@ -334,30 +291,12 @@ private function buildBookingSubMenusParameters() { 'operator' => null, 'separator' => 0, 'parent_name' => 'booking', - ), - ); + ], + ]; return $menus; } - /** - * Obtains ID of Administer menu. - * - * @return null|string - */ - private function getAdministerMenuID() { - $domain_id = CRM_Core_Config::domainID(); - - $administerMenuId = CRM_Core_DAO::singleValueQuery(" - SELECT id - FROM civicrm_navigation - WHERE name = 'Administer' - AND domain_id = $domain_id - "); - - return $administerMenuId; - } - /** * Obtains an option group's ID given its name. * @@ -365,139 +304,10 @@ private function getAdministerMenuID() { * * @return int */ - private function getOptionGroupID($name) { - $result = civicrm_api3('OptionGroup', 'getsingle', array('name' => $name)); - - if($result['id']){ - return $result['id']; - } - + private static function getOptionGroupID($name) { + try { + return civicrm_api3('OptionGroup', 'getsingle', ['name' => $name])['id']; + } catch (Exception $e) {} return 0; } - - /** - * Adds given menu item to CiviCRM navigation. - * - * @param array $menuItem - */ - private function addNav($menuItem) { - if (isset($menuItem['parent_name'])) { - $menuItem['parent_id'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Navigation', $menuItem['parent_name'], 'id', 'name'); - unset($menuItem['parent_name']); - } - - $menuItem['is_active'] = 1; - - CRM_Core_BAO_Navigation::add($menuItem); - } - - /** - * Removes navigation item identified by $name from CiviCRM navigation. - * - * @param string $name - */ - private function removeNav($name) { - $versionNum = $this->versionSwitcher(); - if ($versionNum >= 470){ - civicrm_api3('Navigation', 'get', array( - 'sequential' => 1, - 'name' => $name, - 'api.Navigation.delete' => array('id' => '$value.id'), - )); - } - else { - $query = "DELETE FROM civicrm_navigation WHERE name = '{$name}'"; - CRM_Core_DAO::executeQuery($query); - } - } - - /** - * Get civicrm version. - * - * @return $versionNum - */ - private function versionSwitcher() { - $version = CRM_Utils_System::version(); - preg_match('/[0-9]\.[0-9]\.[0-9]/', $version, $matches); - $versionNum = str_replace(".","",array_pop($matches)); - - return $versionNum; - } - - /** - * Example: Run a couple simple queries - * - * @return TRUE on success - * @throws Exception - * - public function upgrade_4200() { - $this->ctx->log->info('Applying update 4200'); - CRM_Core_DAO::executeQuery('UPDATE foo SET bar = "whiz"'); - CRM_Core_DAO::executeQuery('DELETE FROM bang WHERE willy = wonka(2)'); - return TRUE; - } // */ - - - /** - * Example: Run an external SQL script - * - * @return TRUE on success - * @throws Exception - public function upgrade_4201() { - $this->ctx->log->info('Applying update 4201'); - // this path is relative to the extension base dir - $this->executeSqlFile('sql/upgrade_4201.sql'); - return TRUE; - } // */ - - - /** - * Example: Run a slow upgrade process by breaking it up into smaller chunk - * - * @return TRUE on success - * @throws Exception - public function upgrade_4202() { - $this->ctx->log->info('Planning update 4202'); // PEAR Log interface - - $this->addTask(E::ts('Process first step'), 'processPart1', $arg1, $arg2); - $this->addTask(E::ts('Process second step'), 'processPart2', $arg3, $arg4); - $this->addTask(E::ts('Process second step'), 'processPart3', $arg5); - return TRUE; - } - public function processPart1($arg1, $arg2) { sleep(10); return TRUE; } - public function processPart2($arg3, $arg4) { sleep(10); return TRUE; } - public function processPart3($arg5) { sleep(10); return TRUE; } - // */ - - - /** - * Example: Run an upgrade with a query that touches many (potentially - * millions) of records by breaking it up into smaller chunks. - * - * @return TRUE on success - * @throws Exception - public function upgrade_4203() { - $this->ctx->log->info('Planning update 4203'); // PEAR Log interface - - $minId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(min(id),0) FROM civicrm_contribution'); - $maxId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(max(id),0) FROM civicrm_contribution'); - for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { - $endId = $startId + self::BATCH_SIZE - 1; - $title = E::ts('Upgrade Batch (%1 => %2)', array( - 1 => $startId, - 2 => $endId, - )); - $sql = ' - UPDATE civicrm_contribution SET foobar = whiz(wonky()+wanker) - WHERE id BETWEEN %1 and %2 - '; - $params = array( - 1 => array($startId, 'Integer'), - 2 => array($endId, 'Integer'), - ); - $this->addTask($title, 'executeSql', $sql, $params); - } - return TRUE; - } // */ - } diff --git a/CRM/Booking/Upgrader/Base.php b/CRM/Booking/Upgrader/Base.php index ae54f2ff..54ce5301 100644 --- a/CRM/Booking/Upgrader/Base.php +++ b/CRM/Booking/Upgrader/Base.php @@ -1,7 +1,7 @@ ctx = array_shift($args); $instance->queue = $instance->ctx->queue; $method = array_shift($args); - return call_user_func_array(array($instance, $method), $args); + return call_user_func_array([$instance, $method], $args); } + /** + * CRM_Booking_Upgrader_Base constructor. + * + * @param $extensionName + * @param $extensionDir + */ public function __construct($extensionName, $extensionDir) { $this->extensionName = $extensionName; $this->extensionDir = $extensionDir; @@ -74,49 +88,89 @@ public function __construct($extensionName, $extensionDir) { // ******** Task helpers ******** /** - * Run a CustomData file + * Run a CustomData file. * - * @param string $relativePath the CustomData XML file path (relative to this extension's dir) + * @param string $relativePath + * the CustomData XML file path (relative to this extension's dir) * @return bool */ public function executeCustomDataFile($relativePath) { $xml_file = $this->extensionDir . '/' . $relativePath; - require_once 'CRM/Utils/Migrate/Import.php'; + return $this->executeCustomDataFileByAbsPath($xml_file); + } + + /** + * Run a CustomData file + * + * @param string $xml_file + * the CustomData XML file path (absolute path) + * + * @return bool + */ + protected function executeCustomDataFileByAbsPath($xml_file) { $import = new CRM_Utils_Migrate_Import(); $import->run($xml_file); return TRUE; } /** - * Run a SQL file + * Run a SQL file. + * + * @param string $relativePath + * the SQL file path (relative to this extension's dir) * - * @param string $relativePath the SQL file path (relative to this extension's dir) * @return bool */ public function executeSqlFile($relativePath) { CRM_Utils_File::sourceSQLFile( CIVICRM_DSN, - $this->extensionDir . '/' . $relativePath + $this->extensionDir . DIRECTORY_SEPARATOR . $relativePath ); return TRUE; } /** - * Run one SQL query + * Run the sql commands in the specified file. + * + * @param string $tplFile + * The SQL file path (relative to this extension's dir). + * Ex: "sql/mydata.mysql.tpl". + * + * @return bool + * @throws \CRM_Core_Exception + */ + public function executeSqlTemplate($tplFile) { + // Assign multilingual variable to Smarty. + $upgrade = new CRM_Upgrade_Form(); + + $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->extensionDir . DIRECTORY_SEPARATOR . $tplFile; + $smarty = CRM_Core_Smarty::singleton(); + $smarty->assign('domainID', CRM_Core_Config::domainID()); + CRM_Utils_File::sourceSQLFile( + CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE + ); + return TRUE; + } + + /** + * Run one SQL query. * * This is just a wrapper for CRM_Core_DAO::executeSql, but it - * provides syntatic sugar for queueing several tasks that + * provides syntactic sugar for queueing several tasks that * run different queries + * + * @return bool */ - public function executeSql($query, $params = array()) { + public function executeSql($query, $params = []) { // FIXME verify that we raise an exception on error - CRM_Core_DAO::executeSql($query, $params); + CRM_Core_DAO::executeQuery($query, $params); return TRUE; } /** - * Syntatic sugar for enqueuing a task which calls a function - * in this class. The task is weighted so that it is processed + * Syntactic sugar for enqueuing a task which calls a function in this class. + * + * The task is weighted so that it is processed * as part of the currently-pending revision. * * After passing the $funcName, you can also pass parameters that will go to @@ -126,17 +180,17 @@ public function addTask($title) { $args = func_get_args(); $title = array_shift($args); $task = new CRM_Queue_Task( - array(get_class($this), '_queueAdapter'), + [get_class($this), '_queueAdapter'], $args, $title ); - return $this->queue->createItem($task, array('weight' => -1)); + return $this->queue->createItem($task, ['weight' => -1]); } // ******** Revision-tracking helpers ******** /** - * Determine if there are any pending revisions + * Determine if there are any pending revisions. * * @return bool */ @@ -155,7 +209,9 @@ public function hasPendingRevisions() { } /** - * Add any pending revisions to the queue + * Add any pending revisions to the queue. + * + * @param CRM_Queue_Queue $queue */ public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { $this->queue = $queue; @@ -163,23 +219,23 @@ public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { $currentRevision = $this->getCurrentRevision(); foreach ($this->getRevisions() as $revision) { if ($revision > $currentRevision) { - $title = E::ts('Upgrade %1 to revision %2', array( + $title = E::ts('Upgrade %1 to revision %2', [ 1 => $this->extensionName, 2 => $revision, - )); + ]); // note: don't use addTask() because it sets weight=-1 $task = new CRM_Queue_Task( - array(get_class($this), '_queueAdapter'), - array('upgrade_' . $revision), + [get_class($this), '_queueAdapter'], + ['upgrade_' . $revision], $title ); $this->queue->createItem($task); $task = new CRM_Queue_Task( - array(get_class($this), '_queueAdapter'), - array('setCurrentRevision', $revision), + [get_class($this), '_queueAdapter'], + ['setCurrentRevision', $revision], $title ); $this->queue->createItem($task); @@ -188,13 +244,14 @@ public function enqueuePendingRevisions(CRM_Queue_Queue $queue) { } /** - * Get a list of revisions + * Get a list of revisions. * - * @return array(revisionNumbers) sorted numerically + * @return array + * revisionNumbers sorted numerically */ public function getRevisions() { - if (! is_array($this->revisions)) { - $this->revisions = array(); + if (!is_array($this->revisions)) { + $this->revisions = []; $clazz = new ReflectionClass(get_class($this)); $methods = $clazz->getMethods(); @@ -210,68 +267,130 @@ public function getRevisions() { } public function getCurrentRevision() { - // return CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName); + $revision = CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName); + if (!$revision) { + $revision = $this->getCurrentRevisionDeprecated(); + } + return $revision; + } + + private function getCurrentRevisionDeprecated() { $key = $this->extensionName . ':version'; - return CRM_Core_BAO_Setting::getItem('Extension', $key); + if ($revision = \Civi::settings()->get($key)) { + $this->revisionStorageIsDeprecated = TRUE; + } + return $revision; } public function setCurrentRevision($revision) { - // We call this during hook_civicrm_install, but the underlying SQL - // UPDATE fails because the extension record hasn't been INSERTed yet. - // Instead, track revisions in our own namespace. - // CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision); - - $key = $this->extensionName . ':version'; - CRM_Core_BAO_Setting::setItem($revision, 'Extension', $key); + CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision); + // clean up legacy schema version store (CRM-19252) + $this->deleteDeprecatedRevision(); return TRUE; } + private function deleteDeprecatedRevision() { + if ($this->revisionStorageIsDeprecated) { + $setting = new CRM_Core_BAO_Setting(); + $setting->name = $this->extensionName . ':version'; + $setting->delete(); + CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->extensionName} from civicrm_setting (deprecated) to civicrm_extension.\n"); + } + } + // ******** Hook delegates ******** + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install + */ public function onInstall() { - foreach (glob($this->extensionDir . '/sql/*_install.sql') as $file) { - CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + $files = glob($this->extensionDir . '/sql/*_install.sql'); + if (is_array($files)) { + foreach ($files as $file) { + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + } + } + $files = glob($this->extensionDir . '/sql/*_install.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } + $files = glob($this->extensionDir . '/xml/*_install.xml'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeCustomDataFileByAbsPath($file); + } } - if (is_callable(array($this, 'install'))) { + if (is_callable([$this, 'install'])) { $this->install(); } + } + + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall + */ + public function onPostInstall() { $revisions = $this->getRevisions(); if (!empty($revisions)) { $this->setCurrentRevision(max($revisions)); } + if (is_callable([$this, 'postInstall'])) { + $this->postInstall(); + } } + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall + */ public function onUninstall() { - if (is_callable(array($this, 'uninstall'))) { + $files = glob($this->extensionDir . '/sql/*_uninstall.mysql.tpl'); + if (is_array($files)) { + foreach ($files as $file) { + $this->executeSqlTemplate($file); + } + } + if (is_callable([$this, 'uninstall'])) { $this->uninstall(); } - foreach (glob($this->extensionDir . '/sql/*_uninstall.sql') as $file) { - CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + $files = glob($this->extensionDir . '/sql/*_uninstall.sql'); + if (is_array($files)) { + foreach ($files as $file) { + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file); + } } - $this->setCurrentRevision(NULL); } + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable + */ public function onEnable() { // stub for possible future use - if (is_callable(array($this, 'enable'))) { + if (is_callable([$this, 'enable'])) { $this->enable(); } } + /** + * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable + */ public function onDisable() { // stub for possible future use - if (is_callable(array($this, 'disable'))) { + if (is_callable([$this, 'disable'])) { $this->disable(); } } public function onUpgrade($op, CRM_Queue_Queue $queue = NULL) { - switch($op) { + switch ($op) { case 'check': - return array($this->hasPendingRevisions()); + return [$this->hasPendingRevisions()]; + case 'enqueue': return $this->enqueuePendingRevisions($queue); + default: } } + } diff --git a/api/v3/Booking.php b/api/v3/Booking.php index 99255ffb..0639deb6 100644 --- a/api/v3/Booking.php +++ b/api/v3/Booking.php @@ -27,9 +27,7 @@ function _civicrm_api3_booking_create_spec(&$spec) { * @throws API_Exception */ function civicrm_api3_booking_create($params) { - $bookingBAO = CRM_Booking_BAO_Booking::create($params); - _civicrm_api3_object_to_array($bookingBAO, $bookingArray[$bookingBAO->id]); - return civicrm_api3_create_success($bookingArray, $params, 'Booking', 'create'); + return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'Booking'); } diff --git a/booking.civix.php b/booking.civix.php index e6bd993c..71a0ca75 100644 --- a/booking.civix.php +++ b/booking.civix.php @@ -6,11 +6,10 @@ * The ExtensionUtil class provides small stubs for accessing resources of this * extension. */ - class CRM_Booking_ExtensionUtil { - const SHORT_NAME = "booking"; - const LONG_NAME = "uk.co.compucorp.civicrm.booking"; - const CLASS_PREFIX = "CRM_Booking"; + const SHORT_NAME = 'booking'; + const LONG_NAME = 'uk.co.compucorp.civicrm.booking'; + const CLASS_PREFIX = 'CRM_Booking'; /** * Translate a string using the extension's domain. @@ -25,9 +24,9 @@ class CRM_Booking_ExtensionUtil { * Translated text. * @see ts */ - public static function ts($text, $params = array()) { + public static function ts($text, $params = []) { if (!array_key_exists('domain', $params)) { - $params['domain'] = array(self::LONG_NAME, NULL); + $params['domain'] = [self::LONG_NAME, NULL]; } return ts($text, $params); } @@ -77,33 +76,43 @@ public static function findClass($suffix) { } } + +use CRM_Booking_ExtensionUtil as E; + /** - * (Delegated) Implementation of hook_civicrm_config + * (Delegated) Implements hook_civicrm_config(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config */ function _booking_civix_civicrm_config(&$config = NULL) { static $configured = FALSE; - if ($configured) return; + if ($configured) { + return; + } $configured = TRUE; $template =& CRM_Core_Smarty::singleton(); - $extRoot = dirname( __FILE__ ) . DIRECTORY_SEPARATOR; + $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR; $extDir = $extRoot . 'templates'; - if ( is_array( $template->template_dir ) ) { - array_unshift( $template->template_dir, $extDir ); - } else { - $template->template_dir = array( $extDir, $template->template_dir ); + if (is_array($template->template_dir)) { + array_unshift($template->template_dir, $extDir); + } + else { + $template->template_dir = [$extDir, $template->template_dir]; } - $include_path = $extRoot . PATH_SEPARATOR . get_include_path( ); - set_include_path( $include_path ); + $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); + set_include_path($include_path); } /** - * (Delegated) Implementation of hook_civicrm_xmlMenu + * (Delegated) Implements hook_civicrm_xmlMenu(). * * @param $files array(string) + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu */ function _booking_civix_civicrm_xmlMenu(&$files) { foreach (_booking_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) { @@ -112,57 +121,83 @@ function _booking_civix_civicrm_xmlMenu(&$files) { } /** - * Implementation of hook_civicrm_install + * Implements hook_civicrm_install(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install */ function _booking_civix_civicrm_install() { _booking_civix_civicrm_config(); if ($upgrader = _booking_civix_upgrader()) { - return $upgrader->onInstall(); + $upgrader->onInstall(); } } /** - * Implementation of hook_civicrm_uninstall + * Implements hook_civicrm_postInstall(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall + */ +function _booking_civix_civicrm_postInstall() { + _booking_civix_civicrm_config(); + if ($upgrader = _booking_civix_upgrader()) { + if (is_callable([$upgrader, 'onPostInstall'])) { + $upgrader->onPostInstall(); + } + } +} + +/** + * Implements hook_civicrm_uninstall(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall */ function _booking_civix_civicrm_uninstall() { _booking_civix_civicrm_config(); if ($upgrader = _booking_civix_upgrader()) { - return $upgrader->onUninstall(); + $upgrader->onUninstall(); } } /** - * (Delegated) Implementation of hook_civicrm_enable + * (Delegated) Implements hook_civicrm_enable(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable */ function _booking_civix_civicrm_enable() { _booking_civix_civicrm_config(); if ($upgrader = _booking_civix_upgrader()) { - if (is_callable(array($upgrader, 'onEnable'))) { - return $upgrader->onEnable(); + if (is_callable([$upgrader, 'onEnable'])) { + $upgrader->onEnable(); } } } /** - * (Delegated) Implementation of hook_civicrm_disable + * (Delegated) Implements hook_civicrm_disable(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable + * @return mixed */ function _booking_civix_civicrm_disable() { _booking_civix_civicrm_config(); if ($upgrader = _booking_civix_upgrader()) { - if (is_callable(array($upgrader, 'onDisable'))) { - return $upgrader->onDisable(); + if (is_callable([$upgrader, 'onDisable'])) { + $upgrader->onDisable(); } } } /** - * (Delegated) Implementation of hook_civicrm_upgrade + * (Delegated) Implements hook_civicrm_upgrade(). * * @param $op string, the type of operation being performed; 'check' or 'enqueue' * @param $queue CRM_Queue_Queue, (for 'enqueue') the modifiable list of pending up upgrade tasks * - * @return mixed based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending) - * for 'enqueue', returns void + * @return mixed + * based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending) + * for 'enqueue', returns void + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade */ function _booking_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { if ($upgrader = _booking_civix_upgrader()) { @@ -170,31 +205,36 @@ function _booking_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { } } +/** + * @return CRM_Booking_Upgrader + */ function _booking_civix_upgrader() { - if (!file_exists(__DIR__.'/CRM/Booking/Upgrader.php')) { + if (!file_exists(__DIR__ . '/CRM/Booking/Upgrader.php')) { return NULL; - } else { + } + else { return CRM_Booking_Upgrader_Base::instance(); } } /** - * Search directory tree for files which match a glob pattern + * Search directory tree for files which match a glob pattern. * * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored. * Note: In Civi 4.3+, delegate to CRM_Utils_File::findFiles() * - * @param $dir string, base dir - * @param $pattern string, glob pattern, eg "*.txt" - * @return array(string) + * @param string $dir base dir + * @param string $pattern , glob pattern, eg "*.txt" + * + * @return array */ function _booking_civix_find_files($dir, $pattern) { - if (is_callable(array('CRM_Utils_File', 'findFiles'))) { + if (is_callable(['CRM_Utils_File', 'findFiles'])) { return CRM_Utils_File::findFiles($dir, $pattern); } - $todos = array($dir); - $result = array(); + $todos = [$dir]; + $result = []; while (!empty($todos)) { $subdir = array_shift($todos); foreach (_booking_civix_glob("$subdir/$pattern") as $match) { @@ -205,8 +245,9 @@ function _booking_civix_find_files($dir, $pattern) { if ($dh = opendir($subdir)) { while (FALSE !== ($entry = readdir($dh))) { $path = $subdir . DIRECTORY_SEPARATOR . $entry; - if ($entry{0} == '.') { - } elseif (is_dir($path)) { + if ($entry[0] == '.') { + } + elseif (is_dir($path)) { $todos[] = $path; } } @@ -215,24 +256,103 @@ function _booking_civix_find_files($dir, $pattern) { } return $result; } + /** - * (Delegated) Implementation of hook_civicrm_managed + * (Delegated) Implements hook_civicrm_managed(). * * Find any *.mgd.php files, merge their content, and return. + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed */ function _booking_civix_civicrm_managed(&$entities) { $mgdFiles = _booking_civix_find_files(__DIR__, '*.mgd.php'); + sort($mgdFiles); foreach ($mgdFiles as $file) { $es = include $file; foreach ($es as $e) { if (empty($e['module'])) { - $e['module'] = 'uk.co.compucorp.civicrm.booking'; + $e['module'] = E::LONG_NAME; + } + if (empty($e['params']['version'])) { + $e['params']['version'] = '3'; } $entities[] = $e; } } } +/** + * (Delegated) Implements hook_civicrm_caseTypes(). + * + * Find any and return any files matching "xml/case/*.xml" + * + * Note: This hook only runs in CiviCRM 4.4+. + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes + */ +function _booking_civix_civicrm_caseTypes(&$caseTypes) { + if (!is_dir(__DIR__ . '/xml/case')) { + return; + } + + foreach (_booking_civix_glob(__DIR__ . '/xml/case/*.xml') as $file) { + $name = preg_replace('/\.xml$/', '', basename($file)); + if ($name != CRM_Case_XMLProcessor::mungeCaseType($name)) { + $errorMessage = sprintf("Case-type file name is malformed (%s vs %s)", $name, CRM_Case_XMLProcessor::mungeCaseType($name)); + throw new CRM_Core_Exception($errorMessage); + } + $caseTypes[$name] = [ + 'module' => E::LONG_NAME, + 'name' => $name, + 'file' => $file, + ]; + } +} + +/** + * (Delegated) Implements hook_civicrm_angularModules(). + * + * Find any and return any files matching "ang/*.ang.php" + * + * Note: This hook only runs in CiviCRM 4.5+. + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules + */ +function _booking_civix_civicrm_angularModules(&$angularModules) { + if (!is_dir(__DIR__ . '/ang')) { + return; + } + + $files = _booking_civix_glob(__DIR__ . '/ang/*.ang.php'); + foreach ($files as $file) { + $name = preg_replace(':\.ang\.php$:', '', basename($file)); + $module = include $file; + if (empty($module['ext'])) { + $module['ext'] = E::LONG_NAME; + } + $angularModules[$name] = $module; + } +} + +/** + * (Delegated) Implements hook_civicrm_themes(). + * + * Find any and return any files matching "*.theme.php" + */ +function _booking_civix_civicrm_themes(&$themes) { + $files = _booking_civix_glob(__DIR__ . '/*.theme.php'); + foreach ($files as $file) { + $themeMeta = include $file; + if (empty($themeMeta['name'])) { + $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file)); + } + if (empty($themeMeta['ext'])) { + $themeMeta['ext'] = E::LONG_NAME; + } + $themes[$themeMeta['name']] = $themeMeta; + } +} + /** * Glob wrapper which is guaranteed to return an array. * @@ -241,50 +361,117 @@ function _booking_civix_civicrm_managed(&$entities) { * result for an empty match is sometimes array() and sometimes FALSE. * This wrapper provides consistency. * - * @see http://php.net/glob + * @link http://php.net/glob * @param string $pattern - * @return array, possibly empty + * + * @return array */ function _booking_civix_glob($pattern) { $result = glob($pattern); - return is_array($result) ? $result : array(); + return is_array($result) ? $result : []; } /** - * Inserts a navigation menu item at a given place in the hierarchy + * Inserts a navigation menu item at a given place in the hierarchy. + * + * @param array $menu - menu hierarchy + * @param string $path - path to parent of this item, e.g. 'my_extension/submenu' + * 'Mailing', or 'Administer/System Settings' + * @param array $item - the item to insert (parent/child attributes will be + * filled for you) * - * $menu - menu hierarchy - * $path - path where insertion should happen (ie. Administer/System Settings) - * $item - menu you need to insert (parent/child attributes will be filled for you) - * $parentId - used internally to recurse in the menu structure + * @return bool */ -function _booking_civix_insert_navigation_menu(&$menu, $path, $item, $parentId = NULL) { - static $navId; - +function _booking_civix_insert_navigation_menu(&$menu, $path, $item) { // If we are done going down the path, insert menu if (empty($path)) { - if (!$navId) $navId = CRM_Core_DAO::singleValueQuery("SELECT max(id) FROM civicrm_navigation"); - $navId ++; - $menu[$navId] = array ( - 'attributes' => array_merge($item, array( + $menu[] = [ + 'attributes' => array_merge([ 'label' => CRM_Utils_Array::value('name', $item), 'active' => 1, - 'parentID' => $parentId, - 'navID' => $navId, - )) - ); - return true; - } else { + ], $item), + ]; + return TRUE; + } + else { // Find an recurse into the next level down - $found = false; + $found = FALSE; $path = explode('/', $path); $first = array_shift($path); foreach ($menu as $key => &$entry) { if ($entry['attributes']['name'] == $first) { - if (!$entry['child']) $entry['child'] = array(); - $found = _booking_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item, $key); + if (!isset($entry['child'])) { + $entry['child'] = []; + } + $found = _booking_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item); } } return $found; } } + +/** + * (Delegated) Implements hook_civicrm_navigationMenu(). + */ +function _booking_civix_navigationMenu(&$nodes) { + if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) { + _booking_civix_fixNavigationMenu($nodes); + } +} + +/** + * Given a navigation menu, generate navIDs for any items which are + * missing them. + */ +function _booking_civix_fixNavigationMenu(&$nodes) { + $maxNavID = 1; + array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) { + if ($key === 'navID') { + $maxNavID = max($maxNavID, $item); + } + }); + _booking_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL); +} + +function _booking_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) { + $origKeys = array_keys($nodes); + foreach ($origKeys as $origKey) { + if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) { + $nodes[$origKey]['attributes']['parentID'] = $parentID; + } + // If no navID, then assign navID and fix key. + if (!isset($nodes[$origKey]['attributes']['navID'])) { + $newKey = ++$maxNavID; + $nodes[$origKey]['attributes']['navID'] = $newKey; + $nodes[$newKey] = $nodes[$origKey]; + unset($nodes[$origKey]); + $origKey = $newKey; + } + if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) { + _booking_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']); + } + } +} + +/** + * (Delegated) Implements hook_civicrm_alterSettingsFolders(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders + */ +function _booking_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { + $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings'; + if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) { + $metaDataFolders[] = $settingsDir; + } +} + +/** + * (Delegated) Implements hook_civicrm_entityTypes(). + * + * Find any *.entityType.php files, merge their content, and return. + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes + */ +function _booking_civix_civicrm_entityTypes(&$entityTypes) { + $entityTypes = array_merge($entityTypes, []); +} diff --git a/booking.php b/booking.php index c6d62fa4..adc7a04e 100755 --- a/booking.php +++ b/booking.php @@ -1,10 +1,8 @@ run( $op ); - - return _booking_civix_civicrm_install(); + _booking_civix_civicrm_install(); } /** * Implementation of hook_civicrm_uninstall */ function booking_civicrm_uninstall() { - return _booking_civix_civicrm_uninstall(); + _booking_civix_civicrm_uninstall(); } /** * Implementation of hook_civicrm_enable */ function booking_civicrm_enable() { - // rebuild the menu so our path is picked up - require_once 'CRM/Core/Invoke.php'; - CRM_Core_Invoke::rebuildMenuAndCaches( ); - return _booking_civix_civicrm_enable(); + _booking_civix_civicrm_enable(); } /** * Implementation of hook_civicrm_disable */ function booking_civicrm_disable() { - return _booking_civix_civicrm_disable(); + _booking_civix_civicrm_disable(); } /** @@ -108,7 +92,7 @@ function booking_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { * is installed, disabled, uninstalled. */ function booking_civicrm_managed(&$entities) { - return _booking_civix_civicrm_managed($entities); + _booking_civix_civicrm_managed($entities); } /** @@ -118,15 +102,14 @@ function booking_civicrm_queryObjects(&$queryObjects, $type) { if ($type == 'Contact') { $queryObjects[] = new CRM_Booking_BAO_Query(); } - elseif ($type == 'Report') {} } /** * Implementation of hook_civicrm_postProcess */ -function booking_civicrm_post( $op, $objectName, $objectId, &$objectRef ) { - if($objectName == 'Contribution'){ - if($op == 'delete'){ +function booking_civicrm_post($op, $objectName, $objectId, &$objectRef) { + if ($objectName == 'Contribution') { + if ($op == 'delete') { CRM_Core_DAO::executeQuery("DELETE FROM civicrm_booking_payment WHERE contribution_id = $objectId"); } } @@ -136,64 +119,70 @@ function booking_civicrm_post( $op, $objectName, $objectId, &$objectRef ) { * Implementation of hook_civicrm_entityTypes */ function booking_civicrm_entityTypes(&$entityTypes) { - $entityTypes[] = array( + $entityTypes[] = [ 'name' => 'AdhocCharges', 'class' => 'CRM_Booking_DAO_AdhocCharges', 'table' => 'civicrm_booking_adhoc_charges', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'AdhocChargesItem', 'class' => 'CRM_Booking_DAO_AdhocChargesItem', 'table' => 'civicrm_booking_adhoc_charges_item', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'Booking', 'class' => 'CRM_Booking_DAO_Booking', 'table' => 'civicrm_booking', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'BookingPayment', 'class' => 'CRM_Booking_DAO_Payment', 'table' => 'civicrm_booking_payment', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'Resource', 'class' => 'CRM_Booking_DAO_Resource', 'table' => 'civicrm_booking_resource', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'ResourceConfigOption', 'class' => 'CRM_Booking_DAO_ResourceConfigOption', 'table' => 'civicrm_booking_resource_config_option', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'ResourceConfigSet', 'class' => 'CRM_Booking_DAO_ResourceConfigSet', 'table' => 'civicrm_booking_resource_config_set', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'Slot', 'class' => 'CRM_Booking_DAO_Slot', 'table' => 'civicrm_booking_slot', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'SubSlot', 'class' => 'CRM_Booking_DAO_SubSlot', 'table' => 'civicrm_booking_sub_slot', - ); - $entityTypes[] = array( + ]; + $entityTypes[] = [ 'name' => 'Cancellation', 'class' => 'CRM_Booking_DAO_Cancellation', 'table' => 'civicrm_booking_cancellation' - ); + ]; } /** - * Implementation of hook_civicrm_merge + * Implements hook_civicrm_alterSettingsFolders(). */ -function booking_civicrm_merge ( $type, &$data, $mainId = NULL, $otherId = NULL, $tables = NULL ){ -if (!empty($mainId) && !empty($otherId) && $type == 'sqls'){ +function booking_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { + _booking_civix_civicrm_alterSettingsFolders($metaDataFolders); +} +/** + * Implementation of hook_civicrm_merge + */ +function booking_civicrm_merge ($type, &$data, $mainId = NULL, $otherId = NULL, $tables = NULL) { + if (!empty($mainId) && !empty($otherId) && $type == 'sqls') { $query1 = " UPDATE civicrm_booking SET primary_contact_id=$mainId @@ -205,24 +194,13 @@ function booking_civicrm_merge ( $type, &$data, $mainId = NULL, $otherId = NULL, WHERE secondary_contact_id=$otherId; "; - require_once('CRM/Core/DAO.php'); - $dao = CRM_Core_DAO::executeQuery( $query1 ); - $dao = CRM_Core_DAO::executeQuery( $query2 ); - - } -} + $dao = CRM_Core_DAO::executeQuery($query1); + $dao = CRM_Core_DAO::executeQuery($query2); -function civibooking_getMenuKeyMax($menuArray) { - $max = array(max(array_keys($menuArray))); - foreach($menuArray as $v) { - if (!empty($v['child'])) { - $max[] = civibooking_getMenuKeyMax($v['child']); - } } - return max($max); } -function booking_civicrm_permission(&$permissions){ +function booking_civicrm_permission(&$permissions) { $prefix = E::ts('CiviBooking') . ': '; $permissions['administer CiviBooking'] = $prefix . E::ts('administer CiviBooking'); $permissions['create and update bookings'] = $prefix . E::ts('create and update bookings'); @@ -234,40 +212,40 @@ function booking_civicrm_permission(&$permissions){ * @see function _civicrm_api3_permissions for mentioned uppercase issue */ function booking_civicrm_alterAPIPermissions($entity, $action, &$params, &$permissions) { - $commonBookingAPIPermissions = array( - 'create' => array( + $commonBookingAPIPermissions = [ + 'create' => [ 'administer CiviBooking', - ), - 'delete' => array( + ], + 'delete' => [ 'administer CiviBooking', - ), - 'get' => array( - array( + ], + 'get' => [ + [ 'administer CiviBooking', 'create and update bookings', 'view all bookings', - ) - ), - 'update' => array( + ], + ], + 'update' => [ 'administer CiviBooking', - ), - ); + ], + ]; - $bookingEntities = array( + $bookingEntities = [ 'BookingPayment', 'Booking', 'Cancellation', 'Slot', 'SubSlot' - ); + ]; - $configEntities = array( + $configEntities = [ 'AdhocChargesItem', 'AdhocCharges', 'ResourceConfigOption', 'ResourceConfigSet', 'Resource', - ); + ]; // set common permissions foreach (array_merge($bookingEntities, $configEntities) as $entityName) { @@ -277,7 +255,7 @@ function booking_civicrm_alterAPIPermissions($entity, $action, &$params, &$permi //add custom permissions for create/update role foreach ($bookingEntities as $entityName) { - $permissionArray = array(array('administer CiviBooking', 'create and update bookings')); + $permissionArray = [['administer CiviBooking', 'create and update bookings']]; // permissions implementation needs lowercase entities $entityName = _civicrm_api_get_entity_name_from_camel($entityName); $permissions[$entityName]['create'] = $permissionArray; @@ -297,3 +275,14 @@ function booking_civicrm_apiWrappers(&$wrappers, $apiRequest) { $wrappers[] = new CRM_Booking_APIWrapper(); } } + +/** + * Implements hook_civicrm_navigationMenu(). + */ +function booking_civicrm_navigationMenu(&$menu) { + $menuItems = CRM_Booking_Upgrader::getMenuItems(); + foreach ($menuItems as $item) { + _booking_civix_insert_navigation_menu($menu, $item['parent_name'], $item); + } + _booking_civix_navigationMenu($menu); +} diff --git a/js/booking/add-sub-resource/app.js b/js/booking/add-sub-resource/app.js index e73dd956..412f469a 100644 --- a/js/booking/add-sub-resource/app.js +++ b/js/booking/add-sub-resource/app.js @@ -1,11 +1,9 @@ - -CRM.BookingApp = new Backbone.Marionette.Application(); // see http://lostechies.com/derickbailey/2012/04/17/managing-a-modal-dialog-with-backbone-and-marionette/ -var ModalRegion = Backbone.Marionette.Region.extend({ +var ModalRegion = Marionette.Region.extend({ el: "#crm-booking-dialog", constructor: function(){ - Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); + Marionette.Region.prototype.constructor.apply(this, arguments); this.on("show", this.showModal, this); this.on("close", this.hideModal, this); @@ -27,22 +25,34 @@ var ModalRegion = Backbone.Marionette.Region.extend({ cj('#crm-booking-dialog').dialog().dialog("close"); } }); - -CRM.BookingApp.addRegions({ - main: "#resource-main", - modal: ModalRegion - +var ResourceTotal = new Array(); +var PriceCache = new Array(); +// The addRegions method has been removed and not present in the +// current Marionette version. The Application.extend could be the +// solution for passing the necessary parameters to the application. +var MyApp = Marionette.Application.extend({ + region: "#resource-main", + modal: new ModalRegion(), + Utils: { + //http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript + getCurrentUnixTimstamp: function () { + return unix = Math.round(+new Date()/1000); + }, + //http://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer + isPositiveInteger: function (n){ + return 0 === n % (!isNaN(parseFloat(n)) && 0 <= ~~n); + }, + isPositiveNumber: function (n){ + return !_.isNumber(n) && 0 <= n; + }, + }, + Entities: Entities, + onStart: function(app, options) { + this.getRegion().show(new Views.ResourceTableView({model: new this.Entities.SubResource()})); + }, }); +CRM.BookingApp = new MyApp(); CRM.BookingApp.on("initialize:after", function(){ if( ! Backbone.History.started) Backbone.history.start(); }); - -CRM.BookingApp.addInitializer(function(){ - var model = new CRM.BookingApp.Entities.SubResource(); - var view = new CRM.BookingApp.AddSubResource.ResourceTableView({model: model}); - - CRM.BookingApp.main.show(view); -}); - - diff --git a/js/booking/add-sub-resource/entities.js b/js/booking/add-sub-resource/entities.js index 1a46d193..543d015d 100644 --- a/js/booking/add-sub-resource/entities.js +++ b/js/booking/add-sub-resource/entities.js @@ -1,7 +1,5 @@ - -CRM.BookingApp.module('Entities', function(Entities, BookingApp, Backbone, Marionette, $, _){ - - Entities.SubResource = Backbone.Model.extend({ +var Entities = { + SubResource: Backbone.Model.extend({ defaults: { sub_resources: {}, resources: {}, @@ -11,9 +9,8 @@ CRM.BookingApp.module('Entities', function(Entities, BookingApp, Backbone, Mario total_price:0 }, - }); - - Entities.AddSubResource = Backbone.Model.extend({ + }), + AddSubResource: Backbone.Model.extend({ defaults: { parent_ref_id: null, ref_id: null, @@ -24,14 +21,12 @@ CRM.BookingApp.module('Entities', function(Entities, BookingApp, Backbone, Mario note: null, price_estimate: 0, }, - }); - - Entities.AdhocCharges = Backbone.Model.extend({ + }), + AdhocCharges: Backbone.Model.extend({ defaults: { items: {}, note: null, total: 0, }, - }); - -}); + }), +}; diff --git a/js/booking/add-sub-resource/view.js b/js/booking/add-sub-resource/view.js index a8ced7a1..fea6e229 100755 --- a/js/booking/add-sub-resource/view.js +++ b/js/booking/add-sub-resource/view.js @@ -1,107 +1,111 @@ -(function ($, ts){ - -/* - * View classes belong to the second wizard screen of create/edit booking - */ -CRM.BookingApp.module('AddSubResource', function(AddSubResource, BookingApp, Backbone, Marionette, $, _) { - - var startDate; - var endDate; - var unlimitedTimeConfig; - var resourceTotal = new Array(); - var priceCache = new Array(); - - CRM.BookingApp.vent.on("update:resources", function(model) { - $('#sub_resources').val(JSON.stringify(model.toJSON())); - }); - - CRM.BookingApp.vent.on("render:price", function(model) { - $("#total_price").val(model.attributes.total_price); - var totalText = model.attributes.total_price; - try{ - if(model.attributes.total_price>=0){ - var totalText = model.attributes.total_price.toFixed(2); - } - }catch(err){} - $("#total-price-summary").text(totalText); - - $("#discount_amount").val(model.attributes.discount_amount); - $('#discount_amount_dummy').val(model.attributes.discount_amount); - - $("#sub_total").val(model.attributes.sub_total); - var subtotalText = model.attributes.sub_total; - try{ +var Views = { + /** + * A form that use in a Modal that required the validate in the form + * + */ + BookingProcessModal: Marionette.View.extend({ + onRender: function() { + var rules = this.createValidationRules(); + this.$('form').validate(rules); + }, + /** + * + * @return {*} jQuery.validate rules + */ + createValidationRules: function() { + var rules = _.extend({}, CRM.validate.params); + rules.rules || (rules.rules = {}); + this.triggerMethod("validateRules:create", this, rules); + return rules; + }, + onRenderError: function(errors){ + var view = this; + _.each(errors, function(error) { + console.log($(error).attr('for')); + view.$('[name=' + $(error).attr('for') + ']').crmError($(error).text()); + }); + }, + onRenderOptions: function(options) { + var select = options.context.$el.find(options.element); + if (select.is('[disabled]')) { + select.prop('disabled', false); + } + select.html(options.template({ + options : options.list, + first_option : options.first_option + })); + }, + onUpdateResource: function(model) { + CRM.$('#sub_resources').val(JSON.stringify(model.toJSON())); + }, + onRenderPrice: function(model) { + CRM.$("#total_price").val(model.attributes.total_price); + var totalText = model.attributes.total_price; + try{ + if(model.attributes.total_price>=0){ + var totalText = model.attributes.total_price.toFixed(2); + } + }catch(err){} + CRM.$("#total-price-summary").text(totalText); + CRM.$("#discount_amount").val(model.attributes.discount_amount); + CRM.$('#discount_amount_dummy').val(model.attributes.discount_amount); + CRM.$("#sub_total").val(model.attributes.sub_total); + var subtotalText = model.attributes.sub_total; + try{ var subtotalText = model.attributes.sub_total.toFixed(2); - }catch(err){} - $("#sub-total-summary").text(subtotalText); - - $('#adhoc_charge').val(model.attributes.adhoc_charges.total); - $('#ad-hoc-charge-summary').html(model.attributes.adhoc_charges.total); - }); - - CRM.BookingApp.vent.on("render:options", function(options) { - var select = options.context.$el.find(options.element); - if (select.is('[disabled]')) { - select.prop('disabled', false); - } - select.html(options.template({ - options : options.list, - first_option : options.first_option - })); - }); - + }catch(err){} + CRM.$("#sub-total-summary").text(subtotalText); + CRM.$('#adhoc_charge').val(model.attributes.adhoc_charges.total); + CRM.$('#ad-hoc-charge-summary').html(model.attributes.adhoc_charges.total); + }, + }), //Resource table view - AddSubResource.ResourceTableView = Backbone.Marionette.ItemView.extend({ - template: '#resource-table-template', + ResourceTableView: Marionette.View.extend({ + template: CRM._.template(CRM.$('#resource-table-template').html()), initialize: function(){ - if ($.trim($("#sub_resources").val())) { - this.model.attributes = JSON.parse($.trim($("#sub_resources").val())); + if (CRM.$.trim(CRM.$("#sub_resources").val())) { + this.model.attributes = JSON.parse(CRM.$.trim(CRM.$("#sub_resources").val())); } - this.model.attributes.total_price = $("#total_price").val(); - this.model.attributes.sub_total = $("#sub_total").val(); - //this.model.attributes.adhoc_charges = $("#adhoc_charge").val(); - this.model.attributes.discount_amount = $("#discount_amount").val(); + this.model.attributes.total_price = CRM.$("#total_price").val(); + this.model.attributes.sub_total = CRM.$("#sub_total").val(); + //this.model.attributes.adhoc_charges = CRM.$("#adhoc_charge").val(); + this.model.attributes.discount_amount = CRM.$("#discount_amount").val(); }, - onRender: function(){ var subtotal = 0; var self = this; //init the current price for each resource this.$el.find("span[id^='resource-price-']").each(function(){ - var el = $(this); + var el = CRM.$(this); self.model.attributes.resources[el.data('ref')] = el.text(); }); var items = []; - var template = _.template($('#sub-resource-row-template').html()); - _.each(this.model.get('sub_resources'), function (item, key){ + var template = CRM._.template(CRM.$('#sub-resource-row-template').html()); + CRM._.each(this.model.get('sub_resources'), function (item, key){ self.$el.find("#crm-booking-sub-resource-table-" + item.parent_ref_id).append(template(item)); - priceCache[item.ref_id] = item.price_estimate; + PriceCache[item.ref_id] = item.price_estimate; items.push(item); }); - //if($.trim($("#sub_resources").val())) { - this.$el.find("span[id^='resource-total-price-']").each(function(){ - var el = $(this);/////////////////////////// - var resourceTotalPrice = parseFloat(el.data('price')); - _.find(items, function (item) { - - if(parseInt(item.parent_ref_id) === parseInt(el.data('ref'))){ + this.$el.find("span[id^='resource-total-price-']").each(function(){ + var el = CRM.$(this);/////////////////////////// + var resourceTotalPrice = parseFloat(el.data('price')); + _.find(items, function (item) { + if(parseInt(item.parent_ref_id) === parseInt(el.data('ref'))){ resourceTotalPrice += parseFloat(item.price_estimate); - } - - }); - if(resourceTotalPrice != null){ - subtotal += resourceTotalPrice; - el.text(resourceTotalPrice.toFixed(2)); - resourceTotal[el.data('ref')] = resourceTotalPrice.toFixed(2); - self.$el.find('#crm-booking-sub-resource-row-' + el.data('ref')).show(); } }); - //} + if(resourceTotalPrice != null){ + subtotal += resourceTotalPrice; + el.text(resourceTotalPrice.toFixed(2)); + ResourceTotal[el.data('ref')] = resourceTotalPrice.toFixed(2); + self.$el.find('#crm-booking-sub-resource-row-' + el.data('ref')).show(); + } + }); this.model.attributes.sub_total = subtotal; this.model.attributes.total_price = (subtotal - + parseFloat(this.model.get("adhoc_charges").total)) - - parseFloat(this.model.get("discount_amount")); + + parseFloat(this.model.get("adhoc_charges").total)) + - parseFloat(this.model.get("discount_amount")); this.model.attributes.discount_amount = this.model.get("discount_amount"); unlimitedTimeConfig = timeConfig; @@ -130,66 +134,63 @@ CRM.BookingApp.module('AddSubResource', function(AddSubResource, BookingApp, Bac }, addSubResource: function(e){ - var ref = $(e.currentTarget).data('ref');///////////////// - //resourceTotal[ref] = 0; - endDate = $(e.currentTarget).data('edate'); - startDate = $(e.currentTarget).data('sdate'); - var model = new CRM.BookingApp.Entities.AddSubResource({parent_ref_id:ref, time_required:startDate}); - var view = new AddSubResource.AddSubResourceModal({model: model, is_new: true}); - view.title = ts('Add Unlimited Resource'); - CRM.BookingApp.modal.show(view); + var ref = CRM.$(e.currentTarget).data('ref');///////////////// + endDate = CRM.$(e.currentTarget).data('edate'); + startDate = CRM.$(e.currentTarget).data('sdate'); + var model = new Entities.AddSubResource({parent_ref_id:ref, time_required:startDate}); + var view = new Views.AddSubResourceModal({model: model, is_new: true}); + view.title = ts('Add Unlimited Resource'); + CRM.BookingApp.modal.show(view); }, addDiscountAmount: function(e){ var currentSubTotal = parseFloat(this.model.get('sub_total')); var currentAdhocCharges = parseFloat(this.model.get('adhoc_charges').total); - // Get the discount amount stripping out non-numeric characters - var sDiscountAmount = $(e.currentTarget).val().replace(/[^\d.-]/g, ''); + var sDiscountAmount = CRM.$(e.currentTarget).val().replace(/[^\d.-]/g, ''); var fDiscountAmount = parseFloat(sDiscountAmount); if (!_.isNumber(fDiscountAmount) || _.isNaN(fDiscountAmount)) { - fDiscountAmount = 0; - sDiscountAmount = ''; + fDiscountAmount = 0; + sDiscountAmount = ''; } - var newTotal = (currentSubTotal + currentAdhocCharges) - fDiscountAmount; try{newTotal = newTotal.toFixed(2); }catch(err){} this.model.set("total_price", newTotal); this.model.set("discount_amount", sDiscountAmount); - CRM.BookingApp.vent.trigger('render:price', this.model ); - + this.triggerMethod('render:price', this.model); }, - editAdhocCharge: function(e) { - var model = new CRM.BookingApp.Entities.AdhocCharges({ - items : this.model.get('adhoc_charges').items, - note : this.model.get('adhoc_charges').note, - total : this.model.get('adhoc_charges').total - }); - var view = new AddSubResource.EditAdhocChargesModal({ - model : model - }); - view.title = ts('Edit Additional Charges'); - CRM.BookingApp.modal.show(view); - }, + editAdhocCharge: function(e) { + var model = new Entities.AdhocCharges({ + items : this.model.get('adhoc_charges').items, + note : this.model.get('adhoc_charges').note, + total : this.model.get('adhoc_charges').total + }); + var view = new Views.EditAdhocChargesModal({ + model : model + }); + view.title = ts('Edit Additional Charges'); + CRM.BookingApp.modal.show(view); + }, toggleHiddenElement: function(e){ - var row = $(e.currentTarget).data('ref'); - $('#crm-booking-sub-resource-row-' + row).toggle(); + var row = CRM.$(e.currentTarget).data('ref'); + CRM.$('#crm-booking-sub-resource-row-' + row).toggle(); }, removeSubResource: function(e){ - var ref = $(e.currentTarget).data('ref'); - var parentRef = $(e.currentTarget).data('parent-ref'); - var price = $(e.currentTarget).data('price'); - $('#crm-booking-sub-resource-individual-row-' + ref).remove(); + var self = this; + var ref = CRM.$(e.currentTarget).data('ref'); + var parentRef = CRM.$(e.currentTarget).data('parent-ref'); + var price = CRM.$(e.currentTarget).data('price'); + CRM.$('#crm-booking-sub-resource-individual-row-' + ref).remove(); delete this.model.attributes.sub_resources[ref]; var newResourcePrice = parseFloat(this.model.get("resources")[parentRef]) - parseFloat(price); this.model.attributes.resources[parentRef] = newResourcePrice; - resourceTotal[parentRef] -= parseFloat(price); - try{resourceTotal[parentRef] = resourceTotal[parentRef].toFixed(2);}catch(err){} - $("#resource-total-price-" + parentRef).text(resourceTotal[parentRef]); + ResourceTotal[parentRef] -= parseFloat(price); + try{ResourceTotal[parentRef] = ResourceTotal[parentRef].toFixed(2);}catch(err){} + CRM.$("#resource-total-price-" + parentRef).text(ResourceTotal[parentRef]); var currentSubTotal = this.model.get('sub_total'); var newSubTotal = parseFloat(this.model.get('sub_total') - parseFloat(price)); var currentTotal = this.model.get('total_price'); @@ -198,413 +199,428 @@ CRM.BookingApp.module('AddSubResource', function(AddSubResource, BookingApp, Bac this.model.set("sub_total", newSubTotal); this.model.set("total_price", newTotal); - CRM.BookingApp.vent.trigger('render:price', this.model , parentRef ); - CRM.BookingApp.vent.trigger('update:resources', this.model); + self.triggerMethod('render:price', this.model); + self.triggerMethod('update:resources', this.model); CRM.alert(ts(''), ts('Unlimited resource removed'), 'success'); }, //when edit sub resource editSubResource: function(e) { - var refId = $(e.currentTarget).data('ref'); //retrieve id from attribute data-ref - var parentRef = $(e.currentTarget).data('parent-ref'); //retrieve id from attribute data-parent-ref - var timeRequired = $(e.currentTarget).data('time-required'); //retrieve datetime from attribute data-time-required + var refId = CRM.$(e.currentTarget).data('ref'); //retrieve id from attribute data-ref + var parentRef = CRM.$(e.currentTarget).data('parent-ref'); //retrieve id from attribute data-parent-ref + var timeRequired = CRM.$(e.currentTarget).data('time-required'); //retrieve datetime from attribute data-time-required selectedItem = this.model.attributes.sub_resources[refId]; //create backbone model form json object - var model = new CRM.BookingApp.Entities.AddSubResource({ - parent_ref_id : parentRef, - ref_id : refId, - resource: {id : selectedItem.resource.id, label :selectedItem.resource.label}, - configuration: selectedItem.configuration, - quantity: selectedItem.quantity, - time_required: timeRequired, - note: selectedItem.note, - price_estimate: selectedItem.price_estimate, + var model = new Entities.AddSubResource({ + parent_ref_id : parentRef, + ref_id : refId, + resource: {id : selectedItem.resource.id, label :selectedItem.resource.label}, + configuration: selectedItem.configuration, + quantity: selectedItem.quantity, + time_required: timeRequired, + note: selectedItem.note, + price_estimate: selectedItem.price_estimate, }); //create backbone view - var view = new AddSubResource.AddSubResourceModal({ - model : model, - is_new: false + var view = new Views.AddSubResourceModal({ + model : model, + is_new: false }); view.title = ts('Edit unlimited resource'); CRM.BookingApp.modal.show(view); - } - - }); - - //Sub(Unlimited) resource dialog view - AddSubResource.AddSubResourceModal = BookingApp.Common.Views.BookingProcessModal.extend({ - template: "#add-sub-resource-template", - initialize: function(options){ - this.isNew = options.is_new; - }, - events: { - 'click #add-to-basket': 'addSubResource', - 'change #resource_select': 'getConfigurations', - 'change #configuration_select': 'updatePriceEstmate', - 'keypress #quantity': 'updatePriceEstmate', - 'keyup #quantity': 'updatePriceEstmate', - 'keydown #quantity': 'updatePriceEstmate', }, - onRender: function(){ - BookingApp.Common.Views.BookingProcessModal.prototype.onRender.apply(this, arguments); - - var thisView = this; //set 'this' object for calling inside callback function - this.$el.find('#loading').show(); - - - - var initsdate = moment(this.model.get('time_required'), "YYYY-MM-DD HH:mm:ss"); - var timeTxt = [initsdate.hours() < 10 ? '0' + initsdate.hours() : initsdate.hours(), ":", initsdate.minute() < 10 ? '0' + initsdate.minute() : initsdate.minute()].join(""); - - //set the formatted months - var month=new Array(); - month[0]="01"; - month[1]="02"; - month[2]="03"; - month[3]="04"; - month[4]="05"; - month[5]="06"; - month[6]="07"; - month[7]="08"; - month[8]="09"; - month[9]="10"; - month[10]="11"; - month[11]="12"; - var dateTxt = [ initsdate.format("DD"),"/", month[initsdate.months()],"/", initsdate.years()].join(""); - this.$el.find("#required_date").val(dateTxt); - this.$el.find("#required_time").val(timeTxt); - - CRM.api('Resource', 'get', {'sequential': 1, 'is_unlimited': 1, 'is_deleted': 0, 'is_active': 1}, - {success: function(data) { - thisView.template = _.template($('#add-sub-resource-template').html()); - //var configValue = CRM_Booking_BAO_BookingConfig::getConfig(); - - thisView.$el.find("#required_date").datepicker({changeMonth: true, changeYear: true, dateFormat: 'dd/mm/yy'}); - thisView.$el.find('#required_time').timeEntry({show24Hours: true}).change(function() { - var log = $('#log'); - log.val(log.val() + ($('#defaultEntry').val() || 'blank') + '\n'); - }); - - //thisView.resources = data.values; - var tpl = _.template($('#select-option-template').html()); - var params = { - context: thisView, - template: tpl, - list: data.values, - element: "#resource_select", - first_option: ['- ', ts('select resource'), ' -'].join("") - } - CRM.BookingApp.vent.trigger("render:options", params); - - if(thisView.isNew == false){ - //set values - thisView.$el.find("#resource_select").val(thisView.model.get('resource').id); - //set configuration option value - thisView.$el.find("#configuration_select").attr('data-selected-id',thisView.model.get('configuration').id); - thisView.getConfigurations(); //call inside callback function - thisView.$el.find("#quantity").val(thisView.model.get('quantity')); - thisView.$el.find("#sub-resource-note").val(thisView.model.get('note')); - thisView.$el.find("#price-estimate").html(thisView.model.get('price_estimate')); - - thisView.$el.find("#quantity").prop('disabled', false); //enable quantity input text - } - thisView.$el.find('#loading').hide(); - thisView.$el.find('#content').show(); - } + onRenderPrice: function(model) { + CRM.$("#total_price").val(model.attributes.total_price); + var totalText = model.attributes.total_price; + try{ + if(model.attributes.total_price>=0){ + var totalText = model.attributes.total_price.toFixed(2); } - ); + }catch(err){} + CRM.$("#total-price-summary").text(totalText); + CRM.$("#discount_amount").val(model.attributes.discount_amount); + CRM.$('#discount_amount_dummy').val(model.attributes.discount_amount); + CRM.$("#sub_total").val(model.attributes.sub_total); + var subtotalText = model.attributes.sub_total; + try{ + var subtotalText = model.attributes.sub_total.toFixed(2); + }catch(err){} + CRM.$("#sub-total-summary").text(subtotalText); + CRM.$('#adhoc_charge').val(model.attributes.adhoc_charges.total); + CRM.$('#ad-hoc-charge-summary').html(model.attributes.adhoc_charges.total); }, + }), +}; + //Additaional charges dialog view +Views['EditAdhocChargesModal'] = Views.BookingProcessModal.extend({ + template: CRM._.template(CRM.$('#edit-adhoc-charges-template').html()), + className: "modal-dialog", + onRender: function(){ + var thisView = this; + CRM._.each(this.model.get('items'), function(item){ + thisView.$el.find('#' + item.name).html(item.item_price); + thisView.$el.find('input[name="' + item.name + '"]').val(item.quantity); + }); + this.$el.find('#adhoc-charges-note').val(this.model.get('note')); + this.$el.find('#total-adhoc-charges').html(this.model.get('total')); + Views.BookingProcessModal.prototype.onRender.apply(this, arguments); + }, + events: { + 'keypress .item': 'updatePrice', + 'keyup .item': 'updatePrice', + 'keydown .item': 'updatePrice', + 'click #update-adhoc-charges': 'updateAdhocCharges', + }, + updatePrice: function(e){ + var el = CRM.$(e.currentTarget); + var itemId = el.data('id'); + var price = el.data('price'); + var quantity = el.val(); + var name = el.attr('name'); + if(CRM.BookingApp.Utils.isPositiveInteger(quantity)){ + var itemPrice = parseFloat(price) * parseFloat(quantity); + this.$el.find('#'+ name).html(parseFloat(itemPrice).toFixed(2)); + var item = {item_id: itemId, name: name, price: price, quantity: quantity, item_price: itemPrice} + this.model.attributes.items[itemId] = item; + }else{ + this.$el.find('#'+ name).html(0); + this.$el.find('input[name="'+ name + '"]').val(''); + delete this.model.attributes.items[itemId]; + } + var items = this.model.get('items'); + var total = 0.0; + CRM._.each(items,function(item){ + total = parseFloat(total) + parseFloat(item.item_price); + }); + this.$el.find('#total-adhoc-charges').html(total.toFixed(2)); + this.model.set('total', total.toFixed(2)); + }, + updateAdhocCharges: function(e){ + e.preventDefault(); + var self = this; + var subResourceModel = CRM.BookingApp.getRegion().currentView.model; + this.model.set('note',this.$el.find('#adhoc-charges-note').val() ); + var adhocChargesTotal = this.model.get('total'); + subResourceModel.set('adhoc_charges', this.model.attributes); + var currentTotal = subResourceModel.get('sub_total'); + var discountAmount = subResourceModel.get('discount_amount'); + if(CRM.BookingApp.Utils.isPositiveNumber(discountAmount)){ + var newTotal = (parseFloat(adhocChargesTotal) + parseFloat(currentTotal)) - parseFloat(discountAmount); + }else{ + var newTotal = (parseFloat(adhocChargesTotal) + parseFloat(currentTotal)) - 0; + } + subResourceModel.set("total_price", parseFloat(newTotal).toFixed(2)); + console.log(subResourceModel); + self.triggerMethod('render:price', subResourceModel); + self.triggerMethod('update:resources', subResourceModel); + CRM.BookingApp.modal.triggerMethod('close'); + } +}); +//Sub(Unlimited) resource dialog view +Views['AddSubResourceModal'] = Views.BookingProcessModal.extend({ + template: CRM._.template(CRM.$('#add-sub-resource-template').html()), + initialize: function(options){ + this.isNew = options.is_new; + }, + events: { + 'click #add-to-basket': 'addSubResource', + 'change #resource_select': 'getConfigurations', + 'change #configuration_select': 'updatePriceEstmate', + 'keypress #quantity': 'updatePriceEstmate', + 'keyup #quantity': 'updatePriceEstmate', + 'keydown #quantity': 'updatePriceEstmate', + }, + onRender: function(){ + Views.BookingProcessModal.prototype.onRender.apply(this, arguments); + + var thisView = this; //set 'this' object for calling inside callback function + this.$el.find('#loading').show(); + + var initsdate = moment(this.model.get('time_required'), "YYYY-MM-DD HH:mm:ss"); + var timeTxt = [initsdate.hours() < 10 ? '0' + initsdate.hours() : initsdate.hours(), ":", initsdate.minute() < 10 ? '0' + initsdate.minute() : initsdate.minute()].join(""); + + //set the formatted months + var month=new Array(); + month[0]="01"; + month[1]="02"; + month[2]="03"; + month[3]="04"; + month[4]="05"; + month[5]="06"; + month[6]="07"; + month[7]="08"; + month[8]="09"; + month[9]="10"; + month[10]="11"; + month[11]="12"; + var dateTxt = [ initsdate.format("DD"),"/", month[initsdate.months()],"/", initsdate.years()].join(""); + this.$el.find("#required_date").val(dateTxt); + this.$el.find("#required_time").val(timeTxt); + + CRM.api('Resource', 'get', {'sequential': 1, 'is_unlimited': 1, 'is_deleted': 0, 'is_active': 1}, + {success: function(data) { + thisView.template = CRM._.template(CRM.$('#add-sub-resource-template').html()); + //var configValue = CRM_Booking_BAO_BookingConfig::getConfig(); + + thisView.$el.find("#required_date").datepicker({changeMonth: true, changeYear: true, dateFormat: 'dd/mm/yy'}); + thisView.$el.find('#required_time').timeEntry({show24Hours: true}).change(function() { + var log = CRM.$('#log'); + log.val(log.val() + (CRM.$('#defaultEntry').val() || 'blank') + '\n'); + }); - beforeClose: function() {this.$('form').find("#required_date").datepicker("destroy");}, - - /** - * Define form validation rules - * - * @param View view the view for which validation rules are created - * @param Object r the validation rules for the view - */ - onValidateRulesCreate: function(view, r) { - $.validator.addMethod("withinValidTime", function(value, element) { - var dateVals = $("#required_date").val().split("/"); - var timeVals = $("#required_time").val().split(":"); - var requiredDate = new Date(dateVals[2],dateVals[1]-1,dateVals[0],timeVals[0],timeVals[1]); - var minDate = moment(startDate, "YYYY-MM-DD HH:mm:ss"); - var maxDate = moment(endDate, "YYYY-MM-DD HH:mm:ss"); - if (unlimitedTimeConfig==0){ - return true; - }else{ - var val = requiredDate>=minDate && requiredDate<=maxDate; - return val; - } - }, ts("Please select the date and time during the valid booking time.")); - _.extend(r.rules, { - resource_select: { - required: true - }, - configuration_select: { - required: true - }, - "required_date": { - required: true, - "withinValidTime": true - }, - quantity: { - required: true, - digits: true - }, - }); - }, - //calcualte price - updatePriceEstmate: function(e){ - var quantitySelector = this.$el.find('#quantity'); - if(e.type == 'change'){ - var configSelect = this.$el.find('#configuration_select'); - if(configSelect.val() !== ''){ - configSelect.find(':selected').data('price'); - var price = configSelect.find(':selected').data('price'); - this.model.set('configuration', {id: configSelect.val(), label: configSelect.find(':selected').text(), price: price}); - quantitySelector.prop('disabled', false); - }else{ - qualitySelector.prop('disabled', true); - quantitySelector.val(''); + //thisView.resources = data.values; + var tpl = CRM._.template(CRM.$('#select-option-template').html()); + var params = { + context: thisView, + template: tpl, + list: data.values, + element: "#resource_select", + first_option: ['- ', ts('select resource'), ' -'].join("") + } + thisView.triggerMethod("render:options", params); + + if(thisView.isNew == false){ + //set values + thisView.$el.find("#resource_select").val(thisView.model.get('resource').id); + //set configuration option value + thisView.$el.find("#configuration_select").attr('data-selected-id',thisView.model.get('configuration').id); + thisView.getConfigurations(); //call inside callback function + thisView.$el.find("#quantity").val(thisView.model.get('quantity')); + thisView.$el.find("#sub-resource-note").val(thisView.model.get('note')); + thisView.$el.find("#price-estimate").html(thisView.model.get('price_estimate')); + + thisView.$el.find("#quantity").prop('disabled', false); //enable quantity input text + } + thisView.$el.find('#loading').hide(); + thisView.$el.find('#content').show(); } } - var configPrice = this.model.get('configuration').price - var quantity = quantitySelector.val(); - if(CRM.BookingApp.Utils.isPositiveInteger(quantity)){ - var priceEstimate = quantity * configPrice; - this.model.set('quantity', quantity); - this.model.set('price_estimate', priceEstimate.toFixed(2)); - this.$el.find('#price-estimate').html(priceEstimate.toFixed(2)); + ); + }, + + beforeClose: function() {this.$('form').find("#required_date").datepicker("destroy");}, + + /** + * Define form validation rules + * + * @param View view the view for which validation rules are created + * @param Object r the validation rules for the view + */ + onValidateRulesCreate: function(view, r) { + CRM.$.validator.addMethod("withinValidTime", function(value, element) { + var dateVals = CRM.$("#required_date").val().split("/"); + var timeVals = CRM.$("#required_time").val().split(":"); + var requiredDate = new Date(dateVals[2],dateVals[1]-1,dateVals[0],timeVals[0],timeVals[1]); + var minDate = moment(startDate, "YYYY-MM-DD HH:mm:ss"); + var maxDate = moment(endDate, "YYYY-MM-DD HH:mm:ss"); + if (unlimitedTimeConfig==0){ + return true; + }else{ + var val = requiredDate>=minDate && requiredDate<=maxDate; + return val; } - }, - - //render configuration options - getConfigurations: function(e){ - selectedVal = $('#resource_select').val(); - if(selectedVal !== ""){ - var params = { - id: selectedVal, + }, ts("Please select the date and time during the valid booking time.")); + CRM._.extend(r.rules, { + resource_select: { + required: true + }, + configuration_select: { + required: true + }, + "required_date": { + required: true, + "withinValidTime": true + }, + quantity: { + required: true, + digits: true + }, + }); + }, + //calcualte price + updatePriceEstmate: function(e){ + var quantitySelector = this.$el.find('#quantity'); + if(e.type == 'change'){ + var configSelect = this.$el.find('#configuration_select'); + if(configSelect.val() !== ''){ + configSelect.find(':selected').data('price'); + var price = configSelect.find(':selected').data('price'); + this.model.set('configuration', {id: configSelect.val(), label: configSelect.find(':selected').text(), price: price}); + quantitySelector.prop('disabled', false); + }else{ + qualitySelector.prop('disabled', true); + quantitySelector.val(''); + } + } + var configPrice = this.model.get('configuration').price + var quantity = quantitySelector.val(); + if(CRM.BookingApp.Utils.isPositiveInteger(quantity)){ + var priceEstimate = quantity * configPrice; + this.model.set('quantity', quantity); + this.model.set('price_estimate', priceEstimate.toFixed(2)); + this.$el.find('#price-estimate').html(priceEstimate.toFixed(2)); + } + }, + + //render configuration options + getConfigurations: function(e){ + selectedVal = CRM.$('#resource_select').val(); + if(selectedVal !== ""){ + var params = { + id: selectedVal, + sequential: 1, + 'api.resource_config_set.get': { + id: '$value.set_id', + is_active: 1, + is_deleted: 0, + 'api.resource_config_option.get': { + set_id: '$value.id', + is_active: 1, + is_deleted: 0, + 'api.option_group.get':{ + name: 'booking_size_unit', + }, + 'api.option_value.get':{ + value: '$value.unit_id', sequential: 1, - 'api.resource_config_set.get': { - id: '$value.set_id', - is_active: 1, - is_deleted: 0, - 'api.resource_config_option.get': { - set_id: '$value.id', - is_active: 1, - is_deleted: 0, - 'api.option_group.get':{ - name: 'booking_size_unit', - }, - 'api.option_value.get':{ - value: '$value.unit_id', - sequential: 1, - option_group_id: '$value.api.option_group.get.id' - } - } - } - }; - this.$el.find('#config-loading').show(); - this.$el.find('#configuration_select').hide(); - var self = this; - CRM.api('Resource', 'get', params, - { context: self, - success: function(data) { - var resource = data['values']['0']; - var configSet = data['values']['0']['api.resource_config_set.get']; - if(configSet.count !== 1){ - var url = CRM.url('civicrm/admin/resource/config_set', { - reset: 1, - action: 'update', - id: resource.id - }); - CRM.alert(ts(''), ts('Your resource configuration set is disabled.') - + ' ' + ' ' + ts('Click here to edit configuration set.') + ' ', 'error'); + option_group_id: '$value.api.option_group.get.id' } - else if(configSet['values']['0']['api.resource_config_option.get'].count < 1){ - var url = CRM.url('civicrm/admin/resource/config_set/config_option', { - reset: 1, - sid: configSet['values']['0'].id - }); - CRM.alert(ts(''), ts('Your resource configuration options are all disabled or none have been created.') - + ' ' + ' ' + ts('Click here to edit or create options.') + ' ', 'error'); + } + } + }; + this.$el.find('#config-loading').show(); + this.$el.find('#configuration_select').hide(); + var self = this; + CRM.api('Resource', 'get', params, + { context: self, + success: function(data) { + var resource = data['values']['0']; + var configSet = data['values']['0']['api.resource_config_set.get']; + if(configSet.count !== 1){ + var url = CRM.url('civicrm/admin/resource/config_set', { + reset: 1, + action: 'update', + id: resource.id + }); + CRM.alert(ts(''), ts('Your resource configuration set is disabled.') + + ' ' + ' ' + ts('Click here to edit configuration set.') + ' ', 'error'); + } + else if(configSet['values']['0']['api.resource_config_option.get'].count < 1){ + var url = CRM.url('civicrm/admin/resource/config_set/config_option', { + reset: 1, + sid: configSet['values']['0'].id + }); + CRM.alert(ts(''), ts('Your resource configuration options are all disabled or none have been created.') + + ' ' + ' ' + ts('Click here to edit or create options.') + ' ', 'error'); + } + else{ + var options = data['values']['0']['api.resource_config_set.get']['values']['0']['api.resource_config_option.get']['values']; + self.model.set('resource', {id: resource.id, label: resource.label}); + var params = { + context: self, + template: _.template(CRM.$('#select-config-option-template').html()), + list: options, + element: "#configuration_select", + first_option: '- ' + ts('select configuration') + ' -' } - else{ - var options = data['values']['0']['api.resource_config_set.get']['values']['0']['api.resource_config_option.get']['values']; - self.model.set('resource', {id: resource.id, label: resource.label}); - var params = { - context: self, - template: _.template($('#select-config-option-template').html()), - list: options, - element: "#configuration_select", - first_option: '- ' + ts('select configuration') + ' -' - } - CRM.BookingApp.vent.trigger("render:options", params); - //set configuration options for edit mode of subresource view - var configSelectedId = this.$el.find('#configuration_select').data('selected-id'); //retrieve data from data-selected-id attribute - if(configSelectedId != 'undefined'){ - this.$el.find('#configuration_select').val(configSelectedId); - } - this.$el.find('#config-loading').hide(); - this.$el.find('#configuration_select').show(); + self.triggerMethod("render:options", params); + //set configuration options for edit mode of subresource view + var configSelectedId = this.$el.find('#configuration_select').data('selected-id'); //retrieve data from data-selected-id attribute + if(configSelectedId != 'undefined'){ + this.$el.find('#configuration_select').val(configSelectedId); } + this.$el.find('#config-loading').hide(); + this.$el.find('#configuration_select').show(); } - }); - }else{ - var params = { - context:this, - template: _.template($('#select-config-option-template').html()), - list: new Array(), - element: "#configuration_select", - first_option: '- ' + ts('select configuration') + ' -'} - CRM.BookingApp.vent.trigger("render:options", params); - this.$el.find('#configuration_select').prop('disabled', true); - } - }, - - //save sub-resource - addSubResource: function(e){ - e.preventDefault(); - if (!this.$('form').valid()) { - var errors = this.$('form').validate().errors(); - this.onRenderError(errors); - return false; - } - this.$('form').find("#required_date").datepicker("destroy"); - this.model.set('note', this.$el.find('#sub-resource-note').val()); - var dateVals = this.$el.find("#required_date").val().split("/"); - var timeVals = this.$el.find("#required_time").val().split(":"); - var requiredDate = new Date(dateVals[2],dateVals[1]-1,dateVals[0],timeVals[0],timeVals[1]); - var timeRequired = moment(requiredDate).format("YYYY-M-D HH:mm"); - this.model.set('time_required', timeRequired); - - var parentRefId = this.model.get('parent_ref_id'); - - var refId = null; - if(this.isNew){ - refId = CRM.BookingApp.Utils.getCurrentUnixTimstamp(); - this.model.set('ref_id', refId); - }else{ - refId = this.model.get('ref_id'); - } - - var template = _.template($('#sub-resource-row-template').html()); - - //ui update - if(this.isNew){ - $('#crm-booking-sub-resource-table-' + parentRefId).find('tbody').append(template(this.model.toJSON())); - }else{ - $('#crm-booking-sub-resource-individual-row-'+refId).replaceWith(template(this.model.toJSON())); - } - $('#crm-booking-sub-resource-row-' + parentRefId).show(); - var resourceRefId = this.model.get("parent_ref_id"); - var priceEstimate = this.model.get("price_estimate"); - var subResourceRefId = this.model.get("ref_id"); - - var subResourceModel = CRM.BookingApp.main.currentView.model; - subResourceModel.attributes.sub_resources[refId] = this.model.toJSON(); - - var currentSubTotal = subResourceModel.get('sub_total'); - var newSubTotal = parseFloat(priceEstimate) + parseFloat(subResourceModel.get('sub_total')); - subResourceModel.set("sub_total", newSubTotal); - - var currentTotal = subResourceModel.get('total_price'); - var newTotal = (parseFloat(currentTotal) - parseFloat(currentSubTotal)) + parseFloat(newSubTotal); - subResourceModel.set("total_price", newTotal); - - var currentResourceTotal = resourceTotal[resourceRefId]; - - - if(this.isNew){ - var resourceTotalPrice = parseFloat(currentResourceTotal) + parseFloat(priceEstimate); - priceCache[subResourceRefId] = parseFloat(priceEstimate); - }else{ - var resourceTotalPrice = parseFloat(currentResourceTotal) + parseFloat(priceEstimate) - priceCache[subResourceRefId]; - subResourceModel.attributes.sub_total = subResourceModel.attributes.sub_total - priceCache[subResourceRefId]; - subResourceModel.attributes.total_price = subResourceModel.attributes.total_price - priceCache[subResourceRefId]; - priceCache[subResourceRefId] = parseFloat(priceEstimate); - } - resourceTotal[resourceRefId] = resourceTotalPrice; - subResourceModel.attributes.resources[resourceRefId] = resourceTotalPrice.toFixed(2); - //set total price for resource row - $("#resource-total-price-" + resourceRefId).text(subResourceModel.attributes.resources[resourceRefId]); - CRM.BookingApp.vent.trigger('render:price', subResourceModel, resourceRefId ); - CRM.BookingApp.vent.trigger('update:resources', subResourceModel); - CRM.BookingApp.modal.close(this); - }, - - }); - - //Additaional charges dialog view - AddSubResource.EditAdhocChargesModal = BookingApp.Common.Views.BookingProcessModal.extend({ - template: "#edit-adhoc-charges-template", - className: "modal-dialog", - onRender: function(){ - var thisView = this; - _.each(this.model.get('items'), function(item){ - thisView.$el.find('#' + item.name).html(item.item_price); - thisView.$el.find('input[name="' + item.name + '"]').val(item.quantity); - }); - this.$el.find('#adhoc-charges-note').val(this.model.get('note')); - this.$el.find('#total-adhoc-charges').html(this.model.get('total')); - BookingApp.Common.Views.BookingProcessModal.prototype.onRender.apply(this, arguments); - }, - events: { - 'keypress .item': 'updatePrice', - 'keyup .item': 'updatePrice', - 'keydown .item': 'updatePrice', - 'click #update-adhoc-charges': 'updateAdhocCharges', - }, - updatePrice: function(e){ - var el = $(e.currentTarget); - var itemId = el.data('id'); - var price = el.data('price'); - var quantity = el.val(); - var name = el.attr('name'); - if(CRM.BookingApp.Utils.isPositiveInteger(quantity)){ - var itemPrice = parseFloat(price) * parseFloat(quantity); - this.$el.find('#'+ name).html(parseFloat(itemPrice).toFixed(2)); - var item = {item_id: itemId, name: name, price: price, quantity: quantity, item_price: itemPrice} - this.model.attributes.items[itemId] = item; - }else{ - this.$el.find('#'+ name).html(0); - this.$el.find('input[name="'+ name + '"]').val(''); - delete this.model.attributes.items[itemId]; - } - var items = this.model.get('items'); - var total = 0.0; - _.each(items,function(item){ - total = parseFloat(total) + parseFloat(item.item_price); + } }); - this.$el.find('#total-adhoc-charges').html(total.toFixed(2)); - this.model.set('total', total.toFixed(2)); - }, - updateAdhocCharges: function(e){ - e.preventDefault(); - var subResourceModel = CRM.BookingApp.main.currentView.model; - this.model.set('note',this.$el.find('#adhoc-charges-note').val() ); - var adhocChargesTotal = this.model.get('total'); - //console.log(adhocChargesTotal); - subResourceModel.set('adhoc_charges', this.model.attributes); - var currentTotal = subResourceModel.get('sub_total'); - var discountAmount = subResourceModel.get('discount_amount'); - if(CRM.BookingApp.Utils.isPositiveNumber(discountAmount)){ - var newTotal = (parseFloat(adhocChargesTotal) + parseFloat(currentTotal)) - parseFloat(discountAmount); - }else{ - var newTotal = (parseFloat(adhocChargesTotal) + parseFloat(currentTotal)) - 0; - } - subResourceModel.set("total_price", parseFloat(newTotal).toFixed(2)); - CRM.BookingApp.vent.trigger('render:price', subResourceModel); - CRM.BookingApp.vent.trigger('update:resources', subResourceModel); - //console.log(subResourceModel.attributes); - CRM.BookingApp.modal.close(this); + }else{ + var params = { + context:this, + template: _.template($('#select-config-option-template').html()), + list: new Array(), + element: "#configuration_select", + first_option: '- ' + ts('select configuration') + ' -'} + self.triggerMethod("render:options", params); + this.$el.find('#configuration_select').prop('disabled', true); + } + }, + + //save sub-resource + addSubResource: function(e){ + e.preventDefault(); + var self = this; + var targetForm = e.currentTarget; + while(targetForm.tagName != 'FORM') { + targetForm = targetForm.parentNode; + } + if (!CRM.$(targetForm).valid()) { + var errors = CRM.$(targetForm).validate().errors(); + this.onRenderError(errors); + return false; + } + CRM.$(targetForm).find("#required_date").datepicker("destroy"); + this.model.set('note', this.$el.find('#sub-resource-note').val()); + var dateVals = this.$el.find("#required_date").val().split("/"); + var timeVals = this.$el.find("#required_time").val().split(":"); + var requiredDate = new Date(dateVals[2],dateVals[1]-1,dateVals[0],timeVals[0],timeVals[1]); + var timeRequired = moment(requiredDate).format("YYYY-M-D HH:mm"); + this.model.set('time_required', timeRequired); + + var parentRefId = this.model.get('parent_ref_id'); + + var refId = null; + if(this.isNew){ + refId = CRM.BookingApp.Utils.getCurrentUnixTimstamp(); + this.model.set('ref_id', refId); + }else{ + refId = this.model.get('ref_id'); } - }); + var template = _.template(CRM.$('#sub-resource-row-template').html()); + //ui update + if(this.isNew){ + CRM.$('#crm-booking-sub-resource-table-' + parentRefId).find('tbody').append(template(this.model.toJSON())); + }else{ + CRM.$('#crm-booking-sub-resource-individual-row-'+refId).replaceWith(template(this.model.toJSON())); + } + CRM.$('#crm-booking-sub-resource-row-' + parentRefId).show(); + var resourceRefId = this.model.get("parent_ref_id"); + var priceEstimate = this.model.get("price_estimate"); + var subResourceRefId = this.model.get("ref_id"); + + var subResourceModel = CRM.BookingApp.getRegion().currentView.model; + subResourceModel.attributes.sub_resources[refId] = this.model.toJSON(); + + var currentSubTotal = subResourceModel.get('sub_total'); + var newSubTotal = parseFloat(priceEstimate) + parseFloat(subResourceModel.get('sub_total')); + subResourceModel.set("sub_total", newSubTotal); + + var currentTotal = subResourceModel.get('total_price'); + var newTotal = (parseFloat(currentTotal) - parseFloat(currentSubTotal)) + parseFloat(newSubTotal); + subResourceModel.set("total_price", newTotal); + + var currentResourceTotal = ResourceTotal[resourceRefId]; + + if(this.isNew){ + var resourceTotalPrice = parseFloat(currentResourceTotal) + parseFloat(priceEstimate); + PriceCache[subResourceRefId] = parseFloat(priceEstimate); + }else{ + var resourceTotalPrice = parseFloat(currentResourceTotal) + parseFloat(priceEstimate) - PriceCache[subResourceRefId]; + subResourceModel.attributes.sub_total = subResourceModel.attributes.sub_total - PriceCache[subResourceRefId]; + subResourceModel.attributes.total_price = subResourceModel.attributes.total_price - PriceCache[subResourceRefId]; + PriceCache[subResourceRefId] = parseFloat(priceEstimate); + } + ResourceTotal[resourceRefId] = resourceTotalPrice; + subResourceModel.attributes.resources[resourceRefId] = resourceTotalPrice.toFixed(2); + //set total price for resource row + CRM.$("#resource-total-price-" + resourceRefId).text(subResourceModel.attributes.resources[resourceRefId]); + self.triggerMethod('render:price', subResourceModel, resourceRefId ); + self.triggerMethod('update:resources', subResourceModel); + CRM.BookingApp.modal.triggerMethod('close'); + }, }); -}(CRM.$, CRM.ts('uk.co.compucorp.civicrm.booking'))); diff --git a/js/booking/booking-info/app.js b/js/booking/booking-info/app.js index 49d0d075..d98c5cb9 100644 --- a/js/booking/booking-info/app.js +++ b/js/booking/booking-info/app.js @@ -1,39 +1,37 @@ -CRM.BookingApp = new Backbone.Marionette.Application(); - -var ModalRegion = Backbone.Marionette.Region.extend({ +var ModalRegion = Marionette.Region.extend({ el: "#crm-booking-profile-form", constructor: function(){ - Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); + Marionette.Region.prototype.constructor.apply(this, arguments); this.on("show", this.showModal, this); this.on("close", this.hideModal, this); }, showModal: function(view){ - // view.on("close", this.hideModal, this); - - }, hideModal: function(){ cj('#crm-booking-dialog').dialog().dialog( "destroy" ); } }); - -CRM.BookingApp.addRegions({ +var MyApp = Marionette.Application.extend({ contactRegion: "#contact-container", orgRegion: "#organisation-container", - modal: ModalRegion - + modal: new ModalRegion(), + onRenderDialog: function(profile, title, elementId, targetElementId){ + var view = new BookingInfoViews.Dialog({ + profile: profile, + title: title, + elementId: elementId, + targetElementId: targetElementId, + }); + CRM.BookingApp.modal.show(view); + }, }); +CRM.BookingApp = new MyApp(); + CRM.BookingApp.on("initialize:after", function(){ if( ! Backbone.History.started) Backbone.history.start(); }); - -CRM.BookingApp.addInitializer(function(){ - -}); - - diff --git a/js/booking/booking-info/view.js b/js/booking/booking-info/view.js index 3d1f7d65..842d4204 100644 --- a/js/booking/booking-info/view.js +++ b/js/booking/booking-info/view.js @@ -1,20 +1,6 @@ -(function ($, ts){ -CRM.BookingApp.module('BookingInfo', function(BookingInfo, BookingApp, Backbone, Marionette, $, _) { - - - - CRM.BookingApp.vent.on("render:dialog", function (profile, title, elementId, targetElementId){ - var view = new BookingInfo.Dialog({ - profile: profile, - title: title, - elementId: elementId, - targetElementId: targetElementId}); - CRM.BookingApp.modal.show(view); - }); - - - BookingInfo.Dialog = Backbone.Marionette.ItemView.extend({ - template: '#booking-info-profile-template', +var BookingInfoViews = { + Dialog: Marionette.View.extend({ + template: CRM._.template(CRM.$('#booking-info-profile-template').html()), initialize: function(options){ this.title = options.title; this.profile = options.profile; @@ -33,25 +19,24 @@ CRM.BookingApp.module('BookingInfo', function(BookingInfo, BookingApp, Backbone, modal: true, minWidth: 600, open: function() { - $.getJSON(self.profileUrl, function(data) { + CRM.$.getJSON(self.profileUrl, function(data) { self.displayNewContactProfile(data); }); }, close: function() { - $(this).dialog('destroy'); + CRM.$(this).dialog('destroy'); } }); }, - displayNewContactProfile: function(data) { var self = this; - $('#crm-booking-profile-form').html(data.content); - $("#crm-booking-profile-form .cancel.form-submit").click(function() { - $("#crm-booking-profile-form").dialog('close'); - return false; - }); - $('#email-Primary').addClass('email'); - $("#crm-booking-profile-form form").ajaxForm({ + CRM.$('#crm-booking-profile-form').html(data.content); + CRM.$("#crm-booking-profile-form .cancel.form-submit").click(function() { + CRM.$("#crm-booking-profile-form").dialog('close'); + return false; + }); + CRM.$('#email-Primary').addClass('email'); + CRM.$("#crm-booking-profile-form form").ajaxForm({ // context=dialog triggers civi's profile to respond with json instead of an html redirect // but it also results in lots of unwanted scripts being added to the form snippet, so we // add it here during submission and not during form retrieval. @@ -59,11 +44,11 @@ CRM.BookingApp.module('BookingInfo', function(BookingInfo, BookingApp, Backbone, dataType: 'json', success: function(response) { if (response.newContactSuccess) { - $('#crm-booking-profile-form').dialog('close'); + CRM.$('#crm-booking-profile-form').dialog('close'); CRM.alert(ts('%1 has been created.', {1: response.displayName}), ts('Contact Saved'), 'success'); console.log('test',self.targetElementId); - $('input[name="'+self.targetElementId+'"]').val(response.contactID); - $(self.elementId).val(response.displayName); + CRM.$('input[name="'+self.targetElementId+'"]').val(response.contactID); + CRM.$(self.elementId).val(response.displayName); } else { self.displayNewContactProfile(response); @@ -72,7 +57,5 @@ CRM.BookingApp.module('BookingInfo', function(BookingInfo, BookingApp, Backbone, }).validate(CRM.validate.params); } - }); - -}); -}(CRM.$, CRM.ts('uk.co.compucorp.civicrm.booking'))); + }), +}; diff --git a/js/booking/common/views.js b/js/booking/common/views.js deleted file mode 100644 index fa263b51..00000000 --- a/js/booking/common/views.js +++ /dev/null @@ -1,35 +0,0 @@ -CRM.BookingApp.module('Common.Views', function(Views, BookingApp, Backbone, Marionette, $, _) { - - - /** - * A form that use in a Modal that required the validate in the form - * - */ - Views.BookingProcessModal = Marionette.ItemView.extend({ - onRender: function() { - var rules = this.createValidationRules(); - this.$('form').validate(rules); - }, - /** - * - * @return {*} jQuery.validate rules - */ - createValidationRules: function() { - var rules = _.extend({}, CRM.validate.params); - rules.rules || (rules.rules = {}); - this.triggerMethod("validateRules:create", this, rules); - return rules; - }, - - onRenderError: function(errors){ - var view = this; - _.each(errors, function(error) { - console.log($(error).attr('for')); - view.$('[name=' + $(error).attr('for') + ']').crmError($(error).text()); - }); - } - - }); - - -}); diff --git a/js/booking/utils.js b/js/booking/utils.js deleted file mode 100644 index fcd1711f..00000000 --- a/js/booking/utils.js +++ /dev/null @@ -1,20 +0,0 @@ -CRM.BookingApp.module("Utils", function(Utils, BookingApp, Backbone, Marionette, $, _){ - - //http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript - Utils.getCurrentUnixTimstamp = function () { - return unix = Math.round(+new Date()/1000); - }; - - //http://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer - Utils.isPositiveInteger = function (n){ - return 0 === n % (!isNaN(parseFloat(n)) && 0 <= ~~n); - }; - - Utils.isPositiveNumber = function (n){ - return !_.isNumber(n) && 0 <= n; - }; - -}); - - - diff --git a/js/vendor/crm.backbone.js b/js/vendor/crm.backbone.js new file mode 100644 index 00000000..f18d7b06 --- /dev/null +++ b/js/vendor/crm.backbone.js @@ -0,0 +1,584 @@ +(function($, _, Backbone) { + /* + * In the browser console the following error appeared: + * Uncaught TypeError: Cannot read property 'Model' of undefined + * The issue pointed to the line where the Backbone variable were first used. + * The issue was verified with the debug tool. This function input Backbone was + * undefined, but the window.Backbone were defined. + * This if branch is for fixing the issue. If the Backbone is undefined, fallback to + * the window.Backbone. + * */ + if (typeof Backbone === 'undefined') { + Backbone = window.Backbone; + } + if (!CRM.Backbone) CRM.Backbone = {}; + + /** + * Backbone.sync provider which uses CRM.api() for I/O. + * To support CRUD operations, model classes must be defined with a "crmEntityName" property. + * To load collections using API queries, set the "crmCriteria" property or override the + * method "toCrmCriteria". + * + * @param method Accepts normal Backbone.sync methods; also accepts "crm-replace" + * @param model + * @param options + * @see tests/qunit/crm-backbone + */ + CRM.Backbone.sync = function(method, model, options) { + var isCollection = _.isArray(model.models); + + var apiOptions, params; + if (isCollection) { + apiOptions = { + success: function(data) { + // unwrap data + options.success(_.toArray(data.values)); + }, + error: function(data) { + // CRM.api displays errors by default, but Backbone.sync + // protocol requires us to override "error". This restores + // the default behavior. + $().crmError(data.error_message, ts('Error')); + options.error(data); + } + }; + switch (method) { + case 'read': + CRM.api(model.crmEntityName, model.toCrmAction('get'), model.toCrmCriteria(), apiOptions); + break; + // replace all entities matching "x.crmCriteria" with new entities in "x.models" + case 'crm-replace': + params = this.toCrmCriteria(); + params.version = 3; + params.values = this.toJSON(); + CRM.api(model.crmEntityName, model.toCrmAction('replace'), params, apiOptions); + break; + default: + apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"}); + break; + } + } else { + // callback options to pass to CRM.api + apiOptions = { + success: function(data) { + // unwrap data + var values = _.toArray(data.values); + if (data.count == 1) { + options.success(values[0]); + } else { + data.is_error = 1; + data.error_message = ts("Expected exactly one response"); + apiOptions.error(data); + } + }, + error: function(data) { + // CRM.api displays errors by default, but Backbone.sync + // protocol requires us to override "error". This restores + // the default behavior. + $().crmError(data.error_message, ts('Error')); + options.error(data); + } + }; + switch (method) { + case 'create': // pass-through + case 'update': + params = model.toJSON(); + if (!params.options) params.options = {}; + params.options.reload = 1; + if (!model._isDuplicate) { + CRM.api(model.crmEntityName, model.toCrmAction('create'), params, apiOptions); + } else { + CRM.api(model.crmEntityName, model.toCrmAction('duplicate'), params, apiOptions); + } + break; + case 'read': + case 'delete': + var apiAction = (method == 'delete') ? 'delete' : 'get'; + params = model.toCrmCriteria(); + if (!params.id) { + apiOptions.error({is_error: 1, error_message: 'Missing ID for ' + model.crmEntityName}); + return; + } + CRM.api(model.crmEntityName, model.toCrmAction(apiAction), params, apiOptions); + break; + default: + apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for models"}); + } + } + }; + + /** + * Connect a "model" class to CiviCRM's APIv3 + * + * @code + * // Setup class + * var ContactModel = Backbone.Model.extend({}); + * CRM.Backbone.extendModel(ContactModel, "Contact"); + * + * // Use class + * c = new ContactModel({id: 3}); + * c.fetch(); + * @endcode + * + * @param Class ModelClass + * @param string crmEntityName APIv3 entity name, such as "Contact" or "CustomField" + * @see tests/qunit/crm-backbone + */ + CRM.Backbone.extendModel = function(ModelClass, crmEntityName) { + // Defaults - if specified in ModelClass, preserve + _.defaults(ModelClass.prototype, { + crmEntityName: crmEntityName, + crmActions: {}, // map: string backboneActionName => string serverSideActionName + crmReturn: null, // array: list of fields to return + toCrmAction: function(action) { + return this.crmActions[action] ? this.crmActions[action] : action; + }, + toCrmCriteria: function() { + var result = (this.get('id')) ? {id: this.get('id')} : {}; + if (!_.isEmpty(this.crmReturn)) { + result.return = this.crmReturn; + } + return result; + }, + duplicate: function() { + var newModel = new ModelClass(this.toJSON()); + newModel._isDuplicate = true; + if (newModel.setModified) newModel.setModified(); + newModel.listenTo(newModel, 'sync', function(){ + // may get called on subsequent resaves -- don't care! + delete newModel._isDuplicate; + }); + return newModel; + } + }); + // Overrides - if specified in ModelClass, replace + _.extend(ModelClass.prototype, { + sync: CRM.Backbone.sync + }); + }; + + /** + * Configure a model class to track whether a model has unsaved changes. + * + * Methods: + * - setModified() - flag the model as modified/dirty + * - isSaved() - return true if there have been no changes to the data since the last fetch or save + * Events: + * - saved(object model, bool is_saved) - triggered whenever isSaved() value would change + * + * Note: You should not directly call isSaved() within the context of the success/error/sync callback; + * I haven't found a way to make isSaved() behave correctly within these callbacks without patching + * Backbone. Instead, attach an event listener to the 'saved' event. + * + * @param ModelClass + */ + CRM.Backbone.trackSaved = function(ModelClass) { + // Retain references to some of the original class's functions + var Parent = _.pick(ModelClass.prototype, 'initialize', 'save', 'fetch'); + + // Private callback + var onSyncSuccess = function() { + this._modified = false; + if (this._oldModified.length > 0) { + this._oldModified.pop(); + } + this.trigger('saved', this, this.isSaved()); + }; + var onSaveError = function() { + if (this._oldModified.length > 0) { + this._modified = this._oldModified.pop(); + this.trigger('saved', this, this.isSaved()); + } + }; + + // Defaults - if specified in ModelClass, preserve + _.defaults(ModelClass.prototype, { + isSaved: function() { + var result = !this.isNew() && !this.isModified(); + return result; + }, + isModified: function() { + return this._modified; + }, + _saved_onchange: function(model, options) { + if (options.parse) return; + // console.log('change', model.changedAttributes(), model.previousAttributes()); + this.setModified(); + }, + setModified: function() { + var oldModified = this._modified; + this._modified = true; + if (!oldModified) { + this.trigger('saved', this, this.isSaved()); + } + } + }); + + // Overrides - if specified in ModelClass, replace + _.extend(ModelClass.prototype, { + initialize: function(options) { + this._modified = false; + this._oldModified = []; + this.listenTo(this, 'change', this._saved_onchange); + this.listenTo(this, 'error', onSaveError); + this.listenTo(this, 'sync', onSyncSuccess); + if (Parent.initialize) { + return Parent.initialize.apply(this, arguments); + } + }, + save: function() { + // we'll assume success + this._oldModified.push(this._modified); + return Parent.save.apply(this, arguments); + }, + fetch: function() { + this._oldModified.push(this._modified); + return Parent.fetch.apply(this, arguments); + } + }); + }; + + /** + * Configure a model class to support client-side soft deletion. + * One can call "model.setDeleted(BOOLEAN)" to flag an entity for + * deletion (or not) -- however, deletion will be deferred until save() + * is called. + * + * Methods: + * setSoftDeleted(boolean) - flag the model as deleted (or not-deleted) + * isSoftDeleted() - determine whether model has been soft-deleted + * Events: + * softDelete(model, is_deleted) -- change value of is_deleted + * + * @param ModelClass + */ + CRM.Backbone.trackSoftDelete = function(ModelClass) { + // Retain references to some of the original class's functions + var Parent = _.pick(ModelClass.prototype, 'save'); + + // Defaults - if specified in ModelClass, preserve + _.defaults(ModelClass.prototype, { + is_soft_deleted: false, + setSoftDeleted: function(is_deleted) { + if (this.is_soft_deleted != is_deleted) { + this.is_soft_deleted = is_deleted; + this.trigger('softDelete', this, is_deleted); + if (this.setModified) this.setModified(); // FIXME: ugly interaction, trackSoftDelete-trackSaved + } + }, + isSoftDeleted: function() { + return this.is_soft_deleted; + } + }); + + // Overrides - if specified in ModelClass, replace + _.extend(ModelClass.prototype, { + save: function(attributes, options) { + if (this.isSoftDeleted()) { + return this.destroy(options); + } else { + return Parent.save.apply(this, arguments); + } + } + }); + }; + + /** + * Connect a "collection" class to CiviCRM's APIv3 + * + * Note: the collection supports a special property, crmCriteria, which is an array of + * query options to send to the API. + * + * @code + * // Setup class + * var ContactModel = Backbone.Model.extend({}); + * CRM.Backbone.extendModel(ContactModel, "Contact"); + * var ContactCollection = Backbone.Collection.extend({ + * model: ContactModel + * }); + * CRM.Backbone.extendCollection(ContactCollection); + * + * // Use class (with passive criteria) + * var c = new ContactCollection([], { + * crmCriteria: {contact_type: 'Organization'} + * }); + * c.fetch(); + * c.get(123).set('property', 'value'); + * c.get(456).setDeleted(true); + * c.save(); + * + * // Use class (with active criteria) + * var criteriaModel = new SomeModel({ + * contact_type: 'Organization' + * }); + * var c = new ContactCollection([], { + * crmCriteriaModel: criteriaModel + * }); + * c.fetch(); + * c.get(123).set('property', 'value'); + * c.get(456).setDeleted(true); + * c.save(); + * @endcode + * + * + * @param Class CollectionClass + * @see tests/qunit/crm-backbone + */ + CRM.Backbone.extendCollection = function(CollectionClass) { + var origInit = CollectionClass.prototype.initialize; + // Defaults - if specified in CollectionClass, preserve + _.defaults(CollectionClass.prototype, { + crmEntityName: CollectionClass.prototype.model.prototype.crmEntityName, + crmActions: {}, // map: string backboneActionName => string serverSideActionName + toCrmAction: function(action) { + return this.crmActions[action] ? this.crmActions[action] : action; + }, + toCrmCriteria: function() { + var result = (this.crmCriteria) ? _.extend({}, this.crmCriteria) : {}; + if (!_.isEmpty(this.crmReturn)) { + result.return = this.crmReturn; + } else if (this.model && !_.isEmpty(this.model.prototype.crmReturn)) { + result.return = this.model.prototype.crmReturn; + } + return result; + }, + + /** + * Get an object which represents this collection's criteria + * as a live model. Any changes to the model will be applied + * to the collection, and the collection will be refreshed. + * + * @param criteriaModelClass + */ + setCriteriaModel: function(criteriaModel) { + var collection = this; + this.crmCriteria = criteriaModel.toJSON(); + this.listenTo(criteriaModel, 'change', function() { + collection.crmCriteria = criteriaModel.toJSON(); + collection.debouncedFetch(); + }); + }, + + debouncedFetch: _.debounce(function() { + this.fetch({reset: true}); + }, 100), + + /** + * Reconcile the server's collection with the client's collection. + * New/modified items from the client will be saved/updated on the + * server. Deleted items from the client will be deleted on the + * server. + * + * @param Object options - accepts "success" and "error" callbacks + */ + save: function(options) { + if (!options) options = {}; + var collection = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + collection.reset(resp, options); + if (success) success(collection, resp, options); + // collection.trigger('sync', collection, resp, options); + }; + wrapError(collection, options); + + return this.sync('crm-replace', this, options); + } + }); + // Overrides - if specified in CollectionClass, replace + _.extend(CollectionClass.prototype, { + sync: CRM.Backbone.sync, + initialize: function(models, options) { + if (!options) options = {}; + if (options.crmCriteriaModel) { + this.setCriteriaModel(options.crmCriteriaModel); + } else if (options.crmCriteria) { + this.crmCriteria = options.crmCriteria; + } + if (options.crmActions) { + this.crmActions = _.extend(this.crmActions, options.crmActions); + } + if (origInit) { + return origInit.apply(this, arguments); + } + }, + toJSON: function() { + var result = []; + // filter models list, excluding any soft-deleted items + this.each(function(model) { + // if model doesn't track soft-deletes + // or if model tracks soft-deletes and wasn't soft-deleted + if (!model.isSoftDeleted || !model.isSoftDeleted()) { + result.push(model.toJSON()); + } + }); + return result; + } + }); + }; + + /** + * Find a single record, or create a new record. + * + * @param Object options: + * - CollectionClass: class + * - crmCriteria: Object values to search/default on + * - defaults: Object values to put on newly created model (if needed) + * - success: function(model) + * - error: function(collection, error) + */ + CRM.Backbone.findCreate = function(options) { + if (!options) options = {}; + var collection = new options.CollectionClass([], { + crmCriteria: options.crmCriteria + }); + collection.fetch({ + success: function(collection) { + if (collection.length === 0) { + var attrs = _.extend({}, collection.crmCriteria, options.defaults || {}); + var model = collection._prepareModel(attrs, options); + options.success(model); + } else if (collection.length == 1) { + options.success(collection.first()); + } else { + options.error(collection, { + is_error: 1, + error_message: 'Too many matches' + }); + } + }, + error: function(collection, errorData) { + if (options.error) { + options.error(collection, errorData); + } + } + }); + }; + + + CRM.Backbone.Model = Backbone.Model.extend({ + /** + * Return JSON version of model -- but only include fields that are + * listed in the 'schema'. + * + * @return {*} + */ + toStrictJSON: function() { + var schema = this.schema; + var result = this.toJSON(); + _.each(result, function(value, key) { + if (!schema[key]) { + delete result[key]; + } + }); + return result; + }, + setRel: function(key, value, options) { + this.rels = this.rels || {}; + if (this.rels[key] != value) { + this.rels[key] = value; + this.trigger("rel:" + key, value); + } + }, + getRel: function(key) { + return this.rels ? this.rels[key] : null; + } + }); + + CRM.Backbone.Collection = Backbone.Collection.extend({ + /** + * Store 'key' on this.rel and automatically copy it to + * any children. + * + * @param key + * @param value + * @param initialModels + */ + initializeCopyToChildrenRelation: function(key, value, initialModels) { + this.setRel(key, value, {silent: true}); + this.on('reset', this._copyToChildren, this); + this.on('add', this._copyToChild, this); + }, + _copyToChildren: function() { + var collection = this; + collection.each(function(model) { + collection._copyToChild(model); + }); + }, + _copyToChild: function(model) { + _.each(this.rels, function(relValue, relKey) { + model.setRel(relKey, relValue, {silent: true}); + }); + }, + setRel: function(key, value, options) { + this.rels = this.rels || {}; + if (this.rels[key] != value) { + this.rels[key] = value; + this.trigger("rel:" + key, value); + } + }, + getRel: function(key) { + return this.rels ? this.rels[key] : null; + } + }); + + /* + CRM.Backbone.Form = Backbone.Form.extend({ + validate: function() { + // Add support for form-level validators + var errors = Backbone.Form.prototype.validate.apply(this, []) || {}; + var self = this; + if (this.validators) { + _.each(this.validators, function(validator) { + var modelErrors = validator(this.getValue()); + + // The following if() has been copied-pasted from the parent's + // handling of model-validators. They are similar in that the errors are + // probably keyed by field names... but not necessarily, so we use _others + // as a fallback. + if (modelErrors) { + var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors); + + //If errors are not in object form then just store on the error object + if (!isDictionary) { + errors._others = errors._others || []; + errors._others.push(modelErrors); + } + + //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' }) + if (isDictionary) { + _.each(modelErrors, function(val, key) { + //Set error on field if there isn't one already + if (self.fields[key] && !errors[key]) { + self.fields[key].setError(val); + errors[key] = val; + } + + else { + //Otherwise add to '_others' key + errors._others = errors._others || []; + var tmpErr = {}; + tmpErr[key] = val; + errors._others.push(tmpErr); + } + }); + } + } + + }); + } + return _.isEmpty(errors) ? null : errors; + } + }); + */ + + // Wrap an optional error callback with a fallback error event. + var wrapError = function (model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error(model, resp, optio); + model.trigger('error', model, resp, options); + }; + }; +})(CRM.$, CRM._, CRM.BB); diff --git a/packages/backbone.js b/packages/backbone.js new file mode 100644 index 00000000..3e09d0dc --- /dev/null +++ b/packages/backbone.js @@ -0,0 +1,2096 @@ +// Backbone.js 1.4.0 + +// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(factory) { + + // Establish the root object, `window` (`self`) in the browser, or `global` on the server. + // We use `self` instead of `window` for `WebWorker` support. + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global; + + // Set up Backbone appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + + // Next for Node.js or CommonJS. jQuery may not be needed as a module. + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'), $; + try { $ = require('jquery'); } catch (e) {} + factory(root, exports, _, $); + + // Finally, as a browser global. + } else { + root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$); + } + +})(function(root, Backbone, _, $) { + + // Initial Setup + // ------------- + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to a common array method we'll want to use later. + var slice = Array.prototype.slice; + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.4.0'; + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = $; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... this will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // a custom event channel. You may bind a callback to an event with `on` or + // remove with `off`; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = {}; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // A private global variable to share between listeners and listenees. + var _listening; + + // Iterates over the standard `event, callback` (as well as the fancy multiple + // space-separated events `"change blur", callback` and jQuery-style event + // maps `{event: callback}`). + var eventsApi = function(iteratee, events, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; + for (names = _.keys(name); i < names.length ; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); + } + } else if (name && eventSplitter.test(name)) { + // Handle space-separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); + } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; + }; + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + Events.on = function(name, callback, context) { + this._events = eventsApi(onApi, this._events || {}, name, callback, { + context: context, + ctx: this, + listening: _listening + }); + + if (_listening) { + var listeners = this._listeners || (this._listeners = {}); + listeners[_listening.id] = _listening; + // Allow the listening to use a counter, instead of tracking + // callbacks for library interop + _listening.interop = false; + } + + return this; + }; + + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = _listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + this._listenId || (this._listenId = _.uniqueId('l')); + listening = _listening = listeningTo[id] = new Listening(this, obj); + } + + // Bind callbacks on obj. + var error = tryCatchOn(obj, name, callback, this); + _listening = void 0; + + if (error) throw error; + // If the target obj is not Backbone.Events, track events manually. + if (listening.interop) listening.on(name, callback); + + return this; + }; + + // The reducing API that adds a callback to the `events` object. + var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); + } + return events; + }; + + // An try-catch guarded #on function, to prevent poisoning the global + // `_listening` variable. + var tryCatchOn = function(obj, name, callback, context) { + try { + obj.on(name, callback, context); + } catch (e) { + return e; + } + }; + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + + return this; + }; + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : _.keys(listeningTo); + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + if (listening.interop) listening.off(name, callback); + } + if (_.isEmpty(listeningTo)) this._listeningTo = void 0; + + return this; + }; + + // The reducing API that removes a callback from the `events` object. + var offApi = function(events, name, callback, options) { + if (!events) return; + + var context = options.context, listeners = options.listeners; + var i = 0, names; + + // Delete all event listeners and "drop" events. + if (!name && !context && !callback) { + for (names = _.keys(listeners); i < names.length; i++) { + listeners[names[i]].cleanup(); + } + return; + } + + names = name ? [name] : _.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Find any remaining events. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + var listening = handler.listening; + if (listening) listening.off(name, callback); + } + } + + // Replace events if there are any remaining. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; + } + } + + return events; + }; + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + Events.once = function(name, callback, context) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); + if (typeof name === 'string' && context == null) callback = void 0; + return this.on(events, callback, context); + }; + + // Inversion-of-control versions of `once`. + Events.listenToOnce = function(obj, name, callback) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); + return this.listenTo(obj, events); + }; + + // Reduces the event callbacks into a map of `{event: onceWrapper}`. + // `offer` unbinds the `onceWrapper` after it has been called. + var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = _.once(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; + }; + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; + }; + + // Handles triggering the appropriate event callbacks. + var triggerApi = function(objEvents, name, callback, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + } + }; + + // A listening class that tracks and cleans up memory bindings + // when all callbacks have been offed. + var Listening = function(listener, obj) { + this.id = listener._listenId; + this.listener = listener; + this.obj = obj; + this.interop = true; + this.count = 0; + this._events = void 0; + }; + + Listening.prototype.on = Events.on; + + // Offs a callback (or several). + // Uses an optimized counter if the listenee uses Backbone.Events. + // Otherwise, falls back to manual tracking to support events + // library interop. + Listening.prototype.off = function(name, callback) { + var cleanup; + if (this.interop) { + this._events = eventsApi(offApi, this._events, name, callback, { + context: void 0, + listeners: void 0 + }); + cleanup = !this._events; + } else { + this.count--; + cleanup = this.count === 0; + } + if (cleanup) this.cleanup(); + }; + + // Cleans up memory bindings between the listener and the listenee. + Listening.prototype.cleanup = function() { + delete this.listener._listeningTo[this.obj._listenId]; + if (!this.interop) delete this.obj._listeners[this.id]; + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.preinitialize.apply(this, arguments); + this.cid = _.uniqueId(this.cidPrefix); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + var defaults = _.result(this, 'defaults'); + attrs = _.defaults(_.extend({}, defaults, attrs), defaults); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Model. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Special-cased proxy to underscore's `_.matches` method. + matches: function(attrs) { + return !!_.iteratee(attrs, this)(this.attributes); + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + var unset = options.unset; + var silent = options.silent; + var changes = []; + var changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; + + // For each `set` attribute, update or delete the current value. + for (var attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + changed[attr] = val; + } else { + delete changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Update the `id`. + if (this.idAttribute in attrs) this.id = this.get(this.idAttribute); + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = options; + for (var i = 0; i < changes.length; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + options = this._pending; + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var old = this._changing ? this._previousAttributes : this.attributes; + var changed = {}; + var hasChanged; + for (var attr in diff) { + var val = diff[attr]; + if (_.isEqual(old[attr], val)) continue; + changed[attr] = val; + hasChanged = true; + } + return hasChanged ? changed : false; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server, merging the response with the model's + // local attributes. Any changed attributes will trigger a "change" event. + fetch: function(options) { + options = _.extend({parse: true}, options); + var model = this; + var success = options.success; + options.success = function(resp) { + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (!model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true, parse: true}, options); + var wait = options.wait; + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !wait) { + if (!this.set(attrs, options)) return false; + } else if (!this._validate(attrs, options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + var attributes = this.attributes; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (serverAttrs && !model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + // Set temporary attributes if `{wait: true}` to properly find new ids. + if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); + + var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update'; + if (method === 'patch' && !options.attrs) options.attrs = attrs; + var xhr = this.sync(method, this, options); + + // Restore attributes. + this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + var wait = options.wait; + + var destroy = function() { + model.stopListening(); + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (wait) destroy(); + if (success) success.call(options.context, model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + var xhr = false; + if (this.isNew()) { + _.defer(options.success); + } else { + wrapError(this, options); + xhr = this.sync('delete', this, options); + } + if (!wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = + _.result(this, 'urlRoot') || + _.result(this.collection, 'url') || + urlError(); + if (this.isNew()) return base; + var id = this.get(this.idAttribute); + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return !this.has(this.idAttribute); + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend({}, options, {validate: true})); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analogous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + this.preinitialize.apply(this, arguments); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Splices `insert` into `array` at index `at`. + var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + var i; + for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Collection. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model) { return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. `models` may be Backbone + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + options = _.extend({}, options); + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + var removed = this._removeModels(models, options); + if (!options.silent && removed.length) { + options.changes = {added: [], merged: [], removed: removed}; + this.trigger('update', this, options); + } + return singular ? removed[0] : removed; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + if (models == null) return; + + options = _.extend({}, setOptions, options); + if (options.parse && !this._isModel(models)) { + models = this.parse(models, options) || []; + } + + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + + var at = options.at; + if (at != null) at = +at; + if (at > this.length) at = this.length; + if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toMerge = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; + var sortable = this.comparator && at == null && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + var model, i; + for (i = 0; i < models.length; i++) { + model = models[i]; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + toMerge.push(existing); + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } + } + } + + // Remove stale models. + if (remove) { + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); + } + if (toRemove.length) this._removeModels(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { + return m !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.length) { + if (sortable) sort = true; + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort/update events. + if (!options.silent) { + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); + } + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length || toMerge.length) { + options.changes = { + added: toAdd, + removed: toRemove, + merged: toMerge + }; + this.trigger('update', this, options); + } + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options = options ? _.clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { + this._removeReference(this.models[i], options); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + return this.remove(model, options); + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + return this.remove(model, options); + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id, cid, model object with id or cid + // properties, or an attributes object that is transformed through modelId. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj] || + this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] || + obj.cid && this._byId[obj.cid]; + }, + + // Returns `true` if the model is in the collection. + has: function(obj) { + return this.get(obj) != null; + }, + + // Get the model at the given index. + at: function(index) { + if (index < 0) index += this.length; + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + return this[first ? 'find' : 'filter'](attrs); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + var length = comparator.length; + if (_.isFunction(comparator)) comparator = comparator.bind(this); + + // Run sort based on type of `comparator`. + if (length === 1 || _.isString(comparator)) { + this.models = this.sortBy(comparator); + } else { + this.models.sort(comparator); + } + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return this.map(attr + ''); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = _.extend({parse: true}, options); + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success.call(options.context, collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + var wait = options.wait; + model = this._prepareModel(model, options); + if (!model) return false; + if (!wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(m, resp, callbackOpts) { + if (wait) collection.add(m, callbackOpts); + if (success) success.call(callbackOpts.context, m, resp, callbackOpts); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function(attrs) { + return attrs[this.model.prototype.idAttribute || 'id']; + }, + + // Get an iterator of all models in this collection. + values: function() { + return new CollectionIterator(this, ITERATOR_VALUES); + }, + + // Get an iterator of all model IDs in this collection. + keys: function() { + return new CollectionIterator(this, ITERATOR_KEYS); + }, + + // Get an iterator of all [ID, model] tuples in this collection. + entries: function() { + return new CollectionIterator(this, ITERATOR_KEYSVALUES); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method called by both remove and set. + _removeModels: function(models, options) { + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = this.get(models[i]); + if (!model) continue; + + var index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + + // Remove references before triggering 'remove' event to prevent an + // infinite loop. #3693 + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + + removed.push(model); + this._removeReference(model, options); + } + return removed; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function(model) { + return model instanceof Model; + }, + + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + var id = this.modelId(model.attributes); + if (id != null) this._byId[id] = model; + model.on('all', this._onModelEvent, this); + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if (model) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (event === 'change') { + var prevId = this.modelId(model.previousAttributes()); + var id = this.modelId(model.attributes); + if (prevId !== id) { + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } + } + } + this.trigger.apply(this, arguments); + } + + }); + + // Defining an @@iterator method implements JavaScript's Iterable protocol. + // In modern ES2015 browsers, this value is found at Symbol.iterator. + /* global Symbol */ + var $$iterator = typeof Symbol === 'function' && Symbol.iterator; + if ($$iterator) { + Collection.prototype[$$iterator] = Collection.prototype.values; + } + + // CollectionIterator + // ------------------ + + // A CollectionIterator implements JavaScript's Iterator protocol, allowing the + // use of `for of` loops in modern browsers and interoperation between + // Backbone.Collection and other JavaScript functions and third-party libraries + // which can operate on Iterables. + var CollectionIterator = function(collection, kind) { + this._collection = collection; + this._kind = kind; + this._index = 0; + }; + + // This "enum" defines the three possible kinds of values which can be emitted + // by a CollectionIterator that correspond to the values(), keys() and entries() + // methods on Collection, respectively. + var ITERATOR_VALUES = 1; + var ITERATOR_KEYS = 2; + var ITERATOR_KEYSVALUES = 3; + + // All Iterators should themselves be Iterable. + if ($$iterator) { + CollectionIterator.prototype[$$iterator] = function() { + return this; + }; + } + + CollectionIterator.prototype.next = function() { + if (this._collection) { + + // Only continue iterating if the iterated collection is long enough. + if (this._index < this._collection.length) { + var model = this._collection.at(this._index); + this._index++; + + // Construct a value depending on what kind of values should be iterated. + var value; + if (this._kind === ITERATOR_VALUES) { + value = model; + } else { + var id = this._collection.modelId(model.attributes); + if (this._kind === ITERATOR_KEYS) { + value = id; + } else { // ITERATOR_KEYSVALUES + value = [id, model]; + } + } + return {value: value, done: false}; + } + + // Once exhausted, remove the reference to the collection so future + // calls to the next method always return done. + this._collection = void 0; + } + + return {value: void 0, done: true}; + }; + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + this.preinitialize.apply(this, arguments); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be set as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the View + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this._removeElement(); + this.stopListening(); + return this; + }, + + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); + return this; + }, + + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + delegateEvents: function(events) { + events || (events = _.result(this, 'events')); + if (!events) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[method]; + if (!method) continue; + var match = key.match(delegateEventSplitter); + this.delegate(match[1], match[2], method.bind(this)); + } + return this; + }, + + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Clears all callbacks previously bound to the view by `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + this.setElement(this._createElement(_.result(this, 'tagName'))); + this._setAttributes(attrs); + } else { + this.setElement(_.result(this, 'el')); + } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); + } + + }); + + // Proxy Backbone class methods to Underscore functions, wrapping the model's + // `attributes` object or collection's `models` array behind the scenes. + // + // collection.filter(function(model) { return model.get('age') > 10 }); + // collection.each(this.addView); + // + // `Function#apply` can be slow so we use the method's arg count, if we know it. + var addMethod = function(base, length, method, attribute) { + switch (length) { + case 1: return function() { + return base[method](this[attribute]); + }; + case 2: return function(value) { + return base[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return base[method](this[attribute], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return base[method](this[attribute], cb(iteratee, this), defaultVal, context); + }; + default: return function() { + var args = slice.call(arguments); + args.unshift(this[attribute]); + return base[method].apply(base, args); + }; + } + }; + + var addUnderscoreMethods = function(Class, base, methods, attribute) { + _.each(methods, function(length, method) { + if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute); + }); + }; + + // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. + var cb = function(iteratee, instance) { + if (_.isFunction(iteratee)) return iteratee; + if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; + }; + var modelMatcher = function(attrs) { + var matcher = _.matches(attrs); + return function(model) { + return matcher(model.attributes); + }; + }; + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, + foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, + contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, + head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, + without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, + isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, + sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; + + + // Underscore methods that we want to implement on the Model, mapped to the + // number of arguments they take. + var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, + omit: 0, chain: 1, isEmpty: 1}; + + // Mix in each Underscore method as a proxy to `Collection#models`. + + _.each([ + [Collection, collectionMethods, 'models'], + [Model, modelMethods, 'attributes'] + ], function(config) { + var Base = config[0], + methods = config[1], + attribute = config[2]; + + Base.mixin = function(obj) { + var mappings = _.reduce(_.functions(obj), function(memo, name) { + memo[name] = 0; + return memo; + }, {}); + addUnderscoreMethods(Base, obj, mappings, attribute); + }; + + addUnderscoreMethods(Base, _, methods, attribute); + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // Pass along `textStatus` and `errorThrown` from jQuery. + var error = options.error; + options.error = function(xhr, textStatus, errorThrown) { + options.textStatus = textStatus; + options.errorThrown = errorThrown; + if (error) error.call(options.context, xhr, textStatus, errorThrown); + }; + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + create: 'POST', + update: 'PUT', + patch: 'PATCH', + delete: 'DELETE', + read: 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + this.preinitialize.apply(this, arguments); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Router. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + if (router.execute(callback, args, name) !== false) { + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + } + }); + return this; + }, + + // Execute a route handler with the provided parameters. This is an + // excellent place to do pre-route setup or post-route cleanup. + execute: function(callback, args, name) { + if (callback) callback.apply(this, args); + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param, i) { + // Don't decode the search params. + if (i === params.length - 1) return param || null; + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + this.checkUrl = this.checkUrl.bind(this); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for stripping urls of hash. + var pathStripper = /#.*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Are we at the app root? + atRoot: function() { + var path = this.location.pathname.replace(/[^\/]$/, '$&/'); + return path === this.root && !this.getSearch(); + }, + + // Does the pathname match the root? + matchRoot: function() { + var path = this.decodeFragment(this.location.pathname); + var rootPath = path.slice(0, this.root.length - 1) + '/'; + return rootPath === this.root; + }, + + // Unicode characters in `location.pathname` are percent encoded so they're + // decoded for comparison. `%25` should not be decoded since it may be part + // of an encoded parameter. + decodeFragment: function(fragment) { + return decodeURI(fragment.replace(/%25/g, '%2525')); + }, + + // In IE6, the hash fragment and search params are incorrect if the + // fragment contains `?`. + getSearch: function() { + var match = this.location.href.replace(/#.*/, '').match(/\?.+/); + return match ? match[0] : ''; + }, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the pathname and search params, without the root. + getPath: function() { + var path = this.decodeFragment( + this.location.pathname + this.getSearch() + ).slice(this.root.length - 1); + return path.charAt(0) === '/' ? path.slice(1) : path; + }, + + // Get the cross-browser normalized URL fragment from the path or hash. + getFragment: function(fragment) { + if (fragment == null) { + if (this._usePushState || !this._wantsHashChange) { + fragment = this.getPath(); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error('Backbone.history has already been started'); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); + this._useHashChange = this._wantsHashChange && this._hasHashChange; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.history && this.history.pushState); + this._usePushState = this._wantsPushState && this._hasPushState; + this.fragment = this.getFragment(); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + // Transition from hashChange to pushState or vice versa if both are + // requested. + if (this._wantsHashChange && this._wantsPushState) { + + // If we've started off with a route from a `pushState`-enabled + // browser, but we're currently in a browser that doesn't support it... + if (!this._hasPushState && !this.atRoot()) { + var rootPath = this.root.slice(0, -1) || '/'; + this.location.replace(rootPath + '#' + this.getPath()); + // Return immediately as browser will do redirect to new url + return true; + + // Or if we've started out with a hash-based route, but we're currently + // in a browser where it could be `pushState`-based instead... + } else if (this._hasPushState && this.atRoot()) { + this.navigate(this.getHash(), {replace: true}); + } + + } + + // Proxy an iframe to handle location events if the browser doesn't + // support the `hashchange` event, HTML5 history, or the user wants + // `hashChange` but not `pushState`. + if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { + this.iframe = document.createElement('iframe'); + this.iframe.src = 'javascript:0'; + this.iframe.style.display = 'none'; + this.iframe.tabIndex = -1; + var body = document.body; + // Using `appendChild` will throw on IE < 9 if the document is not ready. + var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; + iWindow.document.open(); + iWindow.document.close(); + iWindow.location.hash = '#' + this.fragment; + } + + // Add a cross-platform `addEventListener` shim for older browsers. + var addEventListener = window.addEventListener || function(eventName, listener) { + return attachEvent('on' + eventName, listener); + }; + + // Depending on whether we're using pushState or hashes, and whether + // 'onhashchange' is supported, determine how we check the URL state. + if (this._usePushState) { + addEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + addEventListener('hashchange', this.checkUrl, false); + } else if (this._wantsHashChange) { + this._checkUrlInterval = setInterval(this.checkUrl, this.interval); + } + + if (!this.options.silent) return this.loadUrl(); + }, + + // Disable Backbone.history, perhaps temporarily. Not useful in a real app, + // but possibly useful for unit testing Routers. + stop: function() { + // Add a cross-platform `removeEventListener` shim for older browsers. + var removeEventListener = window.removeEventListener || function(eventName, listener) { + return detachEvent('on' + eventName, listener); + }; + + // Remove window listeners. + if (this._usePushState) { + removeEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + removeEventListener('hashchange', this.checkUrl, false); + } + + // Clean up the iframe if necessary. + if (this.iframe) { + document.body.removeChild(this.iframe); + this.iframe = null; + } + + // Some environments will throw when clearing an undefined interval. + if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); + History.started = false; + }, + + // Add a route to be tested when the fragment changes. Routes added later + // may override previous routes. + route: function(route, callback) { + this.handlers.unshift({route: route, callback: callback}); + }, + + // Checks the current URL to see if it has changed, and if it has, + // calls `loadUrl`, normalizing across the hidden iframe. + checkUrl: function(e) { + var current = this.getFragment(); + + // If the user pressed the back button, the iframe's hash will have + // changed and we should use that for comparison. + if (current === this.fragment && this.iframe) { + current = this.getHash(this.iframe.contentWindow); + } + + if (current === this.fragment) return false; + if (this.iframe) this.navigate(current); + this.loadUrl(); + }, + + // Attempt to load the current URL fragment. If a route succeeds with a + // match, returns `true`. If no defined routes matches the fragment, + // returns `false`. + loadUrl: function(fragment) { + // If the root doesn't match, no routes can match either. + if (!this.matchRoot()) return false; + fragment = this.fragment = this.getFragment(fragment); + return _.some(this.handlers, function(handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + }, + + // Save a fragment into the hash history, or replace the URL state if the + // 'replace' option is passed. You are responsible for properly URL-encoding + // the fragment in advance. + // + // The options object can contain `trigger: true` if you wish to have the + // route callback be fired (not usually desirable), or `replace: true`, if + // you wish to modify the current URL without adding an entry to the history. + navigate: function(fragment, options) { + if (!History.started) return false; + if (!options || options === true) options = {trigger: !!options}; + + // Normalize the fragment. + fragment = this.getFragment(fragment || ''); + + // Don't include a trailing slash on the root. + var rootPath = this.root; + if (fragment === '' || fragment.charAt(0) === '?') { + rootPath = rootPath.slice(0, -1) || '/'; + } + var url = rootPath + fragment; + + // Strip the fragment of the query and hash for matching. + fragment = fragment.replace(pathStripper, ''); + + // Decode for matching. + var decodedFragment = this.decodeFragment(fragment); + + if (this.fragment === decodedFragment) return; + this.fragment = decodedFragment; + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._usePushState) { + this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this._updateHash(this.location, fragment, options.replace); + if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { + var iWindow = this.iframe.contentWindow; + + // Opening and closing the iframe tricks IE7 and earlier to push a + // history entry on hash-tag change. When replace is true, we don't + // want this. + if (!options.replace) { + iWindow.document.open(); + iWindow.document.close(); + } + + this._updateHash(iWindow.location, fragment, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + return this.location.assign(url); + } + if (options.trigger) return this.loadUrl(fragment); + }, + + // Update the hash location, either replacing the current entry, or adding + // a new one to the browser history. + _updateHash: function(location, fragment, replace) { + if (replace) { + var href = location.href.replace(/(javascript:|#).*$/, ''); + location.replace(href + '#' + fragment); + } else { + // Some browsers require that `hash` contains a leading #. + location.hash = '#' + fragment; + } + } + + }); + + // Create the default Backbone.history. + Backbone.history = new History; + + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var extend = function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && _.has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + _.extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + child.prototype = _.create(parent.prototype, protoProps); + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; + }; + + // Set up inheritance for the model, collection, router, view and history. + Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; + + // Throw an error when a URL is needed, and none is supplied. + var urlError = function() { + throw new Error('A "url" property or function must be specified'); + }; + + // Wrap an optional error callback with a fallback error event. + var wrapError = function(model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error.call(options.context, model, resp, options); + model.trigger('error', model, resp, options); + }; + }; + + return Backbone; +}); diff --git a/packages/backbone.marionette.js b/packages/backbone.marionette.js new file mode 100644 index 00000000..d95ecfee --- /dev/null +++ b/packages/backbone.marionette.js @@ -0,0 +1,203 @@ +(function(global,factory){typeof exports==='object'&&typeof module!=='undefined'?factory(exports,require('backbone'),require('underscore'),require('backbone.radio')):typeof define==='function'&&define.amd?define(['exports','backbone','underscore','backbone.radio'],factory):(global=global||self,(function(){var current=global.Marionette;var exports=global.Marionette={};factory(exports,global.Backbone,global._,global.Backbone.Radio);exports.noConflict=function(){global.Marionette=current;return exports;};}()));}(this,function(exports,Backbone,_,Radio){'use strict';Backbone=Backbone&&Backbone.hasOwnProperty('default')?Backbone['default']:Backbone;_=_&&_.hasOwnProperty('default')?_['default']:_;Radio=Radio&&Radio.hasOwnProperty('default')?Radio['default']:Radio;var version="4.1.2";var proxy=function proxy(method){return function(context){for(var _len=arguments.length,args=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key];} +return method.apply(context,args);};};var extend=Backbone.Model.extend;var normalizeMethods=function normalizeMethods(hash){var _this=this;if(!hash){return;} +return _.reduce(hash,function(normalizedHash,method,name){if(!_.isFunction(method)){method=_this[method];} +if(method){normalizedHash[name]=method;} +return normalizedHash;},{});};var errorProps=['description','fileName','lineNumber','name','message','number','url'];var MarionetteError=extend.call(Error,{urlRoot:"http://marionettejs.com/docs/v".concat(version,"/"),url:'',constructor:function constructor(options){var error=Error.call(this,options.message);_.extend(this,_.pick(error,errorProps),_.pick(options,errorProps));if(Error.captureStackTrace){this.captureStackTrace();} +this.url=this.urlRoot+this.url;},captureStackTrace:function captureStackTrace(){Error.captureStackTrace(this,MarionetteError);},toString:function toString(){return "".concat(this.name,": ").concat(this.message," See: ").concat(this.url);}});function normalizeBindings(context,bindings){if(!_.isObject(bindings)){throw new MarionetteError({message:'Bindings must be an object.',url:'common.html#bindevents'});} +return normalizeMethods.call(context,bindings);} +function bindEvents(entity,bindings){if(!entity||!bindings){return this;} +this.listenTo(entity,normalizeBindings(this,bindings));return this;} +function unbindEvents(entity,bindings){if(!entity){return this;} +if(!bindings){this.stopListening(entity);return this;} +this.stopListening(entity,normalizeBindings(this,bindings));return this;} +function normalizeBindings$1(context,bindings){if(!_.isObject(bindings)){throw new MarionetteError({message:'Bindings must be an object.',url:'common.html#bindrequests'});} +return normalizeMethods.call(context,bindings);} +function bindRequests(channel,bindings){if(!channel||!bindings){return this;} +channel.reply(normalizeBindings$1(this,bindings),this);return this;} +function unbindRequests(channel,bindings){if(!channel){return this;} +if(!bindings){channel.stopReplying(null,null,this);return this;} +channel.stopReplying(normalizeBindings$1(this,bindings));return this;} +var getOption=function getOption(optionName){if(!optionName){return;} +if(this.options&&this.options[optionName]!==undefined){return this.options[optionName];}else{return this[optionName];}};var mergeOptions=function mergeOptions(options,keys){var _this=this;if(!options){return;} +_.each(keys,function(key){var option=options[key];if(option!==undefined){_this[key]=option;}});};function triggerMethodChildren(view,event,shouldTrigger){if(!view._getImmediateChildren){return;} +_.each(view._getImmediateChildren(),function(child){if(!shouldTrigger(child)){return;} +child.triggerMethod(event,child);});} +function shouldTriggerAttach(view){return!view._isAttached;} +function shouldAttach(view){if(!shouldTriggerAttach(view)){return false;} +view._isAttached=true;return true;} +function shouldTriggerDetach(view){return view._isAttached;} +function shouldDetach(view){view._isAttached=false;return true;} +function triggerDOMRefresh(view){if(view._isAttached&&view._isRendered){view.triggerMethod('dom:refresh',view);}} +function triggerDOMRemove(view){if(view._isAttached&&view._isRendered){view.triggerMethod('dom:remove',view);}} +function handleBeforeAttach(){triggerMethodChildren(this,'before:attach',shouldTriggerAttach);} +function handleAttach(){triggerMethodChildren(this,'attach',shouldAttach);triggerDOMRefresh(this);} +function handleBeforeDetach(){triggerMethodChildren(this,'before:detach',shouldTriggerDetach);triggerDOMRemove(this);} +function handleDetach(){triggerMethodChildren(this,'detach',shouldDetach);} +function handleBeforeRender(){triggerDOMRemove(this);} +function handleRender(){triggerDOMRefresh(this);} +function monitorViewEvents(view){if(view._areViewEventsMonitored||view.monitorViewEvents===false){return;} +view._areViewEventsMonitored=true;view.on({'before:attach':handleBeforeAttach,'attach':handleAttach,'before:detach':handleBeforeDetach,'detach':handleDetach,'before:render':handleBeforeRender,'render':handleRender});} +var splitter=/(^|:)(\w)/gi;var methodCache={};function getEventName(match,prefix,eventName){return eventName.toUpperCase();} +var getOnMethodName=function getOnMethodName(event){if(!methodCache[event]){methodCache[event]='on'+event.replace(splitter,getEventName);} +return methodCache[event];};function triggerMethod(event){var methodName=getOnMethodName(event);var method=getOption.call(this,methodName);var result;if(_.isFunction(method)){result=method.apply(this,_.drop(arguments));} +this.trigger.apply(this,arguments);return result;} +var Events={triggerMethod:triggerMethod};var CommonMixin={normalizeMethods:normalizeMethods,_setOptions:function _setOptions(options,classOptions){this.options=_.extend({},_.result(this,'options'),options);this.mergeOptions(options,classOptions);},mergeOptions:mergeOptions,getOption:getOption,bindEvents:bindEvents,unbindEvents:unbindEvents,bindRequests:bindRequests,unbindRequests:unbindRequests,triggerMethod:triggerMethod};_.extend(CommonMixin,Backbone.Events);var DestroyMixin={_isDestroyed:false,isDestroyed:function isDestroyed(){return this._isDestroyed;},destroy:function destroy(options){if(this._isDestroyed){return this;} +this.triggerMethod('before:destroy',this,options);this._isDestroyed=true;this.triggerMethod('destroy',this,options);this.stopListening();return this;}};var RadioMixin={_initRadio:function _initRadio(){var channelName=_.result(this,'channelName');if(!channelName){return;} +if(!Radio){throw new MarionetteError({message:'The dependency "backbone.radio" is missing.',url:'backbone.radio.html#marionette-integration'});} +var channel=this._channel=Radio.channel(channelName);var radioEvents=_.result(this,'radioEvents');this.bindEvents(channel,radioEvents);var radioRequests=_.result(this,'radioRequests');this.bindRequests(channel,radioRequests);this.on('destroy',this._destroyRadio);},_destroyRadio:function _destroyRadio(){this._channel.stopReplying(null,null,this);},getChannel:function getChannel(){return this._channel;}};var ClassOptions=['channelName','radioEvents','radioRequests'];var MarionetteObject=function MarionetteObject(options){this._setOptions(options,ClassOptions);this.cid=_.uniqueId(this.cidPrefix);this._initRadio();this.initialize.apply(this,arguments);};MarionetteObject.extend=extend;_.extend(MarionetteObject.prototype,CommonMixin,DestroyMixin,RadioMixin,{cidPrefix:'mno',initialize:function initialize(){}});var _invoke=_.invokeMap||_.invoke;function getBehaviorClass(options){if(options.behaviorClass){return{BehaviorClass:options.behaviorClass,options:options};} +if(_.isFunction(options)){return{BehaviorClass:options,options:{}};} +throw new MarionetteError({message:'Unable to get behavior class. A Behavior constructor should be passed directly or as behaviorClass property of options',url:'marionette.behavior.html#defining-and-attaching-behaviors'});} +function parseBehaviors(view,behaviors,allBehaviors){return _.reduce(behaviors,function(reducedBehaviors,behaviorDefiniton){var _getBehaviorClass=getBehaviorClass(behaviorDefiniton),BehaviorClass=_getBehaviorClass.BehaviorClass,options=_getBehaviorClass.options;var behavior=new BehaviorClass(options,view);reducedBehaviors.push(behavior);return parseBehaviors(view,_.result(behavior,'behaviors'),reducedBehaviors);},allBehaviors);} +var BehaviorsMixin={_initBehaviors:function _initBehaviors(){this._behaviors=parseBehaviors(this,_.result(this,'behaviors'),[]);},_getBehaviorTriggers:function _getBehaviorTriggers(){var triggers=_invoke(this._behaviors,'_getTriggers');return _.reduce(triggers,function(memo,_triggers){return _.extend(memo,_triggers);},{});},_getBehaviorEvents:function _getBehaviorEvents(){var events=_invoke(this._behaviors,'_getEvents');return _.reduce(events,function(memo,_events){return _.extend(memo,_events);},{});},_proxyBehaviorViewProperties:function _proxyBehaviorViewProperties(){_invoke(this._behaviors,'proxyViewProperties');},_delegateBehaviorEntityEvents:function _delegateBehaviorEntityEvents(){_invoke(this._behaviors,'delegateEntityEvents');},_undelegateBehaviorEntityEvents:function _undelegateBehaviorEntityEvents(){_invoke(this._behaviors,'undelegateEntityEvents');},_destroyBehaviors:function _destroyBehaviors(options){_invoke(this._behaviors,'destroy',options);},_removeBehavior:function _removeBehavior(behavior){if(this._isDestroyed){return;} +this.undelegate(".trig".concat(behavior.cid," .").concat(behavior.cid));this._behaviors=_.without(this._behaviors,behavior);},_bindBehaviorUIElements:function _bindBehaviorUIElements(){_invoke(this._behaviors,'bindUIElements');},_unbindBehaviorUIElements:function _unbindBehaviorUIElements(){_invoke(this._behaviors,'unbindUIElements');},_triggerEventOnBehaviors:function _triggerEventOnBehaviors(eventName,view,options){_invoke(this._behaviors,'triggerMethod',eventName,view,options);}};var DelegateEntityEventsMixin={_delegateEntityEvents:function _delegateEntityEvents(model,collection){if(model){this._modelEvents=_.result(this,'modelEvents');this.bindEvents(model,this._modelEvents);} +if(collection){this._collectionEvents=_.result(this,'collectionEvents');this.bindEvents(collection,this._collectionEvents);}},_undelegateEntityEvents:function _undelegateEntityEvents(model,collection){if(this._modelEvents){this.unbindEvents(model,this._modelEvents);delete this._modelEvents;} +if(this._collectionEvents){this.unbindEvents(collection,this._collectionEvents);delete this._collectionEvents;}},_deleteEntityEventHandlers:function _deleteEntityEventHandlers(){delete this._modelEvents;delete this._collectionEvents;}};var TemplateRenderMixin={_renderTemplate:function _renderTemplate(template){var data=this.mixinTemplateContext(this.serializeData())||{};var html=this._renderHtml(template,data);if(typeof html!=='undefined'){this.attachElContent(html);}},getTemplate:function getTemplate(){return this.template;},mixinTemplateContext:function mixinTemplateContext(serializedData){var templateContext=_.result(this,'templateContext');if(!templateContext){return serializedData;} +if(!serializedData){return templateContext;} +return _.extend({},serializedData,templateContext);},serializeData:function serializeData(){if(this.model){return this.serializeModel();} +if(this.collection){return{items:this.serializeCollection()};}},serializeModel:function serializeModel(){return this.model.attributes;},serializeCollection:function serializeCollection(){return _.map(this.collection.models,function(model){return model.attributes;});},_renderHtml:function _renderHtml(template,data){return template(data);},attachElContent:function attachElContent(html){this.Dom.setContents(this.el,html,this.$el);}};var delegateEventSplitter=/^(\S+)\s*(.*)$/;var getNamespacedEventName=function getNamespacedEventName(eventName,namespace){var match=eventName.match(delegateEventSplitter);return "".concat(match[1],".").concat(namespace," ").concat(match[2]);};var FEATURES={childViewEventPrefix:false,triggersStopPropagation:true,triggersPreventDefault:true,DEV_MODE:false};function isEnabled(name){return!!FEATURES[name];} +function setEnabled(name,state){return FEATURES[name]=state;} +function buildViewTrigger(view,triggerDef){if(_.isString(triggerDef)){triggerDef={event:triggerDef};} +var eventName=triggerDef.event;var shouldPreventDefault=!!triggerDef.preventDefault;if(isEnabled('triggersPreventDefault')){shouldPreventDefault=triggerDef.preventDefault!==false;} +var shouldStopPropagation=!!triggerDef.stopPropagation;if(isEnabled('triggersStopPropagation')){shouldStopPropagation=triggerDef.stopPropagation!==false;} +return function(event){if(shouldPreventDefault){event.preventDefault();} +if(shouldStopPropagation){event.stopPropagation();} +for(var _len=arguments.length,args=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key];} +view.triggerMethod.apply(view,[eventName,view,event].concat(args));};} +var TriggersMixin={_getViewTriggers:function _getViewTriggers(view,triggers){var _this=this;return _.reduce(triggers,function(events,value,key){key=getNamespacedEventName(key,"trig".concat(_this.cid));events[key]=buildViewTrigger(view,value);return events;},{});}};var _normalizeUIKeys=function normalizeUIKeys(hash,ui){return _.reduce(hash,function(memo,val,key){var normalizedKey=_normalizeUIString(key,ui);memo[normalizedKey]=val;return memo;},{});};var uiRegEx=/@ui\.[a-zA-Z-_$0-9]*/g;var _normalizeUIString=function normalizeUIString(uiString,ui){return uiString.replace(uiRegEx,function(r){return ui[r.slice(4)];});};var _normalizeUIValues=function normalizeUIValues(hash,ui,property){_.each(hash,function(val,key){if(_.isString(val)){hash[key]=_normalizeUIString(val,ui);}else if(val){var propertyVal=val[property];if(_.isString(propertyVal)){val[property]=_normalizeUIString(propertyVal,ui);}}});return hash;};var UIMixin={normalizeUIKeys:function normalizeUIKeys(hash){var uiBindings=this._getUIBindings();return _normalizeUIKeys(hash,uiBindings);},normalizeUIString:function normalizeUIString(uiString){var uiBindings=this._getUIBindings();return _normalizeUIString(uiString,uiBindings);},normalizeUIValues:function normalizeUIValues(hash,property){var uiBindings=this._getUIBindings();return _normalizeUIValues(hash,uiBindings,property);},_getUIBindings:function _getUIBindings(){var uiBindings=_.result(this,'_uiBindings');return uiBindings||_.result(this,'ui');},_bindUIElements:function _bindUIElements(){var _this=this;if(!this.ui){return;} +if(!this._uiBindings){this._uiBindings=this.ui;} +var bindings=_.result(this,'_uiBindings');this._ui={};_.each(bindings,function(selector,key){_this._ui[key]=_this.$(selector);});this.ui=this._ui;},_unbindUIElements:function _unbindUIElements(){var _this2=this;if(!this.ui||!this._uiBindings){return;} +_.each(this.ui,function($el,name){delete _this2.ui[name];});this.ui=this._uiBindings;delete this._uiBindings;delete this._ui;},_getUI:function _getUI(name){return this._ui[name];}};function _getEl(el){return el instanceof Backbone.$?el:Backbone.$(el);} +function setDomApi(mixin){this.prototype.Dom=_.extend({},this.prototype.Dom,mixin);return this;} +var DomApi={createBuffer:function createBuffer(){return document.createDocumentFragment();},getDocumentEl:function getDocumentEl(el){return el.ownerDocument.documentElement;},getEl:function getEl(selector){return _getEl(selector);},findEl:function findEl(el,selector){return _getEl(el).find(selector);},hasEl:function hasEl(el,childEl){return el.contains(childEl&&childEl.parentNode);},detachEl:function detachEl(el){var _$el=arguments.length>1&&arguments[1]!==undefined?arguments[1]:_getEl(el);_$el.detach();},replaceEl:function replaceEl(newEl,oldEl){if(newEl===oldEl){return;} +var parent=oldEl.parentNode;if(!parent){return;} +parent.replaceChild(newEl,oldEl);},swapEl:function swapEl(el1,el2){if(el1===el2){return;} +var parent1=el1.parentNode;var parent2=el2.parentNode;if(!parent1||!parent2){return;} +var next1=el1.nextSibling;var next2=el2.nextSibling;parent1.insertBefore(el2,next1);parent2.insertBefore(el1,next2);},setContents:function setContents(el,html){var _$el=arguments.length>2&&arguments[2]!==undefined?arguments[2]:_getEl(el);_$el.html(html);},appendContents:function appendContents(el,contents){var _ref=arguments.length>2&&arguments[2]!==undefined?arguments[2]:{},_ref$_$el=_ref._$el,_$el=_ref$_$el===void 0?_getEl(el):_ref$_$el,_ref$_$contents=_ref._$contents,_$contents=_ref$_$contents===void 0?_getEl(contents):_ref$_$contents;_$el.append(_$contents);},hasContents:function hasContents(el){return!!el&&el.hasChildNodes();},detachContents:function detachContents(el){var _$el=arguments.length>1&&arguments[1]!==undefined?arguments[1]:_getEl(el);_$el.contents().detach();}};var ViewMixin={Dom:DomApi,_isElAttached:function _isElAttached(){return!!this.el&&this.Dom.hasEl(this.Dom.getDocumentEl(this.el),this.el);},supportsRenderLifecycle:true,supportsDestroyLifecycle:true,_isDestroyed:false,isDestroyed:function isDestroyed(){return!!this._isDestroyed;},_isRendered:false,isRendered:function isRendered(){return!!this._isRendered;},_isAttached:false,isAttached:function isAttached(){return!!this._isAttached;},delegateEvents:function delegateEvents(events){this._proxyBehaviorViewProperties();this._buildEventProxies();var combinedEvents=_.extend({},this._getBehaviorEvents(),this._getEvents(events),this._getBehaviorTriggers(),this._getTriggers());Backbone.View.prototype.delegateEvents.call(this,combinedEvents);return this;},_getEvents:function _getEvents(events){if(events){return this.normalizeUIKeys(events);} +if(!this.events){return;} +return this.normalizeUIKeys(_.result(this,'events'));},_getTriggers:function _getTriggers(){if(!this.triggers){return;} +var triggers=this.normalizeUIKeys(_.result(this,'triggers'));return this._getViewTriggers(this,triggers);},delegateEntityEvents:function delegateEntityEvents(){this._delegateEntityEvents(this.model,this.collection);this._delegateBehaviorEntityEvents();return this;},undelegateEntityEvents:function undelegateEntityEvents(){this._undelegateEntityEvents(this.model,this.collection);this._undelegateBehaviorEntityEvents();return this;},destroy:function destroy(options){if(this._isDestroyed||this._isDestroying){return this;} +this._isDestroying=true;var shouldTriggerDetach=this._isAttached&&!this._disableDetachEvents;this.triggerMethod('before:destroy',this,options);if(shouldTriggerDetach){this.triggerMethod('before:detach',this);} +this.unbindUIElements();this._removeElement();if(shouldTriggerDetach){this._isAttached=false;this.triggerMethod('detach',this);} +this._removeChildren();this._isDestroyed=true;this._isRendered=false;this._destroyBehaviors(options);this._deleteEntityEventHandlers();this.triggerMethod('destroy',this,options);this._triggerEventOnBehaviors('destroy',this,options);this.stopListening();return this;},_removeElement:function _removeElement(){this.$el.off().removeData();this.Dom.detachEl(this.el,this.$el);},bindUIElements:function bindUIElements(){this._bindUIElements();this._bindBehaviorUIElements();return this;},unbindUIElements:function unbindUIElements(){this._unbindUIElements();this._unbindBehaviorUIElements();return this;},getUI:function getUI(name){return this._getUI(name);},_buildEventProxies:function _buildEventProxies(){this._childViewEvents=this.normalizeMethods(_.result(this,'childViewEvents'));this._childViewTriggers=_.result(this,'childViewTriggers');this._eventPrefix=this._getEventPrefix();},_getEventPrefix:function _getEventPrefix(){var defaultPrefix=isEnabled('childViewEventPrefix')?'childview':false;var prefix=_.result(this,'childViewEventPrefix',defaultPrefix);return prefix===false?prefix:prefix+':';},_proxyChildViewEvents:function _proxyChildViewEvents(view){if(this._childViewEvents||this._childViewTriggers||this._eventPrefix){this.listenTo(view,'all',this._childViewEventHandler);}},_childViewEventHandler:function _childViewEventHandler(eventName){var childViewEvents=this._childViewEvents;for(var _len=arguments.length,args=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key];} +if(childViewEvents&&childViewEvents[eventName]){childViewEvents[eventName].apply(this,args);} +var childViewTriggers=this._childViewTriggers;if(childViewTriggers&&childViewTriggers[eventName]){this.triggerMethod.apply(this,[childViewTriggers[eventName]].concat(args));} +if(this._eventPrefix){this.triggerMethod.apply(this,[this._eventPrefix+eventName].concat(args));}}};_.extend(ViewMixin,BehaviorsMixin,CommonMixin,DelegateEntityEventsMixin,TemplateRenderMixin,TriggersMixin,UIMixin);function renderView(view){if(view._isRendered){return;} +if(!view.supportsRenderLifecycle){view.triggerMethod('before:render',view);} +view.render();view._isRendered=true;if(!view.supportsRenderLifecycle){view.triggerMethod('render',view);}} +function destroyView(view,disableDetachEvents){if(view.destroy){view._disableDetachEvents=disableDetachEvents;view.destroy();return;} +if(!view.supportsDestroyLifecycle){view.triggerMethod('before:destroy',view);} +var shouldTriggerDetach=view._isAttached&&!disableDetachEvents;if(shouldTriggerDetach){view.triggerMethod('before:detach',view);} +view.remove();if(shouldTriggerDetach){view._isAttached=false;view.triggerMethod('detach',view);} +view._isDestroyed=true;if(!view.supportsDestroyLifecycle){view.triggerMethod('destroy',view);}} +var classErrorName='RegionError';var ClassOptions$1=['allowMissingEl','parentEl','replaceElement'];var Region=function Region(options){this._setOptions(options,ClassOptions$1);this.cid=_.uniqueId(this.cidPrefix);this._initEl=this.el=this.getOption('el');this.el=this.el instanceof Backbone.$?this.el[0]:this.el;this.$el=this._getEl(this.el);this.initialize.apply(this,arguments);};Region.extend=extend;Region.setDomApi=setDomApi;_.extend(Region.prototype,CommonMixin,{Dom:DomApi,cidPrefix:'mnr',replaceElement:false,_isReplaced:false,_isSwappingView:false,initialize:function initialize(){},show:function show(view,options){if(!this._ensureElement(options)){return;} +view=this._getView(view,options);if(view===this.currentView){return this;} +if(view._isShown){throw new MarionetteError({name:classErrorName,message:'View is already shown in a Region or CollectionView',url:'marionette.region.html#showing-a-view'});} +this._isSwappingView=!!this.currentView;this.triggerMethod('before:show',this,view,options);if(this.currentView||!view._isAttached){this.empty(options);} +this._setupChildView(view);this.currentView=view;renderView(view);this._attachView(view,options);this.triggerMethod('show',this,view,options);this._isSwappingView=false;return this;},_getEl:function _getEl(el){if(!el){throw new MarionetteError({name:classErrorName,message:'An "el" must be specified for a region.',url:'marionette.region.html#additional-options'});} +return this.getEl(el);},_setEl:function _setEl(){this.$el=this._getEl(this.el);if(this.$el.length){this.el=this.$el[0];} +if(this.$el.length>1){this.$el=this.Dom.getEl(this.el);}},_setElement:function _setElement(el){if(el===this.el){return this;} +var shouldReplace=this._isReplaced;this._restoreEl();this.el=el;this._setEl();if(this.currentView){var view=this.currentView;if(shouldReplace){this._replaceEl(view);}else{this.attachHtml(view);}} +return this;},_setupChildView:function _setupChildView(view){monitorViewEvents(view);this._proxyChildViewEvents(view);view.on('destroy',this._empty,this);},_proxyChildViewEvents:function _proxyChildViewEvents(view){var parentView=this._parentView;if(!parentView){return;} +parentView._proxyChildViewEvents(view);},_shouldDisableMonitoring:function _shouldDisableMonitoring(){return this._parentView&&this._parentView.monitorViewEvents===false;},_isElAttached:function _isElAttached(){return this.Dom.hasEl(this.Dom.getDocumentEl(this.el),this.el);},_attachView:function _attachView(view){var _ref=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{},replaceElement=_ref.replaceElement;var shouldTriggerAttach=!view._isAttached&&this._isElAttached()&&!this._shouldDisableMonitoring();var shouldReplaceEl=typeof replaceElement==='undefined'?!!_.result(this,'replaceElement'):!!replaceElement;if(shouldTriggerAttach){view.triggerMethod('before:attach',view);} +if(shouldReplaceEl){this._replaceEl(view);}else{this.attachHtml(view);} +if(shouldTriggerAttach){view._isAttached=true;view.triggerMethod('attach',view);} +view._isShown=true;},_ensureElement:function _ensureElement(){var options=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{};if(!_.isObject(this.el)){this._setEl();} +if(!this.$el||this.$el.length===0){var allowMissingEl=typeof options.allowMissingEl==='undefined'?!!_.result(this,'allowMissingEl'):!!options.allowMissingEl;if(allowMissingEl){return false;}else{throw new MarionetteError({name:classErrorName,message:"An \"el\" must exist in DOM for this region ".concat(this.cid),url:'marionette.region.html#additional-options'});}} +return true;},_getView:function _getView(view){if(!view){throw new MarionetteError({name:classErrorName,message:'The view passed is undefined and therefore invalid. You must pass a view instance to show.',url:'marionette.region.html#showing-a-view'});} +if(view._isDestroyed){throw new MarionetteError({name:classErrorName,message:"View (cid: \"".concat(view.cid,"\") has already been destroyed and cannot be used."),url:'marionette.region.html#showing-a-view'});} +if(view instanceof Backbone.View){return view;} +var viewOptions=this._getViewOptions(view);return new View(viewOptions);},_getViewOptions:function _getViewOptions(viewOptions){if(_.isFunction(viewOptions)){return{template:viewOptions};} +if(_.isObject(viewOptions)){return viewOptions;} +var template=function template(){return viewOptions;};return{template:template};},getEl:function getEl(el){var context=_.result(this,'parentEl');if(context&&_.isString(el)){return this.Dom.findEl(context,el);} +return this.Dom.getEl(el);},_replaceEl:function _replaceEl(view){this._restoreEl();view.on('before:destroy',this._restoreEl,this);this.Dom.replaceEl(view.el,this.el);this._isReplaced=true;},_restoreEl:function _restoreEl(){if(!this._isReplaced){return;} +var view=this.currentView;if(!view){return;} +this._detachView(view);this._isReplaced=false;},isReplaced:function isReplaced(){return!!this._isReplaced;},isSwappingView:function isSwappingView(){return!!this._isSwappingView;},attachHtml:function attachHtml(view){this.Dom.appendContents(this.el,view.el,{_$el:this.$el,_$contents:view.$el});},empty:function empty(){var options=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{allowMissingEl:true};var view=this.currentView;if(!view){if(this._ensureElement(options)){this.detachHtml();} +return this;} +this._empty(view,true);return this;},_empty:function _empty(view,shouldDestroy){view.off('destroy',this._empty,this);this.triggerMethod('before:empty',this,view);this._restoreEl();delete this.currentView;if(!view._isDestroyed){if(shouldDestroy){this.removeView(view);}else{this._detachView(view);} +view._isShown=false;this._stopChildViewEvents(view);} +this.triggerMethod('empty',this,view);},_stopChildViewEvents:function _stopChildViewEvents(view){var parentView=this._parentView;if(!parentView){return;} +this._parentView.stopListening(view);},destroyView:function destroyView$1(view){if(view._isDestroyed){return view;} +destroyView(view,this._shouldDisableMonitoring());return view;},removeView:function removeView(view){this.destroyView(view);},detachView:function detachView(){var view=this.currentView;if(!view){return;} +this._empty(view);return view;},_detachView:function _detachView(view){var shouldTriggerDetach=view._isAttached&&!this._shouldDisableMonitoring();var shouldRestoreEl=this._isReplaced;if(shouldTriggerDetach){view.triggerMethod('before:detach',view);} +if(shouldRestoreEl){this.Dom.replaceEl(this.el,view.el);}else{this.detachHtml();} +if(shouldTriggerDetach){view._isAttached=false;view.triggerMethod('detach',view);}},detachHtml:function detachHtml(){this.Dom.detachContents(this.el,this.$el);},hasView:function hasView(){return!!this.currentView;},reset:function reset(options){this.empty(options);this.el=this._initEl;delete this.$el;return this;},_isDestroyed:false,isDestroyed:function isDestroyed(){return this._isDestroyed;},destroy:function destroy(options){if(this._isDestroyed){return this;} +this.triggerMethod('before:destroy',this,options);this._isDestroyed=true;this.reset(options);if(this._name){this._parentView._removeReferences(this._name);} +delete this._parentView;delete this._name;this.triggerMethod('destroy',this,options);this.stopListening();return this;}});function buildRegion(definition,defaults){if(definition instanceof Region){return definition;} +if(_.isString(definition)){return buildRegionFromObject(defaults,{el:definition});} +if(_.isFunction(definition)){return buildRegionFromObject(defaults,{regionClass:definition});} +if(_.isObject(definition)){return buildRegionFromObject(defaults,definition);} +throw new MarionetteError({message:'Improper region configuration type.',url:'marionette.region.html#defining-regions'});} +function buildRegionFromObject(defaults,definition){var options=_.extend({},defaults,definition);var RegionClass=options.regionClass;delete options.regionClass;return new RegionClass(options);} +var RegionsMixin={regionClass:Region,_initRegions:function _initRegions(){this.regions=this.regions||{};this._regions={};this.addRegions(_.result(this,'regions'));},_reInitRegions:function _reInitRegions(){_invoke(this._regions,'reset');},addRegion:function addRegion(name,definition){var regions={};regions[name]=definition;return this.addRegions(regions)[name];},addRegions:function addRegions(regions){if(_.isEmpty(regions)){return;} +regions=this.normalizeUIValues(regions,'el');this.regions=_.extend({},this.regions,regions);return this._addRegions(regions);},_addRegions:function _addRegions(regionDefinitions){var _this=this;var defaults={regionClass:this.regionClass,parentEl:_.partial(_.result,this,'el')};return _.reduce(regionDefinitions,function(regions,definition,name){regions[name]=buildRegion(definition,defaults);_this._addRegion(regions[name],name);return regions;},{});},_addRegion:function _addRegion(region,name){this.triggerMethod('before:add:region',this,name,region);region._parentView=this;region._name=name;this._regions[name]=region;this.triggerMethod('add:region',this,name,region);},removeRegion:function removeRegion(name){var region=this._regions[name];this._removeRegion(region,name);return region;},removeRegions:function removeRegions(){var regions=this._getRegions();_.each(this._regions,this._removeRegion.bind(this));return regions;},_removeRegion:function _removeRegion(region,name){this.triggerMethod('before:remove:region',this,name,region);region.destroy();this.triggerMethod('remove:region',this,name,region);},_removeReferences:function _removeReferences(name){delete this.regions[name];delete this._regions[name];},emptyRegions:function emptyRegions(){var regions=this.getRegions();_invoke(regions,'empty');return regions;},hasRegion:function hasRegion(name){return!!this.getRegion(name);},getRegion:function getRegion(name){if(!this._isRendered){this.render();} +return this._regions[name];},_getRegions:function _getRegions(){return _.clone(this._regions);},getRegions:function getRegions(){if(!this._isRendered){this.render();} +return this._getRegions();},showChildView:function showChildView(name,view,options){var region=this.getRegion(name);region.show(view,options);return view;},detachChildView:function detachChildView(name){return this.getRegion(name).detachView();},getChildView:function getChildView(name){return this.getRegion(name).currentView;}};function setRenderer(renderer){this.prototype._renderHtml=renderer;return this;} +var ClassOptions$2=['behaviors','childViewEventPrefix','childViewEvents','childViewTriggers','collectionEvents','events','modelEvents','regionClass','regions','template','templateContext','triggers','ui'];function childReducer(children,region){if(region.currentView){children.push(region.currentView);} +return children;} +var View=Backbone.View.extend({constructor:function constructor(options){this._setOptions(options,ClassOptions$2);monitorViewEvents(this);this._initBehaviors();this._initRegions();Backbone.View.prototype.constructor.apply(this,arguments);this.delegateEntityEvents();this._triggerEventOnBehaviors('initialize',this,options);},setElement:function setElement(){Backbone.View.prototype.setElement.apply(this,arguments);this._isRendered=this.Dom.hasContents(this.el);this._isAttached=this._isElAttached();if(this._isRendered){this.bindUIElements();} +return this;},render:function render(){var template=this.getTemplate();if(template===false||this._isDestroyed){return this;} +this.triggerMethod('before:render',this);if(this._isRendered){this._reInitRegions();} +this._renderTemplate(template);this.bindUIElements();this._isRendered=true;this.triggerMethod('render',this);return this;},_removeChildren:function _removeChildren(){this.removeRegions();},_getImmediateChildren:function _getImmediateChildren(){return _.reduce(this._regions,childReducer,[]);}},{setRenderer:setRenderer,setDomApi:setDomApi});_.extend(View.prototype,ViewMixin,RegionsMixin);var Container=function Container(){this._init();};var methods=['forEach','each','map','find','detect','filter','select','reject','every','all','some','any','include','contains','invoke','toArray','first','initial','rest','last','without','isEmpty','pluck','reduce','partition'];_.each(methods,function(method){Container.prototype[method]=function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];} +return _[method].apply(_,[this._views].concat(args));};});function stringComparator(comparator,view){return view.model&&view.model.get(comparator);} +_.extend(Container.prototype,{_init:function _init(){this._views=[];this._viewsByCid={};this._indexByModel={};this._updateLength();},_add:function _add(view){var index=arguments.length>1&&arguments[1]!==undefined?arguments[1]:this._views.length;this._addViewIndexes(view);this._views.splice(index,0,view);this._updateLength();},_addViewIndexes:function _addViewIndexes(view){this._viewsByCid[view.cid]=view;if(view.model){this._indexByModel[view.model.cid]=view;}},_sort:function _sort(comparator,context){if(typeof comparator==='string'){comparator=_.partial(stringComparator,comparator);return this._sortBy(comparator);} +if(comparator.length===1){return this._sortBy(comparator.bind(context));} +return this._views.sort(comparator.bind(context));},_sortBy:function _sortBy(comparator){var sortedViews=_.sortBy(this._views,comparator);this._set(sortedViews);return sortedViews;},_set:function _set(views,shouldReset){this._views.length=0;this._views.push.apply(this._views,views.slice(0));if(shouldReset){this._viewsByCid={};this._indexByModel={};_.each(views,this._addViewIndexes.bind(this));this._updateLength();}},_swap:function _swap(view1,view2){var view1Index=this.findIndexByView(view1);var view2Index=this.findIndexByView(view2);if(view1Index===-1||view2Index===-1){return;} +var swapView=this._views[view1Index];this._views[view1Index]=this._views[view2Index];this._views[view2Index]=swapView;},findByModel:function findByModel(model){return this.findByModelCid(model.cid);},findByModelCid:function findByModelCid(modelCid){return this._indexByModel[modelCid];},findByIndex:function findByIndex(index){return this._views[index];},findIndexByView:function findIndexByView(view){return this._views.indexOf(view);},findByCid:function findByCid(cid){return this._viewsByCid[cid];},hasView:function hasView(view){return!!this.findByCid(view.cid);},_remove:function _remove(view){if(!this._viewsByCid[view.cid]){return;} +if(view.model){delete this._indexByModel[view.model.cid];} +delete this._viewsByCid[view.cid];var index=this.findIndexByView(view);this._views.splice(index,1);this._updateLength();},_updateLength:function _updateLength(){this.length=this._views.length;}});var classErrorName$1='CollectionViewError';var ClassOptions$3=['behaviors','childView','childViewContainer','childViewEventPrefix','childViewEvents','childViewOptions','childViewTriggers','collectionEvents','emptyView','emptyViewOptions','events','modelEvents','sortWithCollection','template','templateContext','triggers','ui','viewComparator','viewFilter'];var CollectionView=Backbone.View.extend({sortWithCollection:true,constructor:function constructor(options){this._setOptions(options,ClassOptions$3);monitorViewEvents(this);this._initChildViewStorage();this._initBehaviors();Backbone.View.prototype.constructor.apply(this,arguments);this.getEmptyRegion();this.delegateEntityEvents();this._triggerEventOnBehaviors('initialize',this,options);},_initChildViewStorage:function _initChildViewStorage(){this._children=new Container();this.children=new Container();},getEmptyRegion:function getEmptyRegion(){var $emptyEl=this.$container||this.$el;if(this._emptyRegion&&!this._emptyRegion.isDestroyed()){this._emptyRegion._setElement($emptyEl[0]);return this._emptyRegion;} +this._emptyRegion=new Region({el:$emptyEl[0],replaceElement:false});this._emptyRegion._parentView=this;return this._emptyRegion;},_initialEvents:function _initialEvents(){if(this._isRendered){return;} +this.listenTo(this.collection,{'sort':this._onCollectionSort,'reset':this._onCollectionReset,'update':this._onCollectionUpdate});},_onCollectionSort:function _onCollectionSort(collection,_ref){var add=_ref.add,merge=_ref.merge,remove=_ref.remove;if(!this.sortWithCollection||this.viewComparator===false){return;} +if(add||remove||merge){return;} +this.sort();},_onCollectionReset:function _onCollectionReset(){this._destroyChildren();this._addChildModels(this.collection.models);this.sort();},_onCollectionUpdate:function _onCollectionUpdate(collection,options){var changes=options.changes;var removedViews=changes.removed.length&&this._removeChildModels(changes.removed);this._addedViews=changes.added.length&&this._addChildModels(changes.added);this._detachChildren(removedViews);this.sort();this._removeChildViews(removedViews);},_removeChildModels:function _removeChildModels(models){var _this=this;return _.reduce(models,function(views,model){var removeView=_this._removeChildModel(model);if(removeView){views.push(removeView);} +return views;},[]);},_removeChildModel:function _removeChildModel(model){var view=this._children.findByModel(model);if(view){this._removeChild(view);} +return view;},_removeChild:function _removeChild(view){this.triggerMethod('before:remove:child',this,view);this.children._remove(view);this._children._remove(view);this.triggerMethod('remove:child',this,view);},_addChildModels:function _addChildModels(models){return _.map(models,this._addChildModel.bind(this));},_addChildModel:function _addChildModel(model){var view=this._createChildView(model);this._addChild(view);return view;},_createChildView:function _createChildView(model){var ChildView=this._getChildView(model);var childViewOptions=this._getChildViewOptions(model);var view=this.buildChildView(model,ChildView,childViewOptions);return view;},_addChild:function _addChild(view,index){this.triggerMethod('before:add:child',this,view);this._setupChildView(view);this._children._add(view,index);this.children._add(view,index);this.triggerMethod('add:child',this,view);},_getChildView:function _getChildView(child){var childView=this.childView;if(!childView){throw new MarionetteError({name:classErrorName$1,message:'A "childView" must be specified',url:'marionette.collectionview.html#collectionviews-childview'});} +childView=this._getView(childView,child);if(!childView){throw new MarionetteError({name:classErrorName$1,message:'"childView" must be a view class or a function that returns a view class',url:'marionette.collectionview.html#collectionviews-childview'});} +return childView;},_getView:function _getView(view,child){if(view.prototype instanceof Backbone.View||view===Backbone.View){return view;}else if(_.isFunction(view)){return view.call(this,child);}},_getChildViewOptions:function _getChildViewOptions(child){if(_.isFunction(this.childViewOptions)){return this.childViewOptions(child);} +return this.childViewOptions;},buildChildView:function buildChildView(child,ChildViewClass,childViewOptions){var options=_.extend({model:child},childViewOptions);return new ChildViewClass(options);},_setupChildView:function _setupChildView(view){monitorViewEvents(view);view.on('destroy',this.removeChildView,this);this._proxyChildViewEvents(view);},_getImmediateChildren:function _getImmediateChildren(){return this.children._views;},setElement:function setElement(){Backbone.View.prototype.setElement.apply(this,arguments);this._isAttached=this._isElAttached();return this;},render:function render(){if(this._isDestroyed){return this;} +this.triggerMethod('before:render',this);this._destroyChildren();if(this.collection){this._addChildModels(this.collection.models);this._initialEvents();} +var template=this.getTemplate();if(template){this._renderTemplate(template);this.bindUIElements();} +this._getChildViewContainer();this.sort();this._isRendered=true;this.triggerMethod('render',this);return this;},_getChildViewContainer:function _getChildViewContainer(){var childViewContainer=_.result(this,'childViewContainer');this.$container=childViewContainer?this.$(childViewContainer):this.$el;if(!this.$container.length){throw new MarionetteError({name:classErrorName$1,message:"The specified \"childViewContainer\" was not found: ".concat(childViewContainer),url:'marionette.collectionview.html#defining-the-childviewcontainer'});}},sort:function sort(){this._sortChildren();this.filter();return this;},_sortChildren:function _sortChildren(){if(!this._children.length){return;} +var viewComparator=this.getComparator();if(!viewComparator){return;} +delete this._addedViews;this.triggerMethod('before:sort',this);this._children._sort(viewComparator,this);this.triggerMethod('sort',this);},setComparator:function setComparator(comparator){var _ref2=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{},preventRender=_ref2.preventRender;var comparatorChanged=this.viewComparator!==comparator;var shouldSort=comparatorChanged&&!preventRender;this.viewComparator=comparator;if(shouldSort){this.sort();} +return this;},removeComparator:function removeComparator(options){return this.setComparator(null,options);},getComparator:function getComparator(){if(this.viewComparator){return this.viewComparator;} +if(!this.sortWithCollection||this.viewComparator===false||!this.collection){return false;} +return this._viewComparator;},_viewComparator:function _viewComparator(view){return this.collection.indexOf(view.model);},filter:function filter(){if(this._isDestroyed){return this;} +this._filterChildren();this._renderChildren();return this;},_filterChildren:function _filterChildren(){var _this2=this;if(!this._children.length){return;} +var viewFilter=this._getFilter();if(!viewFilter){var shouldReset=this.children.length!==this._children.length;this.children._set(this._children._views,shouldReset);return;} +delete this._addedViews;this.triggerMethod('before:filter',this);var attachViews=[];var detachViews=[];_.each(this._children._views,function(view,key,children){(viewFilter.call(_this2,view,key,children)?attachViews:detachViews).push(view);});this._detachChildren(detachViews);this.children._set(attachViews,true);this.triggerMethod('filter',this,attachViews,detachViews);},_getFilter:function _getFilter(){var viewFilter=this.getFilter();if(!viewFilter){return false;} +if(_.isFunction(viewFilter)){return viewFilter;} +if(_.isObject(viewFilter)){var matcher=_.matches(viewFilter);return function(view){return matcher(view.model&&view.model.attributes);};} +if(_.isString(viewFilter)){return function(view){return view.model&&view.model.get(viewFilter);};} +throw new MarionetteError({name:classErrorName$1,message:'"viewFilter" must be a function, predicate object literal, a string indicating a model attribute, or falsy',url:'marionette.collectionview.html#defining-the-viewfilter'});},getFilter:function getFilter(){return this.viewFilter;},setFilter:function setFilter(filter){var _ref3=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{},preventRender=_ref3.preventRender;var filterChanged=this.viewFilter!==filter;var shouldRender=filterChanged&&!preventRender;this.viewFilter=filter;if(shouldRender){this.filter();} +return this;},removeFilter:function removeFilter(options){return this.setFilter(null,options);},_detachChildren:function _detachChildren(detachingViews){_.each(detachingViews,this._detachChildView.bind(this));},_detachChildView:function _detachChildView(view){var shouldTriggerDetach=view._isAttached&&this.monitorViewEvents!==false;if(shouldTriggerDetach){view.triggerMethod('before:detach',view);} +this.detachHtml(view);if(shouldTriggerDetach){view._isAttached=false;view.triggerMethod('detach',view);} +view._isShown=false;},detachHtml:function detachHtml(view){this.Dom.detachEl(view.el,view.$el);},_renderChildren:function _renderChildren(){if(this._hasUnrenderedViews){delete this._addedViews;delete this._hasUnrenderedViews;} +var views=this._addedViews||this.children._views;this.triggerMethod('before:render:children',this,views);if(this.isEmpty()){this._showEmptyView();}else{this._destroyEmptyView();var els=this._getBuffer(views);this._attachChildren(els,views);} +delete this._addedViews;this.triggerMethod('render:children',this,views);},_getBuffer:function _getBuffer(views){var _this3=this;var elBuffer=this.Dom.createBuffer();_.each(views,function(view){renderView(view);view._isShown=true;_this3.Dom.appendContents(elBuffer,view.el,{_$contents:view.$el});});return elBuffer;},_attachChildren:function _attachChildren(els,views){var shouldTriggerAttach=this._isAttached&&this.monitorViewEvents!==false;views=shouldTriggerAttach?views:[];_.each(views,function(view){if(view._isAttached){return;} +view.triggerMethod('before:attach',view);});this.attachHtml(els,this.$container);_.each(views,function(view){if(view._isAttached){return;} +view._isAttached=true;view.triggerMethod('attach',view);});},attachHtml:function attachHtml(els,$container){this.Dom.appendContents($container[0],els,{_$el:$container});},isEmpty:function isEmpty(){return!this.children.length;},_showEmptyView:function _showEmptyView(){var EmptyView=this._getEmptyView();if(!EmptyView){return;} +var options=this._getEmptyViewOptions();var emptyRegion=this.getEmptyRegion();emptyRegion.show(new EmptyView(options));},_getEmptyView:function _getEmptyView(){var emptyView=this.emptyView;if(!emptyView){return;} +return this._getView(emptyView);},_destroyEmptyView:function _destroyEmptyView(){var emptyRegion=this.getEmptyRegion();if(emptyRegion.hasView()){emptyRegion.empty();}},_getEmptyViewOptions:function _getEmptyViewOptions(){var emptyViewOptions=this.emptyViewOptions||this.childViewOptions;if(_.isFunction(emptyViewOptions)){return emptyViewOptions.call(this);} +return emptyViewOptions;},swapChildViews:function swapChildViews(view1,view2){if(!this._children.hasView(view1)||!this._children.hasView(view2)){throw new MarionetteError({name:classErrorName$1,message:'Both views must be children of the collection view to swap.',url:'marionette.collectionview.html#swapping-child-views'});} +this._children._swap(view1,view2);this.Dom.swapEl(view1.el,view2.el);if(this.children.hasView(view1)!==this.children.hasView(view2)){this.filter();}else{this.children._swap(view1,view2);} +return this;},addChildView:function addChildView(view,index){var options=arguments.length>2&&arguments[2]!==undefined?arguments[2]:{};if(!view||view._isDestroyed){return view;} +if(view._isShown){throw new MarionetteError({name:classErrorName$1,message:'View is already shown in a Region or CollectionView',url:'marionette.region.html#showing-a-view'});} +if(_.isObject(index)){options=index;} +if(options.index!=null){index=options.index;} +if(!this._isRendered){this.render();} +this._addChild(view,index);if(options.preventRender){this._hasUnrenderedViews=true;return view;} +var hasIndex=typeof index!=='undefined';var isAddedToEnd=!hasIndex||index>=this._children.length;if(isAddedToEnd&&!this._hasUnrenderedViews){this._addedViews=[view];} +if(hasIndex){this._renderChildren();}else{this.sort();} +return view;},detachChildView:function detachChildView(view){this.removeChildView(view,{shouldDetach:true});return view;},removeChildView:function removeChildView(view,options){if(!view){return view;} +this._removeChildView(view,options);this._removeChild(view);if(this.isEmpty()){this._showEmptyView();} +return view;},_removeChildViews:function _removeChildViews(views){_.each(views,this._removeChildView.bind(this));},_removeChildView:function _removeChildView(view){var _ref4=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{},shouldDetach=_ref4.shouldDetach;view.off('destroy',this.removeChildView,this);if(shouldDetach){this._detachChildView(view);}else{this._destroyChildView(view);} +this.stopListening(view);},_destroyChildView:function _destroyChildView(view){if(view._isDestroyed){return;} +var shouldDisableEvents=this.monitorViewEvents===false;destroyView(view,shouldDisableEvents);},_removeChildren:function _removeChildren(){this._destroyChildren();var emptyRegion=this.getEmptyRegion();emptyRegion.destroy();delete this._addedViews;},_destroyChildren:function _destroyChildren(){if(!this._children.length){return;} +this.triggerMethod('before:destroy:children',this);if(this.monitorViewEvents===false){this.Dom.detachContents(this.el,this.$el);} +this._removeChildViews(this._children._views);this._children._init();this.children._init();this.triggerMethod('destroy:children',this);}},{setDomApi:setDomApi,setRenderer:setRenderer});_.extend(CollectionView.prototype,ViewMixin);var ClassOptions$4=['collectionEvents','events','modelEvents','triggers','ui'];var Behavior=function Behavior(options,view){this.view=view;this._setOptions(options,ClassOptions$4);this.cid=_.uniqueId(this.cidPrefix);this.ui=_.extend({},_.result(this,'ui'),_.result(view,'ui'));this.listenTo(view,'all',this.triggerMethod);this.initialize.apply(this,arguments);};Behavior.extend=extend;_.extend(Behavior.prototype,CommonMixin,DelegateEntityEventsMixin,TriggersMixin,UIMixin,{cidPrefix:'mnb',initialize:function initialize(){},$:function $(){return this.view.$.apply(this.view,arguments);},destroy:function destroy(){this.stopListening();this.view._removeBehavior(this);this._deleteEntityEventHandlers();return this;},proxyViewProperties:function proxyViewProperties(){this.$el=this.view.$el;this.el=this.view.el;return this;},bindUIElements:function bindUIElements(){this._bindUIElements();return this;},unbindUIElements:function unbindUIElements(){this._unbindUIElements();return this;},getUI:function getUI(name){return this._getUI(name);},delegateEntityEvents:function delegateEntityEvents(){this._delegateEntityEvents(this.view.model,this.view.collection);return this;},undelegateEntityEvents:function undelegateEntityEvents(){this._undelegateEntityEvents(this.view.model,this.view.collection);return this;},_getEvents:function _getEvents(){var _this=this;if(!this.events){return;} +var behaviorEvents=this.normalizeUIKeys(_.result(this,'events'));return _.reduce(behaviorEvents,function(events,behaviorHandler,key){if(!_.isFunction(behaviorHandler)){behaviorHandler=_this[behaviorHandler];} +if(!behaviorHandler){return events;} +key=getNamespacedEventName(key,_this.cid);events[key]=behaviorHandler.bind(_this);return events;},{});},_getTriggers:function _getTriggers(){if(!this.triggers){return;} +var behaviorTriggers=this.normalizeUIKeys(_.result(this,'triggers'));return this._getViewTriggers(this.view,behaviorTriggers);}});var ClassOptions$5=['channelName','radioEvents','radioRequests','region','regionClass'];var Application=function Application(options){this._setOptions(options,ClassOptions$5);this.cid=_.uniqueId(this.cidPrefix);this._initRegion();this._initRadio();this.initialize.apply(this,arguments);};Application.extend=extend;_.extend(Application.prototype,CommonMixin,DestroyMixin,RadioMixin,{cidPrefix:'mna',initialize:function initialize(){},start:function start(options){this.triggerMethod('before:start',this,options);this.triggerMethod('start',this,options);return this;},regionClass:Region,_initRegion:function _initRegion(){var region=this.region;if(!region){return;} +var defaults={regionClass:this.regionClass};this._region=buildRegion(region,defaults);},getRegion:function getRegion(){return this._region;},showView:function showView(view){var region=this.getRegion();for(var _len=arguments.length,args=new Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key];} +region.show.apply(region,[view].concat(args));return view;},getView:function getView(){return this.getRegion().currentView;}});var bindEvents$1=proxy(bindEvents);var unbindEvents$1=proxy(unbindEvents);var bindRequests$1=proxy(bindRequests);var unbindRequests$1=proxy(unbindRequests);var mergeOptions$1=proxy(mergeOptions);var getOption$1=proxy(getOption);var normalizeMethods$1=proxy(normalizeMethods);var triggerMethod$1=proxy(triggerMethod);var setDomApi$1=function setDomApi(mixin){CollectionView.setDomApi(mixin);Region.setDomApi(mixin);View.setDomApi(mixin);};var setRenderer$1=function setRenderer(renderer){CollectionView.setRenderer(renderer);View.setRenderer(renderer);};var backbone_marionette={View:View,CollectionView:CollectionView,MnObject:MarionetteObject,Object:MarionetteObject,Region:Region,Behavior:Behavior,Application:Application,isEnabled:isEnabled,setEnabled:setEnabled,monitorViewEvents:monitorViewEvents,Events:Events,extend:extend,DomApi:DomApi,VERSION:version};exports.Application=Application;exports.Behavior=Behavior;exports.CollectionView=CollectionView;exports.DomApi=DomApi;exports.Events=Events;exports.MnObject=MarionetteObject;exports.Region=Region;exports.VERSION=version;exports.View=View;exports.bindEvents=bindEvents$1;exports.bindRequests=bindRequests$1;exports.default=backbone_marionette;exports.extend=extend;exports.getOption=getOption$1;exports.isEnabled=isEnabled;exports.mergeOptions=mergeOptions$1;exports.monitorViewEvents=monitorViewEvents;exports.normalizeMethods=normalizeMethods$1;exports.setDomApi=setDomApi$1;exports.setEnabled=setEnabled;exports.setRenderer=setRenderer$1;exports.triggerMethod=triggerMethod$1;exports.unbindEvents=unbindEvents$1;exports.unbindRequests=unbindRequests$1;Object.defineProperty(exports,'__esModule',{value:true});}));this&&this.Marionette&&(this.Mn=this.Marionette); \ No newline at end of file diff --git a/packages/backbone.modelbinder.js b/packages/backbone.modelbinder.js new file mode 100644 index 00000000..4c0886b8 --- /dev/null +++ b/packages/backbone.modelbinder.js @@ -0,0 +1,577 @@ +// Backbone.ModelBinder v1.1.1 +// (c) 2020 Bart Wood +// Distributed Under MIT License + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['underscore', 'jquery', 'backbone'], factory); + } else if(typeof module !== 'undefined' && module.exports) { + // CommonJS + module.exports = factory( + require('underscore'), + require('jquery'), + require('backbone') + ); + } else { + // Browser globals + factory(_, jQuery, Backbone); + } +}(function(_, $, Backbone){ + + if(!Backbone){ + throw new Error('Please include Backbone.js before Backbone.ModelBinder.js'); + } + + Backbone.ModelBinder = function(){ + _.bindAll.apply(_, [this].concat(_.functions(this))); + }; + + // Static setter for class level options + Backbone.ModelBinder.SetOptions = function(options){ + Backbone.ModelBinder.options = options; + }; + + // Current version of the library. + Backbone.ModelBinder.VERSION = '1.1.1'; + Backbone.ModelBinder.Constants = {}; + Backbone.ModelBinder.Constants.ModelToView = 'ModelToView'; + Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel'; + + _.extend(Backbone.ModelBinder.prototype, { + + bind:function (model, rootEl, attributeBindings, options) { + this.unbind(); + + this._model = model; + this._rootEl = rootEl; + this._setOptions(options); + + if (!this._model) this._throwException('model must be specified'); + if (!this._rootEl) this._throwException('rootEl must be specified'); + + if(attributeBindings){ + // Create a deep clone of the attribute bindings + this._attributeBindings = $.extend(true, {}, attributeBindings); + + this._initializeAttributeBindings(); + this._initializeElBindings(); + } + else { + this._initializeDefaultBindings(); + } + + this._bindModelToView(); + this._bindViewToModel(); + }, + + bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) { + this._triggers = triggers; + this.bind(model, rootEl, attributeBindings, modelSetOptions); + }, + + unbind:function () { + this._unbindModelToView(); + this._unbindViewToModel(); + + if(this._attributeBindings){ + delete this._attributeBindings; + this._attributeBindings = undefined; + } + }, + + _setOptions: function(options){ + this._options = _.extend({ + boundAttribute: 'name' + }, Backbone.ModelBinder.options, options); + + // initialize default options + if(!this._options['modelSetOptions']){ + this._options['modelSetOptions'] = {}; + } + this._options['modelSetOptions'].changeSource = 'ModelBinder'; + + if(!this._options['changeTriggers']){ + this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'}; + } + + if(!this._options['initialCopyDirection']){ + this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView; + } + }, + + // Converts the input bindings, which might just be empty or strings, to binding objects + _initializeAttributeBindings:function () { + var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding; + + for (attributeBindingKey in this._attributeBindings) { + inputBinding = this._attributeBindings[attributeBindingKey]; + + if (_.isString(inputBinding)) { + attributeBinding = {elementBindings: [{selector: inputBinding}]}; + } + else if (_.isArray(inputBinding)) { + attributeBinding = {elementBindings: inputBinding}; + } + else if(_.isObject(inputBinding)){ + attributeBinding = {elementBindings: [inputBinding]}; + } + else { + this._throwException('Unsupported type passed to Model Binder ' + attributeBinding); + } + + // Add a linkage from the element binding back to the attribute binding + for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){ + elementBinding = attributeBinding.elementBindings[elementBindingCount]; + elementBinding.attributeBinding = attributeBinding; + } + + attributeBinding.attributeName = attributeBindingKey; + this._attributeBindings[attributeBindingKey] = attributeBinding; + } + }, + + // If the bindings are not specified, the default binding is performed on the specified attribute, name by default + _initializeDefaultBindings: function(){ + var elCount, elsWithAttribute, matchedEl, name, attributeBinding; + + this._attributeBindings = {}; + elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl); + + for(elCount = 0; elCount < elsWithAttribute.length; elCount++){ + matchedEl = elsWithAttribute[elCount]; + name = $(matchedEl).attr(this._options['boundAttribute']); + + // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings + if(!this._attributeBindings[name]){ + attributeBinding = {attributeName: name}; + attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}]; + this._attributeBindings[name] = attributeBinding; + } + else{ + this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]}); + } + } + }, + + _initializeElBindings:function () { + var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el; + for (bindingKey in this._attributeBindings) { + attributeBinding = this._attributeBindings[bindingKey]; + + for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { + elementBinding = attributeBinding.elementBindings[bindingCount]; + if (elementBinding.selector === '') { + foundEls = $(this._rootEl); + } + else { + foundEls = $(elementBinding.selector, this._rootEl); + } + + if (foundEls.length === 0) { + this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector); + } + else { + elementBinding.boundEls = []; + for (elCount = 0; elCount < foundEls.length; elCount++) { + el = foundEls[elCount]; + elementBinding.boundEls.push(el); + } + } + } + } + }, + + _bindModelToView: function () { + this._model.on('change', this._onModelChange, this); + + if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){ + this.copyModelAttributesToView(); + } + }, + + // attributesToCopy is an optional parameter - if empty, all attributes + // that are bound will be copied. Otherwise, only attributeBindings specified + // in the attributesToCopy are copied. + copyModelAttributesToView: function(attributesToCopy){ + var attributeName, attributeBinding; + + for (attributeName in this._attributeBindings) { + if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){ + attributeBinding = this._attributeBindings[attributeName]; + this._copyModelToView(attributeBinding); + } + } + }, + + copyViewValuesToModel: function(){ + var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el; + for (bindingKey in this._attributeBindings) { + attributeBinding = this._attributeBindings[bindingKey]; + + for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { + elementBinding = attributeBinding.elementBindings[bindingCount]; + + if(this._isBindingUserEditable(elementBinding)){ + if(this._isBindingRadioGroup(elementBinding)){ + el = this._getRadioButtonGroupCheckedEl(elementBinding); + if(el){ + this._copyViewToModel(elementBinding, el); + } + } + else { + for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){ + el = $(elementBinding.boundEls[elCount]); + if(this._isElUserEditable(el)){ + this._copyViewToModel(elementBinding, el); + } + } + } + } + } + } + }, + + _unbindModelToView: function(){ + if(this._model){ + this._model.off('change', this._onModelChange); + this._model = undefined; + } + }, + + _bindViewToModel: function () { + _.each(this._options['changeTriggers'], function (event, selector) { + $(this._rootEl).on(event, selector, this._onElChanged); + }, this); + + if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){ + this.copyViewValuesToModel(); + } + }, + + _unbindViewToModel: function () { + if(this._options && this._options['changeTriggers']){ + _.each(this._options['changeTriggers'], function (event, selector) { + $(this._rootEl).off(event, selector, this._onElChanged); + }, this); + } + }, + + _onElChanged:function (event) { + var el, elBindings, elBindingCount, elBinding; + + el = $(event.target)[0]; + elBindings = this._getElBindings(el); + + for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){ + elBinding = elBindings[elBindingCount]; + if (this._isBindingUserEditable(elBinding)) { + this._copyViewToModel(elBinding, el); + } + } + }, + + _isBindingUserEditable: function(elBinding){ + return elBinding.elAttribute === undefined || + elBinding.elAttribute === 'text' || + elBinding.elAttribute === 'html'; + }, + + _isElUserEditable: function(el){ + var isContentEditable = el.attr('contenteditable'); + return isContentEditable || el.is('input') || el.is('select') || el.is('textarea'); + }, + + _isBindingRadioGroup: function(elBinding){ + var elCount, el; + var isAllRadioButtons = elBinding.boundEls.length > 0; + for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ + el = $(elBinding.boundEls[elCount]); + if(el.attr('type') !== 'radio'){ + isAllRadioButtons = false; + break; + } + } + + return isAllRadioButtons; + }, + + _getRadioButtonGroupCheckedEl: function(elBinding){ + var elCount, el; + for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ + el = $(elBinding.boundEls[elCount]); + if(el.attr('type') === 'radio' && el.prop('checked')){ + return el; + } + } + + return undefined; + }, + + _getElBindings:function (findEl) { + var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl; + var elBindings = []; + + for (attributeName in this._attributeBindings) { + attributeBinding = this._attributeBindings[attributeName]; + + for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { + elementBinding = attributeBinding.elementBindings[elementBindingCount]; + + for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { + boundEl = elementBinding.boundEls[boundElCount]; + + if (boundEl === findEl) { + elBindings.push(elementBinding); + } + } + } + } + + return elBindings; + }, + + _onModelChange:function () { + var changedAttribute, attributeBinding; + + for (changedAttribute in this._model.changedAttributes()) { + attributeBinding = this._attributeBindings[changedAttribute]; + + if (attributeBinding) { + this._copyModelToView(attributeBinding); + } + } + }, + + _copyModelToView:function (attributeBinding) { + var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue; + + value = this._model.get(attributeBinding.attributeName); + + for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { + elementBinding = attributeBinding.elementBindings[elementBindingCount]; + + for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { + boundEl = elementBinding.boundEls[boundElCount]; + + if(!boundEl._isSetting){ + convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); + this._setEl($(boundEl), elementBinding, convertedValue); + } + } + } + }, + + _setEl: function (el, elementBinding, convertedValue) { + if (elementBinding.elAttribute) { + this._setElAttribute(el, elementBinding, convertedValue); + } + else { + this._setElValue(el, convertedValue); + } + }, + + _setElAttribute:function (el, elementBinding, convertedValue) { + switch (elementBinding.elAttribute) { + case 'html': + el.html(convertedValue); + break; + case 'text': + el.text(convertedValue); + break; + case 'enabled': + el.prop('disabled', !convertedValue); + break; + case 'displayed': + el[convertedValue ? 'show' : 'hide'](); + break; + case 'hidden': + el[convertedValue ? 'hide' : 'show'](); + break; + case 'css': + el.css(elementBinding.cssAttribute, convertedValue); + break; + case 'class': + var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName); + var currentValue = this._model.get(elementBinding.attributeBinding.attributeName); + // is current value is now defined then remove the class the may have been set for the undefined value + if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){ + previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue); + el.removeClass(previousValue); + } + + if(convertedValue){ + el.addClass(convertedValue); + } + break; + default: + el.attr(elementBinding.elAttribute, convertedValue); + } + }, + + _setElValue:function (el, convertedValue) { + if(el.attr('type')){ + switch (el.attr('type')) { + case 'radio': + el.prop('checked', el.val() === convertedValue); + break; + case 'checkbox': + el.prop('checked', !!convertedValue); + break; + case 'file': + break; + default: + el.val(convertedValue); + } + } + else if(el.is('input') || el.is('select') || el.is('textarea')){ + el.val(convertedValue || (convertedValue === 0 ? '0' : '')); + } + else { + el.text(convertedValue || (convertedValue === 0 ? '0' : '')); + } + }, + + _copyViewToModel: function (elementBinding, el) { + var result, value, convertedValue; + + if (!el._isSetting) { + + el._isSetting = true; + result = this._setModel(elementBinding, $(el)); + el._isSetting = false; + + if(result && elementBinding.converter){ + value = this._model.get(elementBinding.attributeBinding.attributeName); + convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); + this._setEl($(el), elementBinding, convertedValue); + } + } + }, + + _getElValue: function(elementBinding, el){ + switch (el.attr('type')) { + case 'checkbox': + return el.prop('checked') ? true : false; + default: + if(el.attr('contenteditable') !== undefined){ + return el.html(); + } + else { + return el.val(); + } + } + }, + + _setModel: function (elementBinding, el) { + var data = {}; + var elVal = this._getElValue(elementBinding, el); + elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal); + data[elementBinding.attributeBinding.attributeName] = elVal; + return this._model.set(data, this._options['modelSetOptions']); + }, + + _getConvertedValue: function (direction, elementBinding, value) { + + if (elementBinding.converter) { + value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); + } + else if(this._options['converter']){ + value = this._options['converter'](direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); + } + + return value; + }, + + _throwException: function(message){ + if(this._options.suppressThrows){ + if(typeof(console) !== 'undefined' && console.error){ + console.error(message); + } + } + else { + throw new Error(message); + } + } + }); + + Backbone.ModelBinder.CollectionConverter = function(collection){ + this._collection = collection; + + if(!this._collection){ + throw new Error('Collection must be defined'); + } + _.bindAll(this, 'convert'); + }; + + _.extend(Backbone.ModelBinder.CollectionConverter.prototype, { + convert: function(direction, value){ + if (direction === Backbone.ModelBinder.Constants.ModelToView) { + return value ? value.id : undefined; + } + else { + return this._collection.get(value); + } + } + }); + + // A static helper function to create a default set of bindings that you can customize before calling the bind() function + // rootEl - where to find all of the bound elements + // attributeType - probably 'name' or 'id' in most cases + // converter(optional) - the default converter you want applied to all your bindings + // elAttribute(optional) - the default elAttribute you want applied to all your bindings + Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){ + var foundEls, elCount, foundEl, attributeName; + var bindings = {}; + + foundEls = $('[' + attributeType + ']', rootEl); + + for(elCount = 0; elCount < foundEls.length; elCount++){ + foundEl = foundEls[elCount]; + attributeName = $(foundEl).attr(attributeType); + + if(!bindings[attributeName]){ + var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'}; + bindings[attributeName] = attributeBinding; + + if(converter){ + bindings[attributeName].converter = converter; + } + + if(elAttribute){ + bindings[attributeName].elAttribute = elAttribute; + } + } + } + + return bindings; + }; + + // Helps you to combine 2 sets of bindings + Backbone.ModelBinder.combineBindings = function(destination, source){ + _.each(source, function(value, key){ + var elementBinding = {selector: value.selector}; + + if(value.converter){ + elementBinding.converter = value.converter; + } + + if(value.elAttribute){ + elementBinding.elAttribute = value.elAttribute; + } + + if(!destination[key]){ + destination[key] = elementBinding; + } + else { + destination[key] = [destination[key], elementBinding]; + } + }); + + return destination; + }; + + + return Backbone.ModelBinder; + +})); diff --git a/packages/json2.js b/packages/json2.js new file mode 100644 index 00000000..f6fada68 --- /dev/null +++ b/packages/json2.js @@ -0,0 +1,530 @@ +// json2.js +// 2017-06-12 +// Public Domain. +// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO +// NOT CONTROL. + +// This file creates a global JSON object containing two methods: stringify +// and parse. This file provides the ES5 JSON capability to ES3 systems. +// If a project might run on IE8 or earlier, then this file should be included. +// This file does nothing on ES5 systems. + +// JSON.stringify(value, replacer, space) +// value any JavaScript value, usually an object or array. +// replacer an optional parameter that determines how object +// values are stringified for objects. It can be a +// function or an array of strings. +// space an optional parameter that specifies the indentation +// of nested structures. If it is omitted, the text will +// be packed without extra whitespace. If it is a number, +// it will specify the number of spaces to indent at each +// level. If it is a string (such as "\t" or " "), +// it contains the characters used to indent at each level. +// This method produces a JSON text from a JavaScript value. +// When an object value is found, if the object contains a toJSON +// method, its toJSON method will be called and the result will be +// stringified. A toJSON method does not serialize: it returns the +// value represented by the name/value pair that should be serialized, +// or undefined if nothing should be serialized. The toJSON method +// will be passed the key associated with the value, and this will be +// bound to the value. + +// For example, this would serialize Dates as ISO strings. + +// Date.prototype.toJSON = function (key) { +// function f(n) { +// // Format integers to have at least two digits. +// return (n < 10) +// ? "0" + n +// : n; +// } +// return this.getUTCFullYear() + "-" + +// f(this.getUTCMonth() + 1) + "-" + +// f(this.getUTCDate()) + "T" + +// f(this.getUTCHours()) + ":" + +// f(this.getUTCMinutes()) + ":" + +// f(this.getUTCSeconds()) + "Z"; +// }; + +// You can provide an optional replacer method. It will be passed the +// key and value of each member, with this bound to the containing +// object. The value that is returned from your method will be +// serialized. If your method returns undefined, then the member will +// be excluded from the serialization. + +// If the replacer parameter is an array of strings, then it will be +// used to select the members to be serialized. It filters the results +// such that only members with keys listed in the replacer array are +// stringified. + +// Values that do not have JSON representations, such as undefined or +// functions, will not be serialized. Such values in objects will be +// dropped; in arrays they will be replaced with null. You can use +// a replacer function to replace those with JSON values. + +// JSON.stringify(undefined) returns undefined. + +// The optional space parameter produces a stringification of the +// value that is filled with line breaks and indentation to make it +// easier to read. + +// If the space parameter is a non-empty string, then that string will +// be used for indentation. If the space parameter is a number, then +// the indentation will be that many spaces. + +// Example: + +// text = JSON.stringify(["e", {pluribus: "unum"}]); +// // text is '["e",{"pluribus":"unum"}]' + +// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); +// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + +// text = JSON.stringify([new Date()], function (key, value) { +// return this[key] instanceof Date +// ? "Date(" + this[key] + ")" +// : value; +// }); +// // text is '["Date(---current time---)"]' + +// JSON.parse(text, reviver) +// This method parses a JSON text to produce an object or array. +// It can throw a SyntaxError exception. + +// The optional reviver parameter is a function that can filter and +// transform the results. It receives each of the keys and values, +// and its return value is used instead of the original value. +// If it returns what it received, then the structure is not modified. +// If it returns undefined then the member is deleted. + +// Example: + +// // Parse the text. Values that look like ISO date strings will +// // be converted to Date objects. + +// myData = JSON.parse(text, function (key, value) { +// var a; +// if (typeof value === "string") { +// a = +// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); +// if (a) { +// return new Date(Date.UTC( +// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6] +// )); +// } +// return value; +// } +// }); + +// myData = JSON.parse( +// "[\"Date(09/09/2001)\"]", +// function (key, value) { +// var d; +// if ( +// typeof value === "string" +// && value.slice(0, 5) === "Date(" +// && value.slice(-1) === ")" +// ) { +// d = new Date(value.slice(5, -1)); +// if (d) { +// return d; +// } +// } +// return value; +// } +// ); + +// This is a reference implementation. You are free to copy, modify, or +// redistribute. + +/*jslint + eval, for, this +*/ + +/*property + JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== "object") { + JSON = {}; +} + +(function () { + "use strict"; + + var rx_one = /^[\],:{}\s]*$/; + var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; + var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var rx_four = /(?:^|:|,)(?:\s*\[)+/g; + var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + function f(n) { + // Format integers to have at least two digits. + return (n < 10) + ? "0" + n + : n; + } + + function this_value() { + return this.valueOf(); + } + + if (typeof Date.prototype.toJSON !== "function") { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? ( + this.getUTCFullYear() + + "-" + + f(this.getUTCMonth() + 1) + + "-" + + f(this.getUTCDate()) + + "T" + + f(this.getUTCHours()) + + ":" + + f(this.getUTCMinutes()) + + ":" + + f(this.getUTCSeconds()) + + "Z" + ) + : null; + }; + + Boolean.prototype.toJSON = this_value; + Number.prototype.toJSON = this_value; + String.prototype.toJSON = this_value; + } + + var gap; + var indent; + var meta; + var rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + rx_escapable.lastIndex = 0; + return rx_escapable.test(string) + ? "\"" + string.replace(rx_escapable, function (a) { + var c = meta[a]; + return typeof c === "string" + ? c + : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); + }) + "\"" + : "\"" + string + "\""; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i; // The loop counter. + var k; // The member key. + var v; // The member value. + var length; + var mind = gap; + var partial; + var value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if ( + value + && typeof value === "object" + && typeof value.toJSON === "function" + ) { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === "function") { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case "string": + return quote(value); + + case "number": + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return (isFinite(value)) + ? String(value) + : "null"; + + case "boolean": + case "null": + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce "null". The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is "object", we might be dealing with an object or an array or +// null. + + case "object": + +// Due to a specification blunder in ECMAScript, typeof null is "object", +// so watch out for that case. + + if (!value) { + return "null"; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === "[object Array]") { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || "null"; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? "[]" + : gap + ? ( + "[\n" + + gap + + partial.join(",\n" + gap) + + "\n" + + mind + + "]" + ) + : "[" + partial.join(",") + "]"; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === "object") { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === "string") { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? "{}" + : gap + ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" + : "{" + partial.join(",") + "}"; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== "function") { + meta = { // table of character substitutions + "\b": "\\b", + "\t": "\\t", + "\n": "\\n", + "\f": "\\f", + "\r": "\\r", + "\"": "\\\"", + "\\": "\\\\" + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ""; + indent = ""; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === "number") { + for (i = 0; i < space; i += 1) { + indent += " "; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === "string") { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== "function" && ( + typeof replacer !== "object" + || typeof replacer.length !== "number" + )) { + throw new Error("JSON.stringify"); + } + +// Make a fake root object containing our value under the key of "". +// Return the result of stringifying the value. + + return str("", {"": value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== "function") { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k; + var v; + var value = holder[key]; + if (value && typeof value === "object") { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with "()" and "new" +// because they can cause invocation, and "=" because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we +// replace all simple value tokens with "]" characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or "]" or +// "," or ":" or "{" or "}". If that is so, then the text is safe for eval. + + if ( + rx_one.test( + text + .replace(rx_two, "@") + .replace(rx_three, "]") + .replace(rx_four, "") + ) + ) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The "{" operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval("(" + text + ")"); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return (typeof reviver === "function") + ? walk({"": j}, "") + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError("JSON.parse"); + }; + } +}()); diff --git a/packages/underscore-umd.js b/packages/underscore-umd.js new file mode 100644 index 00000000..ffd77af9 --- /dev/null +++ b/packages/underscore-umd.js @@ -0,0 +1,2042 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define('underscore', factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () { + var current = global._; + var exports = global._ = factory(); + exports.noConflict = function () { global._ = current; return exports; }; + }())); +}(this, (function () { + // Underscore.js 1.13.1 + // https://underscorejs.org + // (c) 2009-2021 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors + // Underscore may be freely distributed under the MIT license. + + // Current version. + var VERSION = '1.13.1'; + + // Establish the root object, `window` (`self`) in the browser, `global` + // on the server, or `this` in some virtual machines. We use `self` + // instead of `window` for `WebWorker` support. + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global || + Function('return this')() || + {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype; + var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // Modern feature detection. + var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined', + supportsDataView = typeof DataView !== 'undefined'; + + // All **ECMAScript 5+** native function implementations that we hope to use + // are declared here. + var nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeCreate = Object.create, + nativeIsView = supportsArrayBuffer && ArrayBuffer.isView; + + // Create references to these builtin functions because we override them. + var _isNaN = isNaN, + _isFinite = isFinite; + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + // The largest integer that can be represented exactly. + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + + // Some functions take a variable number of arguments, or a few expected + // arguments at the beginning and then a variable number of values to operate + // on. This helper accumulates all remaining arguments past the function’s + // argument length (or an explicit `startIndex`), into an array that becomes + // the last argument. Similar to ES6’s "rest parameter". + function restArguments(func, startIndex) { + startIndex = startIndex == null ? func.length - 1 : +startIndex; + return function() { + var length = Math.max(arguments.length - startIndex, 0), + rest = Array(length), + index = 0; + for (; index < length; index++) { + rest[index] = arguments[index + startIndex]; + } + switch (startIndex) { + case 0: return func.call(this, rest); + case 1: return func.call(this, arguments[0], rest); + case 2: return func.call(this, arguments[0], arguments[1], rest); + } + var args = Array(startIndex + 1); + for (index = 0; index < startIndex; index++) { + args[index] = arguments[index]; + } + args[startIndex] = rest; + return func.apply(this, args); + }; + } + + // Is a given variable an object? + function isObject(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + } + + // Is a given value equal to null? + function isNull(obj) { + return obj === null; + } + + // Is a given variable undefined? + function isUndefined(obj) { + return obj === void 0; + } + + // Is a given value a boolean? + function isBoolean(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + } + + // Is a given value a DOM element? + function isElement(obj) { + return !!(obj && obj.nodeType === 1); + } + + // Internal function for creating a `toString`-based type tester. + function tagTester(name) { + var tag = '[object ' + name + ']'; + return function(obj) { + return toString.call(obj) === tag; + }; + } + + var isString = tagTester('String'); + + var isNumber = tagTester('Number'); + + var isDate = tagTester('Date'); + + var isRegExp = tagTester('RegExp'); + + var isError = tagTester('Error'); + + var isSymbol = tagTester('Symbol'); + + var isArrayBuffer = tagTester('ArrayBuffer'); + + var isFunction = tagTester('Function'); + + // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old + // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). + var nodelist = root.document && root.document.childNodes; + if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { + isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + var isFunction$1 = isFunction; + + var hasObjectTag = tagTester('Object'); + + // In IE 10 - Edge 13, `DataView` has string tag `'[object Object]'`. + // In IE 11, the most common among them, this problem also applies to + // `Map`, `WeakMap` and `Set`. + var hasStringTagBug = ( + supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))) + ), + isIE11 = (typeof Map !== 'undefined' && hasObjectTag(new Map)); + + var isDataView = tagTester('DataView'); + + // In IE 10 - Edge 13, we need a different heuristic + // to determine whether an object is a `DataView`. + function ie10IsDataView(obj) { + return obj != null && isFunction$1(obj.getInt8) && isArrayBuffer(obj.buffer); + } + + var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView); + + // Is a given value an array? + // Delegates to ECMA5's native `Array.isArray`. + var isArray = nativeIsArray || tagTester('Array'); + + // Internal function to check whether `key` is an own property name of `obj`. + function has$1(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + } + + var isArguments = tagTester('Arguments'); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + (function() { + if (!isArguments(arguments)) { + isArguments = function(obj) { + return has$1(obj, 'callee'); + }; + } + }()); + + var isArguments$1 = isArguments; + + // Is a given object a finite number? + function isFinite$1(obj) { + return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj)); + } + + // Is the given value `NaN`? + function isNaN$1(obj) { + return isNumber(obj) && _isNaN(obj); + } + + // Predicate-generating function. Often useful outside of Underscore. + function constant(value) { + return function() { + return value; + }; + } + + // Common internal logic for `isArrayLike` and `isBufferLike`. + function createSizePropertyCheck(getSizeProperty) { + return function(collection) { + var sizeProperty = getSizeProperty(collection); + return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX; + } + } + + // Internal helper to generate a function to obtain property `key` from `obj`. + function shallowProperty(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + } + + // Internal helper to obtain the `byteLength` property of an object. + var getByteLength = shallowProperty('byteLength'); + + // Internal helper to determine whether we should spend extensive checks against + // `ArrayBuffer` et al. + var isBufferLike = createSizePropertyCheck(getByteLength); + + // Is a given value a typed array? + var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/; + function isTypedArray(obj) { + // `ArrayBuffer.isView` is the most future-proof, so use it when available. + // Otherwise, fall back on the above regular expression. + return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) : + isBufferLike(obj) && typedArrayPattern.test(toString.call(obj)); + } + + var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false); + + // Internal helper to obtain the `length` property of an object. + var getLength = shallowProperty('length'); + + // Internal helper to create a simple lookup structure. + // `collectNonEnumProps` used to depend on `_.contains`, but this led to + // circular imports. `emulatedSet` is a one-off solution that only works for + // arrays of strings. + function emulatedSet(keys) { + var hash = {}; + for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true; + return { + contains: function(key) { return hash[key]; }, + push: function(key) { + hash[key] = true; + return keys.push(key); + } + }; + } + + // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't + // be iterated by `for key in ...` and thus missed. Extends `keys` in place if + // needed. + function collectNonEnumProps(obj, keys) { + keys = emulatedSet(keys); + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = isFunction$1(constructor) && constructor.prototype || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (has$1(obj, prop) && !keys.contains(prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys`. + function keys(obj) { + if (!isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (has$1(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + function isEmpty(obj) { + if (obj == null) return true; + // Skip the more expensive `toString`-based type checks if `obj` has no + // `.length`. + var length = getLength(obj); + if (typeof length == 'number' && ( + isArray(obj) || isString(obj) || isArguments$1(obj) + )) return length === 0; + return getLength(keys(obj)) === 0; + } + + // Returns whether an object has a given set of `key:value` pairs. + function isMatch(object, attrs) { + var _keys = keys(attrs), length = _keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = _keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + } + + // If Underscore is called as a function, it returns a wrapped object that can + // be used OO-style. This wrapper holds altered versions of all functions added + // through `_.mixin`. Wrapped objects may be chained. + function _$1(obj) { + if (obj instanceof _$1) return obj; + if (!(this instanceof _$1)) return new _$1(obj); + this._wrapped = obj; + } + + _$1.VERSION = VERSION; + + // Extracts the result from a wrapped and chained object. + _$1.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxies for some methods used in engine operations + // such as arithmetic and JSON stringification. + _$1.prototype.valueOf = _$1.prototype.toJSON = _$1.prototype.value; + + _$1.prototype.toString = function() { + return String(this._wrapped); + }; + + // Internal function to wrap or shallow-copy an ArrayBuffer, + // typed array or DataView to a new view, reusing the buffer. + function toBufferView(bufferSource) { + return new Uint8Array( + bufferSource.buffer || bufferSource, + bufferSource.byteOffset || 0, + getByteLength(bufferSource) + ); + } + + // We use this string twice, so give it a name for minification. + var tagDataView = '[object DataView]'; + + // Internal recursive comparison function for `_.isEqual`. + function eq(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + } + + // Internal recursive comparison function for `_.isEqual`. + function deepEq(a, b, aStack, bStack) { + // Unwrap any wrapped objects. + if (a instanceof _$1) a = a._wrapped; + if (b instanceof _$1) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasStringTagBug && className == '[object Object]' && isDataView$1(a)) { + if (!isDataView$1(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + case '[object ArrayBuffer]': + case tagDataView: + // Coerce to typed array so we can fall through. + return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); + } + + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray$1(a)) { + var byteLength = getByteLength(a); + if (byteLength !== getByteLength(b)) return false; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + areArrays = true; + } + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + } + + // Perform a deep comparison to check if two objects are equal. + function isEqual(a, b) { + return eq(a, b); + } + + // Retrieve all the enumerable property names of an object. + function allKeys(obj) { + if (!isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + } + + // Since the regular `Object.prototype.toString` type tests don't work for + // some types in IE 11, we use a fingerprinting heuristic instead, based + // on the methods. It's not great, but it's the best we got. + // The fingerprint method lists are defined below. + function ie11fingerprint(methods) { + var length = getLength(methods); + return function(obj) { + if (obj == null) return false; + // `Map`, `WeakMap` and `Set` have no enumerable keys. + var keys = allKeys(obj); + if (getLength(keys)) return false; + for (var i = 0; i < length; i++) { + if (!isFunction$1(obj[methods[i]])) return false; + } + // If we are testing against `WeakMap`, we need to ensure that + // `obj` doesn't have a `forEach` method in order to distinguish + // it from a regular `Map`. + return methods !== weakMapMethods || !isFunction$1(obj[forEachName]); + }; + } + + // In the interest of compact minification, we write + // each string in the fingerprints only once. + var forEachName = 'forEach', + hasName = 'has', + commonInit = ['clear', 'delete'], + mapTail = ['get', hasName, 'set']; + + // `Map`, `WeakMap` and `Set` each have slightly different + // combinations of the above sublists. + var mapMethods = commonInit.concat(forEachName, mapTail), + weakMapMethods = commonInit.concat(mapTail), + setMethods = ['add'].concat(commonInit, forEachName, hasName); + + var isMap = isIE11 ? ie11fingerprint(mapMethods) : tagTester('Map'); + + var isWeakMap = isIE11 ? ie11fingerprint(weakMapMethods) : tagTester('WeakMap'); + + var isSet = isIE11 ? ie11fingerprint(setMethods) : tagTester('Set'); + + var isWeakSet = tagTester('WeakSet'); + + // Retrieve the values of an object's properties. + function values(obj) { + var _keys = keys(obj); + var length = _keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[_keys[i]]; + } + return values; + } + + // Convert an object into a list of `[key, value]` pairs. + // The opposite of `_.object` with one argument. + function pairs(obj) { + var _keys = keys(obj); + var length = _keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [_keys[i], obj[_keys[i]]]; + } + return pairs; + } + + // Invert the keys and values of an object. The values must be serializable. + function invert(obj) { + var result = {}; + var _keys = keys(obj); + for (var i = 0, length = _keys.length; i < length; i++) { + result[obj[_keys[i]]] = _keys[i]; + } + return result; + } + + // Return a sorted list of the function names available on the object. + function functions(obj) { + var names = []; + for (var key in obj) { + if (isFunction$1(obj[key])) names.push(key); + } + return names.sort(); + } + + // An internal function for creating assigner functions. + function createAssigner(keysFunc, defaults) { + return function(obj) { + var length = arguments.length; + if (defaults) obj = Object(obj); + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!defaults || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + } + + // Extend a given object with all the properties in passed-in object(s). + var extend = createAssigner(allKeys); + + // Assigns a given object with all the own properties in the passed-in + // object(s). + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + var extendOwn = createAssigner(keys); + + // Fill in a given object with default properties. + var defaults = createAssigner(allKeys, true); + + // Create a naked function reference for surrogate-prototype-swapping. + function ctor() { + return function(){}; + } + + // An internal function for creating a new object that inherits from another. + function baseCreate(prototype) { + if (!isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + var Ctor = ctor(); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + } + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + function create(prototype, props) { + var result = baseCreate(prototype); + if (props) extendOwn(result, props); + return result; + } + + // Create a (shallow-cloned) duplicate of an object. + function clone(obj) { + if (!isObject(obj)) return obj; + return isArray(obj) ? obj.slice() : extend({}, obj); + } + + // Invokes `interceptor` with the `obj` and then returns `obj`. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + function tap(obj, interceptor) { + interceptor(obj); + return obj; + } + + // Normalize a (deep) property `path` to array. + // Like `_.iteratee`, this function can be customized. + function toPath$1(path) { + return isArray(path) ? path : [path]; + } + _$1.toPath = toPath$1; + + // Internal wrapper for `_.toPath` to enable minification. + // Similar to `cb` for `_.iteratee`. + function toPath(path) { + return _$1.toPath(path); + } + + // Internal function to obtain a nested property in `obj` along `path`. + function deepGet(obj, path) { + var length = path.length; + for (var i = 0; i < length; i++) { + if (obj == null) return void 0; + obj = obj[path[i]]; + } + return length ? obj : void 0; + } + + // Get the value of the (deep) property on `path` from `object`. + // If any property in `path` does not exist or if the value is + // `undefined`, return `defaultValue` instead. + // The `path` is normalized through `_.toPath`. + function get(object, path, defaultValue) { + var value = deepGet(object, toPath(path)); + return isUndefined(value) ? defaultValue : value; + } + + // Shortcut function for checking if an object has a given property directly on + // itself (in other words, not on a prototype). Unlike the internal `has` + // function, this public version can also traverse nested properties. + function has(obj, path) { + path = toPath(path); + var length = path.length; + for (var i = 0; i < length; i++) { + var key = path[i]; + if (!has$1(obj, key)) return false; + obj = obj[key]; + } + return !!length; + } + + // Keep the identity function around for default iteratees. + function identity(value) { + return value; + } + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + function matcher(attrs) { + attrs = extendOwn({}, attrs); + return function(obj) { + return isMatch(obj, attrs); + }; + } + + // Creates a function that, when passed an object, will traverse that object’s + // properties down the given `path`, specified as an array of keys or indices. + function property(path) { + path = toPath(path); + return function(obj) { + return deepGet(obj, path); + }; + } + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + function optimizeCb(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + // The 2-argument case is omitted because we’re not using it. + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + } + + // An internal function to generate callbacks that can be applied to each + // element in a collection, returning the desired result — either `_.identity`, + // an arbitrary callback, a property matcher, or a property accessor. + function baseIteratee(value, context, argCount) { + if (value == null) return identity; + if (isFunction$1(value)) return optimizeCb(value, context, argCount); + if (isObject(value) && !isArray(value)) return matcher(value); + return property(value); + } + + // External wrapper for our callback generator. Users may customize + // `_.iteratee` if they want additional predicate/iteratee shorthand styles. + // This abstraction hides the internal-only `argCount` argument. + function iteratee(value, context) { + return baseIteratee(value, context, Infinity); + } + _$1.iteratee = iteratee; + + // The function we call internally to generate a callback. It invokes + // `_.iteratee` if overridden, otherwise `baseIteratee`. + function cb(value, context, argCount) { + if (_$1.iteratee !== iteratee) return _$1.iteratee(value, context); + return baseIteratee(value, context, argCount); + } + + // Returns the results of applying the `iteratee` to each element of `obj`. + // In contrast to `_.map` it returns an object. + function mapObject(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = keys(obj), + length = _keys.length, + results = {}; + for (var index = 0; index < length; index++) { + var currentKey = _keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Predicate-generating function. Often useful outside of Underscore. + function noop(){} + + // Generates a function for a given object that returns a given property. + function propertyOf(obj) { + if (obj == null) return noop; + return function(path) { + return get(obj, path); + }; + } + + // Run a function **n** times. + function times(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + } + + // Return a random integer between `min` and `max` (inclusive). + function random(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + } + + // A (possibly faster) way to get the current timestamp as an integer. + var now = Date.now || function() { + return new Date().getTime(); + }; + + // Internal helper to generate functions for escaping and unescaping strings + // to/from HTML interpolation. + function createEscaper(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + } + + // Internal list of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + // Function for escaping strings to HTML interpolation. + var _escape = createEscaper(escapeMap); + + // Internal list of HTML entities for unescaping. + var unescapeMap = invert(escapeMap); + + // Function for unescaping strings from HTML interpolation. + var _unescape = createEscaper(unescapeMap); + + // By default, Underscore uses ERB-style template delimiters. Change the + // following template settings to use alternative delimiters. + var templateSettings = _$1.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g + }; + + // When customizing `_.templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; + + function escapeChar(match) { + return '\\' + escapes[match]; + } + + // In order to prevent third-party code injection through + // `_.templateSettings.variable`, we test it against the following regular + // expression. It is intentionally a bit more liberal than just matching valid + // identifiers, but still prevents possible loopholes through defaults or + // destructuring assignment. + var bareIdentifier = /^\s*(\w|\$)+\s*$/; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + function template(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = defaults({}, settings, _$1.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + var argument = settings.variable; + if (argument) { + // Insure against third-party code injection. (CVE-2021-23358) + if (!bareIdentifier.test(argument)) throw new Error( + 'variable is not a bare identifier: ' + argument + ); + } else { + // If a variable is not specified, place data values in local scope. + source = 'with(obj||{}){\n' + source + '}\n'; + argument = 'obj'; + } + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(argument, '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _$1); + }; + + // Provide the compiled source as a convenience for precompilation. + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + } + + // Traverses the children of `obj` along `path`. If a child is a function, it + // is invoked with its parent as context. Returns the value of the final + // child, or `fallback` if any child is undefined. + function result(obj, path, fallback) { + path = toPath(path); + var length = path.length; + if (!length) { + return isFunction$1(fallback) ? fallback.call(obj) : fallback; + } + for (var i = 0; i < length; i++) { + var prop = obj == null ? void 0 : obj[path[i]]; + if (prop === void 0) { + prop = fallback; + i = length; // Ensure we don't continue iterating. + } + obj = isFunction$1(prop) ? prop.call(obj) : prop; + } + return obj; + } + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + function uniqueId(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + } + + // Start chaining a wrapped Underscore object. + function chain(obj) { + var instance = _$1(obj); + instance._chain = true; + return instance; + } + + // Internal function to execute `sourceFunc` bound to `context` with optional + // `args`. Determines whether to execute a function as a constructor or as a + // normal function. + function executeBound(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (isObject(result)) return result; + return self; + } + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. `_` acts + // as a placeholder by default, allowing any combination of arguments to be + // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. + var partial = restArguments(function(func, boundArgs) { + var placeholder = partial.placeholder; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }); + + partial.placeholder = _$1; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). + var bind = restArguments(function(func, context, args) { + if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function'); + var bound = restArguments(function(callArgs) { + return executeBound(func, bound, context, this, args.concat(callArgs)); + }); + return bound; + }); + + // Internal helper for collection methods to determine whether a collection + // should be iterated as an array or as an object. + // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var isArrayLike = createSizePropertyCheck(getLength); + + // Internal implementation of a recursive `flatten` function. + function flatten$1(input, depth, strict, output) { + output = output || []; + if (!depth && depth !== 0) { + depth = Infinity; + } else if (depth <= 0) { + return output.concat(input); + } + var idx = output.length; + for (var i = 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { + // Flatten current level of array or arguments object. + if (depth > 1) { + flatten$1(value, depth - 1, strict, output); + idx = output.length; + } else { + var j = 0, len = value.length; + while (j < len) output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + } + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + var bindAll = restArguments(function(obj, keys) { + keys = flatten$1(keys, false, false); + var index = keys.length; + if (index < 1) throw new Error('bindAll must be passed function names'); + while (index--) { + var key = keys[index]; + obj[key] = bind(obj[key], obj); + } + return obj; + }); + + // Memoize an expensive function by storing its results. + function memoize(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!has$1(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + } + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + var delay = restArguments(function(func, wait, args) { + return setTimeout(function() { + return func.apply(null, args); + }, wait); + }); + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + var defer = partial(delay, _$1, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + function throttle(func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function() { + previous = options.leading === false ? 0 : now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function() { + var _now = now(); + if (!previous && options.leading === false) previous = _now; + var remaining = wait - (_now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = _now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function() { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; + } + + // When a sequence of calls of the returned function ends, the argument + // function is triggered. The end of a sequence is defined by the `wait` + // parameter. If `immediate` is passed, the argument function will be + // triggered at the beginning of the sequence instead of at the end. + function debounce(func, wait, immediate) { + var timeout, previous, args, result, context; + + var later = function() { + var passed = now() - previous; + if (wait > passed) { + timeout = setTimeout(later, wait - passed); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + // This check is needed because `func` can recursively invoke `debounced`. + if (!timeout) args = context = null; + } + }; + + var debounced = restArguments(function(_args) { + context = this; + args = _args; + previous = now(); + if (!timeout) { + timeout = setTimeout(later, wait); + if (immediate) result = func.apply(context, args); + } + return result; + }); + + debounced.cancel = function() { + clearTimeout(timeout); + timeout = args = context = null; + }; + + return debounced; + } + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + function wrap(func, wrapper) { + return partial(wrapper, func); + } + + // Returns a negated version of the passed-in predicate. + function negate(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + } + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + function compose() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + } + + // Returns a function that will only be executed on and after the Nth call. + function after(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + } + + // Returns a function that will only be executed up to (but not including) the + // Nth call. + function before(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + } + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + var once = partial(before, 2); + + // Returns the first key on an object that passes a truth test. + function findKey(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = keys(obj), key; + for (var i = 0, length = _keys.length; i < length; i++) { + key = _keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + } + + // Internal function to generate `_.findIndex` and `_.findLastIndex`. + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a truth test. + var findIndex = createPredicateIndexFinder(1); + + // Returns the last index on an array-like that passes a truth test. + var findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + function sortedIndex(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + } + + // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions. + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), isNaN$1); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + var indexOf = createIndexFinder(1, findIndex, sortedIndex); + + // Return the position of the last occurrence of an item in an array, + // or -1 if the item is not included in the array. + var lastIndexOf = createIndexFinder(-1, findLastIndex); + + // Return the first value which passes a truth test. + function find(obj, predicate, context) { + var keyFinder = isArrayLike(obj) ? findIndex : findKey; + var key = keyFinder(obj, predicate, context); + if (key !== void 0 && key !== -1) return obj[key]; + } + + // Convenience version of a common use case of `_.find`: getting the first + // object containing specific `key:value` pairs. + function findWhere(obj, attrs) { + return find(obj, matcher(attrs)); + } + + // The cornerstone for collection functions, an `each` + // implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + function each(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var _keys = keys(obj); + for (i = 0, length = _keys.length; i < length; i++) { + iteratee(obj[_keys[i]], _keys[i], obj); + } + } + return obj; + } + + // Return the results of applying the iteratee to each element. + function map(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + } + + // Internal helper to create a reducing function, iterating left or right. + function createReduce(dir) { + // Wrap code that reassigns argument variables in a separate function than + // the one that accesses `arguments.length` to avoid a perf hit. (#1991) + var reducer = function(obj, iteratee, memo, initial) { + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length, + index = dir > 0 ? 0 : length - 1; + if (!initial) { + memo = obj[_keys ? _keys[index] : index]; + index += dir; + } + for (; index >= 0 && index < length; index += dir) { + var currentKey = _keys ? _keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + return function(obj, iteratee, memo, context) { + var initial = arguments.length >= 3; + return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + var reduce = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + var reduceRight = createReduce(-1); + + // Return all the elements that pass a truth test. + function filter(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + } + + // Return all the elements for which a truth test fails. + function reject(obj, predicate, context) { + return filter(obj, negate(cb(predicate)), context); + } + + // Determine whether all of the elements pass a truth test. + function every(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + } + + // Determine if at least one element in the object passes a truth test. + function some(obj, predicate, context) { + predicate = cb(predicate, context); + var _keys = !isArrayLike(obj) && keys(obj), + length = (_keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = _keys ? _keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + } + + // Determine if the array or object contains a given item (using `===`). + function contains(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return indexOf(obj, item, fromIndex) >= 0; + } + + // Invoke a method (with arguments) on every item in a collection. + var invoke = restArguments(function(obj, path, args) { + var contextPath, func; + if (isFunction$1(path)) { + func = path; + } else { + path = toPath(path); + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); + }); + }); + + // Convenience version of a common use case of `_.map`: fetching a property. + function pluck(obj, key) { + return map(obj, property(key)); + } + + // Convenience version of a common use case of `_.filter`: selecting only + // objects containing specific `key:value` pairs. + function where(obj, attrs) { + return filter(obj, matcher(attrs)); + } + + // Return the maximum element (or element-based computation). + function max(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Return the minimum element (or element-based computation). + function min(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + } + + // Sample **n** random values from a collection using the modern version of the + // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `_.map`. + function sample(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = values(obj); + return obj[random(obj.length - 1)]; + } + var sample = isArrayLike(obj) ? clone(obj) : values(obj); + var length = getLength(sample); + n = Math.max(Math.min(n, length), 0); + var last = length - 1; + for (var index = 0; index < n; index++) { + var rand = random(index, last); + var temp = sample[index]; + sample[index] = sample[rand]; + sample[rand] = temp; + } + return sample.slice(0, n); + } + + // Shuffle a collection. + function shuffle(obj) { + return sample(obj, Infinity); + } + + // Sort the object's values by a criterion produced by an iteratee. + function sortBy(obj, iteratee, context) { + var index = 0; + iteratee = cb(iteratee, context); + return pluck(map(obj, function(value, key, list) { + return { + value: value, + index: index++, + criteria: iteratee(value, key, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + } + + // An internal function used for aggregate "group by" operations. + function group(behavior, partition) { + return function(obj, iteratee, context) { + var result = partition ? [[], []] : {}; + iteratee = cb(iteratee, context); + each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + } + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + var groupBy = group(function(result, value, key) { + if (has$1(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `_.groupBy`, but for + // when you know that your index values will be unique. + var indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + var countBy = group(function(result, value, key) { + if (has$1(result, key)) result[key]++; else result[key] = 1; + }); + + // Split a collection into two arrays: one whose elements all pass the given + // truth test, and one whose elements all do not pass the truth test. + var partition = group(function(result, value, pass) { + result[pass ? 0 : 1].push(value); + }, true); + + // Safely create a real, live array from anything iterable. + var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + function toArray(obj) { + if (!obj) return []; + if (isArray(obj)) return slice.call(obj); + if (isString(obj)) { + // Keep surrogate pair characters together. + return obj.match(reStrSymbol); + } + if (isArrayLike(obj)) return map(obj, identity); + return values(obj); + } + + // Return the number of elements in a collection. + function size(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : keys(obj).length; + } + + // Internal `_.pick` helper function to determine whether `key` is an enumerable + // property name of `obj`. + function keyInObj(value, key, obj) { + return key in obj; + } + + // Return a copy of the object only containing the allowed properties. + var pick = restArguments(function(obj, keys) { + var result = {}, iteratee = keys[0]; + if (obj == null) return result; + if (isFunction$1(iteratee)) { + if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); + keys = allKeys(obj); + } else { + iteratee = keyInObj; + keys = flatten$1(keys, false, false); + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }); + + // Return a copy of the object without the disallowed properties. + var omit = restArguments(function(obj, keys) { + var iteratee = keys[0], context; + if (isFunction$1(iteratee)) { + iteratee = negate(iteratee); + if (keys.length > 1) context = keys[1]; + } else { + keys = map(flatten$1(keys, false, false), String); + iteratee = function(value, key) { + return !contains(keys, key); + }; + } + return pick(obj, iteratee, context); + }); + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + function initial(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + } + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. The **guard** check allows it to work with `_.map`. + function first(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[0]; + return initial(array, array.length - n); + } + + // Returns everything but the first entry of the `array`. Especially useful on + // the `arguments` object. Passing an **n** will return the rest N values in the + // `array`. + function rest(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + } + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + function last(array, n, guard) { + if (array == null || array.length < 1) return n == null || guard ? void 0 : []; + if (n == null || guard) return array[array.length - 1]; + return rest(array, Math.max(0, array.length - n)); + } + + // Trim out all falsy values from an array. + function compact(array) { + return filter(array, Boolean); + } + + // Flatten out an array, either recursively (by default), or up to `depth`. + // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively. + function flatten(array, depth) { + return flatten$1(array, depth, false); + } + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + var difference = restArguments(function(array, rest) { + rest = flatten$1(rest, true, true); + return filter(array, function(value){ + return !contains(rest, value); + }); + }); + + // Return a version of the array that does not contain the specified value(s). + var without = restArguments(function(array, otherArrays) { + return difference(array, otherArrays); + }); + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // The faster algorithm will not work with an iteratee if the iteratee + // is not a one-to-one function, so providing an iteratee will disable + // the faster algorithm. + function uniq(array, isSorted, iteratee, context) { + if (!isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted && !iteratee) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!contains(result, value)) { + result.push(value); + } + } + return result; + } + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + var union = restArguments(function(arrays) { + return uniq(flatten$1(arrays, true, true)); + }); + + // Produce an array that contains every item shared between all the + // passed-in arrays. + function intersection(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (contains(result, item)) continue; + var j; + for (j = 1; j < argsLength; j++) { + if (!contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + } + + // Complement of zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices. + function unzip(array) { + var length = array && max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = pluck(array, index); + } + return result; + } + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + var zip = restArguments(unzip); + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. Passing by pairs is the reverse of `_.pairs`. + function object(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + } + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](https://docs.python.org/library/functions.html#range). + function range(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + if (!step) { + step = stop < start ? -1 : 1; + } + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + } + + // Chunk a single array into multiple arrays, each containing `count` or fewer + // items. + function chunk(array, count) { + if (count == null || count < 1) return []; + var result = []; + var i = 0, length = array.length; + while (i < length) { + result.push(slice.call(array, i, i += count)); + } + return result; + } + + // Helper function to continue chaining intermediate results. + function chainResult(instance, obj) { + return instance._chain ? _$1(obj).chain() : obj; + } + + // Add your own custom functions to the Underscore object. + function mixin(obj) { + each(functions(obj), function(name) { + var func = _$1[name] = obj[name]; + _$1.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return chainResult(this, func.apply(_$1, args)); + }; + }); + return _$1; + } + + // Add all mutator `Array` functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _$1.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) { + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) { + delete obj[0]; + } + } + return chainResult(this, obj); + }; + }); + + // Add all accessor `Array` functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _$1.prototype[name] = function() { + var obj = this._wrapped; + if (obj != null) obj = method.apply(obj, arguments); + return chainResult(this, obj); + }; + }); + + // Named Exports + + var allExports = { + __proto__: null, + VERSION: VERSION, + restArguments: restArguments, + isObject: isObject, + isNull: isNull, + isUndefined: isUndefined, + isBoolean: isBoolean, + isElement: isElement, + isString: isString, + isNumber: isNumber, + isDate: isDate, + isRegExp: isRegExp, + isError: isError, + isSymbol: isSymbol, + isArrayBuffer: isArrayBuffer, + isDataView: isDataView$1, + isArray: isArray, + isFunction: isFunction$1, + isArguments: isArguments$1, + isFinite: isFinite$1, + isNaN: isNaN$1, + isTypedArray: isTypedArray$1, + isEmpty: isEmpty, + isMatch: isMatch, + isEqual: isEqual, + isMap: isMap, + isWeakMap: isWeakMap, + isSet: isSet, + isWeakSet: isWeakSet, + keys: keys, + allKeys: allKeys, + values: values, + pairs: pairs, + invert: invert, + functions: functions, + methods: functions, + extend: extend, + extendOwn: extendOwn, + assign: extendOwn, + defaults: defaults, + create: create, + clone: clone, + tap: tap, + get: get, + has: has, + mapObject: mapObject, + identity: identity, + constant: constant, + noop: noop, + toPath: toPath$1, + property: property, + propertyOf: propertyOf, + matcher: matcher, + matches: matcher, + times: times, + random: random, + now: now, + escape: _escape, + unescape: _unescape, + templateSettings: templateSettings, + template: template, + result: result, + uniqueId: uniqueId, + chain: chain, + iteratee: iteratee, + partial: partial, + bind: bind, + bindAll: bindAll, + memoize: memoize, + delay: delay, + defer: defer, + throttle: throttle, + debounce: debounce, + wrap: wrap, + negate: negate, + compose: compose, + after: after, + before: before, + once: once, + findKey: findKey, + findIndex: findIndex, + findLastIndex: findLastIndex, + sortedIndex: sortedIndex, + indexOf: indexOf, + lastIndexOf: lastIndexOf, + find: find, + detect: find, + findWhere: findWhere, + each: each, + forEach: each, + map: map, + collect: map, + reduce: reduce, + foldl: reduce, + inject: reduce, + reduceRight: reduceRight, + foldr: reduceRight, + filter: filter, + select: filter, + reject: reject, + every: every, + all: every, + some: some, + any: some, + contains: contains, + includes: contains, + include: contains, + invoke: invoke, + pluck: pluck, + where: where, + max: max, + min: min, + shuffle: shuffle, + sample: sample, + sortBy: sortBy, + groupBy: groupBy, + indexBy: indexBy, + countBy: countBy, + partition: partition, + toArray: toArray, + size: size, + pick: pick, + omit: omit, + first: first, + head: first, + take: first, + initial: initial, + last: last, + rest: rest, + tail: rest, + drop: rest, + compact: compact, + flatten: flatten, + without: without, + uniq: uniq, + unique: uniq, + union: union, + intersection: intersection, + difference: difference, + unzip: unzip, + transpose: unzip, + zip: zip, + object: object, + range: range, + chunk: chunk, + mixin: mixin, + 'default': _$1 + }; + + // Default Export + + // Add all of the Underscore functions to the wrapper object. + var _ = mixin(allExports); + // Legacy Node.js API. + _._ = _; + + return _; + +}))); +//# sourceMappingURL=underscore-umd.js.map diff --git a/sql/civibooking_install.sql b/sql/auto_install.sql similarity index 80% rename from sql/civibooking_install.sql rename to sql/auto_install.sql index 45fa3160..3482e20e 100644 --- a/sql/civibooking_install.sql +++ b/sql/auto_install.sql @@ -1,49 +1,53 @@ +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from schema.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- + +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- +-- /******************************************************* +-- * +-- * Clean up the existing tables +-- * +-- *******************************************************/ + SET FOREIGN_KEY_CHECKS=0; -DROP TABLE IF EXISTS `civicrm_booking_payment`; -DROP TABLE IF EXISTS `civicrm_booking_sub_slot`; -DROP TABLE IF EXISTS `civicrm_booking_slot`; DROP TABLE IF EXISTS `civicrm_booking_resource_criteria`; DROP TABLE IF EXISTS `civicrm_booking_resource_config_option`; DROP TABLE IF EXISTS `civicrm_booking_resource`; DROP TABLE IF EXISTS `civicrm_booking_adhoc_charges`; +DROP TABLE IF EXISTS `civicrm_booking_sub_slot`; +DROP TABLE IF EXISTS `civicrm_booking_slot`; DROP TABLE IF EXISTS `civicrm_booking_resource_config_set`; +DROP TABLE IF EXISTS `civicrm_booking_payment`; DROP TABLE IF EXISTS `civicrm_booking_cancellation`; DROP TABLE IF EXISTS `civicrm_booking_config`; DROP TABLE IF EXISTS `civicrm_booking`; DROP TABLE IF EXISTS `civicrm_booking_adhoc_charges_item`; - SET FOREIGN_KEY_CHECKS=1; - - -- /******************************************************* -- * --- * civicrm_booking_adhoc_charges_item +-- * Create new tables -- * -- *******************************************************/ -CREATE TABLE `civicrm_booking_adhoc_charges_item` ( - - - `id` int unsigned NOT NULL AUTO_INCREMENT , - `name` varchar(255) NOT NULL , - `label` varchar(255) NOT NULL , - `price` decimal(20,2) NOT NULL , - `weight` int unsigned NOT NULL , - `is_active` tinyint DEFAULT 1 , - `is_deleted` tinyint DEFAULT 0 -, - PRIMARY KEY ( `id` ) - - , INDEX `index_is_active`( - is_active - ) - , INDEX `index_is_deleted`( - is_deleted - ) - - -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; -- /******************************************************* -- * @@ -73,16 +77,71 @@ CREATE TABLE `civicrm_booking` ( `created_by` int unsigned NOT NULL , `created_date` datetime NOT NULL , `updated_by` int unsigned NOT NULL , - `updated_date` datetime NOT NULL + `updated_date` datetime NOT NULL , - PRIMARY KEY ( `id` ) - + PRIMARY KEY (`id`) + , INDEX `index_is_deleted`( is_deleted ) + +, CONSTRAINT FK_civicrm_booking_primary_contact_id FOREIGN KEY (`primary_contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_secondary_contact_id FOREIGN KEY (`secondary_contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE +) ; + +-- /******************************************************* +-- * +-- * civicrm_booking_adhoc_charges_item +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_booking_adhoc_charges_item` ( + -, CONSTRAINT FK_civicrm_booking_primary_contact_id FOREIGN KEY (`primary_contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_secondary_contact_id FOREIGN KEY (`secondary_contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + `id` int unsigned NOT NULL AUTO_INCREMENT , + `name` varchar(255) NOT NULL , + `label` varchar(255) NOT NULL , + `price` decimal(20,2) NOT NULL , + `weight` int unsigned NOT NULL , + `is_active` tinyint DEFAULT 1 , + `is_deleted` tinyint DEFAULT 0 +, + PRIMARY KEY (`id`) + + , INDEX `index_is_active`( + is_active + ) + , INDEX `index_is_deleted`( + is_deleted + ) + + +) ; + +-- /******************************************************* +-- * +-- * civicrm_booking_adhoc_charges +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_booking_adhoc_charges` ( + + + `id` int unsigned NOT NULL AUTO_INCREMENT , + `booking_id` int unsigned NOT NULL COMMENT 'FK to Booking ID', + `item_id` int unsigned NOT NULL COMMENT 'FK to Item ID', + `quantity` int NOT NULL , + `is_cancelled` tinyint DEFAULT 0 , + `is_deleted` tinyint DEFAULT 0 +, + PRIMARY KEY (`id`) + + , INDEX `index_is_cancelled`( + is_cancelled + ) + , INDEX `index_is_deleted`( + is_deleted + ) + +, CONSTRAINT FK_civicrm_booking_adhoc_charges_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_adhoc_charges_item_id FOREIGN KEY (`item_id`) REFERENCES `civicrm_booking_adhoc_charges_item`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * @@ -105,13 +164,13 @@ CREATE TABLE `civicrm_booking_config` ( `slot_being_edited_colour` varchar(10) , `slot_booked_colour` varchar(10) , `slot_provisional_colour` varchar(10) , - `slot_unavailable_colour` varchar(10) + `slot_unavailable_colour` varchar(10) , - PRIMARY KEY ( `id` ) - - - -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + PRIMARY KEY (`id`) + + + +) ; -- /******************************************************* -- * @@ -126,67 +185,57 @@ CREATE TABLE `civicrm_booking_cancellation` ( `cancellation_date` datetime NOT NULL , `cancellation_fee` decimal(20,2) NOT NULL , `additional_fee` decimal(20,2) , - `comment` text + `comment` text , - PRIMARY KEY ( `id` ) - - -, CONSTRAINT FK_civicrm_booking_cancellation_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + PRIMARY KEY (`id`) + + +, CONSTRAINT FK_civicrm_booking_cancellation_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * --- * civicrm_booking_resource_config_set +-- * civicrm_booking_payment -- * -- *******************************************************/ -CREATE TABLE `civicrm_booking_resource_config_set` ( +CREATE TABLE `civicrm_booking_payment` ( `id` int unsigned NOT NULL AUTO_INCREMENT , - `title` varchar(255) NOT NULL , - `weight` int unsigned NOT NULL , - `is_active` tinyint DEFAULT 1 , - `is_deleted` tinyint DEFAULT 0 + `booking_id` int unsigned NOT NULL COMMENT 'Foreign key to the booking id for this payment.', + `contribution_id` int unsigned NOT NULL COMMENT 'Foreign key to the contribution for this payment.' , - PRIMARY KEY ( `id` ) - - , INDEX `index_is_active`( - is_active - ) - , INDEX `index_is_deleted`( - is_deleted - ) - - -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; - + PRIMARY KEY (`id`) + + +, CONSTRAINT FK_civicrm_booking_payment_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_payment_contribution_id FOREIGN KEY (`contribution_id`) REFERENCES `civicrm_contribution`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * --- * civicrm_booking_adhoc_charges +-- * civicrm_booking_resource_config_set -- * -- *******************************************************/ -CREATE TABLE `civicrm_booking_adhoc_charges` ( +CREATE TABLE `civicrm_booking_resource_config_set` ( `id` int unsigned NOT NULL AUTO_INCREMENT , - `booking_id` int unsigned NOT NULL COMMENT 'FK to Booking ID', - `item_id` int unsigned NOT NULL COMMENT 'FK to Item ID', - `quantity` int NOT NULL , - `is_cancelled` tinyint DEFAULT 0 , - `is_deleted` tinyint DEFAULT 0 + `title` varchar(255) NOT NULL , + `weight` int unsigned NOT NULL , + `is_active` tinyint DEFAULT 1 , + `is_deleted` tinyint DEFAULT 0 , - PRIMARY KEY ( `id` ) - - , INDEX `index_is_cancelled`( - is_cancelled + PRIMARY KEY (`id`) + + , INDEX `index_is_active`( + is_active ) , INDEX `index_is_deleted`( is_deleted ) - -, CONSTRAINT FK_civicrm_booking_adhoc_charges_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_adhoc_charges_item_id FOREIGN KEY (`item_id`) REFERENCES `civicrm_booking_adhoc_charges_item`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + + +) ; -- /******************************************************* -- * @@ -205,10 +254,10 @@ CREATE TABLE `civicrm_booking_resource` ( `weight` int NOT NULL , `is_unlimited` tinyint NOT NULL DEFAULT 0 , `is_active` tinyint DEFAULT 1 , - `is_deleted` tinyint DEFAULT 0 + `is_deleted` tinyint DEFAULT 0 , - PRIMARY KEY ( `id` ) - + PRIMARY KEY (`id`) + , INDEX `index_is_unlimited`( is_unlimited ) @@ -218,9 +267,9 @@ CREATE TABLE `civicrm_booking_resource` ( , INDEX `index_is_deleted`( is_deleted ) - -, CONSTRAINT FK_civicrm_booking_resource_set_id FOREIGN KEY (`set_id`) REFERENCES `civicrm_booking_resource_config_set`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + +, CONSTRAINT FK_civicrm_booking_resource_set_id FOREIGN KEY (`set_id`) REFERENCES `civicrm_booking_resource_config_set`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * @@ -238,19 +287,19 @@ CREATE TABLE `civicrm_booking_resource_config_option` ( `unit_id` varchar(512) COMMENT 'The unit associated with this config option. Implicit FK to option_value row in booking_size_unit option_group.', `weight` int unsigned NOT NULL , `is_active` tinyint DEFAULT 1 , - `is_deleted` tinyint DEFAULT 0 + `is_deleted` tinyint DEFAULT 0 , - PRIMARY KEY ( `id` ) - + PRIMARY KEY (`id`) + , INDEX `index_is_active`( is_active ) , INDEX `index_is_deleted`( is_deleted ) - -, CONSTRAINT FK_civicrm_booking_resource_config_option_set_id FOREIGN KEY (`set_id`) REFERENCES `civicrm_booking_resource_config_set`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + +, CONSTRAINT FK_civicrm_booking_resource_config_option_set_id FOREIGN KEY (`set_id`) REFERENCES `civicrm_booking_resource_config_set`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * @@ -262,13 +311,13 @@ CREATE TABLE `civicrm_booking_resource_criteria` ( `id` int unsigned NOT NULL AUTO_INCREMENT , `resource_id` int unsigned NOT NULL COMMENT 'Foreign key to the resoure for this resource criteria.', - `criteria_id` varchar(512) NOT NULL COMMENT 'Foreign key to the resource criteria option group.' + `criteria_id` varchar(512) NOT NULL COMMENT 'Foreign key to the resource criteria option group.' , - PRIMARY KEY ( `id` ) - - -, CONSTRAINT FK_civicrm_booking_resource_criteria_resource_id FOREIGN KEY (`resource_id`) REFERENCES `civicrm_booking_resource`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + PRIMARY KEY (`id`) + + +, CONSTRAINT FK_civicrm_booking_resource_criteria_resource_id FOREIGN KEY (`resource_id`) REFERENCES `civicrm_booking_resource`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * @@ -287,19 +336,19 @@ CREATE TABLE `civicrm_booking_slot` ( `quantity` int NOT NULL , `note` text , `is_cancelled` tinyint DEFAULT 0 , - `is_deleted` tinyint DEFAULT 0 + `is_deleted` tinyint DEFAULT 0 , - PRIMARY KEY ( `id` ) - + PRIMARY KEY (`id`) + , INDEX `index_is_cancelled`( is_cancelled ) , INDEX `index_is_deleted`( is_deleted ) - -, CONSTRAINT FK_civicrm_booking_slot_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_slot_resource_id FOREIGN KEY (`resource_id`) REFERENCES `civicrm_booking_resource`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_slot_config_id FOREIGN KEY (`config_id`) REFERENCES `civicrm_booking_resource_config_option`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; + +, CONSTRAINT FK_civicrm_booking_slot_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_slot_resource_id FOREIGN KEY (`resource_id`) REFERENCES `civicrm_booking_resource`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_slot_config_id FOREIGN KEY (`config_id`) REFERENCES `civicrm_booking_resource_config_option`(`id`) ON DELETE CASCADE +) ; -- /******************************************************* -- * @@ -317,36 +366,16 @@ CREATE TABLE `civicrm_booking_sub_slot` ( `quantity` int NOT NULL , `note` text , `is_cancelled` tinyint DEFAULT 0 , - `is_deleted` tinyint DEFAULT 0 + `is_deleted` tinyint DEFAULT 0 , - PRIMARY KEY ( `id` ) - + PRIMARY KEY (`id`) + , INDEX `index_is_cancelled`( is_cancelled ) , INDEX `index_is_deleted`( is_deleted ) - -, CONSTRAINT FK_civicrm_booking_sub_slot_slot_id FOREIGN KEY (`slot_id`) REFERENCES `civicrm_booking_slot`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_sub_slot_resource_id FOREIGN KEY (`resource_id`) REFERENCES `civicrm_booking_resource`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_sub_slot_config_id FOREIGN KEY (`config_id`) REFERENCES `civicrm_booking_resource_config_option`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; - --- /******************************************************* --- * --- * civicrm_booking_payment --- * --- *******************************************************/ -CREATE TABLE `civicrm_booking_payment` ( - - - `id` int unsigned NOT NULL AUTO_INCREMENT , - `booking_id` int unsigned NOT NULL COMMENT 'Foreign key to the booking id for this payment.', - `contribution_id` int unsigned NOT NULL COMMENT 'Foreign key to the contribution for this payment.' -, - PRIMARY KEY ( `id` ) - - -, CONSTRAINT FK_civicrm_booking_payment_booking_id FOREIGN KEY (`booking_id`) REFERENCES `civicrm_booking`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_payment_contribution_id FOREIGN KEY (`contribution_id`) REFERENCES `civicrm_contribution`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ; - - + +, CONSTRAINT FK_civicrm_booking_sub_slot_slot_id FOREIGN KEY (`slot_id`) REFERENCES `civicrm_booking_slot`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_sub_slot_resource_id FOREIGN KEY (`resource_id`) REFERENCES `civicrm_booking_resource`(`id`) ON DELETE CASCADE, CONSTRAINT FK_civicrm_booking_sub_slot_config_id FOREIGN KEY (`config_id`) REFERENCES `civicrm_booking_resource_config_option`(`id`) ON DELETE CASCADE +) ; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql new file mode 100644 index 00000000..b6bb62bd --- /dev/null +++ b/sql/auto_uninstall.sql @@ -0,0 +1,33 @@ +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- +-- /******************************************************* +-- * +-- * Clean up the existing tables +-- * +-- *******************************************************/ + +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `civicrm_booking_resource_criteria`; +DROP TABLE IF EXISTS `civicrm_booking_resource_config_option`; +DROP TABLE IF EXISTS `civicrm_booking_resource`; +DROP TABLE IF EXISTS `civicrm_booking_adhoc_charges`; +DROP TABLE IF EXISTS `civicrm_booking_sub_slot`; +DROP TABLE IF EXISTS `civicrm_booking_slot`; +DROP TABLE IF EXISTS `civicrm_booking_resource_config_set`; +DROP TABLE IF EXISTS `civicrm_booking_payment`; +DROP TABLE IF EXISTS `civicrm_booking_cancellation`; +DROP TABLE IF EXISTS `civicrm_booking_config`; +DROP TABLE IF EXISTS `civicrm_booking`; +DROP TABLE IF EXISTS `civicrm_booking_adhoc_charges_item`; + +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/sql/civibooking_uninstall.sql b/sql/civibooking_uninstall.sql index 078b2c8a..26aa28fa 100644 --- a/sql/civibooking_uninstall.sql +++ b/sql/civibooking_uninstall.sql @@ -43,19 +43,4 @@ DELETE FROM civicrm_option_group WHERE name = 'booking_cancellation_charges'; DELETE FROM civicrm_option_group WHERE name = 'booking_size_unit'; DELETE FROM civicrm_option_group WHERE name = 'msg_tpl_workflow_booking'; - -DROP TABLE IF EXISTS `civicrm_booking_payment`; -DROP TABLE IF EXISTS `civicrm_booking_sub_slot`; -DROP TABLE IF EXISTS `civicrm_booking_slot`; -DROP TABLE IF EXISTS `civicrm_booking_resource_criteria`; -DROP TABLE IF EXISTS `civicrm_booking_resource_config_option`; -DROP TABLE IF EXISTS `civicrm_booking_resource`; -DROP TABLE IF EXISTS `civicrm_booking_adhoc_charges`; -DROP TABLE IF EXISTS `civicrm_booking_resource_config_set`; -DROP TABLE IF EXISTS `civicrm_booking_cancellation`; -DROP TABLE IF EXISTS `civicrm_booking_config`; -DROP TABLE IF EXISTS `civicrm_booking`; -DROP TABLE IF EXISTS `civicrm_booking_adhoc_charges_item`; - - SET FOREIGN_KEY_CHECKS=1; diff --git a/templates/CRM/Booking/Form/DayView.tpl b/templates/CRM/Booking/Form/DayView.tpl index 8a4198ff..301a1be6 100644 --- a/templates/CRM/Booking/Form/DayView.tpl +++ b/templates/CRM/Booking/Form/DayView.tpl @@ -23,7 +23,7 @@ {* Search DayView results *} -{if $resources} +{if $resultDisplay} {if empty($resources)} {* No matches for submitted search request or viewing an empty result. *}
{/if} -{literal} - -{/literal} {/crmScope} diff --git a/xml/data/OptionGroups.xml b/xml/OptionGroups_install.xml similarity index 100% rename from xml/data/OptionGroups.xml rename to xml/OptionGroups_install.xml diff --git a/xml/schema/Booking/files.xml b/xml/schema/Booking/files.xml deleted file mode 100644 index 9139f566..00000000 --- a/xml/schema/Booking/files.xml +++ /dev/null @@ -1,17 +0,0 @@ - - -