mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Allow ranges in exclusion contexts
This commit is contained in:
parent
70443a5264
commit
0dc82f64d5
5 changed files with 142 additions and 117 deletions
|
@ -6,8 +6,6 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Context;
|
namespace JKingWeb\Arsse\Context;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
|
||||||
|
|
||||||
class Context extends ExclusionContext {
|
class Context extends ExclusionContext {
|
||||||
/** @var ExclusionContext */
|
/** @var ExclusionContext */
|
||||||
public $not;
|
public $not;
|
||||||
|
@ -18,14 +16,6 @@ class Context extends ExclusionContext {
|
||||||
public $starred;
|
public $starred;
|
||||||
public $labelled;
|
public $labelled;
|
||||||
public $annotated;
|
public $annotated;
|
||||||
public $oldestArticle;
|
|
||||||
public $latestArticle;
|
|
||||||
public $oldestEdition;
|
|
||||||
public $latestEdition;
|
|
||||||
public $modifiedSince;
|
|
||||||
public $notModifiedSince;
|
|
||||||
public $markedSince;
|
|
||||||
public $notMarkedSince;
|
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->not = new ExclusionContext($this);
|
$this->not = new ExclusionContext($this);
|
||||||
|
@ -67,40 +57,4 @@ class Context extends ExclusionContext {
|
||||||
public function annotated(bool $spec = null) {
|
public function annotated(bool $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function latestArticle(int $spec = null) {
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function oldestArticle(int $spec = null) {
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function latestEdition(int $spec = null) {
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function oldestEdition(int $spec = null) {
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function modifiedSince($spec = null) {
|
|
||||||
$spec = Date::normalize($spec);
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notModifiedSince($spec = null) {
|
|
||||||
$spec = Date::normalize($spec);
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function markedSince($spec = null) {
|
|
||||||
$spec = Date::normalize($spec);
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notMarkedSince($spec = null) {
|
|
||||||
$spec = Date::normalize($spec);
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Context;
|
namespace JKingWeb\Arsse\Context;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||||
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
|
||||||
class ExclusionContext {
|
class ExclusionContext {
|
||||||
public $folder;
|
public $folder;
|
||||||
|
@ -22,6 +23,14 @@ class ExclusionContext {
|
||||||
public $searchTerms;
|
public $searchTerms;
|
||||||
public $titleTerms;
|
public $titleTerms;
|
||||||
public $authorTerms;
|
public $authorTerms;
|
||||||
|
public $oldestArticle;
|
||||||
|
public $latestArticle;
|
||||||
|
public $oldestEdition;
|
||||||
|
public $latestEdition;
|
||||||
|
public $modifiedSince;
|
||||||
|
public $notModifiedSince;
|
||||||
|
public $markedSince;
|
||||||
|
public $notMarkedSince;
|
||||||
|
|
||||||
protected $props = [];
|
protected $props = [];
|
||||||
protected $parent;
|
protected $parent;
|
||||||
|
@ -152,4 +161,40 @@ class ExclusionContext {
|
||||||
}
|
}
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function latestArticle(int $spec = null) {
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function oldestArticle(int $spec = null) {
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latestEdition(int $spec = null) {
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function oldestEdition(int $spec = null) {
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function modifiedSince($spec = null) {
|
||||||
|
$spec = Date::normalize($spec);
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notModifiedSince($spec = null) {
|
||||||
|
$spec = Date::normalize($spec);
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markedSince($spec = null) {
|
||||||
|
$spec = Date::normalize($spec);
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notMarkedSince($spec = null) {
|
||||||
|
$spec = Date::normalize($spec);
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1179,32 +1179,34 @@ class Database {
|
||||||
// if there are no output columns requested we're getting a count and should not group, but otherwise we should
|
// if there are no output columns requested we're getting a count and should not group, but otherwise we should
|
||||||
$q->setGroup("arsse_articles.id", "arsse_marks.note", "arsse_enclosures.url", "arsse_enclosures.type", "arsse_subscriptions.title", "arsse_feeds.title", "arsse_subscriptions.id", "arsse_marks.modified", "arsse_label_members.modified", "arsse_marks.read", "arsse_marks.starred", "latest_editions.edition");
|
$q->setGroup("arsse_articles.id", "arsse_marks.note", "arsse_enclosures.url", "arsse_enclosures.type", "arsse_subscriptions.title", "arsse_feeds.title", "arsse_subscriptions.id", "arsse_marks.modified", "arsse_label_members.modified", "arsse_marks.read", "arsse_marks.starred", "latest_editions.edition");
|
||||||
}
|
}
|
||||||
$excContext = new ExclusionContext;
|
|
||||||
// handle the simple context options
|
// handle the simple context options
|
||||||
$options = [
|
$options = [
|
||||||
// each context array consists of a column identifier (see $colDefs above), a comparison operator, a data type, and an upper bound if the value is an array
|
// each context array consists of a column identifier (see $colDefs above), a comparison operator, a data type, an option to pair with for BETWEEN evaluation, and an upper bound if the value is an array
|
||||||
"edition" => ["edition", "=", "int", 1],
|
"edition" => ["edition", "=", "int", "", 1],
|
||||||
"editions" => ["edition", "in", "int", self::LIMIT_ARTICLES],
|
"editions" => ["edition", "in", "int", "", self::LIMIT_ARTICLES],
|
||||||
"article" => ["id", "=", "int", 1],
|
"article" => ["id", "=", "int", "", 1],
|
||||||
"articles" => ["id", "in", "int", self::LIMIT_ARTICLES],
|
"articles" => ["id", "in", "int", "", self::LIMIT_ARTICLES],
|
||||||
"oldestArticle" => ["id", ">=", "int", 1],
|
"oldestArticle" => ["id", ">=", "int", "latestArticle", 1],
|
||||||
"latestArticle" => ["id", "<=", "int", 1],
|
"latestArticle" => ["id", "<=", "int", "oldestArticle", 1],
|
||||||
"oldestEdition" => ["edition", ">=", "int", 1],
|
"oldestEdition" => ["edition", ">=", "int", "latestEdition", 1],
|
||||||
"latestEdition" => ["edition", "<=", "int", 1],
|
"latestEdition" => ["edition", "<=", "int", "oldestEdition", 1],
|
||||||
"modifiedSince" => ["modified_date", ">=", "datetime", 1],
|
"modifiedSince" => ["modified_date", ">=", "datetime", "notModifiedSince", 1],
|
||||||
"notModifiedSince" => ["modified_date", "<=", "datetime", 1],
|
"notModifiedSince" => ["modified_date", "<=", "datetime", "modifiedSince", 1],
|
||||||
"markedSince" => ["marked_date", ">=", "datetime", 1],
|
"markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince", 1],
|
||||||
"notMarkedSince" => ["marked_date", "<=", "datetime", 1],
|
"notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince", 1],
|
||||||
"folderShallow" => ["folder", "=", "int", 1],
|
"folderShallow" => ["folder", "=", "int", "", 1],
|
||||||
"subscription" => ["subscription", "=", "int", 1],
|
"subscription" => ["subscription", "=", "int", "", 1],
|
||||||
"unread" => ["unread", "=", "bool", 1],
|
"unread" => ["unread", "=", "bool", "", 1],
|
||||||
"starred" => ["starred", "=", "bool", 1],
|
"starred" => ["starred", "=", "bool", "", 1],
|
||||||
];
|
];
|
||||||
foreach ($options as $m => list($col, $op, $type, $max)) {
|
$optionsSeen = [];
|
||||||
|
foreach ($options as $m => list($col, $op, $type, $pair, $max)) {
|
||||||
|
|
||||||
if (!$context->$m()) {
|
if (!$context->$m()) {
|
||||||
// context is not being used
|
// context is not being used
|
||||||
continue;
|
continue;
|
||||||
} elseif (is_array($context->$m)) {
|
} elseif (is_array($context->$m)) {
|
||||||
|
// context option is an array of values
|
||||||
if (!$context->$m) {
|
if (!$context->$m) {
|
||||||
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
|
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
|
||||||
} elseif (sizeof($context->$m) > $max) {
|
} elseif (sizeof($context->$m) > $max) {
|
||||||
|
@ -1212,27 +1214,42 @@ class Database {
|
||||||
}
|
}
|
||||||
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
|
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
|
||||||
$q->setWhere("{$colDefs[$col]} $op ($clause)", $types, $values);
|
$q->setWhere("{$colDefs[$col]} $op ($clause)", $types, $values);
|
||||||
|
} elseif ($pair && $context->$pair()) {
|
||||||
|
// option is paired with another which is also being used
|
||||||
|
if ($op === ">=") {
|
||||||
|
$q->setWhere("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], [$context->$m, $context->$pair]);
|
||||||
|
} else {
|
||||||
|
// option has already been paired
|
||||||
|
continue;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$q->setWhere("{$colDefs[$col]} $op ?", $type, $context->$m);
|
$q->setWhere("{$colDefs[$col]} $op ?", $type, $context->$m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($context->not != $excContext) {
|
|
||||||
// further handle exclusionary options if specified
|
// further handle exclusionary options if specified
|
||||||
foreach ($options as $m => list($col, $op, $type, $max)) {
|
foreach ($options as $m => list($col, $op, $type, $pair, $max)) {
|
||||||
if (!method_exists($context->not, $m) || !$context->not->$m()) {
|
if (!method_exists($context->not, $m) || !$context->not->$m()) {
|
||||||
// context option is not being used
|
// context option is not being used
|
||||||
continue;
|
continue;
|
||||||
} elseif (is_array($context->not->$m)) {
|
} elseif (is_array($context->not->$m)) {
|
||||||
if (!$context->not->$m) {
|
if (!$context->not->$m) {
|
||||||
// for exclusions we don't care if the array is empty
|
// for exclusions we don't care if the array is empty
|
||||||
|
continue;
|
||||||
} elseif (sizeof($context->not->$m) > $max) {
|
} elseif (sizeof($context->not->$m) > $max) {
|
||||||
throw new Db\ExceptionInput("tooLong", ['field' => $m, 'action' => $this->caller(), 'max' => $max]); // @codeCoverageIgnore
|
throw new Db\ExceptionInput("tooLong", ['field' => "$m (not)", 'action' => $this->caller(), 'max' => $max]);
|
||||||
}
|
}
|
||||||
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
|
list($clause, $types, $values) = $this->generateIn($context->not->$m, $type);
|
||||||
$q->setWhereNot("{$colDefs[$col]} $op ($clause)", $types, $values);
|
$q->setWhereNot("{$colDefs[$col]} $op ($clause)", $types, $values);
|
||||||
|
} elseif ($pair && $context->not->$pair()) {
|
||||||
|
// option is paired with another which is also being used
|
||||||
|
if ($op === ">=") {
|
||||||
|
$q->setWhereNot("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], [$context->not->$m, $context->not->$pair]);
|
||||||
} else {
|
} else {
|
||||||
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->$m);
|
// option has already been paired
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->not->$m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// handle complex context options
|
// handle complex context options
|
||||||
|
|
|
@ -113,6 +113,9 @@ class Query {
|
||||||
$this->qWhere = [];
|
$this->qWhere = [];
|
||||||
$this->tWhere = [];
|
$this->tWhere = [];
|
||||||
$this->vWhere = [];
|
$this->vWhere = [];
|
||||||
|
$this->qWhereNot = [];
|
||||||
|
$this->tWhereNot = [];
|
||||||
|
$this->vWhereNot = [];
|
||||||
$this->qJoin = [];
|
$this->qJoin = [];
|
||||||
$this->tJoin = [];
|
$this->tJoin = [];
|
||||||
$this->vJoin = [];
|
$this->vJoin = [];
|
||||||
|
|
|
@ -377,43 +377,6 @@ trait SeriesArticle {
|
||||||
unset($this->data, $this->matches, $this->fields, $this->checkTables, $this->user);
|
unset($this->data, $this->matches, $this->fields, $this->checkTables, $this->user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRetrieveArticleIdsForEditions() {
|
|
||||||
$exp = [
|
|
||||||
1 => 1,
|
|
||||||
2 => 2,
|
|
||||||
3 => 3,
|
|
||||||
4 => 4,
|
|
||||||
5 => 5,
|
|
||||||
6 => 6,
|
|
||||||
7 => 7,
|
|
||||||
8 => 8,
|
|
||||||
9 => 9,
|
|
||||||
10 => 10,
|
|
||||||
11 => 11,
|
|
||||||
12 => 12,
|
|
||||||
13 => 13,
|
|
||||||
14 => 14,
|
|
||||||
15 => 15,
|
|
||||||
16 => 16,
|
|
||||||
17 => 17,
|
|
||||||
18 => 18,
|
|
||||||
19 => 19,
|
|
||||||
20 => 20,
|
|
||||||
101 => 101,
|
|
||||||
102 => 102,
|
|
||||||
103 => 103,
|
|
||||||
104 => 104,
|
|
||||||
105 => 105,
|
|
||||||
202 => 102,
|
|
||||||
203 => 103,
|
|
||||||
204 => 104,
|
|
||||||
205 => 105,
|
|
||||||
305 => 105,
|
|
||||||
1001 => 20,
|
|
||||||
];
|
|
||||||
$this->assertEquals($exp, Arsse::$db->editionArticle(...range(1, 1001)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @dataProvider provideContextMatches */
|
/** @dataProvider provideContextMatches */
|
||||||
public function testListArticlesCheckingContext(Context $c, array $exp) {
|
public function testListArticlesCheckingContext(Context $c, array $exp) {
|
||||||
$ids = array_column($ids = Arsse::$db->articleList("john.doe@example.com", $c)->getAll(), "id");
|
$ids = array_column($ids = Arsse::$db->articleList("john.doe@example.com", $c)->getAll(), "id");
|
||||||
|
@ -454,6 +417,8 @@ trait SeriesArticle {
|
||||||
"Marked or labelled since 2010" => [(new Context)->markedSince("2010-01-01T00:00:00Z"), [2,4,6,8,19,20]],
|
"Marked or labelled since 2010" => [(new Context)->markedSince("2010-01-01T00:00:00Z"), [2,4,6,8,19,20]],
|
||||||
"Not marked or labelled since 2014" => [(new Context)->notMarkedSince("2014-01-01T00:00:00Z"), [1,2,3,4,5,6,7,20]],
|
"Not marked or labelled since 2014" => [(new Context)->notMarkedSince("2014-01-01T00:00:00Z"), [1,2,3,4,5,6,7,20]],
|
||||||
"Not marked or labelled since 2005" => [(new Context)->notMarkedSince("2005-01-01T00:00:00Z"), [1,3,5,7]],
|
"Not marked or labelled since 2005" => [(new Context)->notMarkedSince("2005-01-01T00:00:00Z"), [1,3,5,7]],
|
||||||
|
"Marked or labelled between 2000 and 2015" => [(new Context)->markedSince("2000-01-01T00:00:00Z")->notMarkedSince("2015-12-31T23:59:59Z"), [1,2,3,4,5,6,7,8,20]],
|
||||||
|
"Marked or labelled in 2010" => [(new Context)->markedSince("2010-01-01T00:00:00Z")->notMarkedSince("2010-12-31T23:59:59Z"), [2,4,6,20]],
|
||||||
"Paged results" => [(new Context)->limit(2)->oldestEdition(4), [4,5]],
|
"Paged results" => [(new Context)->limit(2)->oldestEdition(4), [4,5]],
|
||||||
"Reversed paged results" => [(new Context)->limit(2)->latestEdition(7)->reverse(true), [7,6]],
|
"Reversed paged results" => [(new Context)->limit(2)->latestEdition(7)->reverse(true), [7,6]],
|
||||||
"With label ID 1" => [(new Context)->label(1), [1,19]],
|
"With label ID 1" => [(new Context)->label(1), [1,19]],
|
||||||
|
@ -483,9 +448,50 @@ trait SeriesArticle {
|
||||||
"Search author 2" => [(new Context)->authorTerms(["jane doe"]), [6,7]],
|
"Search author 2" => [(new Context)->authorTerms(["jane doe"]), [6,7]],
|
||||||
"Search author 3" => [(new Context)->authorTerms(["doe", "jane"]), [6,7]],
|
"Search author 3" => [(new Context)->authorTerms(["doe", "jane"]), [6,7]],
|
||||||
"Search author 4" => [(new Context)->authorTerms(["doe jane"]), []],
|
"Search author 4" => [(new Context)->authorTerms(["doe jane"]), []],
|
||||||
|
"Folder tree 1 excluding subscription 4" => [(new Context)->not->subscription(4)->folder(1), [5,6]],
|
||||||
|
"Folder tree 1 excluding articles 7 and 8" => [(new Context)->folder(1)->not->articles([7,8]), [5,6]],
|
||||||
|
"Folder tree 1 excluding no articles" => [(new Context)->folder(1)->not->articles([]), [5,6,7,8]],
|
||||||
|
"Marked or labelled between 2000 and 2015 excluding in 2010" => [(new Context)->markedSince("2000-01-01T00:00:00Z")->notMarkedSince("2015-12-31T23:59:59")->not->markedSince("2010-01-01T00:00:00Z")->not->notMarkedSince("2010-12-31T23:59:59Z"), [1,3,5,7,8]],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRetrieveArticleIdsForEditions() {
|
||||||
|
$exp = [
|
||||||
|
1 => 1,
|
||||||
|
2 => 2,
|
||||||
|
3 => 3,
|
||||||
|
4 => 4,
|
||||||
|
5 => 5,
|
||||||
|
6 => 6,
|
||||||
|
7 => 7,
|
||||||
|
8 => 8,
|
||||||
|
9 => 9,
|
||||||
|
10 => 10,
|
||||||
|
11 => 11,
|
||||||
|
12 => 12,
|
||||||
|
13 => 13,
|
||||||
|
14 => 14,
|
||||||
|
15 => 15,
|
||||||
|
16 => 16,
|
||||||
|
17 => 17,
|
||||||
|
18 => 18,
|
||||||
|
19 => 19,
|
||||||
|
20 => 20,
|
||||||
|
101 => 101,
|
||||||
|
102 => 102,
|
||||||
|
103 => 103,
|
||||||
|
104 => 104,
|
||||||
|
105 => 105,
|
||||||
|
202 => 102,
|
||||||
|
203 => 103,
|
||||||
|
204 => 104,
|
||||||
|
205 => 105,
|
||||||
|
305 => 105,
|
||||||
|
1001 => 20,
|
||||||
|
];
|
||||||
|
$this->assertEquals($exp, Arsse::$db->editionArticle(...range(1, 1001)));
|
||||||
|
}
|
||||||
|
|
||||||
public function testListArticlesOfAMissingFolder() {
|
public function testListArticlesOfAMissingFolder() {
|
||||||
$this->assertException("idMissing", "Db", "ExceptionInput");
|
$this->assertException("idMissing", "Db", "ExceptionInput");
|
||||||
Arsse::$db->articleList($this->user, (new Context)->folder(1));
|
Arsse::$db->articleList($this->user, (new Context)->folder(1));
|
||||||
|
|
Loading…
Reference in a new issue