jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/371459 )
Change subject: New Data interfaces
......................................................................
New Data interfaces
Change-Id: Ie4ee268a2c42c9066a4c8667f01dce0b2db6799c
---
M extension.json
M i18n/api/en.json
M i18n/api/qqq.json
A src/Api/WatchlistStore.php
M src/Data/DatabaseReader.php
M src/Data/DatabaseWriter.php
A src/Data/FieldType.php
A src/Data/Filter.php
A src/Data/Filter/Boolean.php
A src/Data/Filter/Date.php
A src/Data/Filter/ListValue.php
A src/Data/Filter/Numeric.php
A src/Data/Filter/Range.php
A src/Data/Filter/StringValue.php
A src/Data/Filter/TemplateTitle.php
A src/Data/Filter/Title.php
A src/Data/FilterFactory.php
A src/Data/FilterFinder.php
A src/Data/Filterer.php
A src/Data/IPrimaryDataProvider.php
M src/Data/IReader.php
A src/Data/IRecord.php
A src/Data/ISecondaryDataProvider.php
A src/Data/ITrimmer.php
A src/Data/LimitOffsetTrimmer.php
A src/Data/Reader.php
A src/Data/ReaderParams.php
A src/Data/Record.php
A src/Data/RecordConverter.php
M src/Data/ResultSet.php
A src/Data/Schema.php
A src/Data/SecondaryDataProvider.php
A src/Data/Sort.php
A src/Data/Sorter.php
A src/Data/Watchlist/PrimaryDataProvider.php
A src/Data/Watchlist/Reader.php
A src/Data/Watchlist/Record.php
A src/Data/Watchlist/Schema.php
A src/Data/Watchlist/SecondaryDataProvider.php
A src/Data/Watchlist/Store.php
A src/StoreApiBase.php
A tests/phpunit/Data/Filter/BooleanTest.php
A tests/phpunit/Data/Filter/DateTest.php
A tests/phpunit/Data/Filter/ListValueTest.php
A tests/phpunit/Data/Filter/NumericTest.php
A tests/phpunit/Data/Filter/StringValueTest.php
A tests/phpunit/Data/Filter/TemplateTitleTest.php
A tests/phpunit/Data/Filter/TitleTest.php
A tests/phpunit/Data/FiltererTest.php
A tests/phpunit/Data/LimitOffsetTrimmerTest.php
A tests/phpunit/Data/ReaderParamsTest.php
A tests/phpunit/Data/SorterTest.php
A tests/phpunit/Data/Watchlist/ReaderTest.php
53 files changed, 2,545 insertions(+), 10 deletions(-)
Approvals:
Pwirth: Looks good to me, approved
jenkins-bot: Verified
diff --git a/extension.json b/extension.json
index be5502c..4e4d878 100644
--- a/extension.json
+++ b/extension.json
@@ -27,7 +27,8 @@
"bs-titlequery-store": "BSApiTitleQueryStore",
"bs-ping-tasks": "BSApiPingTasks",
"bs-upload-license-store": "BSApiUploadLicenseStore",
- "bs-category-treestore": "BSApiCategoryTreeStore"
+ "bs-category-treestore": "BSApiCategoryTreeStore",
+ "bs-watchlist-store": "BlueSpice\\Api\\WatchlistStore"
},
"ResourceModules": {
"ext.bluespice": {
diff --git a/i18n/api/en.json b/i18n/api/en.json
index 49e5b44..b8991a3 100644
--- a/i18n/api/en.json
+++ b/i18n/api/en.json
@@ -62,5 +62,6 @@
"apihelp-bs-task-param-examples": "Show examples of task parameters",
"bs-api-task-taskData-page-id": "The ID of an existing wiki page",
"bs-api-task-taskData-page-title": "A valid title of a wiki page",
- "bs-api-task-wikipagetasks-taskData-categories": "Array of strings.
Values need to be valid category names."
+ "bs-api-task-wikipagetasks-taskData-categories": "Array of strings.
Values need to be valid category names.",
+ "apihelp-bs-watchlist-store-summary": "Lists all watchlist entries.
Allows sorting, filtering and pagination. Implements store parameters."
}
diff --git a/i18n/api/qqq.json b/i18n/api/qqq.json
index c1e88df..d14444a 100644
--- a/i18n/api/qqq.json
+++ b/i18n/api/qqq.json
@@ -64,5 +64,6 @@
"apihelp-bs-task-param-examples":
"{{doc-apihelp-param|bs-task|examples}}",
"bs-api-task-taskData-page-id": "Description of
<code>taskData.page_id</code> field",
"bs-api-task-taskData-page-title": "Description of
<code>taskData.page_title</code> field",
- "bs-api-task-wikipagetasks-taskData-categories": "Description of
<code>taskData.categories</code> field in WikiPageTasks API"
+ "bs-api-task-wikipagetasks-taskData-categories": "Description of
<code>taskData.categories</code> field in WikiPageTasks API",
+ "apihelp-bs-watchlist-store-summary":
"{{doc-apihelp-description|bs-watchlist-store}}"
}
diff --git a/src/Api/WatchlistStore.php b/src/Api/WatchlistStore.php
new file mode 100644
index 0000000..fb883e9
--- /dev/null
+++ b/src/Api/WatchlistStore.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace BlueSpice\Api;
+
+class WatchlistStore extends \BlueSpice\StoreApiBase {
+
+ protected function makeDataStore() {
+ return new \BlueSpice\Data\Watchlist\Store( $this->getContext()
);
+ }
+}
\ No newline at end of file
diff --git a/src/Data/DatabaseReader.php b/src/Data/DatabaseReader.php
index 9c066f5..856bb4e 100644
--- a/src/Data/DatabaseReader.php
+++ b/src/Data/DatabaseReader.php
@@ -2,19 +2,22 @@
namespace BlueSpice\Data;
-abstract class DatabaseReader implements IReader {
+abstract class DatabaseReader extends Reader {
/**
*
- * @var \DatabaseBase
+ * @var \Wikimedia\Rdbms\IDatabase
*/
protected $db = null;
/**
*
- * @param \LoadBalancer $loadBalancer
+ * @param \Wikimedia\Rdbms\LoadBalancer $loadBalancer
+ * @param \IContextSource $context
+ * @param \Config $config
*/
- public function __construct( $loadBalancer ) {
+ public function __construct( $loadBalancer, \IContextSource $context =
null, \Config $config = null ) {
+ parent::__construct( $context, $config );
$this->db = $loadBalancer->getConnection( DB_REPLICA );
}
}
diff --git a/src/Data/DatabaseWriter.php b/src/Data/DatabaseWriter.php
index 5e4fb98..979b2cf 100644
--- a/src/Data/DatabaseWriter.php
+++ b/src/Data/DatabaseWriter.php
@@ -6,7 +6,7 @@
/**
*
- * @var \DatabaseBase
+ * @var \Wikimedia\Rdbms\IDatabase
*/
protected $db = null;
diff --git a/src/Data/FieldType.php b/src/Data/FieldType.php
new file mode 100644
index 0000000..fb32551
--- /dev/null
+++ b/src/Data/FieldType.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace BlueSpice\Data;
+
+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/Data/Filter.php b/src/Data/Filter.php
new file mode 100644
index 0000000..ea87ceb
--- /dev/null
+++ b/src/Data/Filter.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace BlueSpice\Data;
+
+abstract 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 \BlueSpice\Data\Record $dataSet
+ * @return boolean
+ */
+ public function matches( $dataSet ) {
+ return $this->doesMatch( $dataSet );
+ }
+
+
+ /**
+ *
+ * @param stdClass[]|array[] $filters
+ * @return Filter[]
+ */
+ public static function newCollectionFromArray( $filters ) {
+ $filterObjects = [];
+ foreach( $filters as $filter ) {
+ if( is_object( $filter ) ) {
+ $filter = (array) $filter;
+ }
+ $filterObjects[] = FilterFactory::newFromArray( $filter
);
+ }
+
+ return $filterObjects;
+ }
+
+ protected abstract function doesMatch( $dataSet );
+}
diff --git a/src/Data/Filter/Boolean.php b/src/Data/Filter/Boolean.php
new file mode 100644
index 0000000..22fa99f
--- /dev/null
+++ b/src/Data/Filter/Boolean.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+use BlueSpice\Data\Filter;
+
+class Boolean extends Filter {
+
+ /**
+ * Performs filtering based on given filter of type bool on a dataset
+ *
+ * @param \BlueSpice\Data\Record $dataSet
+ * @return boolean
+ */
+ protected function doesMatch( $dataSet ) {
+ $fieldValue = $dataSet->get( $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/Data/Filter/Date.php b/src/Data/Filter/Date.php
new file mode 100644
index 0000000..e45dc37
--- /dev/null
+++ b/src/Data/Filter/Date.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace BlueSpice\Data\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 \BlueSpice\Data\Record $dataSet
+ * @return boolean
+ */
+ protected function doesMatch( $dataSet ) {
+ $filterValue = strtotime( $this->getValue() ); // Format:
"m/d/Y"
+ $fieldValue = strtotime( $dataSet->get( $this->getField() ) );
// 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/Data/Filter/ListValue.php b/src/Data/Filter/ListValue.php
new file mode 100644
index 0000000..00bc7ca
--- /dev/null
+++ b/src/Data/Filter/ListValue.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+use BlueSpice\Data\Filter;
+
+class ListValue extends Filter {
+
+ /**
+ * Performs list filtering based on given filter of type array on a
dataset
+ * @param \BlueSpice\Data\Record $dataSet
+ * @return boolean
+ */
+ protected function doesMatch( $dataSet ) {
+ if( !is_array( $this->getValue() ) ) {
+ return true; //TODO: Warning
+ }
+ $fieldValues = $dataSet->get( $this->getField() );
+ if( empty( $fieldValues ) ) {
+ return false;
+ }
+
+ $intersection = array_intersect( $fieldValues,
$this->getValue() );
+ if( empty( $intersection ) ) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/Data/Filter/Numeric.php b/src/Data/Filter/Numeric.php
new file mode 100644
index 0000000..0d84024
--- /dev/null
+++ b/src/Data/Filter/Numeric.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+class Numeric extends Range {
+ /**
+ * Performs numeric filtering based on given filter of type integer on a
+ * dataset
+ *
+ * @param \BlueSpice\Data\Record $dataSet
+ * @return boolean
+ */
+ protected function doesMatch( $dataSet ) {
+ if( !is_numeric( $this->getValue() ) ) {
+ return true; //TODO: Warning
+ }
+ $fieldValue = (int) $dataSet->get( $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/Data/Filter/Range.php b/src/Data/Filter/Range.php
new file mode 100644
index 0000000..b85b724
--- /dev/null
+++ b/src/Data/Filter/Range.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+use BlueSpice\Data\Filter;
+
+abstract class Range extends Filter {
+ const COMPARISON_LOWER_THAN = 'lt';
+ const COMPARISON_GREATER_THAN = 'gt';
+}
\ No newline at end of file
diff --git a/src/Data/Filter/StringValue.php b/src/Data/Filter/StringValue.php
new file mode 100644
index 0000000..d569ab6
--- /dev/null
+++ b/src/Data/Filter/StringValue.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+use BlueSpice\Data\Filter;
+
+/**
+ * Class name "String" is reserved
+ */
+class StringValue extends 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 \BlueSpice\Data\Record $dataSet
+ */
+ protected function doesMatch( $dataSet ) {
+ $fieldValue = $dataSet->get( $this->getField() );
+
+ return \BsStringHelper::filter( $this->getComparison(),
$fieldValue, $this->getValue() );
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Filter/TemplateTitle.php
b/src/Data/Filter/TemplateTitle.php
new file mode 100644
index 0000000..0ef445b
--- /dev/null
+++ b/src/Data/Filter/TemplateTitle.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+class TemplateTitle extends Title {
+ protected function getDefaultTitleNamespace() {
+ return NS_TEMPLATE;
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Filter/Title.php b/src/Data/Filter/Title.php
new file mode 100644
index 0000000..b4d0187
--- /dev/null
+++ b/src/Data/Filter/Title.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace BlueSpice\Data\Filter;
+
+class Title extends Range {
+
+ /**
+ * Performs string filtering based on given filter of type Title on a
+ * dataset
+ *
+ * @param \BlueSpice\Data\Record $dataSet
+ * @return boolean
+ */
+ protected function doesMatch( $dataSet ) {
+ if( !is_string( $this->getValue() ) ) {
+ return true; //TODO: Warning
+ }
+ $fieldValue = \Title::newFromText(
+ $dataSet->get($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/Data/FilterFactory.php b/src/Data/FilterFactory.php
new file mode 100644
index 0000000..40544d4
--- /dev/null
+++ b/src/Data/FilterFactory.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class FilterFactory {
+ /**
+ *
+ * @var array
+ */
+ public static $typeMap = [
+ 'string' => 'BlueSpice\Data\Filter\StringValue',
+ 'date'=> 'BlueSpice\Data\Filter\Date',
+ #'datetime'=> 'BlueSpice\Data\Filter\DateTime',
+ 'boolean'=> 'BlueSpice\Data\Filter\Boolean',
+ 'numeric' => 'BlueSpice\Data\Filter\Numeric',
+ 'title' => 'BlueSpice\Data\Filter\Title',
+ 'templatetitle' => 'BlueSpice\Data\Filter\TemplateTitle',
+ 'list' => 'BlueSpice\Data\Filter\ListValue'
+ ];
+
+ /**
+ *
+ * @param array $filter
+ * @return \BlueSpice\Data\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/Data/FilterFinder.php b/src/Data/FilterFinder.php
new file mode 100644
index 0000000..f585982
--- /dev/null
+++ b/src/Data/FilterFinder.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class FilterFinder {
+
+ /**
+ *
+ * @var Filter[]
+ */
+ protected $filters = [];
+
+ /**
+ *
+ * @param Filter[] $filters
+ */
+ public function __construct( $filters ) {
+ $this->filters = $filters;
+ }
+
+ /**
+ *
+ * @param string $fieldName
+ * @return Filter|null
+ */
+ public function findByField( $fieldName ) {
+ foreach( $this->filters as $filter ) {
+ if( $filter->getField() === $fieldName ) {
+ return $filter;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Filterer.php b/src/Data/Filterer.php
new file mode 100644
index 0000000..0b2b354
--- /dev/null
+++ b/src/Data/Filterer.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class Filterer {
+
+ /**
+ *
+ * @var Filter[]
+ */
+ protected $filters = null;
+
+ /**
+ *
+ * @param Filter $filters
+ */
+ public function __construct( $filters ) {
+ $this->filters = $filters;
+ }
+
+ /**
+ *
+ * @param \BlueSpice\Data\Record[] $data
+ * @return \BlueSpice\Data\Record[]
+ */
+ public function filter( $data ) {
+ $filteredData = array_filter( $data, function ( $aDataSet ) {
+ return $this->matchFilter( $aDataSet );
+ }
+ );
+
+ return array_values( $filteredData );
+ }
+
+ /**
+ *
+ * @param object $dataSet
+ * @return boolean
+ */
+ protected function matchFilter( $dataSet ) {
+ foreach( $this->filters as $filter ) {
+ //If just one of these filters does not apply, the
dataset needs
+ //to be removed
+ if( !$filter->matches( $dataSet ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/Data/IPrimaryDataProvider.php
b/src/Data/IPrimaryDataProvider.php
new file mode 100644
index 0000000..5871aab
--- /dev/null
+++ b/src/Data/IPrimaryDataProvider.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace BlueSpice\Data;
+
+interface IPrimaryDataProvider {
+
+ /**
+ *
+ * @param string $query Special simple filter that aims at one specific
+ * field that the DataProvider needs to define.
+ * @param Filter[] $preFilters Complete set of filters that will also be
+ * applied later during the process by the "Filterer" step. Having it
here
+ * allows us to prefilter and tweak performance
+ * @return \BlueSpice\Data\Record[]
+ */
+ public function makeData( $query = '', $preFilters = [] );
+}
\ No newline at end of file
diff --git a/src/Data/IReader.php b/src/Data/IReader.php
index f321a26..2e8914f 100644
--- a/src/Data/IReader.php
+++ b/src/Data/IReader.php
@@ -10,4 +10,10 @@
* @return ResultSet
*/
public function read( $params );
+
+ /**
+ * @return Schema Column definition compatible to
+ * https://docs.sencha.com/extjs/4.2.1/#!/api/Ext.grid.Panel-cfg-columns
+ */
+ public function getSchema();
}
\ No newline at end of file
diff --git a/src/Data/IRecord.php b/src/Data/IRecord.php
new file mode 100644
index 0000000..fa2b839
--- /dev/null
+++ b/src/Data/IRecord.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace BlueSpice\Data;
+
+interface IRecord {
+
+ /**
+ *
+ * @param string $fieldName
+ * @param mixed $default
+ */
+ public function get( $fieldName, $default = null );
+
+ /**
+ *
+ * @param string $fieldName
+ * @param mixed $value
+ */
+ public function set( $fieldName, $value );
+
+ /**
+ * @return \stdClass
+ */
+ public function getData();
+}
diff --git a/src/Data/ISecondaryDataProvider.php
b/src/Data/ISecondaryDataProvider.php
new file mode 100644
index 0000000..11a75c7
--- /dev/null
+++ b/src/Data/ISecondaryDataProvider.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace BlueSpice\Data;
+
+interface ISecondaryDataProvider {
+
+ /**
+ *
+ * @param \BlueSpice\Data\Record[] $dataSets
+ * @return \BlueSpice\Data\Record[]
+ */
+ public function extend( $dataSets );
+}
\ No newline at end of file
diff --git a/src/Data/ITrimmer.php b/src/Data/ITrimmer.php
new file mode 100644
index 0000000..cd1e60f
--- /dev/null
+++ b/src/Data/ITrimmer.php
@@ -0,0 +1,13 @@
+<?php
+
+ namespace BlueSpice\Data;
+
+ interface ITrimmer {
+
+ /**
+ *
+ * @param \BlueSpice\Data\Record[] $dataSets
+ * @return \BlueSpice\Data\Record[]
+ */
+ public function trim( $dataSets );
+}
diff --git a/src/Data/LimitOffsetTrimmer.php b/src/Data/LimitOffsetTrimmer.php
new file mode 100644
index 0000000..0b3e1e2
--- /dev/null
+++ b/src/Data/LimitOffsetTrimmer.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class LimitOffsetTrimmer implements ITrimmer {
+
+ /**
+ *
+ * @var int
+ */
+ protected $limit = 25;
+
+ /**
+ *
+ * @var int
+ */
+ protected $offset = 0;
+
+ public function __construct( $limit = 25, $offset = 0 ) {
+ $this->limit = $limit;
+ $this->offset = $offset;
+ }
+
+ /**
+ *
+ * @param \BlueSpice\Data\Record[] $dataSets
+ * @return \BlueSpice\Data\Record[]
+ */
+ public function trim( $dataSets ) {
+ $total = count( $dataSets );
+ $end = $this->limit + $this->offset;
+
+ if( $end > $total || $end === 0 ) {
+ $end = $total;
+ }
+
+ $trimmedData = [];
+ for( $i = $this->offset; $i < $end; $i++ ) {
+ $trimmedData[] = $dataSets[$i];
+ }
+
+ return array_values( $trimmedData );
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Reader.php b/src/Data/Reader.php
new file mode 100644
index 0000000..05621d2
--- /dev/null
+++ b/src/Data/Reader.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace BlueSpice\Data;
+
+use \BlueSpice\Data\IReader;
+
+abstract class Reader implements IReader {
+
+ /**
+ *
+ * @var \IContextSource
+ */
+ private $context = null;
+
+ /**
+ *
+ * @var \Config
+ */
+ protected $config = null;
+
+ /**
+ *
+ * @param \IContextSource $context
+ * @param \Config $config
+ */
+ public function __construct( \IContextSource $context = null, \Config
$config = 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();
+ }
+ }
+
+ /**
+ *
+ * @return \User
+ */
+ protected function getUser() {
+ return $this->context->getUser();
+ }
+
+ /**
+ *
+ * @return \Title
+ */
+ protected function getTitle() {
+ return $this->context->getTitle();
+ }
+
+ /**
+ *
+ * @param ReaderParams $params
+ * @return ResultSet
+ */
+ public function read( $params ) {
+ $primaryDataProvider = $this->makePrimaryDataProvider( $params
);
+ $dataSets = $primaryDataProvider->makeData(
$params->getQuery(), $params->getFilter() );
+
+ $filterer = $this->makeFilterer( $params );
+ $dataSets = $filterer->filter( $dataSets );
+ $total = count( $dataSets );
+
+ $sorter = $this->makeSorter( $params );
+ $dataSets = $sorter->sort(
+ $dataSets,
+ $this->getSchema()->getUnsortableFields()
+ );
+
+ $trimmer = $this->makeTrimmer( $params );
+ $dataSets = $trimmer->trim( $dataSets );
+
+ $secondaryDataProvider = $this->makeSecondaryDataProvider();
+ $dataSets = $secondaryDataProvider->extend( $dataSets );
+
+ $resultSet = new ResultSet( $dataSets, $total );
+ return $resultSet;
+ }
+
+ /**
+ * @param ReaderParams $params
+ * @return IPrimaryDataProvider
+ */
+ protected abstract function makePrimaryDataProvider( $params );
+
+ /**
+ * @param ReaderParams $params
+ * @return Filterer
+ */
+ protected function makeFilterer( $params ) {
+ return new Filterer( $params->getFilter() );
+ }
+
+ /**
+ * @param ReaderParams $params
+ * @return Sorter
+ */
+ protected function makeSorter( $params ) {
+ return new Sorter( $params->getSort() );
+ }
+
+ /**
+ * @param ReaderParams $params
+ * @return ITrimmer
+ */
+ protected function makeTrimmer( $params ) {
+ return new LimitOffsetTrimmer(
+ $params->getLimit(),
+ $params->getStart()
+ );
+ }
+
+ /**
+ * @return ISecondaryDataProvider
+ */
+ protected abstract function makeSecondaryDataProvider();
+}
\ No newline at end of file
diff --git a/src/Data/ReaderParams.php b/src/Data/ReaderParams.php
new file mode 100644
index 0000000..5265c78
--- /dev/null
+++ b/src/Data/ReaderParams.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace BlueSpice\Data;
+
+use BlueSpice\Data\Sort;
+use BlueSpice\Data\Filter;
+
+class ReaderParams {
+
+ /**
+ * For pre filtering
+ * @var string
+ */
+ protected $query = '';
+
+ /**
+ * For paging
+ * @var int
+ */
+ protected $start = 0;
+
+ /**
+ * For paging
+ * @var int
+ */
+ protected $limit = 25;
+
+ /**
+ *
+ * @var Sort[]
+ */
+ protected $sort = [];
+
+ /**
+ *
+ * @var Filter[]
+ */
+ protected $filter = [];
+
+ /**
+ *
+ * @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 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 Filter[]
+ */
+ public function getFilter() {
+ return $this->filter;
+ }
+
+ protected function setSort( $params ) {
+ if( !isset( $params['sort'] ) || !is_array( $params['sort'] )
) {
+ return;
+ }
+
+ $this->sort = Sort::newCollectionFromArray( $params['sort'] );
+ }
+
+ protected function setFilter( $params ) {
+ if( !isset( $params['filter'] ) || !is_array(
$params['filter'] ) ) {
+ return;
+ }
+ $this->filter = Filter::newCollectionFromArray(
$params['filter'] );
+ }
+
+}
\ No newline at end of file
diff --git a/src/Data/Record.php b/src/Data/Record.php
new file mode 100644
index 0000000..bc3bc06
--- /dev/null
+++ b/src/Data/Record.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class Record implements IRecord, \JsonSerializable {
+
+ /**
+ *
+ * @var \stdClass
+ */
+ protected $dataSet = null;
+
+ /**
+ *
+ * @param \stdClass $dataSet
+ */
+ public function __construct( $dataSet ) {
+ $this->dataSet = $dataSet;
+ }
+
+ /**
+ *
+ * @param string $fieldName
+ * @param mixed $default
+ * @return mixed
+ */
+ public function get( $fieldName, $default = null ) {
+ if( isset( $this->dataSet->{$fieldName} ) ) {
+ return $this->dataSet->{$fieldName};
+ }
+ return $default;
+ }
+
+ /**
+ *
+ * @param string $fieldName
+ * @param mixed $value
+ */
+ public function set( $fieldName, $value ) {
+ $this->dataSet->{$fieldName} = $value;
+ }
+
+ /**
+ *
+ * @return array
+ */
+ public function jsonSerialize() {
+ return (array)$this->dataSet;
+ }
+
+ /**
+ *
+ * @return \stdClass
+ */
+ public function getData() {
+ return $this->dataSet;
+ }
+
+}
+
diff --git a/src/Data/RecordConverter.php b/src/Data/RecordConverter.php
new file mode 100644
index 0000000..6b8cb15
--- /dev/null
+++ b/src/Data/RecordConverter.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class RecordConverter {
+
+ /**
+ *
+ * @var Record[]
+ */
+ protected $records = [];
+
+ /**
+ *
+ * @param Record[] $records
+ */
+ public function __construct( $records ) {
+ $this->records = $records;
+ }
+
+ public function convertToRawData() {
+ $rawData = [];
+ foreach( $this->records as $record ) {
+ $rawData[] = $record->getData();
+ }
+ return $rawData;
+ }
+}
diff --git a/src/Data/ResultSet.php b/src/Data/ResultSet.php
index c7e371e..f4fbf60 100644
--- a/src/Data/ResultSet.php
+++ b/src/Data/ResultSet.php
@@ -6,7 +6,7 @@
/**
*
- * @var \stdClass[]
+ * @var \BlueSpice\Data\Record[]
*/
protected $records = [];
@@ -18,7 +18,7 @@
/**
*
- * @param \stdClass[] $records
+ * @param \BlueSpice\Data\Record[] $records
* @param int $total
*/
public function __construct( $records, $total ) {
@@ -26,10 +26,18 @@
$this->total = $total;
}
+ /**
+ *
+ * @return \BlueSpice\Data\Record[]
+ */
public function getRecords() {
return $this->records;
}
+ /**
+ *
+ * @return int
+ */
public function getTotal() {
return $this->total;
}
diff --git a/src/Data/Schema.php b/src/Data/Schema.php
new file mode 100644
index 0000000..a59990f
--- /dev/null
+++ b/src/Data/Schema.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class Schema extends \ArrayObject {
+ const FILTERABLE = 'filterable';
+ const SORTABLE = 'sortable';
+ const TYPE = 'type';
+
+ /**
+ * @return string[]
+ */
+ public function getUnsortableFields() {
+ $unsortableFields = [];
+ foreach( $this as $fieldName => $fieldDef ) {
+ if( $this->fieldIsSortable( $fieldDef ) ) {
+ continue;
+ }
+
+ $unsortableFields[] = $fieldName;
+ }
+
+ return $unsortableFields;
+ }
+
+ protected function fieldIsSortable( $fieldDef ) {
+ if( !isset( $fieldDef[self::SORTABLE] ) ) {
+ return false;
+ }
+
+ return $fieldDef[self::SORTABLE];
+ }
+
+}
\ No newline at end of file
diff --git a/src/Data/SecondaryDataProvider.php
b/src/Data/SecondaryDataProvider.php
new file mode 100644
index 0000000..32df422
--- /dev/null
+++ b/src/Data/SecondaryDataProvider.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace BlueSpice\Data;
+
+use \BlueSpice\Data\ISecondaryDataProvider;
+
+abstract class SecondaryDataProvider implements ISecondaryDataProvider {
+ public function extend( $dataSets ) {
+ foreach( $dataSets as &$dataSet ) {
+ $this->doExtend( $dataSet );
+ }
+
+ return $dataSets;
+ }
+
+ protected abstract function doExtend( &$dataSet );
+
+}
\ No newline at end of file
diff --git a/src/Data/Sort.php b/src/Data/Sort.php
new file mode 100644
index 0000000..d392e11
--- /dev/null
+++ b/src/Data/Sort.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace BlueSpice\Data;
+
+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 One of Sort::ASCENDING or Sort::DESCENDING
+ */
+ public function getDirection() {
+ return $this->direction;
+ }
+
+ /**
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getProperty().' '.$this->getDirection();
+ }
+
+ /**
+ *
+ * @param stdClass[]|array[] $sorts
+ * @return Sort[]
+ */
+ public static function newCollectionFromArray( $sorts ) {
+ $sortObjects = [];
+ foreach( $sorts as $sort ) {
+ if( is_array( $sort ) ) {
+ $sort = (object) $sort;
+ }
+
+ $sortObjects[] = new Sort(
+ $sort->property,
+ isset( $sort->direction ) ? $sort->direction :
null
+ );
+ }
+ return $sortObjects;
+ }
+}
diff --git a/src/Data/Sorter.php b/src/Data/Sorter.php
new file mode 100644
index 0000000..b2fec4c
--- /dev/null
+++ b/src/Data/Sorter.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace BlueSpice\Data;
+
+class Sorter {
+
+ /**
+ *
+ * @var Sort[];
+ */
+ protected $sorts = null;
+
+ /**
+ *
+ * @param Sort[] $sorts
+ */
+ public function __construct( $sorts ) {
+ $this->sorts = $sorts;
+ }
+
+ /**
+ *
+ * @param \BlueSpice\Data\Record[] $dataSets
+ * @param array $unsortableProps
+ * @return \BlueSpice\Data\Record[]
+ */
+ public function sort( $dataSets, $unsortableProps = [] ) {
+ $numberOfSorts = count( $this->sorts );
+ $sortParams = [];
+ foreach ( $this->sorts as $sort ) {
+ $property = $sort->getProperty();
+ if( in_array( $property, $unsortableProps ) ) {
+ continue;
+ }
+
+ $valuesOf{$property} = array();
+ foreach( $dataSets as $idx => $dataSet ) {
+ $valuesOf{$property}[$idx] =
+ $this->getSortValue( $dataSet,
$property );
+ }
+
+ $sortParams[] = $valuesOf{$property};
+ $sortParams[] = $this->getSortDirection( $sort );
+ $sortParams[] = $this->getSortFlags( $property );
+ }
+
+ if( !empty( $sortParams ) ) {
+ $sortParams[] = &$dataSets;
+ call_user_func_array( 'array_multisort', $sortParams );
+ }
+
+ $dataSets = array_values( $dataSets );
+ return $dataSets;
+ }
+
+ /**
+ * Returns the flags for PHP 'array_multisort' function
+ * May be overridden by subclasses to provide different sort flags
+ * depending on the property
+ * @param string $property
+ * @return int see http://php.net/manual/en/array.constants.php for
details
+ */
+ protected function getSortFlags( $property ) {
+ 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 \BlueSpice\Data\Record $dataSet
+ * @param string $property
+ * @return string
+ */
+ protected function getSortValue( $dataSet, $property ) {
+ $value = $dataSet->get( $property );
+ if( is_array( $value ) ) {
+ return $this->getSortValueFromList( $value, $dataSet,
$property );
+ }
+
+ return $value;
+ }
+
+ /**
+ * 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 $values
+ * @param \BlueSpice\Data\Record $dataSet
+ * @param string $property
+ * @return string
+ */
+ protected function getSortValueFromList( $values, $dataSet, $property )
{
+ $combinedValue = '';
+ foreach( $values as $value ) {
+ // PHP 7 workaround. In PHP 7 cast throws no exception.
It's a fatal error so no way to catch
+ if( $this->canBeCastedToString( $value ) )
+ {
+ $combinedValue .= (string)$value;
+ } else {
+ $combinedValue .= FormatJson::encode( $value );
+ }
+ }
+ return $combinedValue;
+ }
+
+
+ /**
+ * Checks if a array or object ist castable to string.
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ protected function canBeCastedToString( $value ) {
+ if ( !is_array( $value ) &&
+ ( !is_object( $value ) && settype( $value, 'string' )
!== false ) ||
+ ( is_object( $value ) && method_exists( $value,
'__toString' ) ) ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ *
+ * @param Sort $sort
+ * @return int Constant value of SORT_ASC or SORT_DESC
+ */
+ protected function getSortDirection( $sort ) {
+ if( $sort->getDirection() === Sort::ASCENDING ) {
+ return SORT_ASC;
+ }
+ return SORT_DESC;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Data/Watchlist/PrimaryDataProvider.php
b/src/Data/Watchlist/PrimaryDataProvider.php
new file mode 100644
index 0000000..1de86d5
--- /dev/null
+++ b/src/Data/Watchlist/PrimaryDataProvider.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace BlueSpice\Data\Watchlist;
+
+use \BlueSpice\Data\IPrimaryDataProvider;
+use \BlueSpice\Data\Filter;
+use \BlueSpice\Data\FilterFinder;
+
+class PrimaryDataProvider implements IPrimaryDataProvider {
+
+ /**
+ *
+ * @var \BlueSpice\Data\Record
+ */
+ protected $data = [];
+
+ /**
+ *
+ * @var int[]
+ */
+ protected $userIds = [];
+
+ /**
+ *
+ * @var int[]
+ */
+ protected $namespaceIds = [];
+
+ /**
+ *
+ * @var \Wikimedia\Rdbms\IDatabase
+ */
+ protected $db = null;
+
+ /**
+ *
+ * @param \Wikimedia\Rdbms\IDatabase $db
+ */
+ public function __construct( $db ) {
+ $this->db = $db;
+ }
+
+ /**
+ *
+ * @param string $query
+ * @param type $preFilters
+ */
+ public function makeData( $query = '', $preFilters = [] ) {
+ $res = $this->db->select(
+ 'watchlist',
+ '*',
+ $this->makePreFilterConds( $preFilters )
+ );
+
+ $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->addUserFields();
+
+ $this->namespaceIds = array_keys( $distinctNamespaceIds );
+ $this->addTitleFields();
+
+ return $this->data;
+ }
+
+ /**
+ *
+ * @param Filter[] $preFilters
+ * @return array
+ */
+ protected function makePreFilterConds( $preFilters ) {
+ $conds = [];
+ $filterFinder = new FilterFinder( $preFilters );
+ $userIdFilter = $filterFinder->findByField( 'user_id' );
+
+ if( $userIdFilter instanceof Filter ) {
+ $conds['wl_user'] = $userIdFilter->getValue();
+ }
+
+ return $conds;
+ }
+
+ protected function appendRowToData( $row ) {
+ $title = \Title::makeTitle( $row->wl_namespace, $row->wl_title
);
+
+ $this->data[] = new Record( (object) [
+ Record::USER_ID => $row->wl_user,
+ Record::USER_DISPLAY_NAME => '',
+ Record::USER_LINK => '',
+ Record::PAGE_ID => '',
+ Record::PAGE_PREFIXED_TEXT =>
$title->getPrefixedText(), //Not expensive, as all required information
available on instantiation
+ Record::PAGE_LINK => '-',
+ Record::NOTIFICATIONTIMESTAMP =>
$row->wl_notificationtimestamp,
+ Record::HAS_UNREAD_CHANGES =>
$row->wl_notificationtimestamp !== null,
+ Record::IS_TALK_PAGE => $title->isTalkPage()
+ ] );
+ }
+
+ protected function addUserFields() {
+ $res = $this->db->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 ) {
+ $userId = $dataSet->get( Record::USER_ID );
+ $dataSet->set( Record::USER_DISPLAY_NAME,
$userDisplayNames[$userId] );
+ }
+ }
+
+ protected function addTitleFields() {
+ $res = $this->db->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 ) {
+ $pagePrefixedText = $dataSet->get(
Record::PAGE_PREFIXED_TEXT );
+ $pageId = 0;
+
+ //It is possible to watch non existing pages
+ if( isset( $pageIds[$pagePrefixedText] ) ) {
+ $pageId = $pageIds[$pagePrefixedText];
+ }
+ $dataSet->set( Record::PAGE_ID, $pageId );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Watchlist/Reader.php b/src/Data/Watchlist/Reader.php
new file mode 100644
index 0000000..68ab2da
--- /dev/null
+++ b/src/Data/Watchlist/Reader.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace BlueSpice\Data\Watchlist;
+
+use \BlueSpice\Data\DatabaseReader;
+
+class Reader extends DatabaseReader {
+
+ protected function makePrimaryDataProvider( $params ) {
+ return new PrimaryDataProvider( $this->db );
+ }
+
+ protected function makeSecondaryDataProvider() {
+ return new SecondaryDataProvider(
+
\MediaWiki\MediaWikiServices::getInstance()->getLinkRenderer()
+ );
+ }
+
+ public function getSchema() {
+ return new Schema();
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Watchlist/Record.php b/src/Data/Watchlist/Record.php
new file mode 100644
index 0000000..b7f9c5c
--- /dev/null
+++ b/src/Data/Watchlist/Record.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace BlueSpice\Data\Watchlist;
+
+class Record extends \BlueSpice\Data\Record {
+ const USER_ID = 'user_id';
+ const USER_DISPLAY_NAME = 'user_display_name';
+ const USER_LINK = 'user_link';
+ const PAGE_ID = 'page_id';
+ const PAGE_PREFIXED_TEXT = 'page_prefixedtext';
+ const PAGE_LINK = 'page_link';
+ const NOTIFICATIONTIMESTAMP = 'notificationtimestamp';
+ const HAS_UNREAD_CHANGES = 'has_unread_changes';
+ const IS_TALK_PAGE = 'is_talk_page';
+}
\ No newline at end of file
diff --git a/src/Data/Watchlist/Schema.php b/src/Data/Watchlist/Schema.php
new file mode 100644
index 0000000..e83951f
--- /dev/null
+++ b/src/Data/Watchlist/Schema.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace BlueSpice\Data\Watchlist;
+
+use BlueSpice\Data\FieldType;
+
+class Schema extends \BlueSpice\Data\Schema {
+ public function __construct() {
+ parent::__construct( [
+ Record::USER_ID => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::TYPE => FieldType::INT
+ ],
+ Record::USER_DISPLAY_NAME => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::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!
+ */
+ Record::USER_LINK => [
+ self::FILTERABLE => false,
+ self::SORTABLE => false,
+ self::TYPE => FieldType::STRING
+ ],
+ Record::PAGE_ID => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::TYPE => FieldType::INT
+ ],
+ Record::PAGE_PREFIXED_TEXT => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::TYPE => FieldType::STRING
+ ],
+ Record::PAGE_LINK => [
+ self::FILTERABLE => false,
+ self::SORTABLE => false,
+ self::TYPE => FieldType::STRING
+ ],
+ Record::NOTIFICATIONTIMESTAMP => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::TYPE => FieldType::DATE
+ ],
+ Record::HAS_UNREAD_CHANGES => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::TYPE => FieldType::BOOLEAN
+ ],
+ Record::IS_TALK_PAGE => [
+ self::FILTERABLE => true,
+ self::SORTABLE => true,
+ self::TYPE => FieldType::BOOLEAN
+ ],
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Watchlist/SecondaryDataProvider.php
b/src/Data/Watchlist/SecondaryDataProvider.php
new file mode 100644
index 0000000..0fc91a5
--- /dev/null
+++ b/src/Data/Watchlist/SecondaryDataProvider.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace BlueSpice\Data\Watchlist;
+
+class SecondaryDataProvider extends \BlueSpice\Data\SecondaryDataProvider {
+
+ /**
+ *
+ * @var \MediaWiki\Linker\LinkRenderer
+ */
+ protected $linkrenderer = null;
+
+ /**
+ *
+ * @param \MediaWiki\Linker\LinkRenderer $linkrenderer
+ */
+ public function __construct( $linkrenderer ) {
+ $this->linkrenderer = $linkrenderer;
+ }
+
+ protected function doExtend( &$dataSet ){
+ $user = \User::newFromId( $dataSet->get( Record::USER_ID ) );
+ $dataSet->set(
+ Record::USER_LINK,
+ $this->linkrenderer->makeLink( $user->getUserPage() )
+ );
+
+ $title = \Title::newFromText( $dataSet->get(
Record::PAGE_PREFIXED_TEXT ) );
+ $dataSet->set(
+ Record::PAGE_LINK,
+ $this->linkrenderer->makeLink( $title )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Data/Watchlist/Store.php b/src/Data/Watchlist/Store.php
new file mode 100644
index 0000000..bb72191
--- /dev/null
+++ b/src/Data/Watchlist/Store.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace BlueSpice\Data\Watchlist;
+
+class Store implements \BlueSpice\Data\IStore {
+
+ /**
+ *
+ * @var \IContextSource
+ */
+ protected $context = null;
+
+ /**
+ *
+ * @param \IContextSource $context
+ */
+ public function __construct( $context ) {
+ $this->context = $context;
+ }
+
+ public function getReader() {
+ return new Reader(
\MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer(),
$this->context );
+ }
+
+ public function getWriter() {
+ throw new Exception( 'This store does not support writing!' );
+ }
+}
\ No newline at end of file
diff --git a/src/StoreApiBase.php b/src/StoreApiBase.php
new file mode 100644
index 0000000..04ac94c
--- /dev/null
+++ b/src/StoreApiBase.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace BlueSpice;
+
+/**
+ * Example request parameters of an ExtJS store
+
+ _dc:1430126252980
+ filter:[
+ {
+ "type":"string",
+ "comparison":"ct",
+ "value":"some text ...",
+ "field":"someField"
+ }
+ ]
+ group:[
+ {
+ "property":"someOtherField",
+ "direction":"ASC"
+ }
+ ]
+ sort:[
+ {
+ "property":"someOtherField",
+ "direction":"ASC"
+ }
+ ]
+ page:1
+ start:0
+ limit:25
+ */
+abstract class StoreApiBase extends \BSApiBase {
+
+ /**
+ * May be overwritten by subclass
+ * @var string
+ */
+ protected $root = 'results';
+
+ /**
+ * May be overwritten by subclass
+ * @var string
+ */
+ protected $totalProperty = 'total';
+
+ /**
+ * May be overwritten by subclass
+ * @var string
+ */
+ protected $metaData = 'metadata';
+
+ /**
+ * Main method called by \ApiMain
+ */
+ public function execute() {
+ $dataStore = $this->makeDataStore();
+ $result = $dataStore->getReader()->read(
$this->getReaderParams() );
+ $schema = $dataStore->getReader()->getSchema();
+ $this->returnData( $result, $schema );
+ }
+
+ /**
+ * Creates a proper output format based on the class's properties
+ * @param \BlueSpice\Data\ResultSet $resultSet Holds the records as an
+ * array of plain old data objects
+ * @param \BlueSpice\Data\Schema $schema An array of meta data items
+ */
+ protected function returnData( $resultSet, $schema = null ) {
+ \Hooks::run( 'BSApiStoreBaseBeforeReturnData', array( $this,
&$resultSet, &$schema ) );
+ $apiResult = $this->getResult();
+
+ //Unfortunately \ApiResult does not like \JsonSerializable[],
so we
+ //need to provide a \stdClass[] or array[]
+ $converter = new Data\RecordConverter( $resultSet->getRecords()
);
+ $records = $converter->convertToRawData();
+
+ $apiResult->setIndexedTagName( $records, $this->root );
+ $apiResult->addValue( null, $this->root, $records );
+ $apiResult->addValue( null, $this->totalProperty,
$resultSet->getTotal() );
+ if( $schema !== null ) {
+ $apiResult->addValue( null, $this->metaData, $schema );
+ }
+ }
+
+ /**
+ * Called by ApiMain
+ * @return array
+ */
+ public function getAllowedParams() {
+ return array(
+ 'sort' => array(
+ \ApiBase::PARAM_TYPE => 'string',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_DFLT => '[]',
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-sort',
+ ),
+ 'group' => array(
+ \ApiBase::PARAM_TYPE => 'string',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_DFLT => '[]',
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-group',
+ ),
+ 'filter' => array(
+ \ApiBase::PARAM_TYPE => 'string',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_DFLT => '[]',
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-filter',
+ ),
+ 'page' => array(
+ \ApiBase::PARAM_TYPE => 'integer',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_DFLT => 0,
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-page',
+ ),
+ 'limit' => array(
+ \ApiBase::PARAM_TYPE => 'integer',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_DFLT => 25,
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-limit',
+ ),
+ 'start' => array(
+ \ApiBase::PARAM_TYPE => 'integer',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_DFLT => 0,
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-start',
+ ),
+ 'callback' => array(
+ \ApiBase::PARAM_TYPE => 'string',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-callback',
+ ),
+ 'query' => array(
+ \ApiBase::PARAM_TYPE => 'string',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-query',
+ ),
+ '_dc' => array(
+ \ApiBase::PARAM_TYPE => 'integer',
+ \ApiBase::PARAM_REQUIRED => false,
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-dc',
+ ),
+ 'format' => array(
+ \ApiBase::PARAM_DFLT => 'json',
+ \ApiBase::PARAM_TYPE => array( 'json', 'jsonfm'
),
+ \ApiBase::PARAM_HELP_MSG =>
'apihelp-bs-store-param-format',
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'sort' => 'JSON string with sorting info; deserializes
to "array of objects" that hold field name and direction for each sorting
option',
+ 'group' => 'JSON string with grouping info;
deserializes to "array of objects" that hold field name and direction for each
grouping option',
+ 'filter' => 'JSON string with filter info; deserializes
to "array of objects" that hold field name, filter type, and filter value for
each filtering option',
+ 'page' => 'Allows server side calculation of
start/limit',
+ 'limit' => 'Number of results to return',
+ 'start' => 'The offset to start the result list from',
+ 'query' => 'Similar to "filter", but the provided value
serves as a filter only for the "value" field of an ExtJS component',
+ 'callback' => 'A method name in the client code that
should be called in the response (JSONP)',
+ '_dc' => '"Disable cache" flag',
+ 'format' => 'The format of the output (only JSON or
formatted JSON)'
+ );
+ }
+
+ protected function getParameterFromSettings( $paramName,
$paramSettings, $parseLimit ) {
+ $value = parent::getParameterFromSettings( $paramName,
$paramSettings, $parseLimit );
+ //Unfortunately there is no way to register custom types for
parameters
+ if( in_array( $paramName, [ 'sort', 'group', 'filter' ] ) ) {
+ $value = \FormatJson::decode( $value );
+ if( empty( $value ) ) {
+ return [];
+ }
+ }
+ return $value;
+ }
+
+ public function getParameter( $paramName, $parseLimit = true ) {
+ //Make this public, so hook handler could get the params
+ return parent::getParameter( $paramName, $parseLimit );
+ }
+
+ /**
+ * @return \BlueSpice\Data\IStore
+ */
+ protected abstract function makeDataStore();
+
+ /**
+ *
+ * @return \BlueSpice\Data\ReaderParams
+ */
+ protected function getReaderParams() {
+ return new \BlueSpice\Data\ReaderParams([
+ 'query' => $this->getParameter( 'query', null ),
+ 'start' => $this->getParameter( 'start', null ),
+ 'limit' => $this->getParameter( 'limit', null ),
+ 'filter' => $this->getParameter( 'filter', null ),
+ 'sort' => $this->getParameter( 'sort', null ),
+ ]);
+ }
+
+}
\ No newline at end of file
diff --git a/tests/phpunit/Data/Filter/BooleanTest.php
b/tests/phpunit/Data/Filter/BooleanTest.php
new file mode 100644
index 0000000..59f07e0
--- /dev/null
+++ b/tests/phpunit/Data/Filter/BooleanTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use BlueSpice\Data\Filter;
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class BooleanTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new Filter\Boolean( [
+ 'field' => 'field1',
+ 'comparison' => 'eq',
+ 'value' => true
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => true,
+ 'field2' => false
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new Filter\Boolean( [
+ 'field' => 'field1',
+ 'comparison' => 'eq',
+ 'value' => false
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => true,
+ 'field2' => false
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+
+
+ /**
+ * @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 = new Record( (object) [
+ 'field_A' => $fieldValue
+ ] );
+
+ if( $expectation ) {
+ $this->assertTrue( $filter->matches( $dataSet ),
'Filter should apply' );
+ }
+ else {
+ $this->assertFalse( $filter->matches( $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/Data/Filter/DateTest.php
b/tests/phpunit/Data/Filter/DateTest.php
new file mode 100644
index 0000000..d54830a
--- /dev/null
+++ b/tests/phpunit/Data/Filter/DateTest.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use \BlueSpice\Data\Filter;
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class DateTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new Filter\Date( [
+ 'field' => 'field1',
+ 'comparison' => 'gt',
+ 'value' => '2017/01/01'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => '20170101000001',
+ 'field2' => false
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new \BlueSpice\Data\Filter\Date( [
+ 'field' => 'field1',
+ 'comparison' => 'gt',
+ 'value' => '2017/01/02'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => '20170101000001',
+ 'field2' => false
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * @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 = new Record( (object)[
+ 'field_A' => $fieldValue
+ ] );
+
+ if( $expectation ) {
+ $this->assertTrue( $filter->matches( $dataSet ),
'Filter should apply' );
+ }
+ else {
+ $this->assertFalse( $filter->matches( $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/Data/Filter/ListValueTest.php
b/tests/phpunit/Data/Filter/ListValueTest.php
new file mode 100644
index 0000000..feacbfc
--- /dev/null
+++ b/tests/phpunit/Data/Filter/ListValueTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class ListValueTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new \BlueSpice\Data\Filter\ListValue( [
+ 'field' => 'field1',
+ 'comparison' => 'ct',
+ 'value' => [ 'Hello' ]
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => [ 'Hello', 'World' ],
+ 'field2' => false
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new \BlueSpice\Data\Filter\ListValue( [
+ 'field' => 'field1',
+ 'comparison' => 'ct',
+ 'value' => [ 'Hello' ]
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => [ 'Hallo', 'Welt' ],
+ 'field2' => false
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+}
diff --git a/tests/phpunit/Data/Filter/NumericTest.php
b/tests/phpunit/Data/Filter/NumericTest.php
new file mode 100644
index 0000000..d9d137d
--- /dev/null
+++ b/tests/phpunit/Data/Filter/NumericTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class NumericTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new \BlueSpice\Data\Filter\Numeric( [
+ 'field' => 'field1',
+ 'comparison' => 'gt',
+ 'value' => 5
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 7,
+ 'field2' => 3
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new \BlueSpice\Data\Filter\Numeric( [
+ 'field' => 'field1',
+ 'comparison' => 'gt',
+ 'value' => 5
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 3,
+ 'field2' => 7
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+}
diff --git a/tests/phpunit/Data/Filter/StringValueTest.php
b/tests/phpunit/Data/Filter/StringValueTest.php
new file mode 100644
index 0000000..4456358
--- /dev/null
+++ b/tests/phpunit/Data/Filter/StringValueTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class StringValueTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new \BlueSpice\Data\Filter\StringValue( [
+ 'field' => 'field1',
+ 'comparison' => 'ct',
+ 'value' => 'ello'
+ ] );
+
+ $result = $filter->matches( new Record( (object) [
+ 'field1' => 'Hello World',
+ 'field2' => 'Hallo Welt'
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new \BlueSpice\Data\Filter\StringValue( [
+ 'field' => 'field1',
+ 'comparison' => 'ct',
+ 'value' => 'allo'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 'Hello World',
+ 'field2' => 'Hallo Welt'
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+}
diff --git a/tests/phpunit/Data/Filter/TemplateTitleTest.php
b/tests/phpunit/Data/Filter/TemplateTitleTest.php
new file mode 100644
index 0000000..789fc28
--- /dev/null
+++ b/tests/phpunit/Data/Filter/TemplateTitleTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class TemplateTitleTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new \BlueSpice\Data\Filter\TemplateTitle( [
+ 'field' => 'field1',
+ 'comparison' => 'eq',
+ 'value' => 'Help'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 'Template:Help',
+ 'field2' => 'User:WikiSysop'
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new \BlueSpice\Data\Filter\TemplateTitle( [
+ 'field' => 'field1',
+ 'comparison' => 'eq',
+ 'value' => 'Help'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 'Vorlage:Hilfe',
+ 'field2' => 'User:WikiSysop'
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+}
diff --git a/tests/phpunit/Data/Filter/TitleTest.php
b/tests/phpunit/Data/Filter/TitleTest.php
new file mode 100644
index 0000000..412a703
--- /dev/null
+++ b/tests/phpunit/Data/Filter/TitleTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace BlueSpice\Tests\Data\Filter;
+
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class TitleTest extends \PHPUnit_Framework_TestCase {
+ public function testPositive() {
+ $filter = new \BlueSpice\Data\Filter\Title( [
+ 'field' => 'field2',
+ 'comparison' => 'eq',
+ 'value' => 'User:WikiSysop'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 'Template:Help',
+ 'field2' => 'User:WikiSysop'
+ ] ) );
+
+ $this->assertTrue( $result );
+ }
+
+ public function testNegative() {
+ $filter = new \BlueSpice\Data\Filter\Title( [
+ 'field' => 'field1',
+ 'comparison' => 'eq',
+ 'value' => 'Hilfe'
+ ] );
+
+ $result = $filter->matches( new Record( (object)[
+ 'field1' => 'Vorlage:Hilfe',
+ 'field2' => 'User:WikiSysop'
+ ] ) );
+
+ $this->assertFalse( $result );
+ }
+}
diff --git a/tests/phpunit/Data/FiltererTest.php
b/tests/phpunit/Data/FiltererTest.php
new file mode 100644
index 0000000..f7bf7ad
--- /dev/null
+++ b/tests/phpunit/Data/FiltererTest.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace BlueSpice\Tests\Data;
+
+use BlueSpice\Data\Filterer;
+use BlueSpice\Data\Filter;
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class FiltererTest extends \PHPUnit_Framework_TestCase {
+ protected $testDataSets = [
+ [
+ 'field1' => 1,
+ 'field2' => '20170101000000',
+ 'field3' => '1 item',
+ 'field4' => [ 1, 2, 3 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ [
+ 'field1' => 3,
+ 'field2' => '20170101000002',
+ 'field3' => '10 items',
+ 'field4' => [ 2, 3, 4 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ [
+ 'field1' => 4,
+ 'field2' => '20170101000001',
+ 'field3' => 'an item',
+ 'field4' => [ 4, 5, 6 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ [
+ 'field1' => 2,
+ 'field2' => '20170101000003',
+ 'field3' => 'An eloquent item',
+ 'field4' => [ 3, 1, 2 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ]
+ ];
+
+ /**
+ *
+ * @param array $filter
+ * @param array $expectedCount
+ * @dataProvider provideFilterData
+ */
+ public function testFilter( $filters, $expectedCount ) {
+ $filterer = new Filterer( Filter::newCollectionFromArray(
$filters ) );
+ $dataSets = $this->makeDataSets();
+ $filteredDataSets = $filterer->filter( $dataSets );
+
+ $this->assertEquals( $expectedCount, count( $filteredDataSets )
);
+ }
+
+ public function provideFilterData() {
+ return [
+ 'numeric' => [
+ [ [
+ 'type' => 'numeric',
+ 'field' => 'field1',
+ 'value' => 2,
+ 'comparison' => 'gt'
+ ] ],
+ 2
+ ],
+ 'list' => [
+ [ [
+ 'type' => 'list',
+ 'field' => 'field4',
+ 'value' => [ 2 ],
+ 'comparison' => 'ct'
+ ] ],
+ 3
+ ],
+ 'string and datetime' => [
+ [ [
+ 'type' => 'string',
+ 'field' => 'field3',
+ 'value' => 'item',
+ 'comparison' => 'ew'
+ ],
+ [
+ 'type' => 'date',
+ 'field' => 'field2',
+ 'value' => '20170101000000',
+ 'comparison' => 'gt'
+ ] ],
+ 2
+ ],
+ 'string' => [
+ [ [
+ 'type' => 'string',
+ 'field' => 'field3',
+ 'value' => 'an',
+ 'comparison' => 'ct'
+ ] ],
+ 2
+ ],
+ ];
+ }
+
+ protected function makeDataSets() {
+ $dataSets = [];
+ foreach( $this->testDataSets as $dataSet ) {
+ $dataSets[] = new Record( (object) $dataSet );
+ }
+
+ return $dataSets;
+ }
+
+}
diff --git a/tests/phpunit/Data/LimitOffsetTrimmerTest.php
b/tests/phpunit/Data/LimitOffsetTrimmerTest.php
new file mode 100644
index 0000000..f0ba04f
--- /dev/null
+++ b/tests/phpunit/Data/LimitOffsetTrimmerTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace BlueSpice\Tests\Data;
+
+use BlueSpice\Data\LimitOffsetTrimmer;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class LimitOffsetTrimmerTest extends \PHPUnit_Framework_TestCase {
+
+ protected $testDataSets = [
+ //Page 1
+ 'Zero',
+ 'One',
+ 'Two',
+ 'Three',
+ 'Four',
+
+ //Page 2
+ 'Five',
+ 'Six',
+ 'Seven',
+ 'Eight',
+ 'Nine',
+
+ //Page 3
+ 'Ten',
+ 'Eleven',
+ 'Twelve',
+ 'Thirteen'
+ ];
+
+ public function testNormalPage() {
+ $trimmer = new LimitOffsetTrimmer( 5, 5 );
+ $trimmedData = $trimmer->trim( $this->testDataSets );
+
+ $this->assertEquals( count( $trimmedData ), 5 );
+ $this->assertEquals( $trimmedData[0], 'Five' );
+ }
+
+ public function testLastPage() {
+ $trimmer = new LimitOffsetTrimmer( 5, 10 );
+ $trimmedData = $trimmer->trim( $this->testDataSets );
+
+ $this->assertEquals( count( $trimmedData ), 4 );
+ $this->assertEquals( $trimmedData[0], 'Ten' );
+ }
+}
\ No newline at end of file
diff --git a/tests/phpunit/Data/ReaderParamsTest.php
b/tests/phpunit/Data/ReaderParamsTest.php
new file mode 100644
index 0000000..2af1858
--- /dev/null
+++ b/tests/phpunit/Data/ReaderParamsTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace BlueSpice\Tests\Data;
+
+use \BlueSpice\Data\ReaderParams;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class ReaderParamsTest extends \PHPUnit_Framework_TestCase {
+ public function testInitFromArray() {
+ $params = new ReaderParams([
+ '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\Data\ReaderParams',
$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\Data\Sort', $firstSort
+ );
+
+ $this->assertEquals(
+ \BlueSpice\Data\Sort::ASCENDING,
+ $firstSort->getDirection()
+ );
+
+ $filter = $params->getFilter();
+ $this->assertEquals( 2, count( $filter ) );
+
+ $firstFilter = $filter[0];
+ $this->assertInstanceOf(
+ '\BlueSpice\Data\Filter', $firstFilter
+ );
+
+ $this->assertEquals(
+ \BlueSpice\Data\Filter\StringValue::COMPARISON_CONTAINS,
+ $firstFilter->getComparison()
+ );
+
+ $filedNames = [];
+ foreach( $filter as $filterObject ) {
+ $filedNames[] = $filterObject->getField();
+ }
+
+ $this->assertTrue( in_array( 'prop_a', $filedNames ) );
+ }
+}
\ No newline at end of file
diff --git a/tests/phpunit/Data/SorterTest.php
b/tests/phpunit/Data/SorterTest.php
new file mode 100644
index 0000000..59c0868
--- /dev/null
+++ b/tests/phpunit/Data/SorterTest.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace BlueSpice\Tests\Data;
+
+use BlueSpice\Data\Sorter;
+use BlueSpice\Data\Sort;
+use BlueSpice\Data\Record;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ */
+class SorterTest extends \PHPUnit_Framework_TestCase {
+ protected $testDataSets = [
+ [
+ 'field1' => 1,
+ 'field2' => '20170101000000',
+ 'field3' => '1 item',
+ 'field4' => [ 1, 2, 3 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ [
+ 'field1' => 3,
+ 'field2' => '20170101000002',
+ 'field3' => '10 items',
+ 'field4' => [ 2, 3, 4 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ [
+ 'field1' => 4,
+ 'field2' => '20170101000001',
+ 'field3' => 'an item',
+ 'field4' => [ 4, 5, 6 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ [
+ 'field1' => 2,
+ 'field2' => '20170101000003',
+ 'field3' => 'An eloquent item',
+ 'field4' => [ 3, 1, 2 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+
+ //Identically to filed1=4 but with different timestamp to allow
testing
+ //of multible sorters
+ [
+ 'field1' => 5,
+ 'field2' => '20170101000002',
+ 'field3' => 'an item',
+ 'field4' => [ 4, 5, 6 ],
+ 'field5' => [
+ [ 'a' => 1 ],
+ [ 'a' => 2 ],
+ [ 'a' => 3 ]
+ ]
+ ],
+ ];
+
+ /**
+ *
+ * @param array $sort
+ * @param array $expectedSorting
+ * @dataProvider provideSortData
+ */
+ public function testSort( $sort, $expectedSorting ) {
+ $sorter = new Sorter( Sort::newCollectionFromArray( $sort ) );
+ $dataSets = $this->makeDataSets();
+ $sortedDataSets = $sorter->sort( $dataSets );
+
+ foreach( $sortedDataSets as $index => $dataSet ) {
+ $this->assertEquals( $expectedSorting[$index],
$dataSet->get( 'field1' ) );
+ }
+ }
+
+ public function provideSortData() {
+ return [
+ 'numeric-asc' => [
+ [ [ 'property' => 'field1', 'direction' =>
'ASC' ] ],
+ [ 1, 2, 3, 4, 5 ]
+ ],
+ 'numeric-desc' => [
+ [ [ 'property' => 'field1', 'direction' =>
'DESC' ] ],
+ [ 5, 4, 3, 2, 1 ]
+ ],
+ 'string-asc' => [
+ [ [ 'property' => 'field3', 'direction' =>
'ASC' ] ],
+ [ 1, 3, 2, 4, 5 ]
+ ],
+ 'datetime-asc' => [
+ [ [ 'property' => 'field2', 'direction' =>
'ASC' ] ],
+ [ 1, 4, 3, 2, 5 ]
+ ],
+ 'datetime-asc' => [
+ [ [ 'property' => 'field2', 'direction' =>
'ASC' ] ],
+ [ 1, 4, 3, 5, 2 ]
+ ]
+ ];
+ }
+
+ protected function makeDataSets() {
+ $dataSets = [];
+ foreach( $this->testDataSets as $dataSet ) {
+ $dataSets[] = new Record( (object)$dataSet );
+ }
+
+ return $dataSets;
+ }
+
+}
diff --git a/tests/phpunit/Data/Watchlist/ReaderTest.php
b/tests/phpunit/Data/Watchlist/ReaderTest.php
new file mode 100644
index 0000000..222d112
--- /dev/null
+++ b/tests/phpunit/Data/Watchlist/ReaderTest.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace BlueSpice\Tests\DataSources;
+
+/**
+ * @group BlueSpice
+ * @group BlueSpiceFoundation
+ * @group Database
+ * @group heavy
+ */
+class WatchlistTest extends \MediaWikiTestCase {
+
+ protected $tablesUsed = [ 'watchlist', 'user' ];
+
+ public function addDBData() {
+
+ $this->insertPage( 'Test A' );
+ $this->insertPage( 'Talk:Test A' );
+ $this->insertPage( 'Test B' );
+ $this->insertPage( 'Talk:Test B' );
+ $this->insertPage( 'Test C' );
+ $this->insertPage( 'Test D' );
+
+ $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],
+ ] );
+ }
+
+ $user = \User::newFromName( 'UTWatchlist' );
+ if ( $user->getId() == 0 ) {
+ $user->addToDatabase();
+ \TestUser::setPasswordForUser( $user, 'UTWatchlist' );
+
+ $user->saveSettings();
+ }
+ }
+
+ public function testCanConstruct() {
+ $loadBalancer = $this->getMockBuilder(
'\Wikimedia\Rdbms\LoadBalancer' )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->assertInstanceOf(
+ '\BlueSpice\Data\Watchlist\Reader',
+ new \BlueSpice\Data\Watchlist\Reader(
+ $loadBalancer
+ )
+ );
+ }
+
+ public function testUnfilteredFetching() {
+ $watchlist = $this->makeInstance();
+ $resultSet = $watchlist->read( new
\BlueSpice\Data\ReaderParams() );
+
+ $records = $resultSet->getRecords();
+ $total = $resultSet->getTotal();
+
+ $this->assertEquals( 9 , count( $records ), 'Count of datasets
in result is wrong' );
+ $this->assertEquals( 9 , $total, 'Count of total datasets is
wrong' );
+ }
+
+ /**
+ *
+ * @return \BlueSpice\Data\Watchlist\Reader
+ */
+ protected function makeInstance() {
+ return new \BlueSpice\Data\Watchlist\Reader(
+
\MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()
+ );
+ }
+}
\ 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: merged
Gerrit-Change-Id: Ie4ee268a2c42c9066a4c8667f01dce0b2db6799c
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/BlueSpiceFoundation
Gerrit-Branch: master
Gerrit-Owner: Robert Vogel <[email protected]>
Gerrit-Reviewer: Ljonka <[email protected]>
Gerrit-Reviewer: Mglaser <[email protected]>
Gerrit-Reviewer: Pwirth <[email protected]>
Gerrit-Reviewer: Raimond Spekking <[email protected]>
Gerrit-Reviewer: Robert Vogel <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits