N°4454 - Measuring the use of the queryphrase book

This commit is contained in:
Benjamin Dalsass
2022-01-14 11:12:30 +01:00
parent dcd52d6919
commit 91d4da85e1
8 changed files with 509 additions and 5 deletions

View File

@@ -18,8 +18,7 @@
*/
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Field\Field;
use Combodo\iTop\Application\UI\Base\Component\Field\FieldUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\FieldSet\FieldSetUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\Html\Html;
use Combodo\iTop\Application\UI\Base\Component\Input\TextArea;
@@ -51,6 +50,7 @@ abstract class Query extends cmdbAbstractObject
"is_null_allowed" => false,
"depends_on" => array(),
)));
MetaModel::Init_AddAttribute(new AttributeText("description", array(
"allowed_values" => null,
"sql" => "description",
@@ -68,6 +68,41 @@ abstract class Query extends cmdbAbstractObject
'display_style' => 'radio_horizontal',
)));
MetaModel::Init_AddAttribute(new AttributeInteger("export_count", array(
"allowed_values" => null,
"sql" => "export_count",
"default_value" => 0,
"is_null_allowed" => false,
"depends_on" => array(),
)));
MetaModel::Init_AddAttribute(new AttributeDateTime("export_last_date", array(
"allowed_values" => null,
"sql" => "export_last_date",
"default_value" => null,
"is_null_allowed" => true,
"depends_on" => array(),
)));
MetaModel::Init_AddAttribute(new AttributeExternalKey("export_last_user_id",
array(
"targetclass"=>'User',
"allowed_values"=>null,
"sql"=>'user_id',
"is_null_allowed"=>true,
"depends_on"=>array(),
"display_style"=>'select',
"always_load_in_tables"=>false,
"on_target_delete"=>DEL_SILENT
)));
MetaModel::Init_AddAttribute(new AttributeExternalField("export_last_user_contact",
array(
"allowed_values"=>null,
"extkey_attcode"=> "export_last_user_id",
"target_attcode"=>"contactid"
)));
// Display lists
MetaModel::Init_SetZListItems('details',
array('name', 'is_template', 'description')); // Attributes to be displayed for the complete details
@@ -78,6 +113,51 @@ abstract class Query extends cmdbAbstractObject
array('name', 'description', 'is_template')); // Criteria of the default search form
// MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
}
/** @inheritdoc */
public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
{
// read only attribute
if (in_array($sAttCode, ['export_count', 'export_last_date', 'export_last_user_id'])){
return(OPT_ATT_READONLY);
}
return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
}
/**
* Return export url.
*
* @param array|null $aValues optional values for the query
*
* @return string|null
*/
public abstract function GetExportUrl(array $aValues = null) : ?string;
/**
* Update last export information.
*
* @todo validation with Pierre
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @since 3.1.0
*/
public function UpdateLastExportInformation() : void
{
// last export information
$this->Set('export_last_date', date(AttributeDateTime::GetSQLFormat()));
$this->Set('export_last_user_id', UserRights::GetUserObject());
$this->DBUpdate();
// increment usage counter
$this->DBIncrement('export_count');
}
}
class QueryOQL extends Query
@@ -116,13 +196,51 @@ class QueryOQL extends Query
// Display lists
MetaModel::Init_SetZListItems('details',
array('name', 'is_template', 'description', 'oql', 'fields')); // Attributes to be displayed for the complete details
array(
'col:col1' => array('fieldset:Query:baseinfo' => array('name', 'is_template', 'description', 'oql', 'fields')),
'col:col2' => array('fieldset:Query:exportInfo' => array('export_count', 'export_last_date', 'export_last_user_id', 'export_last_user_contact'))
)
); // Attributes to be displayed for the complete details
MetaModel::Init_SetZListItems('list', array('description')); // Attributes to be displayed for a list
// Search criteria
MetaModel::Init_SetZListItems('standard_search',
array('name', 'description', 'is_template', 'fields', 'oql')); // Criteria of the std search form
}
/** @inheritdoc */
public function GetExportUrl(array $aValues = null) : ?string
{
try{
// retrieve attributes
$sFields = trim($this->Get('fields'));
$sOql = $this->Get('oql');
// construct base url depending on version
$bExportV1Recommended = ($sFields == '');
if ($bExportV1Recommended) {
$sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export.php?format=spreadsheet&login_mode=basic&query='.$this->GetKey();
}
else{
$sUrl = utils::GetAbsoluteUrlAppRoot().'webservices/export-v2.php?format=spreadsheet&login_mode=basic&date_format='.urlencode((string)AttributeDateTime::GetFormat()).'&query='.$this->GetKey();
}
// search object from OQL
$oSearch = DBObjectSearch::FromOQL($sOql);
// inject parameters
$aParameters = $oSearch->GetQueryParams();
foreach ($aParameters as $sParam => $val) {
($aValues === null || $aValues[$sParam] === null) ? $paramValue = $sParam : $paramValue = $aValues[$sParam];
$sUrl .= '&arg_' . $sParam . '=' . $paramValue;
}
return $sUrl;
}
catch(Exception $e){
return null;
}
}
function DisplayBareProperties(WebPage $oPage, $bEditMode = false, $sPrefix = '', $aExtraParams = array())
{
$aFieldsMap = parent::DisplayBareProperties($oPage, $bEditMode, $sPrefix, $aExtraParams);
@@ -152,9 +270,11 @@ class QueryOQL extends Query
$sUrl .= '&arg_'.$sParam.'=["'.$sParam.'"]';
}
// add text area inside field set
$oFieldSet = FieldSetUIBlockFactory::MakeStandard(Dict::S('UI:Query:UrlForExcel'));
$oTextArea = new TextArea("", $sUrl, null, 80, 3);
$oFieldUrl = FieldUIBlockFactory::MakeFromObject(Dict::S('UI:Query:UrlForExcel'), $oTextArea, Field::ENUM_FIELD_LAYOUT_LARGE);
$oPage->AddSubBlock($oFieldUrl);
$oFieldSet->AddSubBlock($oTextArea);
$oPage->AddSubBlock($oFieldSet);
if (count($aParameters) == 0) {
$oBlock = new DisplayBlock($oSearch, 'list');
@@ -178,6 +298,7 @@ class QueryOQL extends Query
return $aFieldsMap;
}
// Rolled back until 'fields' can be properly managed by AttributeQueryAttCodeSet
//
// public function ComputeValues()

View File

@@ -3453,6 +3453,54 @@ abstract class DBObject implements iDisplay
return $this->m_iKey;
}
/**
* Increment attribute with specified value.
* This function is only applicable with AttributeInteger.
*
* @api
*
* @param string $sAttCode attribute code
* @param int $iValue value to increment (default value 1)
*
* @return int incremented value
*
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
*/
public function DBIncrement(string $sAttCode, int $iValue = 1)
{
// retrieve instance class
$sClass = get_class($this);
// dirty object not allowed
if($this->m_bDirty){
throw new CoreException("Invalid DBIncrement usage, dirty objects are not allowed. Call DBUpdate before calling DBIncrement.");
}
// ensure attribute type is AttributeInteger
$oAttr = MetaModel::GetAttributeDef($sClass, $sAttCode);
if(!$oAttr instanceof AttributeInteger){
throw new CoreException(sprintf("Invalid DBIncrement usage, attribute type of {$sAttCode} is %s. Only AttributeInteger are compatibles with DBIncrement.", get_class($oAttr)));
}
// prepare SQL statement
$sTable = MetaModel::DBGetTable($sClass, $sAttCode);
$sPKField = '`'.MetaModel::DBGetKey($sClass).'`';
$sKey = CMDBSource::Quote($this->m_iKey);
$sUpdateSQL = "UPDATE `{$sTable}` SET `{$sAttCode}` = `{$sAttCode}`+{$iValue} WHERE {$sPKField} = {$sKey}";
// execute SQL query
CMDBSource::Query($sUpdateSQL);
// reload instance with new value
$this->Reload();
return $this->Get($sAttCode);
}
/**
* @internal
* Save updated fields previous values for {@see DBObject::DBUpdate()} callbacks

View File

@@ -75,6 +75,12 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:Query/Attribute:is_template+' => 'Usable as source for recipient OQL in Notifications',
'Class:Query/Attribute:is_template/Value:yes' => 'Yes',
'Class:Query/Attribute:is_template/Value:no' => 'No',
'Class:Query/Attribute:export_count' => 'Export counter',
'Class:Query/Attribute:export_last_date' => 'Last export',
'Class:Query/Attribute:export_last_user_id' => 'User',
'Class:Query/Attribute:export_last_user_contact' => 'Contact',
'Query:baseinfo' => 'General information',
'Query:exportInfo' => 'Export information',
'Class:QueryOQL/Attribute:fields' => 'Fields',
'Class:QueryOQL/Attribute:fields+' => 'Comma separated list of attributes (or alias.attribute) to export',
'Class:QueryOQL' => 'OQL Query',

View File

@@ -59,6 +59,12 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Class:Query/Attribute:is_template+' => 'Utilisable comme base pour les destinataires des Notifications',
'Class:Query/Attribute:is_template/Value:yes' => 'Oui',
'Class:Query/Attribute:is_template/Value:no' => 'Non',
'Class:Query/Attribute:export_count' => 'Nombre d\'exports',
'Class:Query/Attribute:export_last_date' => 'Dernier export',
'Class:Query/Attribute:export_last_user_id' => 'Utilisateur',
'Class:Query/Attribute:export_last_user_contact' => 'Contact',
'Query:baseinfo' => 'Informations générales',
'Query:exportInfo' => 'Informations sur les exports',
'Class:QueryOQL/Attribute:fields' => 'Champs',
'Class:QueryOQL/Attribute:fields+' => 'Liste CSV des attributs (ou alias.attribut) à exporter',
'Class:QueryOQL' => 'Requête OQL',

View File

@@ -0,0 +1,194 @@
<?php
/**
* Copyright (C) 2018 Dennis Lassiter
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop 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
* along with iTop. If not, see <http://www.gnu.org/licenses/>
*
*/
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* This test creates call export on requests and check request usage counter.
*
* Transaction are disabled to avoid data inconsistency between test and call to export (outside test scope)
* All objects created in this test will be deleted by the test.
*
* @group iTopQuery
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class QueryTest extends ItopDataTestCase
{
// disable transaction to avoid data inconsistency between test and call to export (outside test scope)
const USE_TRANSACTION = false;
// user for exportation process
const USER = 'dani2';
const PASSWORD = '1TopCombodo+';
private $oUser;
/** @inheritDoc */
public function setUp()
{
parent::setUp();
// create export user
$this->CreateExportUser();
}
/**
* Create new user for export authentication purpose.
*/
private function CreateExportUser()
{
$oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", array('name' => 'Administrator'), true);
$this->oUser = $this->CreateUser(self::USER, $oAdminProfile->GetKey(), self::PASSWORD);
}
/**
* Create an OQL query to list Person objects.
*
* @param string $sName query name
* @param string $sDescription query description
* @param string $sOql query oql phrase
* @param string|null $sFields fields to export
*/
private function CreateQueryOQL(string $sName, string $sDescription, string $sOql, string $sFields = null) : QueryOQL
{
$oQuery = new QueryOQL();
$oQuery->Set('name', $sName);
$oQuery->Set('description', $sDescription);
$oQuery->Set('oql', $sOql);
if($sFields != null){
$oQuery->Set('fields', $sFields);
}
$oQuery->DBInsert();
return $oQuery;
}
/**
* Test query export V1 usage.
*
* @param string $sDescription query description
* @param string $sOql query oql phrase
*
* @dataProvider getQueryProvider
*/
public function testQueryExportV1Usage(string $sDescription, string $sOql)
{
// create query OQL
$oQuery = $this->CreateQueryOQL($this->dataName(), $sDescription, $sOql);
// call export service
$this->CallExportService($oQuery);
// reload to update counter (done by export process)
$oQuery->Reload();
// extract counter
$iResult = $oQuery->Get('export_count');
// delete the query
$oQuery->DBDelete();
// test
$this->assertEquals(1, $iResult);
}
/**
* Test query export V2 usage.
*
* @param string $sDescription query description
* @param string $sOql query oql phrase
*
* @dataProvider getQueryProvider
*/
public function testQueryExportV2Usage(string $sDescription, string $sOql)
{
// create query OQL
$oQuery = $this->CreateQueryOQL($this->dataName(), $sDescription, $sOql, 'first_name');
// call export service
$this->CallExportService($oQuery);
// reload to update counter (done by export process)
$oQuery->Reload();
// extract counter
$iResult = $oQuery->Get('export_count');
// delete the query
$oQuery->DBDelete();
// test
$this->assertEquals(1, $iResult);
}
/**
* Data provide for test.
*
* @return array
*/
public function getQueryProvider()
{
return array(
'Export #1' => array('query without params', 'SELECT Person'),
'Export #2' => array('query with params', "SELECT Person WHERE first_name LIKE 'B%'")
);
}
/**
* Call export for given query object.
*
* @param \Query $oQuery
*
* @return bool|string
*/
private function CallExportService(Query $oQuery)
{
// compute request url
$url = $oQuery->GetExportUrl();
// open curl
$curl = curl_init();
// curl options
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curl, CURLOPT_USERPWD, self::USER . ':' . self::PASSWORD);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
// execute curl
$result = curl_exec($curl);
// close curl
curl_close($curl);
return $result;
}
/** @inheritDoc */
protected function tearDown()
{
$this->oUser->DBDelete();
}
}

View File

@@ -27,6 +27,7 @@
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreException;
use DBObject;
@@ -250,4 +251,121 @@ class DBObjectTest extends ItopDataTestCase
}
}
}
/**
* Test attribute integer incrementation.
*
* @covers DBObject::DBIncrement
*
* @dataProvider getAttributeIntegerDBIncrementProvider
*
*/
public function testAttributeIntegerDBIncrement(string $sAttrCode, array $aValues, $expectedResult)
{
// create query object
$oQueryOQL = \MetaModel::NewObject('QueryOQL', [
'name' => 'Test Query',
'description' => 'Test Query',
'oql' => 'SELECT Person'
]);
$oQueryOQL->DBInsert();
// iterate throw increments...
foreach ($aValues as $aValue) {
$oQueryOQL->DBIncrement($sAttrCode, $aValue);
}
// retrieve counter current value
$iCounter = $oQueryOQL->Get($sAttrCode);
// assert equals
$this->assertEquals($expectedResult, $iCounter);
}
/**
* Data provider for test attribute integer incrementation.
*
* @return array data
*/
public function getAttributeIntegerDBIncrementProvider()
{
return array(
'Incrementation #1' => array('export_count', [5], 5),
'Incrementation #2' => array('export_count', [5, 10], 15),
'Incrementation #3' => array('export_count', [50, 20, 10, 100], 180),
'Incrementation #4' => array('export_count', [50, 20, -10, 1000], 1060)
);
}
/**
* Test attribute integer increment with AttributeText.
*
* @covers DBObject::DBIncrement
*
*/
public function testAttributeTextDBIncrement()
{
// create query object
$oQueryOQL = \MetaModel::NewObject('QueryOQL', [
'name' => 'Test Query',
'description' => 'Test Query',
'oql' => 'SELECT Person'
]);
$oQueryOQL->DBInsert();
// assert exception
$this->expectException(CoreException::class);
// try incrementation
$oQueryOQL->DBIncrement('description');
}
/**
* Test attribute integer increment when object dirty.
*
* @covers DBObject::DBIncrement
*
*/
public function testAttributeIntegerDBIncrementDirty()
{
// create query object
$oQueryOQL = \MetaModel::NewObject('QueryOQL', [
'name' => 'Test Query',
'description' => 'Test Query',
'oql' => 'SELECT Person'
]);
$oQueryOQL->DBInsert();
// change description
$oQueryOQL->Set('description', 'new name');
// assert exception
$this->expectException(CoreException::class);
// try incrementation
$oQueryOQL->DBIncrement('export_count');
}
/**
* Test query count with attribute integer increment.
*
* @covers DBObject::DBIncrement
*
*/
public function testAttributeIntegerDBIncrementQueryCount()
{
// create query object
$oQueryOQL = \MetaModel::NewObject('QueryOQL', [
'name' => 'Test Query',
'description' => 'Test Query',
'oql' => 'SELECT Person'
]);
$oQueryOQL->DBInsert();
// assert query count
static::assertDBQueryCount(2, function() use (&$oQueryOQL) {
$oQueryOQL->DBIncrement('export_count', 1);
});
}
}

View File

@@ -452,6 +452,7 @@ EOF
function CheckParameters($sExpression, $sQueryId, $sFormat)
{
$oExporter = null;
$oQuery = null;
if (utils::IsArchiveMode() && !UserRights::CanBrowseArchive()) {
ReportErrorAndExit("The user account is not authorized to access the archives");
@@ -516,6 +517,11 @@ function CheckParameters($sExpression, $sQueryId, $sFormat)
ReportErrorAndExit(utils::HtmlEntities($e->getMessage()));
}
// update last export information if check parameters ok
if($oQuery != null){
$oQuery->UpdateLastExportInformation();
}
$oExporter->SetFormat($sFormat);
$oExporter->SetChunkSize(EXPORTER_DEFAULT_CHUNK_SIZE);
$oExporter->SetObjectList($oSearch);

View File

@@ -184,6 +184,11 @@ if (!empty($sExpression))
}
}
// update last export information if check parameters ok
if($oQuery != null){
$oQuery->UpdateLastExportInformation();
}
if ($oFilter)
{
$oSet = new CMDBObjectSet($oFilter, array(), $aArgs);