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

Reply via email to