Robert Vogel has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/371459 )
Change subject: [WiP] Generic DataSources ...................................................................... [WiP] Generic DataSources Change-Id: Ie4ee268a2c42c9066a4c8667f01dce0b2db6799c --- A src/DataSources/DataSource.php A src/DataSources/FieldType.php A src/DataSources/LocalDatabase.php A src/DataSources/Params.php A src/DataSources/Params/Filter.php A src/DataSources/Params/Filter/Boolean.php A src/DataSources/Params/Filter/Date.php A src/DataSources/Params/Filter/ListValue.php A src/DataSources/Params/Filter/Numeric.php A src/DataSources/Params/Filter/Range.php A src/DataSources/Params/Filter/StringValue.php A src/DataSources/Params/Filter/TemplateTitle.php A src/DataSources/Params/Filter/Title.php A src/DataSources/Params/FilterCollection.php A src/DataSources/Params/FilterFactory.php A src/DataSources/Params/Sort.php A src/DataSources/Params/SortCollection.php A src/DataSources/Users.php A src/DataSources/Watchlist.php A tests/phpunit/DataSources/Params/Filter/BooleanTest.php A tests/phpunit/DataSources/Params/Filter/DateTest.php A tests/phpunit/DataSources/ParamsTest.php A tests/phpunit/DataSources/WatchlistTest.php 23 files changed, 1,247 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/BlueSpiceFoundation refs/changes/59/371459/1 diff --git a/src/DataSources/DataSource.php b/src/DataSources/DataSource.php new file mode 100644 index 0000000..eba4dc3 --- /dev/null +++ b/src/DataSources/DataSource.php @@ -0,0 +1,255 @@ +<?php + +namespace BlueSpice\DataSources; + +abstract class DataSource { + + const FIELD_FILTERABLE = 'filterable'; + const FIELD_SORTABLE = 'sortable'; + const FIELD_TYPE = 'type'; + + /** + * + * @var \IContextSource + */ + private $context = null; + + /** + * + * @var \Config + */ + protected $config = null; + + /** + * The current parameters + * @var Params + */ + protected $params = null; + + /** + * + * @var stdClass[] + */ + protected $data = []; + + /** + * + * @var int + */ + protected $total = 0; + + + /** + * + * @param \IContextSource $context + * @param \Config $config + * @param Params $params + */ + public function __construct( \IContextSource $context = null, + \Config $config = null, $params = null + ) { + $this->context = $context; + if( $this->context === null ) { + $this->context = \RequestContext::getMain(); + } + + $this->config = $config; + if( $this->config === null ) { + $this->config = \MediaWiki\MediaWikiServices::getInstance()->getMainConfig(); + } + + $this->params = $params; + if( $this->params === null ) { + $this->params = new Params(); + } + } + + /** + * + * @return \User + */ + protected function getUser() { + return $this->context->getUser(); + } + + /** + * + * @return \Title + */ + protected function getTitle() { + return $this->context->getTitle(); + } + + /** + * @return array Column definition compatible to + * https://docs.sencha.com/extjs/4.2.1/#!/api/Ext.grid.Panel-cfg-columns + */ + abstract public function getSchemaDefinition(); + + abstract protected function makeData(); + + public function fetch() { + $this->clearData(); + $this->makeData(); + $this->filterData(); + $this->saveTotalCount(); + $this->sortData(); + $this->trimData(); + $this->applySecondaryFields(); + } + + /** + * + * @return stdClass[] + */ + public function getResult() { + return $this->data; + } + + public function getTotal() { + return $this->total; + } + + /** + * + * @param object $aDataSet + * @return boolean + */ + protected function applyFilter( $aDataSet ) { + /** + * @var Filter[] + */ + $filter = $this->params->getFilter(); + foreach( $filter as $filter ) { + //If just one of these filters does not apply, the dataset needs + //to be removed + + if( !$filter->appliesTo( $aDataSet ) ) { + return false; + } + } + + return true; + } + + /** + * Applies pagination on the result + */ + protected function trimData() { + $start = $this->params->getStart(); + $end = $this->params->getLimit() + $start; + + if( $end > $this->total || $end === 0 ) { + $end = $this->total; + } + + $trimmedData = array(); + for( $i = $start; $i < $end; $i++ ) { + $trimmedData[] = $this->data[$i]; + } + + $this->data = $trimmedData; + } + + /** + * Performs fast sorting on the results. Thanks to user "pigpen" + */ + protected function sortData() { + $sortParams = array(); + foreach( $this->params->getSort() as $sort ) { + $property = $sort->getProperty(); + $a{$property} = array(); + foreach( $this->data as $idx => $dataSet ) { + $a{$property}[$idx] = $this->getSortValue( $dataSet, $property ); + } + + $sortParams[] = $a{$property}; + if( $sort->getDirection() === Params\Sort::ASCENDING ) { + $sortParams[] = SORT_ASC; + } + else { + $sortParams[] = SORT_DESC; + } + $sortParams[] = $this->getSortFlags( $property ); + } + + if( !empty( $sortParams ) ) { + $sortParams[] = &$this->data; + call_user_func_array( 'array_multisort', $sortParams ); + } + + $this->data = array_values( $this->data ); + } + + /** + * Returns the flags for PHP 'array_multisort' function + * May be overridden by subclasses to provide different sort flags + * depending on the property + * @param string $sProperty + * @return int see http://php.net/manual/en/array.constants.php for details + */ + protected function getSortFlags( $sProperty ) { + return SORT_NATURAL; + } + + /** + * Returns the value a for a field a dataset is being sorted by. + * May be overridden by subclass to allow custom sorting + * @param stdClass $oDataSet + * @param string $sProperty + * @return string + */ + protected function getSortValue( $oDataSet, $sProperty ) { + $mValue = $oDataSet->{$sProperty}; + if( is_array( $mValue ) ) { + return $this->getSortValueFromList( $mValue, $oDataSet, $sProperty ); + } + + return $mValue; + } + + /** + * Normalizes an array to a string value that can be used in sort logic. + * May be overridden by subclass to customize sorting. + * Assumes that array entries can be casted to string. + * @param array $aValues + * @param stdClass $oDataSet + * @param string $sProperty + * @return string + */ + protected function getSortValueFromList( $aValues, $oDataSet, $sProperty ) { + $sCombinedValue = ''; + foreach( $aValues as $sValue ) { + $sCombinedValue .= (string)$sValue; + } + return $sCombinedValue; + } + + protected function filterData() { + $this->data = array_filter( + $this->data, + function ( $aDataSet ) { + //This way 'applyFilter' can be 'protected' + return $this->applyFilter( $aDataSet ); + } + ); + } + + protected function saveTotalCount() { + $this->total = count( $this->data ); + } + + /** + * May be overridden by subclass to add additional fields to the data sets + * ATTENTION: Those fields are not filterable and sortable! This should be + * declared in "makeMetadata" + */ + protected function applySecondaryFields() { + + } + + protected function clearData() { + $this->data = []; + } + +} diff --git a/src/DataSources/FieldType.php b/src/DataSources/FieldType.php new file mode 100644 index 0000000..efbad0e --- /dev/null +++ b/src/DataSources/FieldType.php @@ -0,0 +1,15 @@ +<?php + +namespace BlueSpice\DataSources; + +class FieldType { + /** + * https://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.Field + */ + const AUTO = 'auto'; + const BOOLEAN = 'boolean'; + const DATE = 'date'; + const FLOAT = 'float'; + const INT = 'int'; + const STRING = 'string'; +} diff --git a/src/DataSources/LocalDatabase.php b/src/DataSources/LocalDatabase.php new file mode 100644 index 0000000..8743155 --- /dev/null +++ b/src/DataSources/LocalDatabase.php @@ -0,0 +1,38 @@ +<?php + +namespace BlueSpice\DataSources; + +abstract class LocalDatabase extends DataSource { + + /** + * + * @var \LoadBalancer + */ + private $dbLoadBalancer = null; + + /** + * + * @param \IContextSource $context + * @param \Config $config + * @param Params $params + * @param \LoadBalancer $dbLoadBalancer + */ + public function __construct( \IContextSource $context = null, + \Config $config = null, $params = null, \LoadBalancer $dbLoadBalancer = null + ) { + parent::__construct( $context, $config, $params ); + + $this->dbLoadBalancer = $dbLoadBalancer; + if( $this->dbLoadBalancer === null ) { + $this->dbLoadBalancer = \LBFactory::singleton()->getMainLB(); + } + } + + /** + * + * @return \DatabaseBase + */ + protected function getDB() { + return $this->dbLoadBalancer->getConnection( DB_SLAVE ); + } +} \ No newline at end of file diff --git a/src/DataSources/Params.php b/src/DataSources/Params.php new file mode 100644 index 0000000..8b68869 --- /dev/null +++ b/src/DataSources/Params.php @@ -0,0 +1,122 @@ +<?php + +namespace BlueSpice\DataSources; + +class Params { + + /** + * For pre filtering + * @var string + */ + protected $query = ''; + + /** + * For paging + * @var int + */ + protected $start = 0; + + /** + * For paging + * @var int + */ + protected $limit = 25; + + /** + * + * @var Params\SortCollection + */ + protected $sort = null; + + /** + * + * @var Params\FilterCollection + */ + protected $filter = null; + + /** + * + * @param array $params + */ + public function __construct( $params = [] ) { + $this->setIfAvailable( $this->query, $params, 'query' ); + $this->setIfAvailable( $this->start, $params, 'start' ); + $this->setIfAvailable( $this->limit, $params, 'limit' ); + $this->setSort( $params ); + $this->setFilter( $params ); + } + + private function setIfAvailable( &$property, $source, $field ) { + if( isset( $source[$field] ) ) { + $property = $source[$field]; + } + } + + /** + * Getter for "limit" param + * @return int The "limit" parameter + */ + public function getLimit() { + return $this->limit; + } + + /** + * Getter for "start" param + * @return int The "start" parameter + */ + public function getStart() { + //TODO: mabye this can be calculated from "page" and "limit"; Examine behavior of Ext.data.Store / Ext.data.Proxy + return $this->start; + } + + /** + * Getter for "sort" param + * @return Params\Sort[] + */ + public function getSort() { + return $this->sort; + } + + /** + * Getter for "query" param + * @return string The "query" parameter + */ + public function getQuery() { + return $this->query; + } + + /** + * Getter for "filter" param + * @return Params\Filter[] + */ + public function getFilter() { + return $this->filter; + } + + protected function setSort( $params ) { + $this->sort = new Params\SortCollection(); + + if( !isset( $params['sort'] ) || !is_array( $params['sort'] ) ) { + return; + } + + foreach( $params['sort'] as $sortParam ) { + $sortParam = (array)$sortParam; + $this->sort[] = new Params\Sort( $sortParam['property'], $sortParam['direction'] ); + } + } + + protected function setFilter( $params ) { + $this->filter = new Params\FilterCollection(); + + if( !isset( $params['filter'] ) || !is_array( $params['filter'] ) ) { + return; + } + + foreach( $params['filter'] as $filterParam ) { + $filterParam = (array)$filterParam; + $this->filter[] = Params\FilterFactory::newFromArray( $filterParam ); + } + } + +} \ No newline at end of file diff --git a/src/DataSources/Params/Filter.php b/src/DataSources/Params/Filter.php new file mode 100644 index 0000000..bac7b3f --- /dev/null +++ b/src/DataSources/Params/Filter.php @@ -0,0 +1,73 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class Filter { + const COMPARISON_EQUALS = 'eq'; + const COMPARISON_NOT_EQUALS = 'neq'; + + const KEY_TYPE = 'type'; + const KEY_COMPARISON = 'comparison'; + const KEY_FIELD = 'field'; + const KEY_VALUE = 'value'; + + /** + * + * @var string + */ + protected $field = ''; + + /** + * + * @var mixed + */ + protected $value = null; + + /** + * + * @var string + */ + protected $comparison = ''; + + /** + * + * @param array $params + */ + public function __construct( $params ) { + $this->field = $params[self::KEY_FIELD]; + $this->value = $params[self::KEY_VALUE]; + $this->comparison = $params[self::KEY_COMPARISON]; + } + + /** + * + * @return string + */ + public function getField() { + return $this->field; + } + + /** + * + * @return mixed + */ + public function getValue() { + return $this->value; + } + + /** + * + * @return string + */ + public function getComparison() { + return $this->comparison; + } + + /** + * + * @param stdClass $dataSet + */ + public function appliesTo( $dataSet ) { + + } +} diff --git a/src/DataSources/Params/Filter/Boolean.php b/src/DataSources/Params/Filter/Boolean.php new file mode 100644 index 0000000..508dd8e --- /dev/null +++ b/src/DataSources/Params/Filter/Boolean.php @@ -0,0 +1,25 @@ +<?php + +namespace BlueSpice\DataSources\Params\Filter; + +class Boolean extends \BlueSpice\DataSources\Params\Filter { + + /** + * Performs filtering based on given filter of type bool on a dataset + * + * @param stdClass $dataSet + * @return boolean + */ + public function appliesTo( $dataSet ) { + $fieldValue = $dataSet->{ $this->getField() }; + $filterValue = $this->getValue(); + + switch( $this->getComparison() ) { + case self::COMPARISON_EQUALS: + return $fieldValue == $filterValue; + case self::COMPARISON_NOT_EQUALS: + return $fieldValue != $filterValue; + } + return false; + } +}; \ No newline at end of file diff --git a/src/DataSources/Params/Filter/Date.php b/src/DataSources/Params/Filter/Date.php new file mode 100644 index 0000000..ee95a61 --- /dev/null +++ b/src/DataSources/Params/Filter/Date.php @@ -0,0 +1,32 @@ +<?php + +namespace BlueSpice\DataSources\Params\Filter; + +class Date extends Range { + + /** + * Performs filtering based on given filter of type date on a dataset + * "Ext.ux.grid.filter.DateFilter" by default sends filter value in format + * of m/d/Y + * @param stdClass $dataSet + * @return boolean + */ + public function appliesTo( $dataSet ) { + $filterValue = strtotime( $this->getValue() ); // Format: "m/d/Y" + $fieldValue = strtotime( $dataSet->{$this->field} ); // Format "YmdHis", or something else... + + switch( $this->getComparison() ) { + case self::COMPARISON_GREATER_THAN: + return $fieldValue > $filterValue; + case self::COMPARISON_LOWER_THAN: + return $fieldValue < $filterValue; + case self::COMPARISON_EQUALS: + //We need to normalise the date on day-level + $fieldValue = strtotime( + date( 'm/d/Y', $fieldValue ) + ); + return $fieldValue === $filterValue; + } + return true; + } +}; \ No newline at end of file diff --git a/src/DataSources/Params/Filter/ListValue.php b/src/DataSources/Params/Filter/ListValue.php new file mode 100644 index 0000000..b128ce9 --- /dev/null +++ b/src/DataSources/Params/Filter/ListValue.php @@ -0,0 +1,27 @@ +<?php + +namespace BlueSpice\DataSources\Params\Filter; + +class ListValue extends BlueSpice\DataSources\Params\Filter { + + /** + * Performs list filtering based on given filter of type array on a dataset + * @param stdClass $dataSet + * @return boolean + */ + public function appliesTo( $dataSet ) { + if( !is_array( $this->getValue() ) ) { + return true; //TODO: Warning + } + $fieldValues = $dataSet->{ $this->getField() }; + if( empty( $fieldValues ) ) { + return false; + } + + $intersection = array_intersect( $fieldValues, $this->getValue() ); + if( empty( $intersection ) ) { + return false; + } + return true; + } +} diff --git a/src/DataSources/Params/Filter/Numeric.php b/src/DataSources/Params/Filter/Numeric.php new file mode 100644 index 0000000..98b1da0 --- /dev/null +++ b/src/DataSources/Params/Filter/Numeric.php @@ -0,0 +1,32 @@ +<?php + +namespace BlueSpice\DataSources\Params\Filter; + +class Numeric extends Range { + /** + * Performs numeric filtering based on given filter of type integer on a + * dataset + * + * @param stdClass $dataSet + * @return boolean + */ + public function appliesTo( $dataSet ) { + if( !is_numeric( $this->getValue() ) ) { + return true; //TODO: Warning + } + $fieldValue = (int) $dataSet->{$this->getField()}; + $filterValue = (int) $this->getValue(); + + switch( $this->getComparison() ) { + case self::COMPARISON_GREATER_THAN: + return $fieldValue > $filterValue; + case self::COMPARISON_LOWER_THAN: + return $fieldValue < $filterValue; + case self::COMPARISON_EQUALS: + return $fieldValue === $filterValue; + case self::COMPARISON_NOT_EQUALS: + return $fieldValue !== $filterValue; + } + return true; + } +}; \ No newline at end of file diff --git a/src/DataSources/Params/Filter/Range.php b/src/DataSources/Params/Filter/Range.php new file mode 100644 index 0000000..0051ae8 --- /dev/null +++ b/src/DataSources/Params/Filter/Range.php @@ -0,0 +1,8 @@ +<?php + +namespace BlueSpice\DataSources\Params\Filter; + +class Range extends \BlueSpice\DataSources\Params\Filter { + const COMPARISON_LOWER_THAN = 'lt'; + const COMPARISON_GREATER_THAN = 'gt'; +}; \ No newline at end of file diff --git a/src/DataSources/Params/Filter/StringValue.php b/src/DataSources/Params/Filter/StringValue.php new file mode 100644 index 0000000..fdd7e60 --- /dev/null +++ b/src/DataSources/Params/Filter/StringValue.php @@ -0,0 +1,24 @@ +<?php + +namespace BlueSpice\DataSources\Params\Filter; + +/** + * Class name "String" is reserved + */ +class StringValue extends \BlueSpice\DataSources\Params\Filter { + const COMPARISON_STARTS_WITH = 'sw'; + const COMPARISON_ENDS_WITH = 'ew'; + const COMPARISON_CONTAINS = 'ct'; + const COMPARISON_NOT_CONTAINS = 'nct'; + + /** + * Performs string filtering based on given filter of type string on a + * dataset + * @param stdClass $dataSet + */ + public function appliesTo( $dataSet ) { + $sFieldValue = $dataSet->{ $this->getField() }; + + return \BsStringHelper::filter( $this->getComparison(), $sFieldValue, $this->getValue() ); + } +}; \ No newline at end of file diff --git a/src/DataSources/Params/Filter/TemplateTitle.php b/src/DataSources/Params/Filter/TemplateTitle.php new file mode 100644 index 0000000..21c63ef --- /dev/null +++ b/src/DataSources/Params/Filter/TemplateTitle.php @@ -0,0 +1,9 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class TemplateTitle extends BlueSpice\DataSources\Params\Title { + protected function getDefaultTitleNamespace() { + return NS_TEMPLATE; + } +} \ No newline at end of file diff --git a/src/DataSources/Params/Filter/Title.php b/src/DataSources/Params/Filter/Title.php new file mode 100644 index 0000000..1060b1a --- /dev/null +++ b/src/DataSources/Params/Filter/Title.php @@ -0,0 +1,39 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class Title extends BlueSpice\DataSources\Params\Filter\Range { + + /** + * Performs string filtering based on given filter of type Title on a + * dataset + * + * @param stdClass $dataSet + * @return boolean + */ + public function appliesTo( $dataSet ) { + if( !is_string( $this->getValue() ) ) { + return true; //TODO: Warning + } + $fieldValue = \Title::newFromText( $dataSet->{$this->getField()}, $this->getDefaultTitleNamespace() ); + $filterValue = \Title::newFromText( $this->getValue(), $this->getDefaultTitleNamespace() ); + + switch( $this->getComparison() ) { + case self::COMPARISON_GREATER_THAN: + return \Title::compare( $fieldValue, $filterValue ) > 0; + case self::COMPARISON_LOWER_THAN: + return \Title::compare( $fieldValue, $filterValue ) < 0; + case self::COMPARISON_EQUALS: + return \Title::compare( $fieldValue, $filterValue ) == 0; + case self::COMPARISON_NOT_EQUALS: + return \Title::compare( $fieldValue, $filterValue ) != 0; + } + return true; + } + + protected function getDefaultTitleNamespace() { + return NS_MAIN; + } + +} + diff --git a/src/DataSources/Params/FilterCollection.php b/src/DataSources/Params/FilterCollection.php new file mode 100644 index 0000000..c00c002 --- /dev/null +++ b/src/DataSources/Params/FilterCollection.php @@ -0,0 +1,22 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class FilterCollection extends \ArrayObject { + + /** + * + * @param string $propertyName + * @return boolean + */ + public function has( $propertyName ) { + foreach( $this as $filter ) { + $filter instanceof Filter; + if( $filter->getField() === $propertyName ) { + return true; + } + } + + return false; + } +} diff --git a/src/DataSources/Params/FilterFactory.php b/src/DataSources/Params/FilterFactory.php new file mode 100644 index 0000000..3067d9b --- /dev/null +++ b/src/DataSources/Params/FilterFactory.php @@ -0,0 +1,35 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class FilterFactory { + protected static $typeMap = [ + 'string' => 'BlueSpice\DataSources\Params\Filter\StringValue', + 'date'=> 'BlueSpice\DataSources\Params\Filter\Date', + #'datetime'=> 'BlueSpice\DataSources\Params\Filter\DateTime', + 'boolean'=> 'BlueSpice\DataSources\Params\Filter\Boolean', + 'numeric' => 'BlueSpice\DataSources\Params\Filter\Numeric', + 'title' => 'BlueSpice\DataSources\Params\Filter\Title', + 'templatetitle' => 'BlueSpice\DataSources\Params\Filter\TemplateTitle', + 'list' => 'BlueSpice\DataSources\Params\Filter\ListValue' + ]; + + /** + * + * @param array $filter + * @return \BlueSpice\DataSources\Params\Filter + * @throws \UnexpectedValueException + */ + public static function newFromArray( $filter ) { + if( isset( self::$typeMap[$filter[Filter::KEY_TYPE]]) ) { + return new self::$typeMap[$filter[Filter::KEY_TYPE]]( $filter ); + } + else { + throw new \UnexpectedValueException( + "No filter class for '{$filter[Filter::KEY_TYPE]}' available!" + ); + } + } +} + + diff --git a/src/DataSources/Params/Sort.php b/src/DataSources/Params/Sort.php new file mode 100644 index 0000000..55eaad4 --- /dev/null +++ b/src/DataSources/Params/Sort.php @@ -0,0 +1,54 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class Sort { + + const ASCENDING = 'ASC'; + const DESCENDING = 'DESC'; + + protected $property = ''; + + protected $direction = ''; + + /** + * + * @param string $property + * @param string $direction + * @throws UnexpectedValueException + */ + public function __construct( $property, $direction = self::ASCENDING ) { + $this->property = $property; + $this->direction = strtoupper( $direction ); + + if( !in_array( $this->direction, [ self::ASCENDING, self::DESCENDING ] ) ) { + throw new UnexpectedValueException( + "'{$this->direction}' is not an allowed value for argument \$direction" + ); + } + } + + /** + * + * @return string + */ + public function getProperty() { + return $this->property; + } + + /** + * + * @return string + */ + public function getDirection() { + return $this->direction; + } + + /** + * + * @return string + */ + public function __toString() { + return $this->getProperty().' '.$this->getDirection(); + } +} diff --git a/src/DataSources/Params/SortCollection.php b/src/DataSources/Params/SortCollection.php new file mode 100644 index 0000000..d08a751 --- /dev/null +++ b/src/DataSources/Params/SortCollection.php @@ -0,0 +1,5 @@ +<?php + +namespace BlueSpice\DataSources\Params; + +class SortCollection extends \ArrayObject {} diff --git a/src/DataSources/Users.php b/src/DataSources/Users.php new file mode 100644 index 0000000..3ac00fa --- /dev/null +++ b/src/DataSources/Users.php @@ -0,0 +1,8 @@ +<?php + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + diff --git a/src/DataSources/Watchlist.php b/src/DataSources/Watchlist.php new file mode 100644 index 0000000..fae5bbf --- /dev/null +++ b/src/DataSources/Watchlist.php @@ -0,0 +1,170 @@ +<?php + +namespace BlueSpice\DataSources; + +class Watchlist extends LocalDatabase { + + const CONFIG_USER = 'user'; + + /** + * + * @var int[] + */ + protected $userIds = []; + + protected $namespaceIds = []; + + protected function makeData() { + $res = $this->getDB()->select( + 'watchlist', + '*', + $this->makePreFilterConds() + ); + + $distinctUserIds = []; + $distinctNamespaceIds = []; + foreach( $res as $row ) { + $distinctUserIds[$row->wl_user] = true; + $distinctNamespaceIds[$row->wl_namespace] = true; + $this->appendRowToData( $row ); + } + + $this->userIds = array_keys( $distinctUserIds ); + $this->addPrimaryUserFields(); + + $this->namespaceIds = array_keys( $distinctNamespaceIds ); + $this->addPrimaryTitleFields(); + } + + protected function makePreFilterConds() { + $conds = []; + /*if( $this->params->getFilter()->has( 'user_id' ) ) { + $aFilter = $this->params->getFilter()->get( 'user_id' ); + $conds['wl_user'] = $aFilter[Filter::VALUE]; + }*/ + if( $this->config->has( self::CONFIG_USER ) ) { + $user = $this->config->get( self::CONFIG_USER ); + $conds['wl_user'] = $user->getId(); + } + + return $conds; + + } + + public function getSchemaDefinition() { + return [ + 'user_id' => [ + self::FIELD_FILTERABLE => true, + self::FIELD_SORTABLE => true, + self::FIELD_TYPE => FieldType::INT + ], + 'user_display_name' => [ + self::FIELD_FILTERABLE => true, + self::FIELD_SORTABLE => true, + self::FIELD_TYPE => FieldType::STRING + ], + /** + * Creating a link is expensive and the result is not filterable by + * standard filters. Still they are important as hooks may modify + * their content (e.g. by providing data attributes or other) and + * they can contain additional information (e.g. redlink). + * Therefore links always get created _after_ filtering and paging! + */ + 'user_link' => [ + self::FIELD_FILTERABLE => false, + self::FIELD_SORTABLE => false, + self::FIELD_TYPE => FieldType::STRING + ], + 'page_id' => [ + self::FIELD_FILTERABLE => true, + self::FIELD_SORTABLE => true, + self::FIELD_TYPE => FieldType::INT + ], + 'page_prefixedtext' => [ + self::FIELD_FILTERABLE => true, + self::FIELD_SORTABLE => true, + self::FIELD_TYPE => FieldType::STRING + ], + 'page_link' => [ + self::FIELD_FILTERABLE => false, + self::FIELD_SORTABLE => false, + self::FIELD_TYPE => FieldType::STRING + ], + 'notificationtimestamp' => [ + self::FIELD_FILTERABLE => true, + self::FIELD_SORTABLE => true, + self::FIELD_TYPE => FieldType::DATE + ], + 'has_unread_changes' => [ + self::FIELD_FILTERABLE => true, + self::FIELD_SORTABLE => true, + self::FIELD_TYPE => FieldType::BOOLEAN + ], + ]; + } + + protected function appendRowToData( $row ) { + $title = \Title::makeTitle( $row->wl_namespace, $row->wl_title ); + + $this->data[] = (object) [ + 'user_id' => $row->wl_user, + 'user_display_name' => '', + 'user_link' => '', + 'page_id' => '', + 'page_prefixedtext' => $title->getPrefixedText(), //Not expensive as all required information available on instantiation + 'page_link' => '-', + 'notificationtimestamp' => $row->wl_notificationtimestamp, + 'has_unread_changes' => $row->wl_notificationtimestamp !== null + ]; + } + + protected function addPrimaryUserFields() { + $res = $this->getDB()->select( + 'user', + [ 'user_id', 'user_name', 'user_real_name' ] , + [ 'user_id' => $this->userIds ] + ); + + $userDisplayNames = []; + foreach( $res as $row ) { + $userDisplayNames[ $row->user_id ] = $row->user_real_name != null + ? $row->user_real_name + : $row->user_name; + } + + foreach( $this->data as &$dataSet ) { + $dataSet->user_display_name = $userDisplayNames[$dataSet->user_id]; + } + } + + protected function addPrimaryTitleFields() { + $res = $this->getDB()->select( + 'page', + [ 'page_id', 'page_title', 'page_namespace' ], + [ 'page_namespace' => $this->namespaceIds ] //TODO maybe also add a collection of "page_title"s to narrow result + ); + + $pageIds = []; + foreach( $res as $row ) { + $title = \Title::makeTitle( $row->page_namespace, $row->page_title ); + $pageIds[$title->getPrefixedText()] = $row->page_id; + } + + foreach( $this->data as &$dataSet ) { + $dataSet->page_id = $pageIds[$dataSet->page_prefixedtext]; + } + } + + protected function applySecondaryFields() { + //$this->data is already a very reduced set. It has been filtered, + //sorted and trimmed by the base class + foreach( $this->data as &$dataSet ) { + $user = \User::newFromId( $dataSet->user_id ); + $dataSet->user_link = \Linker::link( $user->getUserPage() ); + + $title = \Title::newFromText( $dataSet->page_prefixedtext ); + $dataSet->page_link = \Linker::link( $title ); + } + } + +} \ No newline at end of file diff --git a/tests/phpunit/DataSources/Params/Filter/BooleanTest.php b/tests/phpunit/DataSources/Params/Filter/BooleanTest.php new file mode 100644 index 0000000..1192b56 --- /dev/null +++ b/tests/phpunit/DataSources/Params/Filter/BooleanTest.php @@ -0,0 +1,61 @@ +<?php + +use BlueSpice\DataSources\Params\Filter; + +class BooleanTest extends PHPUnit_Framework_TestCase { + + /** + * @dataProvider provideAppliesToValues + * @param boolean $expecation + * @param mixed $fieldValue + * @param mixed $filterValue + * @param string $comparison + */ + public function testAppliesTo ( $expectation, $comparison, $fieldValue, $filterValue ) { + $filter = new Filter\Boolean([ + Filter\Boolean::KEY_FIELD => 'field_A', + Filter\Boolean::KEY_VALUE => $filterValue, + Filter\Boolean::KEY_COMPARISON => $comparison + ]); + + $dataSet = (object)[ + 'field_A' => $fieldValue + ]; + + if( $expectation ) { + $this->assertTrue( $filter->appliesTo( $dataSet ), 'Filter should apply' ); + } + else { + $this->assertFalse( $filter->appliesTo( $dataSet ), 'Filter should not apply' ); + } + } + + public function provideAppliesToValues() { + return [ + [ true, Filter\Boolean::COMPARISON_EQUALS, true, true ], + [ true, Filter\Boolean::COMPARISON_EQUALS, 1, true ], + [ true, Filter\Boolean::COMPARISON_EQUALS, '1', true ], + [ true, Filter\Boolean::COMPARISON_EQUALS, false, false ], + [ true, Filter\Boolean::COMPARISON_EQUALS, 0, false ], + [ true, Filter\Boolean::COMPARISON_EQUALS, '0', false ], + [ true, Filter\Boolean::COMPARISON_NOT_EQUALS, true, false ], + [ true, Filter\Boolean::COMPARISON_NOT_EQUALS, 1, false ], + [ true, Filter\Boolean::COMPARISON_NOT_EQUALS, '1', false ], + [ true, Filter\Boolean::COMPARISON_NOT_EQUALS, false, true ], + [ true, Filter\Boolean::COMPARISON_NOT_EQUALS, 0, true ], + [ true, Filter\Boolean::COMPARISON_NOT_EQUALS, '0', true ], + [ false, Filter\Boolean::COMPARISON_EQUALS, true, false ], + [ false, Filter\Boolean::COMPARISON_EQUALS, 1, false ], + [ false, Filter\Boolean::COMPARISON_EQUALS, '1', false ], + [ false, Filter\Boolean::COMPARISON_EQUALS, false, true ], + [ false, Filter\Boolean::COMPARISON_EQUALS, 0, true ], + [ false, Filter\Boolean::COMPARISON_EQUALS, '0', true ], + [ false, Filter\Boolean::COMPARISON_NOT_EQUALS, true, true ], + [ false, Filter\Boolean::COMPARISON_NOT_EQUALS, 1, true ], + [ false, Filter\Boolean::COMPARISON_NOT_EQUALS, '1', true ], + [ false, Filter\Boolean::COMPARISON_NOT_EQUALS, false, false ], + [ false, Filter\Boolean::COMPARISON_NOT_EQUALS, 0, false ], + [ false, Filter\Boolean::COMPARISON_NOT_EQUALS, '0', false ] + ]; + } +} diff --git a/tests/phpunit/DataSources/Params/Filter/DateTest.php b/tests/phpunit/DataSources/Params/Filter/DateTest.php new file mode 100644 index 0000000..8571379 --- /dev/null +++ b/tests/phpunit/DataSources/Params/Filter/DateTest.php @@ -0,0 +1,44 @@ +<?php + +use BlueSpice\DataSources\Params\Filter; + +class Date extends PHPUnit_Framework_TestCase { + + /** + * @dataProvider provideAppliesToValues + * @param boolean $expecation + * @param mixed $fieldValue + * @param mixed $filterValue + * @param string $comparison + */ + public function testAppliesTo ( $expectation, $comparison, $fieldValue, $filterValue ) { + $filter = new Filter\Date([ + Filter::KEY_FIELD => 'field_A', + Filter::KEY_VALUE => $filterValue, + Filter::KEY_COMPARISON => $comparison + ]); + + $dataSet = (object)[ + 'field_A' => $fieldValue + ]; + + if( $expectation ) { + $this->assertTrue( $filter->appliesTo( $dataSet ), 'Filter should apply' ); + } + else { + $this->assertFalse( $filter->appliesTo( $dataSet ), 'Filter should not apply' ); + } + } + + public function provideAppliesToValues() { + return [ + [ true, Filter::COMPARISON_EQUALS, '2017-07-01', '20170701000000' ], + [ true, Filter::COMPARISON_EQUALS, 0, '1970-01-01' ], + [ true, Filter::COMPARISON_EQUALS, '1970/01/02', '1970-01-02' ], + [ true, Filter\Range::COMPARISON_GREATER_THAN, '1970/01/02', 1 ], + [ true, Filter\Range::COMPARISON_LOWER_THAN, '1970/01/02', 'now' ], + [ true, Filter\Range::COMPARISON_LOWER_THAN, 'now - 1 week', 'now' ], + [ false, Filter\Range::COMPARISON_EQUALS, 'now - 1 week', 'now' ], + ]; + } +} diff --git a/tests/phpunit/DataSources/ParamsTest.php b/tests/phpunit/DataSources/ParamsTest.php new file mode 100644 index 0000000..d6b968f --- /dev/null +++ b/tests/phpunit/DataSources/ParamsTest.php @@ -0,0 +1,66 @@ +<?php + +namespace BlueSpice\Tests\DataSources; + +class ParamsTest extends \PHPUnit_Framework_TestCase { + public function testInitFromArray() { + $params = new \BlueSpice\DataSources\Params([ + 'query' => 'Some query', + 'limit' => 50, + 'start' => 100, + 'sort' => [ + [ 'property' => 'prop_a', 'direction' => 'asc' ], + [ 'property' => 'prop_b', 'direction' => 'desc' ] + ], + 'filter' => [ + [ + 'type' => 'string', + 'comparison' => 'ct', + 'value' => 'test', + 'field' => 'prop_a' + ], + [ + 'type' => 'numeric', + 'comparison' => 'gt', + 'value' => 99, + 'field' => 'prop_b' + ] + ] + ]); + + $this->assertInstanceOf( '\BlueSpice\DataSources\Params', $params ); + + //TODO: Split test + $this->assertEquals( 'Some query', $params->getQuery() ); + $this->assertEquals( 100, $params->getStart() ); + $this->assertEquals( 50, $params->getLimit() ); + + $sort = $params->getSort(); + $this->assertEquals( 2, count( $sort ) ); + $firstSort = $sort[0]; + $this->assertInstanceOf( + '\BlueSpice\DataSources\Params\Sort', $firstSort + ); + + $this->assertEquals( + \BlueSpice\DataSources\Params\Sort::ASCENDING, + $firstSort->getDirection() + ); + + $filter = $params->getFilter(); + $this->assertEquals( 2, count( $filter ) ); + + $firstFilter = $filter[0]; + $this->assertInstanceOf( + '\BlueSpice\DataSources\Params\Filter', $firstFilter + ); + + $this->assertEquals( + \BlueSpice\DataSources\Params\Filter\StringValue::COMPARISON_CONTAINS, + $firstFilter->getComparison() + ); + + $this->assertTrue( $filter->has( 'prop_a' ) ); + $this->assertFalse( $filter->has( 'prop_x' ) ); + } +} \ No newline at end of file diff --git a/tests/phpunit/DataSources/WatchlistTest.php b/tests/phpunit/DataSources/WatchlistTest.php new file mode 100644 index 0000000..e9f58aa --- /dev/null +++ b/tests/phpunit/DataSources/WatchlistTest.php @@ -0,0 +1,83 @@ +<?php + +namespace BlueSpice\Tests\DataSources; + +class WatchlistTest extends \MediaWikiTestCase { + + protected $tablesUsed = [ 'watchlist' ]; + + public function addDBData() { + + $dummyDbEntries = [ + [ 1, 0, 'Test A', null ], + [ 1, 1, 'Test A', null ], + [ 1, 0, 'Test B', null ], + [ 1, 1, 'Test B', '19700101000000' ], + [ 1, 0, 'Test C', null ], + [ 1, 0, 'Test D', null ], + [ 2, 0, 'Test A', null ], + [ 2, 1, 'Test A', null ], + [ 2, 0, 'Test B', null ] + ]; + + $dbw = wfGetDB( DB_MASTER ); + foreach( $dummyDbEntries as $dummyDbEntry ) { + $dbw->insert( 'watchlist', [ + 'wl_user' => $dummyDbEntry[0], + 'wl_namespace' => $dummyDbEntry[1], + 'wl_title' => $dummyDbEntry[2], + 'wl_notificationtimestamp' => $dummyDbEntry[3], + ] ); + } + } + + public function testCanConstruct() { + + $context = $this->getMockBuilder( '\RequestContext' ) + ->disableOriginalConstructor() + ->getMock(); + + $config = $this->getMockBuilder( '\HashConfig' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->assertInstanceOf( + '\BlueSpice\DataSources\Watchlist', + new \BlueSpice\DataSources\Watchlist( + $context, + $config + ) + ); + } + + public function testUnfilteredFetching() { + $watchlist = $this->makeInstance(); + $watchlist->fetch(); + + $datasets = $watchlist->getResult(); + $total = $watchlist->getTotal(); + + $this->assertEquals( 6 , count( $datasets ), 'Count of datasets in result is wrong' ); + $this->assertEquals( 6 , $total, 'Count of total datasets is wrong' ); + } + + /** + * + * @return \BlueSpice\DataSources\Watchlist + */ + protected function makeInstance() { + $context = $this->getMockBuilder( '\RequestContext' ) + ->disableOriginalConstructor() + ->getMock(); + + $config = new \HashConfig( [ + \BlueSpice\DataSources\Watchlist::CONFIG_USER => \User::newFromId( 1 ) + ] ); + + return new \BlueSpice\DataSources\Watchlist( + $context, + $config + ); + } + +} \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/371459 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ie4ee268a2c42c9066a4c8667f01dce0b2db6799c Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/BlueSpiceFoundation Gerrit-Branch: master Gerrit-Owner: Robert Vogel <vo...@hallowelt.biz> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits