diff --git a/lib/Context/ExclusionContext.php b/lib/Context/ExclusionContext.php index d5299fe..1f91994 100644 --- a/lib/Context/ExclusionContext.php +++ b/lib/Context/ExclusionContext.php @@ -12,6 +12,8 @@ use JKingWeb\Arsse\Misc\Date; class ExclusionContext { public $folder; public $folderShallow; + public $tag; + public $tagName; public $subscription; public $edition; public $article; @@ -101,6 +103,14 @@ class ExclusionContext { return $this->act(__FUNCTION__, func_num_args(), $spec); } + public function tag(int $spec = null) { + return $this->act(__FUNCTION__, func_num_args(), $spec); + } + + public function tagName(string $spec = null) { + return $this->act(__FUNCTION__, func_num_args(), $spec); + } + public function subscription(int $spec = null) { return $this->act(__FUNCTION__, func_num_args(), $spec); } diff --git a/lib/Database.php b/lib/Database.php index f5409d1..dc2c74d 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1325,6 +1325,21 @@ class Database { $q->setWhereNot("arsse_articles.id in (select article from labelled where label_name = ?)", "str", $context->not->labelName); } } + if ($context->tag() || $context->not->tag() || $context->tagName() || $context->not->tagName()) { + $q->setCTE("tagged(id,name,subscription)","SELECT arsse_tags.id, arsse_tags.name, arsse_tag_members.subscription FROM arsse_tag_members join arsse_tags on arsse_tags.id = arsse_tag_members.tag WHERE arsse_tags.owner = ? and assigned = 1", "str", $user); + if ($context->tag()) { + $q->setWhere("arsse_subscriptions.id in (select subscription from tagged where id = ?)", "int", $context->tag); + } + if ($context->not->tag()) { + $q->setWhereNot("arsse_subscriptions.id in (select subscription from tagged where id = ?)", "int", $context->not->tag); + } + if ($context->tagName()) { + $q->setWhere("arsse_subscriptions.id in (select subscription from tagged where name = ?)", "str", $context->tagName); + } + if ($context->not->tagName()) { + $q->setWhereNot("arsse_subscriptions.id in (select subscription from tagged where name = ?)", "str", $context->not->tagName); + } + } if ($context->folder()) { // add a common table expression to list the folder and its children so that we select from the entire subtree $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $context->folder); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index f652c6f..9fb893b 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -28,6 +28,28 @@ trait SeriesArticle { ["john.doe@example.net", "", "John Doe"], ], ], + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'title' => "str", + ], + 'rows' => [ + [1,"http://example.com/1", "Feed 1"], + [2,"http://example.com/2", "Feed 2"], + [3,"http://example.com/3", "Feed 3"], + [4,"http://example.com/4", "Feed 4"], + [5,"http://example.com/5", "Feed 5"], + [6,"http://example.com/6", "Feed 6"], + [7,"http://example.com/7", "Feed 7"], + [8,"http://example.com/8", "Feed 8"], + [9,"http://example.com/9", "Feed 9"], + [10,"http://example.com/10", "Feed 10"], + [11,"http://example.com/11", "Feed 11"], + [12,"http://example.com/12", "Feed 12"], + [13,"http://example.com/13", "Feed 13"], + ] + ], 'arsse_folders' => [ 'columns' => [ 'id' => "int", @@ -47,26 +69,21 @@ trait SeriesArticle { [9, "john.doe@example.net", null, "Politics"], ] ], - 'arsse_feeds' => [ + 'arsse_tags' => [ 'columns' => [ - 'id' => "int", - 'url' => "str", - 'title' => "str", + 'id' => "int", + 'owner' => "str", + 'name' => "str", ], 'rows' => [ - [1,"http://example.com/1", "Feed 1"], - [2,"http://example.com/2", "Feed 2"], - [3,"http://example.com/3", "Feed 3"], - [4,"http://example.com/4", "Feed 4"], - [5,"http://example.com/5", "Feed 5"], - [6,"http://example.com/6", "Feed 6"], - [7,"http://example.com/7", "Feed 7"], - [8,"http://example.com/8", "Feed 8"], - [9,"http://example.com/9", "Feed 9"], - [10,"http://example.com/10", "Feed 10"], - [11,"http://example.com/11", "Feed 11"], - [12,"http://example.com/12", "Feed 12"], - [13,"http://example.com/13", "Feed 13"], + [1, "john.doe@example.com", "Technology"], + [2, "john.doe@example.com", "Software"], + [3, "john.doe@example.com", "Rocketry"], + [4, "jane.doe@example.com", "Politics"], + [5, "john.doe@example.com", "Politics"], + [6, "john.doe@example.net", "Technology"], + [7, "john.doe@example.net", "Software"], + [8, "john.doe@example.net", "Politics"], ] ], 'arsse_subscriptions' => [ @@ -94,6 +111,25 @@ trait SeriesArticle { [14,"john.doe@example.net",4, 7,null], ] ], + 'arsse_tag_members' => [ + 'columns' => [ + 'tag' => "int", + 'subscription' => "int", + 'assigned' => "bool", + ], + 'rows' => [ + [1,3,1], + [1,4,1], + [2,4,1], + [5,1,0], + [5,4,1], + [5,5,1], + [6,13,1], + [6,14,1], + [7,13,1], + [8,12,1], + ], + ], 'arsse_articles' => [ 'columns' => [ 'id' => "int", @@ -387,76 +423,84 @@ trait SeriesArticle { public function provideContextMatches() { return [ - "Blank context" => [new Context, [1,2,3,4,5,6,7,8,19,20]], - "Folder tree" => [(new Context)->folder(1), [5,6,7,8]], - "Leaf folder" => [(new Context)->folder(6), [7,8]], - "Root folder only" => [(new Context)->folderShallow(0), [1,2,3,4]], - "Shallow folder" => [(new Context)->folderShallow(1), [5,6]], - "Subscription" => [(new Context)->subscription(5), [19,20]], - "Unread" => [(new Context)->subscription(5)->unread(true), [20]], - "Read" => [(new Context)->subscription(5)->unread(false), [19]], - "Starred" => [(new Context)->starred(true), [1,20]], - "Unstarred" => [(new Context)->starred(false), [2,3,4,5,6,7,8,19]], - "Starred and Read" => [(new Context)->starred(true)->unread(false), [1]], - "Starred and Read in subscription" => [(new Context)->starred(true)->unread(false)->subscription(5), []], - "Annotated" => [(new Context)->annotated(true), [2]], - "Not annotated" => [(new Context)->annotated(false), [1,3,4,5,6,7,8,19,20]], - "Labelled" => [(new Context)->labelled(true), [1,5,8,19,20]], - "Not labelled" => [(new Context)->labelled(false), [2,3,4,6,7]], - "Not after edition 999" => [(new Context)->subscription(5)->latestEdition(999), [19]], - "Not after edition 19" => [(new Context)->subscription(5)->latestEdition(19), [19]], - "Not before edition 999" => [(new Context)->subscription(5)->oldestEdition(999), [20]], - "Not before edition 1001" => [(new Context)->subscription(5)->oldestEdition(1001), [20]], - "Not after article 3" => [(new Context)->latestArticle(3), [1,2,3]], - "Not before article 19" => [(new Context)->oldestArticle(19), [19,20]], - "Modified by author since 2005" => [(new Context)->modifiedSince("2005-01-01T00:00:00Z"), [2,4,6,8,20]], - "Modified by author since 2010" => [(new Context)->modifiedSince("2010-01-01T00:00:00Z"), [2,4,6,8,20]], - "Not modified by author since 2005" => [(new Context)->notModifiedSince("2005-01-01T00:00:00Z"), [1,3,5,7,19]], - "Not modified by author since 2000" => [(new Context)->notModifiedSince("2000-01-01T00:00:00Z"), [1,3,5,7,19]], - "Marked or labelled since 2014" => [(new Context)->markedSince("2014-01-01T00:00:00Z"), [8,19]], - "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]], - "With label ID 2" => [(new Context)->label(2), [1,5,20]], - "With label 'Interesting'" => [(new Context)->labelName("Interesting"), [1,19]], - "With label 'Fascinating'" => [(new Context)->labelName("Fascinating"), [1,5,20]], - "Article ID 20" => [(new Context)->article(20), [20]], - "Edition ID 1001" => [(new Context)->edition(1001), [20]], - "Multiple articles" => [(new Context)->articles([1,20,50]), [1,20]], - "Multiple starred articles" => [(new Context)->articles([1,2,3])->starred(true), [1]], - "Multiple unstarred articles" => [(new Context)->articles([1,2,3])->starred(false), [2,3]], - "Multiple articles" => [(new Context)->articles([1,20,50]), [1,20]], - "Multiple editions" => [(new Context)->editions([1,1001,50]), [1,20]], - "150 articles" => [(new Context)->articles(range(1, Database::LIMIT_SET_SIZE * 3)), [1,2,3,4,5,6,7,8,19,20]], - "Search title or content 1" => [(new Context)->searchTerms(["Article"]), [1,2,3]], - "Search title or content 2" => [(new Context)->searchTerms(["one", "first"]), [1]], - "Search title or content 3" => [(new Context)->searchTerms(["one first"]), []], - "Search title 1" => [(new Context)->titleTerms(["two"]), [2]], - "Search title 2" => [(new Context)->titleTerms(["title two"]), [2]], - "Search title 3" => [(new Context)->titleTerms(["two", "title"]), [2]], - "Search title 4" => [(new Context)->titleTerms(["two title"]), []], - "Search note 1" => [(new Context)->annotationTerms(["some"]), [2]], - "Search note 2" => [(new Context)->annotationTerms(["some Note"]), [2]], - "Search note 3" => [(new Context)->annotationTerms(["note", "some"]), [2]], - "Search note 4" => [(new Context)->annotationTerms(["some", "sauce"]), []], - "Search author 1" => [(new Context)->authorTerms(["doe"]), [4,5,6,7]], - "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]], - "Search with exclusion" => [(new Context)->searchTerms(["Article"])->not->searchTerms(["one", "two"]), [3]], - "Excluded folder tree" => [(new Context)->not->folder(1), [1,2,3,4,19,20]], - "Excluding label ID 2" => [(new Context)->not->label(2), [2,3,4,6,7,8,19]], - "Excluding label 'Fascinating'" => [(new Context)->not->labelName("Fascinating"), [2,3,4,6,7,8,19]], - "Search 501 terms" => [(new Context)->searchTerms(array_merge(range(1,500),[str_repeat("a", 1000)])), []], + 'Blank context' => [new Context, [1,2,3,4,5,6,7,8,19,20]], + 'Folder tree' => [(new Context)->folder(1), [5,6,7,8]], + 'Leaf folder' => [(new Context)->folder(6), [7,8]], + 'Root folder only' => [(new Context)->folderShallow(0), [1,2,3,4]], + 'Shallow folder' => [(new Context)->folderShallow(1), [5,6]], + 'Subscription' => [(new Context)->subscription(5), [19,20]], + 'Unread' => [(new Context)->subscription(5)->unread(true), [20]], + 'Read' => [(new Context)->subscription(5)->unread(false), [19]], + 'Starred' => [(new Context)->starred(true), [1,20]], + 'Unstarred' => [(new Context)->starred(false), [2,3,4,5,6,7,8,19]], + 'Starred and Read' => [(new Context)->starred(true)->unread(false), [1]], + 'Starred and Read in subscription' => [(new Context)->starred(true)->unread(false)->subscription(5), []], + 'Annotated' => [(new Context)->annotated(true), [2]], + 'Not annotated' => [(new Context)->annotated(false), [1,3,4,5,6,7,8,19,20]], + 'Labelled' => [(new Context)->labelled(true), [1,5,8,19,20]], + 'Not labelled' => [(new Context)->labelled(false), [2,3,4,6,7]], + 'Not after edition 999' => [(new Context)->subscription(5)->latestEdition(999), [19]], + 'Not after edition 19' => [(new Context)->subscription(5)->latestEdition(19), [19]], + 'Not before edition 999' => [(new Context)->subscription(5)->oldestEdition(999), [20]], + 'Not before edition 1001' => [(new Context)->subscription(5)->oldestEdition(1001), [20]], + 'Not after article 3' => [(new Context)->latestArticle(3), [1,2,3]], + 'Not before article 19' => [(new Context)->oldestArticle(19), [19,20]], + 'Modified by author since 2005' => [(new Context)->modifiedSince("2005-01-01T00:00:00Z"), [2,4,6,8,20]], + 'Modified by author since 2010' => [(new Context)->modifiedSince("2010-01-01T00:00:00Z"), [2,4,6,8,20]], + 'Not modified by author since 2005' => [(new Context)->notModifiedSince("2005-01-01T00:00:00Z"), [1,3,5,7,19]], + 'Not modified by author since 2000' => [(new Context)->notModifiedSince("2000-01-01T00:00:00Z"), [1,3,5,7,19]], + 'Marked or labelled since 2014' => [(new Context)->markedSince("2014-01-01T00:00:00Z"), [8,19]], + '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]], + 'With label ID 2' => [(new Context)->label(2), [1,5,20]], + 'With label "Interesting"' => [(new Context)->labelName("Interesting"), [1,19]], + 'With label "Fascinating"' => [(new Context)->labelName("Fascinating"), [1,5,20]], + 'Article ID 20' => [(new Context)->article(20), [20]], + 'Edition ID 1001' => [(new Context)->edition(1001), [20]], + 'Multiple articles' => [(new Context)->articles([1,20,50]), [1,20]], + 'Multiple starred articles' => [(new Context)->articles([1,2,3])->starred(true), [1]], + 'Multiple unstarred articles' => [(new Context)->articles([1,2,3])->starred(false), [2,3]], + 'Multiple articles' => [(new Context)->articles([1,20,50]), [1,20]], + 'Multiple editions' => [(new Context)->editions([1,1001,50]), [1,20]], + '150 articles' => [(new Context)->articles(range(1, Database::LIMIT_SET_SIZE * 3)), [1,2,3,4,5,6,7,8,19,20]], + 'Search title or content 1' => [(new Context)->searchTerms(["Article"]), [1,2,3]], + 'Search title or content 2' => [(new Context)->searchTerms(["one", "first"]), [1]], + 'Search title or content 3' => [(new Context)->searchTerms(["one first"]), []], + 'Search title 1' => [(new Context)->titleTerms(["two"]), [2]], + 'Search title 2' => [(new Context)->titleTerms(["title two"]), [2]], + 'Search title 3' => [(new Context)->titleTerms(["two", "title"]), [2]], + 'Search title 4' => [(new Context)->titleTerms(["two title"]), []], + 'Search note 1' => [(new Context)->annotationTerms(["some"]), [2]], + 'Search note 2' => [(new Context)->annotationTerms(["some Note"]), [2]], + 'Search note 3' => [(new Context)->annotationTerms(["note", "some"]), [2]], + 'Search note 4' => [(new Context)->annotationTerms(["some", "sauce"]), []], + 'Search author 1' => [(new Context)->authorTerms(["doe"]), [4,5,6,7]], + '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]], + 'Search with exclusion' => [(new Context)->searchTerms(["Article"])->not->searchTerms(["one", "two"]), [3]], + 'Excluded folder tree' => [(new Context)->not->folder(1), [1,2,3,4,19,20]], + 'Excluding label ID 2' => [(new Context)->not->label(2), [2,3,4,6,7,8,19]], + 'Excluding label "Fascinating"' => [(new Context)->not->labelName("Fascinating"), [2,3,4,6,7,8,19]], + 'Search 501 terms' => [(new Context)->searchTerms(array_merge(range(1,500),[str_repeat("a", 1000)])), []], + 'With tag ID 1' => [(new Context)->tag(1), [5,6,7,8]], + 'With tag ID 5' => [(new Context)->tag(5), [7,8,19,20]], + 'With tag "Technology"' => [(new Context)->tagName("Technology"), [5,6,7,8]], + 'With tag "Politics"' => [(new Context)->tagName("Politics"), [7,8,19,20]], + 'Excluding tag ID 1' => [(new Context)->not->tag(1), [1,2,3,4,19,20]], + 'Excluding tag ID 5' => [(new Context)->not->tag(5), [1,2,3,4,5,6]], + 'Excluding tag "Technology"' => [(new Context)->not->tagName("Technology"), [1,2,3,4,19,20]], + 'Excluding tag "Politics"' => [(new Context)->not->tagName("Politics"), [1,2,3,4,5,6]], ]; } diff --git a/tests/cases/Misc/TestContext.php b/tests/cases/Misc/TestContext.php index d134c0f..e85d58e 100644 --- a/tests/cases/Misc/TestContext.php +++ b/tests/cases/Misc/TestContext.php @@ -30,6 +30,8 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest { 'offset' => 5, 'folder' => 42, 'folderShallow' => 42, + 'tag' => 44, + 'tagName' => "XLIV", 'subscription' => 2112, 'article' => 255, 'edition' => 65535,