* @return CRM_Activity_BAO_Activity|null|object
*/
public static function create(&$params) {
+ // CRM-20958 - These fields are managed by MySQL triggers. Watch out for clients resaving stale timestamps.
+ unset($params['created_date']);
+ unset($params['modified_date']);
+
// check required params
if (!self::dataExists($params)) {
throw new CRM_Core_Exception('Not enough data to create activity object');
*
* Generated from xml/schema/CRM/Activity/Activity.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:a530f1fb1a27c5a15b5d138732b4c581)
+ * (GenCodeChecksum:dfa63754ef6ea1a9c7148e735dd6ff8a)
*/
require_once 'CRM/Core/DAO.php';
require_once 'CRM/Utils/Type.php';
* @var boolean
*/
public $is_star;
+ /**
+ * When was the activity was created.
+ *
+ * @var timestamp
+ */
+ public $created_date;
+ /**
+ * When was the activity (or closely related entity) was created or modified or deleted.
+ *
+ * @var timestamp
+ */
+ public $modified_date;
/**
* Class constructor.
*/
'bao' => 'CRM_Activity_BAO_Activity',
'localizable' => 0,
) ,
+ 'activity_created_date' => array(
+ 'name' => 'created_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => ts('Created Date') ,
+ 'description' => 'When was the activity was created.',
+ 'required' => false,
+ 'export' => true,
+ 'where' => 'civicrm_activity.created_date',
+ 'headerPattern' => '',
+ 'dataPattern' => '',
+ 'default' => 'NULL',
+ 'table_name' => 'civicrm_activity',
+ 'entity' => 'Activity',
+ 'bao' => 'CRM_Activity_BAO_Activity',
+ 'localizable' => 0,
+ ) ,
+ 'activity_modified_date' => array(
+ 'name' => 'modified_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => ts('Modified Date') ,
+ 'description' => 'When was the activity (or closely related entity) was created or modified or deleted.',
+ 'required' => false,
+ 'export' => true,
+ 'where' => 'civicrm_activity.modified_date',
+ 'headerPattern' => '',
+ 'dataPattern' => '',
+ 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+ 'table_name' => 'civicrm_activity',
+ 'entity' => 'Activity',
+ 'bao' => 'CRM_Activity_BAO_Activity',
+ 'localizable' => 0,
+ ) ,
);
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
}
* @return CRM_Case_BAO_Case
*/
public static function &create(&$params) {
+ // CRM-20958 - These fields are managed by MySQL triggers. Watch out for clients resaving stale timestamps.
+ unset($params['created_date']);
+ unset($params['modified_date']);
+
$transaction = new CRM_Core_Transaction();
if (!empty($params['id'])) {
*
* Generated from xml/schema/CRM/Case/Case.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:e45e7e2a53a945c4659cf393410a9d7a)
+ * (GenCodeChecksum:2a046fd795b19790f45c5d9dde06a538)
*/
require_once 'CRM/Core/DAO.php';
require_once 'CRM/Utils/Type.php';
* @var boolean
*/
public $is_deleted;
+ /**
+ * When was the case was created.
+ *
+ * @var timestamp
+ */
+ public $created_date;
+ /**
+ * When was the case (or closely related entity) was created or modified or deleted.
+ *
+ * @var timestamp
+ */
+ public $modified_date;
/**
* Class constructor.
*/
'bao' => 'CRM_Case_BAO_Case',
'localizable' => 0,
) ,
+ 'case_created_date' => array(
+ 'name' => 'created_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => ts('Created Date') ,
+ 'description' => 'When was the case was created.',
+ 'required' => false,
+ 'export' => true,
+ 'where' => 'civicrm_case.created_date',
+ 'headerPattern' => '',
+ 'dataPattern' => '',
+ 'default' => 'NULL',
+ 'table_name' => 'civicrm_case',
+ 'entity' => 'Case',
+ 'bao' => 'CRM_Case_BAO_Case',
+ 'localizable' => 0,
+ ) ,
+ 'case_modified_date' => array(
+ 'name' => 'modified_date',
+ 'type' => CRM_Utils_Type::T_TIMESTAMP,
+ 'title' => ts('Modified Date') ,
+ 'description' => 'When was the case (or closely related entity) was created or modified or deleted.',
+ 'required' => false,
+ 'export' => true,
+ 'where' => 'civicrm_case.modified_date',
+ 'headerPattern' => '',
+ 'dataPattern' => '',
+ 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+ 'table_name' => 'civicrm_case',
+ 'entity' => 'Case',
+ 'bao' => 'CRM_Case_BAO_Case',
+ 'localizable' => 0,
+ ) ,
);
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
}
}
}
- /**
- * Generate triggers to update the timestamp.
- *
- * The corresponding civicrm_contact row is updated on insert/update/delete
- * to a table that extends civicrm_contact.
- * Don't regenerate triggers for all such tables if only asked for one table.
- *
- * @param array $info
- * Reference to the array where generated trigger information is being stored
- * @param string|null $reqTableName
- * Name of the table for which triggers are being generated, or NULL if all tables
- * @param array $relatedTableNames
- * Array of all core or all custom table names extending civicrm_contact
- * @param string $contactRefColumn
- * 'contact_id' if processing core tables, 'entity_id' if processing custom tables
- *
- * @link https://issues.civicrm.org/jira/browse/CRM-15602
- * @see triggerInfo
- */
- public static function generateTimestampTriggers(&$info, $reqTableName, $relatedTableNames, $contactRefColumn) {
- // Safety
- $contactRefColumn = CRM_Core_DAO::escapeString($contactRefColumn);
- // If specific related table requested, just process that one
- if (in_array($reqTableName, $relatedTableNames)) {
- $relatedTableNames = array($reqTableName);
- }
-
- // If no specific table requested (include all related tables),
- // or a specific related table requested (as matched above)
- if (empty($reqTableName) || in_array($reqTableName, $relatedTableNames)) {
- $info[] = array(
- 'table' => $relatedTableNames,
- 'when' => 'AFTER',
- 'event' => array('INSERT', 'UPDATE'),
- 'sql' => "\nUPDATE civicrm_contact SET modified_date = CURRENT_TIMESTAMP WHERE id = NEW.$contactRefColumn;\n",
- );
- $info[] = array(
- 'table' => $relatedTableNames,
- 'when' => 'AFTER',
- 'event' => array('DELETE'),
- 'sql' => "\nUPDATE civicrm_contact SET modified_date = CURRENT_TIMESTAMP WHERE id = OLD.$contactRefColumn;\n",
- );
- }
- }
-
/**
* Get a list of triggers for the contact table.
*
}
}
- // Update timestamp for civicrm_contact itself
- if ($tableName == NULL || $tableName == self::getTableName()) {
- $info[] = array(
- 'table' => array(self::getTableName()),
- 'when' => 'BEFORE',
- 'event' => array('INSERT'),
- 'sql' => "\nSET NEW.created_date = CURRENT_TIMESTAMP;\n",
- );
- }
-
- // Update timestamp when modifying closely related core tables
- $relatedTables = array(
- 'civicrm_address',
- 'civicrm_email',
- 'civicrm_im',
- 'civicrm_phone',
- 'civicrm_website',
- );
- self::generateTimestampTriggers($info, $tableName, $relatedTables, 'contact_id');
-
- // Update timestamp when modifying related custom-data tables
- $customGroupTables = array();
- $customGroupDAO = CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity('Contact');
- $customGroupDAO->is_multiple = 0;
- $customGroupDAO->find();
- while ($customGroupDAO->fetch()) {
- $customGroupTables[] = $customGroupDAO->table_name;
- }
- if (!empty($customGroupTables)) {
- self::generateTimestampTriggers($info, $tableName, $customGroupTables, 'entity_id');
- }
+ // Modifications to these records should update the contact timestamps.
+ \Civi\Core\SqlTrigger\TimestampTriggers::create('civicrm_contact', 'Contact')
+ ->setRelations(array(
+ array('table' => 'civicrm_address', 'column' => 'contact_id'),
+ array('table' => 'civicrm_email', 'column' => 'contact_id'),
+ array('table' => 'civicrm_im', 'column' => 'contact_id'),
+ array('table' => 'civicrm_phone', 'column' => 'contact_id'),
+ array('table' => 'civicrm_website', 'column' => 'contact_id'),
+ )
+ )
+ ->alterTriggerInfo($info, $tableName);
// Update phone table to populate phone_numeric field
if (!$tableName || $tableName == 'civicrm_phone') {
'civicrm_uf_group', 'cancel_button_text', "varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Custom Text to display on the cancel button when used in create or edit mode'", TRUE);
$this->addTask('Add Submit button text column to civicrm_uf_group', 'addColumn',
'civicrm_uf_group', 'submit_button_text', "varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Custom Text to display on the submit button on profile edit/create screens'", TRUE);
+
+ $this->addTask('CRM-20958 - Add created_date to civicrm_activity', 'addColumn',
+ 'civicrm_activity', 'created_date', "timestamp NULL DEFAULT NULL COMMENT 'When was the activity was created.'");
+ $this->addTask('CRM-20958 - Add modified_date to civicrm_activity', 'addColumn',
+ 'civicrm_activity', 'modified_date', "timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'When was the activity (or closely related entity) was created or modified or deleted.'");
+ $this->addTask('CRM-20958 - Add created_date to civicrm_case', 'addColumn',
+ 'civicrm_case', 'created_date', "timestamp NULL DEFAULT NULL COMMENT 'When was the case was created.'");
+ $this->addTask('CRM-20958 - Add modified_date to civicrm_case', 'addColumn',
+ 'civicrm_case', 'modified_date', "timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'When was the case (or closely related entity) was created or modified or deleted.'");
+
+ return TRUE;
}
*/
class CRM_Utils_Check_Component_Case extends CRM_Utils_Check_Component {
+ const DOCTOR_WHEN = 'https://github.com/civicrm/org.civicrm.doctorwhen';
+
/**
* @var CRM_Case_XMLRepository
*/
return $messages;
}
+ /**
+ * Check that the timestamp columns are populated. (CRM-20958)
+ *
+ * @return array<CRM_Utils_Check_Message>
+ * An empty array, or a list of warnings
+ */
+ public function checkNullTimestamps() {
+ $messages = array();
+
+ $nullCount = 0;
+ $nullCount += CRM_Utils_SQL_Select::from('civicrm_activity')
+ ->where('created_date IS NULL OR modified_date IS NULL')
+ ->select('COUNT(*)')
+ ->execute()
+ ->fetchValue();
+ $nullCount += CRM_Utils_SQL_Select::from('civicrm_case')
+ ->where('created_date IS NULL OR modified_date IS NULL')
+ ->select('COUNT(*)')
+ ->execute()
+ ->fetchValue();
+
+ if ($nullCount > 0) {
+ $messages[] = new CRM_Utils_Check_Message(
+ __FUNCTION__,
+ '<p>' .
+ ts('The tables "<em>civicrm_activity</em>" and "<em>civicrm_case</em>" were updated to support two new fields, "<em>created_date</em>" and "<em>modified_date</em>". For historical data, these fields may appear blank. (%1 records have NULL timestamps.)', array(
+ 1 => $nullCount,
+ )) .
+ '</p><p>' .
+ ts('At time of writing, this is not a problem. However, future extensions and improvements could rely on these fields, so it may be useful to back-fill them.') .
+ '</p><p>' .
+ ts('For further discussion, please visit %1', array(
+ 1 => sprintf('<a href="%s" target="_blank">%s</a>', self::DOCTOR_WHEN, self::DOCTOR_WHEN),
+ )) .
+ '</p>',
+ ts('Timestamps for Activities and Cases'),
+ \Psr\Log\LogLevel::NOTICE,
+ 'fa-clock-o'
+ );
+ }
+
+ return $messages;
+ }
+
}
->setFactory(array($class, 'singleton'));
}
+ $container->setDefinition('civi.activity.triggers', new Definition(
+ 'Civi\Core\SqlTrigger\TimestampTriggers',
+ array('civicrm_activity', 'Activity')
+ ))->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
+
+ $container->setDefinition('civi.case.triggers', new Definition(
+ 'Civi\Core\SqlTrigger\TimestampTriggers',
+ array('civicrm_case', 'Case')
+ ))->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
+
+ $container->setDefinition('civi.case.staticTriggers', new Definition(
+ 'Civi\Core\SqlTrigger\StaticTriggers',
+ array(
+ array(
+ array(
+ 'upgrade_check' => array('table' => 'civicrm_case', 'column' => 'modified_date'),
+ 'table' => 'civicrm_case_activity',
+ 'when' => 'AFTER',
+ 'event' => array('INSERT'),
+ 'sql' => "\nUPDATE civicrm_case SET modified_date = CURRENT_TIMESTAMP WHERE id = NEW.case_id;\n",
+ ),
+ array(
+ 'upgrade_check' => array('table' => 'civicrm_case', 'column' => 'modified_date'),
+ 'table' => 'civicrm_activity',
+ 'when' => 'BEFORE',
+ 'event' => array('UPDATE', 'DELETE'),
+ 'sql' => "\nUPDATE civicrm_case SET modified_date = CURRENT_TIMESTAMP WHERE id IN (SELECT ca.case_id FROM civicrm_case_activity ca WHERE ca.activity_id = OLD.id);\n",
+ ),
+ ),
+ )
+ ))
+ ->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
+
$container->setDefinition('civi_token_compat', new Definition(
'Civi\Token\TokenCompatSubscriber',
array()
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.7 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2017 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Core\SqlTrigger;
+
+/**
+ * Build a set of simple, literal SQL triggers.
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2017
+ */
+class StaticTriggers {
+
+ /**
+ * @var array
+ * A list of triggers, in the same format as hook_civicrm_triggerInfo.
+ * Additionally, you may specify `upgrade_check` to ensure that the trigger
+ * is *not* installed during early upgrade steps (before key dependencies are met).
+ *
+ * Ex: $triggers[0]['upgrade_check'] = array('table' => 'civicrm_case', 'column'=> 'modified_date');
+ *
+ * @see \CRM_Utils_Hook::triggerInfo
+ */
+ private $triggers;
+
+ /**
+ * StaticTriggers constructor.
+ * @param $triggers
+ */
+ public function __construct($triggers) {
+ $this->triggers = $triggers;
+ }
+
+
+ /**
+ * Add our list of triggers to the global list.
+ *
+ * @param \Civi\Core\Event\GenericHookEvent $e
+ * @see \CRM_Utils_Hook::triggerInfo
+ */
+ public function onTriggerInfo($e) {
+ $this->alterTriggerInfo($e->info, $e->tableName);
+ }
+
+ /**
+ * Add our list of triggers to the global list.
+ *
+ * @see \CRM_Utils_Hook::triggerInfo
+ * @see \CRM_Core_DAO::triggerRebuild
+ *
+ * @param array $info
+ * See hook_civicrm_triggerInfo.
+ * @param string|NULL $tableFilter
+ * See hook_civicrm_triggerInfo.
+ */
+ public function alterTriggerInfo(&$info, $tableFilter = NULL) {
+ foreach ($this->getTriggers() as $trigger) {
+ if ($tableFilter !== NULL) {
+ // Because sadism.
+ if (in_array($tableFilter, (array) $trigger['table'])) {
+ $trigger['table'] = $tableFilter;
+ }
+ }
+
+ if (\CRM_Core_Config::isUpgradeMode() && isset($trigger['upgrade_check'])) {
+ $uc = $trigger['upgrade_check'];
+ if (!\CRM_Core_DAO::checkFieldExists($uc['table'], $uc['column'])
+ ) {
+ continue;
+ }
+ }
+ unset($trigger['upgrade_check']);
+ $info[] = $trigger;
+ }
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getTriggers() {
+ return $this->triggers;
+ }
+
+ /**
+ * @param mixed $triggers
+ * @return StaticTriggers
+ */
+ public function setTriggers($triggers) {
+ $this->triggers = $triggers;
+ return $this;
+ }
+
+ /**
+ * @param $trigger
+ * @return StaticTriggers
+ */
+ public function addTrigger($trigger) {
+ $this->triggers[] = $trigger;
+ return $this;
+ }
+
+}
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.7 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2017 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Core\SqlTrigger;
+
+use Civi\Core\Event\GenericHookEvent;
+
+/**
+ * Build a set of SQL triggers for tracking timestamps on an entity.
+ *
+ * This class is a generalization of CRM-10554 with the aim of enabling CRM-20958.
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2017
+ */
+class TimestampTriggers {
+
+ /**
+ * @var string
+ * SQL table name.
+ * Ex: 'civicrm_contact', 'civicrm_activity'.
+ */
+ private $tableName;
+
+ /**
+ * @var string
+ * An entity name (from civicrm_custom_group.extends).
+ * Ex: 'Contact', 'Activity'.
+ */
+ private $customDataEntity;
+
+ /**
+ * @var string
+ * SQL column name.
+ * Ex: 'created_date'.
+ */
+ private $createdDate;
+
+ /**
+ * @var string
+ * SQL column name.
+ * Ex: 'modified_date'.
+ */
+ private $modifiedDate;
+
+ /**
+ * @var array
+ * Ex: $relations[0] == array('table' => 'civicrm_bar', 'column' => 'foo_id');
+ */
+ private $relations;
+
+ /**
+ * @param string $tableName
+ * SQL table name.
+ * Ex: 'civicrm_contact', 'civicrm_activity'.
+ * @param string $customDataEntity
+ * An entity name (from civicrm_custom_group.extends).
+ * Ex: 'Contact', 'Activity'.
+ * @return TimestampTriggers
+ */
+ public static function create($tableName, $customDataEntity) {
+ return new static($tableName, $customDataEntity);
+ }
+
+ /**
+ * TimestampTriggers constructor.
+ *
+ * @param string $tableName
+ * SQL table name.
+ * Ex: 'civicrm_contact', 'civicrm_activity'.
+ * @param string $customDataEntity
+ * An entity name (from civicrm_custom_group.extends).
+ * Ex: 'Contact', 'Activity'.
+ * @param string $createdDate
+ * SQL column name.
+ * Ex: 'created_date'.
+ * @param string $modifiedDate
+ * SQL column name.
+ * Ex: 'modified_date'.
+ * @param array $relations
+ * Ex: $relations[0] == array('table' => 'civicrm_bar', 'column' => 'foo_id');
+ */
+ public function __construct(
+ $tableName,
+ $customDataEntity,
+ $createdDate = 'created_date',
+ $modifiedDate = 'modified_date',
+ $relations = array()
+ ) {
+ $this->tableName = $tableName;
+ $this->customDataEntity = $customDataEntity;
+ $this->createdDate = $createdDate;
+ $this->modifiedDate = $modifiedDate;
+ $this->relations = $relations;
+ }
+
+ /**
+ * Add our list of triggers to the global list.
+ *
+ * @param \Civi\Core\Event\GenericHookEvent $e
+ * @see \CRM_Utils_Hook::triggerInfo
+ */
+ public function onTriggerInfo($e) {
+ $this->alterTriggerInfo($e->info, $e->tableName);
+ }
+
+ /**
+ * Add our list of triggers to the global list.
+ *
+ * @see \CRM_Utils_Hook::triggerInfo
+ * @see \CRM_Core_DAO::triggerRebuild
+ *
+ * @param array $info
+ * See hook_civicrm_triggerInfo.
+ * @param string|NULL $tableFilter
+ * See hook_civicrm_triggerInfo.
+ */
+ public function alterTriggerInfo(&$info, $tableFilter = NULL) {
+ // If we haven't upgraded yet, then the created_date/modified_date may not exist.
+ // In the past, this was a version-based check, but checkFieldExists()
+ // seems more robust.
+ if (\CRM_Core_Config::isUpgradeMode()) {
+ if (!\CRM_Core_DAO::checkFieldExists($this->getTableName(),
+ $this->getCreatedDate())
+ ) {
+ return;
+ }
+ }
+
+ if ($tableFilter == NULL || $tableFilter == $this->getTableName()) {
+ $info[] = array(
+ 'table' => array($this->getTableName()),
+ 'when' => 'BEFORE',
+ 'event' => array('INSERT'),
+ 'sql' => "\nSET NEW.{$this->getCreatedDate()} = CURRENT_TIMESTAMP;\n",
+ );
+ }
+
+ // Update timestamp when modifying closely related tables
+ $relIdx = \CRM_Utils_Array::index(
+ array('column', 'table'),
+ $this->getAllRelations()
+ );
+ foreach ($relIdx as $column => $someRelations) {
+ $this->generateTimestampTriggers($info, $tableFilter,
+ array_keys($someRelations), $column);
+ }
+ }
+
+ /**
+ * Generate triggers to update the timestamp.
+ *
+ * The corresponding civicrm_FOO row is updated on insert/update/delete
+ * to a table that extends civicrm_FOO.
+ * Don't regenerate triggers for all such tables if only asked for one table.
+ *
+ * @param array $info
+ * Reference to the array where generated trigger information is being stored
+ * @param string|null $tableFilter
+ * Name of the table for which triggers are being generated, or NULL if all tables
+ * @param array $relatedTableNames
+ * Array of all core or all custom table names extending civicrm_FOO
+ * @param string $contactRefColumn
+ * 'contact_id' if processing core tables, 'entity_id' if processing custom tables
+ *
+ * @link https://issues.civicrm.org/jira/browse/CRM-15602
+ * @see triggerInfo
+ */
+ public function generateTimestampTriggers(
+ &$info,
+ $tableFilter,
+ $relatedTableNames,
+ $contactRefColumn
+ ) {
+ // Safety
+ $contactRefColumn = \CRM_Core_DAO::escapeString($contactRefColumn);
+
+ // If specific related table requested, just process that one.
+ // (Reply: This feels fishy.)
+ if (in_array($tableFilter, $relatedTableNames)) {
+ $relatedTableNames = array($tableFilter);
+ }
+
+ // If no specific table requested (include all related tables),
+ // or a specific related table requested (as matched above)
+ if (empty($tableFilter) || isset($relatedTableNames[$tableFilter])) {
+ $info[] = array(
+ 'table' => $relatedTableNames,
+ 'when' => 'AFTER',
+ 'event' => array('INSERT', 'UPDATE'),
+ 'sql' => "\nUPDATE {$this->getTableName()} SET {$this->getModifiedDate()} = CURRENT_TIMESTAMP WHERE id = NEW.$contactRefColumn;\n",
+ );
+ $info[] = array(
+ 'table' => $relatedTableNames,
+ 'when' => 'AFTER',
+ 'event' => array('DELETE'),
+ 'sql' => "\nUPDATE {$this->getTableName()} SET {$this->getModifiedDate()} = CURRENT_TIMESTAMP WHERE id = OLD.$contactRefColumn;\n",
+ );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function getTableName() {
+ return $this->tableName;
+ }
+
+ /**
+ * @param string $tableName
+ * @return TimestampTriggers
+ */
+ public function setTableName($tableName) {
+ $this->tableName = $tableName;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCustomDataEntity() {
+ return $this->customDataEntity;
+ }
+
+ /**
+ * @param string $customDataEntity
+ * @return TimestampTriggers
+ */
+ public function setCustomDataEntity($customDataEntity) {
+ $this->customDataEntity = $customDataEntity;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCreatedDate() {
+ return $this->createdDate;
+ }
+
+ /**
+ * @param string $createdDate
+ * @return TimestampTriggers
+ */
+ public function setCreatedDate($createdDate) {
+ $this->createdDate = $createdDate;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getModifiedDate() {
+ return $this->modifiedDate;
+ }
+
+ /**
+ * @param string $modifiedDate
+ * @return TimestampTriggers
+ */
+ public function setModifiedDate($modifiedDate) {
+ $this->modifiedDate = $modifiedDate;
+ return $this;
+ }
+
+ /**
+ * @return array
+ * Each item is an array('table' => string, 'column' => string)
+ */
+ public function getRelations() {
+ return $this->relations;
+ }
+
+ /**
+ * @param array $relations
+ * @return TimestampTriggers
+ */
+ public function setRelations($relations) {
+ $this->relations = $relations;
+ return $this;
+ }
+
+ /**
+ * Get a list of all tracked relations.
+ *
+ * This is basically the curated list (`$this->relations`) plus any custom data.
+ *
+ * @return array
+ * Each item is an array('table' => string, 'column' => string)
+ */
+ public function getAllRelations() {
+ $relations = $this->getRelations();
+
+ if ($this->getCustomDataEntity()) {
+ $customGroupDAO = \CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity($this->getCustomDataEntity());
+ $customGroupDAO->is_multiple = 0;
+ $customGroupDAO->find();
+ while ($customGroupDAO->fetch()) {
+ $relations[] = array(
+ 'table' => $customGroupDAO->table_name,
+ 'column' => 'entity_id',
+ );
+ }
+ }
+
+ return $relations;
+ }
+
+}
require_once 'api/v3/examples/Activity/Create.php';
$result = activity_create_example();
$expectedResult = activity_create_expectedresult();
+ // Compare everything *except* timestamps.
+ unset($result['values'][1]['created_date']);
+ unset($result['values'][1]['modified_date']);
+ unset($expectedResult['values'][1]['created_date']);
+ unset($expectedResult['values'][1]['modified_date']);
$this->assertEquals($result, $expectedResult);
}
$params['subject'] = $case['subject'] = 'Something Else';
$this->callAPISuccess('case', 'create', $params);
- // Verify that updated case is exactly equal to the original with new subject.
+ // Verify that updated case is equal to the original with new subject.
$result = $this->callAPISuccessGetSingle('Case', array('case_id' => $id));
+ // Modification dates are likely to differ by 0-2 sec. Check manually.
+ $this->assertGreaterThanOrEqual($result['modified_date'], $case['modified_date']);
+ unset($result['modified_date']);
+ unset($case['modified_date']);
+ // Everything else should be identical.
$this->assertAPIArrayComparison($result, $case);
}
return $examples;
}
+ public function testTimestamps() {
+ $params = $this->_params;
+ $case_created = $this->callAPISuccess('case', 'create', $params);
+
+ $case_1 = $this->callAPISuccess('Case', 'getsingle', array(
+ 'id' => $case_created['id'],
+ ));
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $case_1['created_date']);
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $case_1['modified_date']);
+ $this->assertApproxEquals(strtotime($case_1['created_date']), strtotime($case_1['modified_date']), 2);
+
+ $activity_1 = $this->callAPISuccess('activity', 'getsingle', array(
+ 'case_id' => $case_created['id'],
+ 'options' => array(
+ 'limit' => 1,
+ ),
+ ));
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $activity_1['created_date']);
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $activity_1['modified_date']);
+ $this->assertApproxEquals(strtotime($activity_1['created_date']), strtotime($activity_1['modified_date']), 2);
+
+ usleep(1.5 * 1000000);
+ $this->callAPISuccess('activity', 'create', array(
+ 'id' => $activity_1['id'],
+ 'subject' => 'Make cheese',
+ ));
+
+ $activity_2 = $this->callAPISuccess('activity', 'getsingle', array(
+ 'id' => $activity_1['id'],
+ ));
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $activity_2['created_date']);
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $activity_2['modified_date']);
+ $this->assertNotEquals($activity_2['created_date'], $activity_2['modified_date']);
+
+ $this->assertEquals($activity_1['created_date'], $activity_2['created_date']);
+ $this->assertNotEquals($activity_1['modified_date'], $activity_2['modified_date']);
+ $this->assertLessThan($activity_2['modified_date'], $activity_1['modified_date'],
+ sprintf("Original modification time (%s) should predate later modification time (%s)", $activity_1['modified_date'], $activity_2['modified_date']));
+
+ $case_2 = $this->callAPISuccess('Case', 'getsingle', array(
+ 'id' => $case_created['id'],
+ ));
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $case_2['created_date']);
+ $this->assertRegExp(';^\d\d\d\d-\d\d-\d\d \d\d:\d\d;', $case_2['modified_date']);
+ $this->assertEquals($case_1['created_date'], $case_2['created_date']);
+ $this->assertNotEquals($case_2['created_date'], $case_2['modified_date']);
+ }
+
}
<headerPattern>/(activity.)?(star|favorite)/i</headerPattern>
<add>4.7</add>
</field>
+ <field>
+ <name>created_date</name>
+ <uniqueName>activity_created_date</uniqueName>
+ <type>timestamp</type>
+ <comment>When was the activity was created.</comment>
+ <required>false</required>
+ <export>true</export>
+ <default>NULL</default>
+ <add>4.7</add>
+ </field>
+ <field>
+ <name>modified_date</name>
+ <uniqueName>activity_modified_date</uniqueName>
+ <type>timestamp</type>
+ <comment>When was the activity (or closely related entity) was created or modified or deleted.</comment>
+ <required>false</required>
+ <export>true</export>
+ <default>CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP</default>
+ <add>4.7</add>
+ </field>
</table>
<fieldName>is_deleted</fieldName>
<add>2.2</add>
</index>
+ <field>
+ <name>created_date</name>
+ <uniqueName>case_created_date</uniqueName>
+ <type>timestamp</type>
+ <comment>When was the case was created.</comment>
+ <required>false</required>
+ <export>true</export>
+ <default>NULL</default>
+ <add>4.7</add>
+ </field>
+ <field>
+ <name>modified_date</name>
+ <uniqueName>case_modified_date</uniqueName>
+ <type>timestamp</type>
+ <comment>When was the case (or closely related entity) was created or modified or deleted.</comment>
+ <required>false</required>
+ <export>true</export>
+ <default>CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP</default>
+ <add>4.7</add>
+ </field>
</table>