mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +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);
|
||||
namespace JKingWeb\Arsse\Context;
|
||||
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
|
||||
class Context extends ExclusionContext {
|
||||
/** @var ExclusionContext */
|
||||
public $not;
|
||||
|
@ -18,14 +16,6 @@ class Context extends ExclusionContext {
|
|||
public $starred;
|
||||
public $labelled;
|
||||
public $annotated;
|
||||
public $oldestArticle;
|
||||
public $latestArticle;
|
||||
public $oldestEdition;
|
||||
public $latestEdition;
|
||||
public $modifiedSince;
|
||||
public $notModifiedSince;
|
||||
public $markedSince;
|
||||
public $notMarkedSince;
|
||||
|
||||
public function __construct() {
|
||||
$this->not = new ExclusionContext($this);
|
||||
|
@ -67,40 +57,4 @@ class Context extends ExclusionContext {
|
|||
public function annotated(bool $spec = null) {
|
||||
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;
|
||||
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
|
||||
class ExclusionContext {
|
||||
public $folder;
|
||||
|
@ -22,6 +23,14 @@ class ExclusionContext {
|
|||
public $searchTerms;
|
||||
public $titleTerms;
|
||||
public $authorTerms;
|
||||
public $oldestArticle;
|
||||
public $latestArticle;
|
||||
public $oldestEdition;
|
||||
public $latestEdition;
|
||||
public $modifiedSince;
|
||||
public $notModifiedSince;
|
||||
public $markedSince;
|
||||
public $notMarkedSince;
|
||||
|
||||
protected $props = [];
|
||||
protected $parent;
|
||||
|
@ -152,4 +161,40 @@ class ExclusionContext {
|
|||
}
|
||||
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
|
||||
$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
|
||||
$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
|
||||
"edition" => ["edition", "=", "int", 1],
|
||||
"editions" => ["edition", "in", "int", self::LIMIT_ARTICLES],
|
||||
"article" => ["id", "=", "int", 1],
|
||||
"articles" => ["id", "in", "int", self::LIMIT_ARTICLES],
|
||||
"oldestArticle" => ["id", ">=", "int", 1],
|
||||
"latestArticle" => ["id", "<=", "int", 1],
|
||||
"oldestEdition" => ["edition", ">=", "int", 1],
|
||||
"latestEdition" => ["edition", "<=", "int", 1],
|
||||
"modifiedSince" => ["modified_date", ">=", "datetime", 1],
|
||||
"notModifiedSince" => ["modified_date", "<=", "datetime", 1],
|
||||
"markedSince" => ["marked_date", ">=", "datetime", 1],
|
||||
"notMarkedSince" => ["marked_date", "<=", "datetime", 1],
|
||||
"folderShallow" => ["folder", "=", "int", 1],
|
||||
"subscription" => ["subscription", "=", "int", 1],
|
||||
"unread" => ["unread", "=", "bool", 1],
|
||||
"starred" => ["starred", "=", "bool", 1],
|
||||
// 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],
|
||||
"editions" => ["edition", "in", "int", "", self::LIMIT_ARTICLES],
|
||||
"article" => ["id", "=", "int", "", 1],
|
||||
"articles" => ["id", "in", "int", "", self::LIMIT_ARTICLES],
|
||||
"oldestArticle" => ["id", ">=", "int", "latestArticle", 1],
|
||||
"latestArticle" => ["id", "<=", "int", "oldestArticle", 1],
|
||||
"oldestEdition" => ["edition", ">=", "int", "latestEdition", 1],
|
||||
"latestEdition" => ["edition", "<=", "int", "oldestEdition", 1],
|
||||
"modifiedSince" => ["modified_date", ">=", "datetime", "notModifiedSince", 1],
|
||||
"notModifiedSince" => ["modified_date", "<=", "datetime", "modifiedSince", 1],
|
||||
"markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince", 1],
|
||||
"notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince", 1],
|
||||
"folderShallow" => ["folder", "=", "int", "", 1],
|
||||
"subscription" => ["subscription", "=", "int", "", 1],
|
||||
"unread" => ["unread", "=", "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()) {
|
||||
// context is not being used
|
||||
continue;
|
||||
} elseif (is_array($context->$m)) {
|
||||
// context option is an array of values
|
||||
if (!$context->$m) {
|
||||
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
|
||||
} elseif (sizeof($context->$m) > $max) {
|
||||
|
@ -1212,27 +1214,42 @@ class Database {
|
|||
}
|
||||
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
|
||||
$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 {
|
||||
$q->setWhere("{$colDefs[$col]} $op ?", $type, $context->$m);
|
||||
}
|
||||
}
|
||||
if ($context->not != $excContext) {
|
||||
// further handle exclusionary options if specified
|
||||
foreach ($options as $m => list($col, $op, $type, $max)) {
|
||||
if (!method_exists($context->not, $m) || !$context->not->$m()) {
|
||||
// context option is not being used
|
||||
// further handle exclusionary options if specified
|
||||
foreach ($options as $m => list($col, $op, $type, $pair, $max)) {
|
||||
if (!method_exists($context->not, $m) || !$context->not->$m()) {
|
||||
// context option is not being used
|
||||
continue;
|
||||
} elseif (is_array($context->not->$m)) {
|
||||
if (!$context->not->$m) {
|
||||
// for exclusions we don't care if the array is empty
|
||||
continue;
|
||||
} elseif (is_array($context->not->$m)) {
|
||||
if (!$context->not->$m) {
|
||||
// for exclusions we don't care if the array is empty
|
||||
} elseif (sizeof($context->not->$m) > $max) {
|
||||
throw new Db\ExceptionInput("tooLong", ['field' => $m, 'action' => $this->caller(), 'max' => $max]); // @codeCoverageIgnore
|
||||
}
|
||||
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
|
||||
$q->setWhereNot("{$colDefs[$col]} $op ($clause)", $types, $values);
|
||||
} else {
|
||||
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->$m);
|
||||
} elseif (sizeof($context->not->$m) > $max) {
|
||||
throw new Db\ExceptionInput("tooLong", ['field' => "$m (not)", 'action' => $this->caller(), 'max' => $max]);
|
||||
}
|
||||
list($clause, $types, $values) = $this->generateIn($context->not->$m, $type);
|
||||
$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 {
|
||||
// option has already been paired
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->not->$m);
|
||||
}
|
||||
}
|
||||
// handle complex context options
|
||||
|
|
|
@ -113,6 +113,9 @@ class Query {
|
|||
$this->qWhere = [];
|
||||
$this->tWhere = [];
|
||||
$this->vWhere = [];
|
||||
$this->qWhereNot = [];
|
||||
$this->tWhereNot = [];
|
||||
$this->vWhereNot = [];
|
||||
$this->qJoin = [];
|
||||
$this->tJoin = [];
|
||||
$this->vJoin = [];
|
||||
|
|
|
@ -377,43 +377,6 @@ trait SeriesArticle {
|
|||
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 */
|
||||
public function testListArticlesCheckingContext(Context $c, array $exp) {
|
||||
$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]],
|
||||
"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]],
|
||||
"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]],
|
||||
"Reversed paged results" => [(new Context)->limit(2)->latestEdition(7)->reverse(true), [7,6]],
|
||||
"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 3" => [(new Context)->authorTerms(["doe", "jane"]), [6,7]],
|
||||
"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() {
|
||||
$this->assertException("idMissing", "Db", "ExceptionInput");
|
||||
Arsse::$db->articleList($this->user, (new Context)->folder(1));
|
||||
|
|
Loading…
Reference in a new issue