diff --git a/sources/application/search/criterionconversion/criteriontooql.class.inc.php b/sources/application/search/criterionconversion/criteriontooql.class.inc.php index 1b43e52f69..c4fb75e4c5 100644 --- a/sources/application/search/criterionconversion/criteriontooql.class.inc.php +++ b/sources/application/search/criterionconversion/criteriontooql.class.inc.php @@ -23,8 +23,11 @@ namespace Combodo\iTop\Application\Search\CriterionConversion; +use AttributeDateTime; +use AttributeDefinition; use Combodo\iTop\Application\Search\CriterionConversionAbstract; use Combodo\iTop\Application\Search\SearchForm; +use DateInterval; class CriterionToOQL extends CriterionConversionAbstract { @@ -51,6 +54,8 @@ class CriterionToOQL extends CriterionConversionAbstract self::OP_ENDS_WITH => 'EndsWithToOql', self::OP_EMPTY => 'EmptyToOql', self::OP_NOT_EMPTY => 'NotEmptyToOql', + self::OP_BETWEEN_DAYS => 'BetweenDaysToOql', + self::OP_BETWEEN_HOURS => 'BetweenHoursToOql', self::OP_IN => 'InToOql', self::OP_ALL => 'AllToOql', ); @@ -59,7 +64,7 @@ class CriterionToOQL extends CriterionConversionAbstract { $sFct = $aMappedOperators[$sOperator]; - return self::$sFct($sRef, $sOperator, $aCriteria); + return self::$sFct($sRef, $aCriteria); } $sValue = self::GetValue(self::GetValues($aCriteria), 0); @@ -73,6 +78,7 @@ class CriterionToOQL extends CriterionConversionAbstract { return array(); } + return $aCriteria['values']; } @@ -86,10 +92,11 @@ class CriterionToOQL extends CriterionConversionAbstract { return null; } + return $aValues[$iIndex]['value']; } - protected static function ContainsToOql($sRef, $sOperator, $aCriteria) + protected static function ContainsToOql($sRef, $aCriteria) { $aValues = self::GetValues($aCriteria); $sValue = self::GetValue($aValues, 0); @@ -97,7 +104,7 @@ class CriterionToOQL extends CriterionConversionAbstract return "({$sRef} LIKE '%{$sValue}%')"; } - protected static function StartsWithToOql($sRef, $sOperator, $aCriteria) + protected static function StartsWithToOql($sRef, $aCriteria) { $aValues = self::GetValues($aCriteria); $sValue = self::GetValue($aValues, 0); @@ -105,7 +112,7 @@ class CriterionToOQL extends CriterionConversionAbstract return "({$sRef} LIKE '{$sValue}%')"; } - protected static function EndsWithToOql($sRef, $sOperator, $aCriteria) + protected static function EndsWithToOql($sRef, $aCriteria) { $aValues = self::GetValues($aCriteria); $sValue = self::GetValue($aValues, 0); @@ -113,25 +120,26 @@ class CriterionToOQL extends CriterionConversionAbstract return "({$sRef} LIKE '%{$sValue}')"; } - protected static function EmptyToOql($sRef, $sOperator, $aCriteria) + protected static function EmptyToOql($sRef, $aCriteria) { return "({$sRef} = '')"; } - protected static function NotEmptyToOql($sRef, $sOperator, $aCriteria) + protected static function NotEmptyToOql($sRef, $aCriteria) { return "({$sRef} != '')"; } - protected static function InToOql($sRef, $sOperator, $aCriteria) + protected static function InToOql($sRef, $aCriteria) { $sAttCode = $aCriteria['code']; $sClass = $aCriteria['class']; - $aValues = $aCriteria['values']; + $aValues = self::GetValues($aCriteria); if (count($aValues) == 0) { - return "({$sRef} = '')"; + // Ignore when nothing is selected + return "1"; } try @@ -143,17 +151,21 @@ class CriterionToOQL extends CriterionConversionAbstract $aAllowedValues = SearchForm::GetFieldAllowedValues($oAttDef); if (array_key_exists('values', $aAllowedValues)) { - $aAllowedValues = $aAllowedValues['values']; - // more selected values than remaining so use NOT IN - if (count($aValues) > (count($aAllowedValues) / 2)) + // Can't invert the test if NULL is allowed + if (!$oAttDef->IsNullAllowed()) { - foreach($aValues as $aValue) + $aAllowedValues = $aAllowedValues['values']; + // more selected values than remaining so use NOT IN + if (count($aValues) > (count($aAllowedValues) / 2)) { - unset($aAllowedValues[$aValue['value']]); - } - $sInList = implode(',', array_keys($aAllowedValues)); + foreach($aValues as $aValue) + { + unset($aAllowedValues[$aValue['value']]); + } + $sInList = implode("','", array_keys($aAllowedValues)); - return "({$sRef} NOT IN ($sInList))"; + return "({$sRef} NOT IN ('$sInList'))"; + } } } } @@ -166,17 +178,104 @@ class CriterionToOQL extends CriterionConversionAbstract { $aInValues[] = $aValue['value']; } - $sInList = implode(',', $aInValues); + $sInList = implode("','", $aInValues); if (count($aInValues) == 1) { return "({$sRef} = '$sInList')"; } - return "({$sRef} IN ($sInList))"; + return "({$sRef} IN ('$sInList'))"; } - protected static function AllToOql($sRef, $sOperator, $aCriteria) + protected static function BetweenDaysToOql($sRef, $aCriteria) + { + $aOQL = array(); + + $aValues = self::GetValues($aCriteria); + if (count($aValues) != 2) + { + return "1"; + } + + $oFormat = AttributeDateTime::GetFormat(); + + $sStartDate = $aValues[0]['value']; + if (!empty($sStartDate)) + { + $oDate = $oFormat->parse($sStartDate); + $sStartDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $aOQL[] = "({$sRef} >= '$sStartDate')"; + } + + $sEndDate = $aValues[1]['value']; + if (!empty($sEndDate)) + { + $oDate = $oFormat->parse($sEndDate); + if ($aCriteria['widget'] == AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME) + { + $oDate->add(DateInterval::createFromDateString('1 day')); + $sEndDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $aOQL[] = "({$sRef} < '$sEndDate')"; + } + else + { + $sEndDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $aOQL[] = "({$sRef} <= '$sEndDate')"; + } + } + + $sOQL = implode(' AND ', $aOQL); + + if (empty($sOQL)) + { + $sOQL = "1"; + } + + return $sOQL; + } + + protected static function BetweenHoursToOql($sRef, $aCriteria) + { + $aOQL = array(); + + $aValues = self::GetValues($aCriteria); + if (count($aValues) != 2) + { + return "1"; + } + + $oFormat = AttributeDateTime::GetFormat(); + + $sStartDate = $aValues[0]['value']; + if (!empty($sStartDate)) + { + $oDate = $oFormat->parse($sStartDate); + $sStartDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $aOQL[] = "({$sRef} >= '$sStartDate')"; + } + + $sEndDate = $aValues[1]['value']; + if (!empty($sEndDate)) + { + $oDate = $oFormat->parse($sEndDate); + $oDate->add(DateInterval::createFromDateString('1 second')); + $sEndDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $aOQL[] = "({$sRef} < '$sEndDate')"; + } + + $sOQL = implode(' AND ', $aOQL); + + if (empty($sOQL)) + { + $sOQL = "1"; + } + + return $sOQL; + } + + + protected static function AllToOql($sRef, $aCriteria) { return "1"; } diff --git a/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php b/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php index 5c1d5620a4..5389f3d2e0 100644 --- a/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php +++ b/sources/application/search/criterionconversion/criteriontosearchform.class.inc.php @@ -25,8 +25,11 @@ namespace Combodo\iTop\Application\Search\CriterionConversion; +use AttributeDateTime; use AttributeDefinition; use Combodo\iTop\Application\Search\CriterionConversionAbstract; +use DateInterval; +use DateTime; class CriterionToSearchForm extends CriterionConversionAbstract { @@ -67,12 +70,158 @@ class CriterionToSearchForm extends CriterionConversionAbstract // Regroup criterion by variable name usort($aAndCriterion, function ($a, $b) { - return strcmp($a['ref'], $b['ref']); + $iRefCmp = strcmp($a['ref'], $b['ref']); + if ($iRefCmp != 0) return $iRefCmp; + $iOpCmp = strcmp($a['operator'], $b['operator']); + + return $iOpCmp; }); - return $aAndCriterion; + $aMergeFctByWidget = array( + AttributeDefinition::SEARCH_WIDGET_TYPE_DATE => 'MergeDate', + AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME => 'MergeDateTime', + ); + + $aPrevCriterion = null; + $aMergedCriterion = array(); + foreach($aAndCriterion as $aCurrCriterion) + { + if (!is_null($aPrevCriterion)) + { + if (strcmp($aPrevCriterion['ref'], $aCurrCriterion['ref']) == 0) + { + // Same attribute, try to merge + if (array_key_exists('widget', $aCurrCriterion)) + { + if (array_key_exists($aCurrCriterion['widget'], $aMergeFctByWidget)) + { + $sFct = $aMergeFctByWidget[$aCurrCriterion['widget']]; + $aPrevCriterion = self::$sFct($aPrevCriterion, $aCurrCriterion, $aMergedCriterion); + continue; + } + } + } + $aMergedCriterion[] = $aPrevCriterion; + } + + $aPrevCriterion = $aCurrCriterion; + } + if (!is_null($aPrevCriterion)) + { + $aMergedCriterion[] = $aPrevCriterion; + } + + return $aMergedCriterion; } + /** + * @param $aPrevCriterion + * @param $aCurrCriterion + * @param $aMergedCriterion + * + * @return null + * @throws \Exception + */ + protected static function MergeDate($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion) + { + $sPrevOperator = $aPrevCriterion['operator']; + $sCurrOperator = $aCurrCriterion['operator']; + if ((($sPrevOperator != '<') && ($sPrevOperator != '<=')) || (($sCurrOperator != '>') && ($sCurrOperator != '>='))) + { + $aMergedCriterion[] = $aPrevCriterion; + + return $aCurrCriterion; + } + + // Merge into 'between' operation. + // The ends of the interval are included + $aCurrCriterion['operator'] = 'between_days'; + $sFormat = AttributeDateTime::GetFormat()->ToMomentJS(); + $sLastDate = $aPrevCriterion['values'][0]['value']; + if ($sPrevOperator == '<') + { + // previous day to include ends + $oDate = new DateTime($sLastDate); + $oDate->sub(DateInterval::createFromDateString('1 day')); + $sLastDate = $oDate->format($sFormat); + } + + $sFirstDate = $aCurrCriterion['values'][0]['value']; + if ($sCurrOperator == '>') + { + // next day to include ends + $oDate = new DateTime($sFirstDate); + $oDate->add(DateInterval::createFromDateString('1 day')); + $sFirstDate = $oDate->format($sFormat); + } + + $aCurrCriterion['values'] = array(); + $aCurrCriterion['values'][] = array('value' => $sFirstDate, 'label' => $sFirstDate); + $aCurrCriterion['values'][] = array('value' => $sLastDate, 'label' => $sLastDate); + + $aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})"; + + $aMergedCriterion[] = $aCurrCriterion; + + return null; + } + + protected static function MergeDateTime($aPrevCriterion, $aCurrCriterion, &$aMergedCriterion) + { + $sPrevOperator = $aPrevCriterion['operator']; + $sCurrOperator = $aCurrCriterion['operator']; + if ((($sPrevOperator != '<') && ($sPrevOperator != '<=')) || (($sCurrOperator != '>') && ($sCurrOperator != '>='))) + { + $aMergedCriterion[] = $aPrevCriterion; + + return $aCurrCriterion; + } + + // Merge into 'between' operation. + // The ends of the interval are included + $sLastDate = $aPrevCriterion['values'][0]['value']; + $sFirstDate = $aCurrCriterion['values'][0]['value']; + $oDate = new DateTime($sLastDate); + if ((strpos($sFirstDate, '00:00:00') != false) && (strpos($sLastDate, '00:00:00') != false)) + { + $aCurrCriterion['operator'] = 'between_days'; + $sInterval = '1 day'; + } + else + { + $aCurrCriterion['operator'] = 'between_hours'; + $sInterval = '1 second'; + } + + if ($sPrevOperator == '<') + { + // previous day/second to include ends + $oDate->sub(DateInterval::createFromDateString($sInterval)); + } + $sLastDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $sLastDate = AttributeDateTime::GetFormat()->Format($sLastDate); + + $oDate = new DateTime($sFirstDate); + if ($sCurrOperator == '>') + { + // next day/second to include ends + $oDate->add(DateInterval::createFromDateString($sInterval)); + } + $sFirstDate = $oDate->format(AttributeDateTime::GetSQLFormat()); + $sFirstDate = AttributeDateTime::GetFormat()->Format($sFirstDate); + + $aCurrCriterion['values'] = array(); + $aCurrCriterion['values'][] = array('value' => $sFirstDate, 'label' => $sFirstDate); + $aCurrCriterion['values'][] = array('value' => $sLastDate, 'label' => $sLastDate); + + $aCurrCriterion['oql'] = "({$aPrevCriterion['oql']} AND {$aCurrCriterion['oql']})"; + + $aMergedCriterion[] = $aCurrCriterion; + + return null; + } + + protected static function TextToSearchForm($aCriteria, $aFields) { $sOperator = $aCriteria['operator']; @@ -115,6 +264,14 @@ class CriterionToSearchForm extends CriterionConversionAbstract protected static function EnumToSearchForm($aCriteria, $aFields) { $sOperator = $aCriteria['operator']; + if ($sOperator == '=') + { + $aCriteria['operator'] = 'IN'; + } + if ($sOperator != 'NOT IN') + { + return $aCriteria; + } $sRef = $aCriteria['ref']; $aValues = $aCriteria['values']; if (array_key_exists($sRef, $aFields)) @@ -126,26 +283,21 @@ class CriterionToSearchForm extends CriterionConversionAbstract } } - switch (true) + if (isset($aAllowedValues)) { - case ($sOperator == 'NOT IN'): - if (isset($aAllowedValues)) - { - foreach($aValues as $aValue) - { - $sValue = $aValue['value']; - unset($aAllowedValues[$sValue]); - } - $aCriteria['values'] = array(); - - foreach($aAllowedValues as $sValue => $sLabel) - { - $aValue = array('value' => $sValue, 'label' => $sLabel); - $aCriteria['values'][] = $aValue; - } - $aCriteria['operator'] = 'IN'; - } - break; + foreach($aValues as $aValue) + { + $sValue = $aValue['value']; + unset($aAllowedValues[$sValue]); + } + $aCriteria['values'] = array(); + + foreach($aAllowedValues as $sValue => $sLabel) + { + $aValue = array('value' => $sValue, 'label' => $sLabel); + $aCriteria['values'][] = $aValue; + } + $aCriteria['operator'] = 'IN'; } return $aCriteria; diff --git a/sources/application/search/criterionconversionabstract.class.inc.php b/sources/application/search/criterionconversionabstract.class.inc.php index 469cfd6313..e0eb743be9 100644 --- a/sources/application/search/criterionconversionabstract.class.inc.php +++ b/sources/application/search/criterionconversionabstract.class.inc.php @@ -32,6 +32,8 @@ abstract class CriterionConversionAbstract const OP_EMPTY = 'empty'; const OP_NOT_EMPTY = 'not_empty'; const OP_IN = 'IN'; + const OP_BETWEEN_DAYS = 'between_days'; + const OP_BETWEEN_HOURS = 'between_hours'; const OP_ALL = 'all'; } diff --git a/sources/application/search/searchform.class.inc.php b/sources/application/search/searchform.class.inc.php index 8f51a81007..6ff64038ec 100644 --- a/sources/application/search/searchform.class.inc.php +++ b/sources/application/search/searchform.class.inc.php @@ -24,6 +24,7 @@ namespace Combodo\iTop\Application\Search; use ApplicationContext; +use AttributeDateTime; use AttributeDefinition; use CMDBObjectSet; use Combodo\iTop\Application\Search\CriterionConversion\CriterionToSearchForm; @@ -149,6 +150,7 @@ class SearchForm 'result_list_outer_selector' => "#{$aExtraParams['table_id']}", 'data_config_list_selector' => null, //this one will be set just bellow, it mean to tell the widget where to find the initial list configuration 'endpoint' => utils::GetAbsoluteUrlAppRoot().'pages/ajax.searchform.php', + 'date_format' => AttributeDateTime::GetFormat()->ToMomentJS(), 'list_params' => $aListParams, 'search' => array( 'fields' => $aFields, diff --git a/test/application/search/CriterionConversionTest.php b/test/application/search/CriterionConversionTest.php index 239781fa4c..876c4cdc9c 100644 --- a/test/application/search/CriterionConversionTest.php +++ b/test/application/search/CriterionConversionTest.php @@ -30,6 +30,7 @@ namespace Combodo\iTop\Test\UnitTest\Application\Search; use Combodo\iTop\Application\Search\CriterionConversion\CriterionToOQL; use Combodo\iTop\Application\Search\CriterionConversion\CriterionToSearchForm; +use Combodo\iTop\Application\Search\CriterionParser; use Combodo\iTop\Application\Search\SearchForm; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; @@ -293,4 +294,62 @@ class CriterionConversionTest extends ItopDataTestCase ), ); } + + /** + * @dataProvider OqlProvider + * @throws \OQLException + */ + function testOqlToForlSearchToOql($sOQL) + { + $oSearchForm = new SearchForm(); + $oSearch = \DBSearch::FromOQL($sOQL); + $aFields = $oSearchForm->GetFields(new \DBObjectSet($oSearch)); + $aCriterion = $oSearchForm->GetCriterion($oSearch, $aFields); + $this->debug($sOQL); + + $aAndCriterion = $aCriterion['or'][0]['and']; + + $aNewCriterion = array(); + foreach($aAndCriterion as $aCriteria) + { + if (($aCriteria['widget'] == \AttributeDefinition::SEARCH_WIDGET_TYPE_STRING) + || ($aCriteria['widget'] == \AttributeDefinition::SEARCH_WIDGET_TYPE_DATE_TIME) + || ($aCriteria['widget'] == \AttributeDefinition::SEARCH_WIDGET_TYPE_ENUM)) + { + unset($aCriteria['oql']); + $aField = $aFields['zlist'][$aCriteria['ref']]; + $aCriteria['code'] = $aField['code']; + $aCriteria['class'] = $aField['class']; + } + + $aNewCriterion[] = $aCriteria; + } + $this->debug($aNewCriterion); + + $aCriterion['or'][0]['and'] = $aNewCriterion; + + $oSearch->ResetCondition(); + $oFilter = CriterionParser::Parse($oSearch->ToOQL(), $aCriterion); + + $this->debug($oFilter->ToOQL()); + + $this->assertTrue(true); + } + + function OqlProvider() + { + return array( + array('OQL' => "SELECT Contact WHERE status = 'active'"), + array('OQL' => "SELECT Contact WHERE status = 'active' AND name LIKE 'toto%'"), + array('OQL' => "SELECT Contact WHERE status = 'active' AND org_id = 3"), + array('OQL' => "SELECT Contact WHERE status IN ('active', 'inactive')"), + array('OQL' => "SELECT Contact WHERE status NOT IN ('active')"), + array('OQL' => "SELECT UserRequest WHERE DATE_SUB(NOW(), INTERVAL 14 DAY) < start_date"), + array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND '2018-01-01 00:00:00' >= start_date"), + array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND status = 'active' AND org_id = 3 AND '2018-01-01 00:00:00' >= start_date"), + array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 00:00:00' >= start_date"), + array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 01:00:00' > start_date"), + array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-02 00:00:00' > start_date"), + ); + } } diff --git a/test/application/search/SearchFormTest.php b/test/application/search/SearchFormTest.php index 97e5d59244..054fdd3bb8 100644 --- a/test/application/search/SearchFormTest.php +++ b/test/application/search/SearchFormTest.php @@ -98,8 +98,11 @@ class SearchFormTest extends ItopDataTestCase array('OQL' => "SELECT Contact WHERE status NOT IN ('active', 'inactive')", 1), array('OQL' => "SELECT Contact WHERE status = 'active' OR name LIKE 'toto%'", 2), array('OQL' => "SELECT UserRequest WHERE DATE_SUB(NOW(), INTERVAL 14 DAY) < start_date", 1), - array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01' AND '2018-01-01' >= start_date", 1), - array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01' AND status = 'active' AND org_id = 3 AND '2018-01-01' >= start_date", 1), + array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND '2018-01-01 00:00:00' >= start_date", 1), + array('OQL' => "SELECT UserRequest WHERE start_date > '2017-01-01 00:00:00' AND status = 'active' AND org_id = 3 AND '2018-01-01 00:00:00' >= start_date", 1), + array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 00:00:00' >= start_date", 1), + array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-01 01:00:00' > start_date", 1), + array('OQL' => "SELECT UserRequest WHERE start_date >= '2017-01-01 00:00:00' AND '2017-01-02 00:00:00' > start_date", 1), ); } }