Supports relative date range expressions with most operators and adds test coverage
* @return array
* start date, end date
*/
- public static function getFromTo($relative, $from, $to, $fromTime = NULL, $toTime = '235959') {
+ public static function getFromTo($relative, $from = NULL, $to = NULL, $fromTime = NULL, $toTime = '235959') {
if ($relative) {
list($term, $unit) = explode('.', $relative, 2);
$dateRange = self::relativeToAbsolute($term, $unit);
// For WHERE clause, expr must be the name of a field.
if ($type === 'WHERE') {
$field = $this->getField($expr, TRUE);
- FormattingUtil::formatInputValue($value, $expr, $field);
+ FormattingUtil::formatInputValue($value, $expr, $field, $operator);
$fieldAlias = $field['sql_name'];
}
// For HAVING, expr must be an item in the SELECT clause
// Attempt to format if this is a real field
if (isset($this->apiFieldSpec[$expr])) {
$field = $this->getField($expr);
- FormattingUtil::formatInputValue($value, $expr, $field);
+ FormattingUtil::formatInputValue($value, $expr, $field, $operator);
}
}
// Expr references a non-field expression like a function; convert to alias
list($selectField) = explode(':', $selectAlias);
if ($selectAlias === $selectExpr && $fieldName === $selectField && isset($this->apiFieldSpec[$fieldName])) {
$field = $this->getField($fieldName);
- FormattingUtil::formatInputValue($value, $expr, $field);
+ FormattingUtil::formatInputValue($value, $expr, $field, $operator);
$fieldAlias = $selectAlias;
break;
}
if (is_string($value)) {
$valExpr = $this->getExpression($value);
if ($fieldName && $valExpr->getType() === 'SqlString') {
- FormattingUtil::formatInputValue($valExpr->expr, $fieldName, $this->apiFieldSpec[$fieldName]);
+ FormattingUtil::formatInputValue($valExpr->expr, $fieldName, $this->apiFieldSpec[$fieldName], $operator);
}
return sprintf('%s %s %s', $fieldAlias, $operator, $valExpr->render($this->apiFieldSpec));
}
elseif ($fieldName) {
$field = $this->getField($fieldName);
- FormattingUtil::formatInputValue($value, $fieldName, $field);
+ FormattingUtil::formatInputValue($value, $fieldName, $field, $operator);
}
}
if ($value === 'null') {
$value = 'Null';
}
- self::formatInputValue($value, $name, $field, 'create');
+ self::formatInputValue($value, $name, $field);
// Ensure we have an array for serialized fields
if (!empty($field['serialize'] && !is_array($value))) {
$value = (array) $value;
* @param $value
* @param string $fieldName
* @param array $fieldSpec
- * @param string $action
+ * @param string $operator (only for 'get' actions)
+ * @param int $index (for recursive loops)
* @throws \API_Exception
* @throws \CRM_Core_Exception
*/
- public static function formatInputValue(&$value, $fieldName, $fieldSpec, $action = 'get') {
+ public static function formatInputValue(&$value, $fieldName, $fieldSpec, &$operator = NULL, $index = NULL) {
// Evaluate pseudoconstant suffix
$suffix = strpos($fieldName, ':');
if ($suffix) {
- $options = self::getPseudoconstantList($fieldSpec, substr($fieldName, $suffix + 1), $action);
+ $options = self::getPseudoconstantList($fieldSpec, substr($fieldName, $suffix + 1), [], $operator ? 'get' : 'create');
$value = self::replacePseudoconstant($options, $value, TRUE);
return;
}
elseif (is_array($value)) {
+ $i = 0;
foreach ($value as &$val) {
- self::formatInputValue($val, $fieldName, $fieldSpec, $action);
+ self::formatInputValue($val, $fieldName, $fieldSpec, $operator, $i++);
}
return;
}
switch ($fieldSpec['data_type'] ?? NULL) {
case 'Timestamp':
- $value = date('Y-m-d H:i:s', strtotime($value));
+ $value = self::formatDateValue('Y-m-d H:i:s', $value, $operator, $index);
break;
case 'Date':
- $value = date('Ymd', strtotime($value));
+ $value = self::formatDateValue('Ymd', $value, $operator, $index);
break;
}
$hic = \CRM_Utils_API_HTMLInputCoder::singleton();
- if (!$hic->isSkippedField($fieldSpec['name']) && is_string($value)) {
+ if (is_string($value) && !$hic->isSkippedField($fieldSpec['name'])) {
$value = $hic->encodeValue($value);
}
}
+ /**
+ * Parse date expressions.
+ *
+ * Expands relative date range expressions, modifying the sql operator if necessary
+ *
+ * @param $format
+ * @param $value
+ * @param $operator
+ * @param $index
+ * @return array|string
+ */
+ private static function formatDateValue($format, $value, &$operator = NULL, $index = NULL) {
+ // Non-relative dates (or if no search operator)
+ if (!$operator || !array_key_exists($value, \CRM_Core_OptionGroup::values('relative_date_filters'))) {
+ return date($format, strtotime($value));
+ }
+ if (isset($index) && !strstr($operator, 'BETWEEN')) {
+ throw new \API_Exception("Relative dates cannot be in an array using the $operator operator.");
+ }
+ [$dateFrom, $dateTo] = \CRM_Utils_Date::getFromTo($value);
+ switch ($operator) {
+ // Convert relative date filters to use BETWEEN/NOT BETWEEN operator
+ case '=':
+ case '!=':
+ case '<>':
+ case 'LIKE':
+ case 'NOT LIKE':
+ $operator = ($operator === '=' || $operator === 'LIKE') ? 'BETWEEN' : 'NOT BETWEEN';
+ return [self::formatDateValue($format, $dateFrom), self::formatDateValue($format, $dateTo)];
+
+ // Less-than or greater-than-equal-to comparisons use the lower value
+ case '<':
+ case '>=':
+ return self::formatDateValue($format, $dateFrom);
+
+ // Greater-than or less-than-equal-to comparisons use the higher value
+ case '>':
+ case '<=':
+ return self::formatDateValue($format, $dateTo);
+
+ // For BETWEEN expressions, we are already inside a loop of the 2 values, so give the lower value if index=0, higher value if index=1
+ case 'BETWEEN':
+ case 'NOT BETWEEN':
+ return self::formatDateValue($format, $index ? $dateTo : $dateFrom);
+
+ default:
+ throw new \API_Exception("Relative dates cannot be used with the $operator operator.");
+ }
+ }
+
/**
* Unserialize raw DAO values and convert to correct type
*
namespace api\v4\Action;
+use Civi\Api4\Activity;
use Civi\Api4\Contact;
use Civi\Api4\Relationship;
use api\v4\UnitTestCase;
$this->assertArrayNotHasKey($r, $result);
}
+ public function testRelativeDateRanges() {
+ $c1 = Contact::create()
+ ->addValue('first_name', 'c')
+ ->addValue('last_name', 'one')
+ ->execute()
+ ->first()['id'];
+ $act = Activity::save()
+ ->setDefaults(['activity_type_id:name' => 'Meeting', 'source_contact_id' => $c1])
+ ->addRecord(['activity_date_time' => 'now - 3 year'])
+ ->addRecord(['activity_date_time' => 'now - 1 year'])
+ ->addRecord(['activity_date_time' => 'now - 1 month'])
+ ->addRecord(['activity_date_time' => 'now'])
+ ->addRecord(['activity_date_time' => 'now + 1 month'])
+ ->addRecord(['activity_date_time' => 'now + 1 year'])
+ ->addRecord(['activity_date_time' => 'now + 3 year'])
+ ->execute()->column('id');
+
+ $result = Activity::get(FALSE)->addSelect('id')
+ ->addWhere('activity_date_time', '>', 'previous.year')
+ ->execute()->column('id');
+ $this->assertNotContains($act[0], $result);
+ $this->assertContains($act[3], $result);
+ $this->assertContains($act[4], $result);
+ $this->assertContains($act[5], $result);
+ $this->assertContains($act[6], $result);
+
+ $result = Activity::get(FALSE)->addSelect('id')
+ ->addWhere('activity_date_time', '>', 'this.year')
+ ->execute()->column('id');
+ $this->assertNotContains($act[0], $result);
+ $this->assertNotContains($act[1], $result);
+ $this->assertNotContains($act[2], $result);
+ $this->assertNotContains($act[3], $result);
+ $this->assertContains($act[5], $result);
+ $this->assertContains($act[6], $result);
+
+ $result = Activity::get(FALSE)->addSelect('id')
+ ->addWhere('activity_date_time', '>=', 'this.year')
+ ->execute()->column('id');
+ $this->assertNotContains($act[0], $result);
+ $this->assertNotContains($act[1], $result);
+ $this->assertContains($act[3], $result);
+ $this->assertContains($act[4], $result);
+ $this->assertContains($act[5], $result);
+ $this->assertContains($act[6], $result);
+
+ $result = Activity::get(FALSE)->addSelect('id')
+ ->addWhere('activity_date_time', '<', 'previous.year')
+ ->execute()->column('id');
+ $this->assertContains($act[0], $result);
+ $this->assertNotContains($act[4], $result);
+ $this->assertNotContains($act[5], $result);
+ $this->assertNotContains($act[6], $result);
+
+ $result = Activity::get(FALSE)->addSelect('id')
+ ->addWhere('activity_date_time', '=', 'next.month')
+ ->execute()->column('id');
+ $this->assertNotContains($act[0], $result);
+ $this->assertNotContains($act[1], $result);
+ $this->assertNotContains($act[2], $result);
+ $this->assertContains($act[4], $result);
+ $this->assertNotContains($act[5], $result);
+ $this->assertNotContains($act[6], $result);
+
+ $result = Activity::get(FALSE)->addSelect('id')
+ ->addWhere('activity_date_time', 'BETWEEN', ['previous.year', 'this.year'])
+ ->execute()->column('id');
+ $this->assertContains($act[2], $result);
+ $this->assertContains($act[3], $result);
+ $this->assertNotContains($act[0], $result);
+ $this->assertNotContains($act[6], $result);
+ }
+
}
$otherParticipantResult->offsetExists($firstParticipantId),
'excluded wrong record');
+ // check syntax for date-range
+
+ $getParticipantsById = function($wheres = []) {
+ return Participant::get(FALSE)
+ ->setWhere($wheres)
+ ->execute()
+ ->indexBy('id');
+ };
+
+ $thisYearParticipants = $getParticipantsById([['register_date', '=', 'this.year']]);
+ $this->assertFalse(isset($thisYearParticipants[$firstParticipantId]));
+
+ $otherYearParticipants = $getParticipantsById([['register_date', '!=', 'this.year']]);
+ $this->assertTrue(isset($otherYearParticipants[$firstParticipantId]));
+
+ Participant::update()->setCheckPermissions(FALSE)
+ ->addWhere('id', '=', $firstParticipantId)
+ ->addValue('register_date', 'now')
+ ->execute();
+
+ $thisYearParticipants = $getParticipantsById([['register_date', '=', 'this.year']]);
+ $this->assertTrue(isset($thisYearParticipants[$firstParticipantId]));
+
// retrieve a participant record and update some records
$patchRecord = [
'source' => "not " . $firstResult['source'],