1
1
Fork 0
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:
J. King 2019-02-26 11:11:42 -05:00
parent 70443a5264
commit 0dc82f64d5
5 changed files with 142 additions and 117 deletions

View file

@ -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);
}
} }

View file

@ -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);
}
} }

View file

@ -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, $pair, $max)) {
foreach ($options as $m => list($col, $op, $type, $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;
} elseif (is_array($context->not->$m)) {
if (!$context->not->$m) {
// for exclusions we don't care if the array is empty
continue; continue;
} elseif (is_array($context->not->$m)) { } elseif (sizeof($context->not->$m) > $max) {
if (!$context->not->$m) { throw new Db\ExceptionInput("tooLong", ['field' => "$m (not)", 'action' => $this->caller(), 'max' => $max]);
// 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);
} }
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 // handle complex context options

View file

@ -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 = [];

View file

@ -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));