mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2025-01-03 14:32:40 +00:00
Added context options to mark multiple specific articles or editions
This required significant rewrites of queries because of stale editions
This commit is contained in:
parent
c51ee594aa
commit
761c1054f3
5 changed files with 174 additions and 24 deletions
|
@ -681,19 +681,19 @@ class Database {
|
|||
$queries = [
|
||||
"UPDATE arsse_marks
|
||||
set
|
||||
read = coalesce((select read from target_values),read),
|
||||
read = case when (select honour_read from target_articles where target_articles.id is article) is 1 then (select read from target_values) else read end,
|
||||
starred = coalesce((select starred from target_values),starred),
|
||||
modified = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
owner is (select user from user)
|
||||
and article in (select id from target_articles where to_update is 1)",
|
||||
and article in (select id from target_articles where to_insert is 0 and (honour_read is 1 or honour_star is 1))",
|
||||
"INSERT INTO arsse_marks(owner,article,read,starred)
|
||||
select
|
||||
(select user from user),
|
||||
id,
|
||||
coalesce((select read from target_values),0),
|
||||
coalesce((select read from target_values) * honour_read,0),
|
||||
coalesce((select starred from target_values),0)
|
||||
from target_articles where to_insert is 1"
|
||||
from target_articles where to_insert is 1 and (honour_read is 1 or honour_star is 1)"
|
||||
];
|
||||
$out = 0;
|
||||
// wrap this UPDATE and INSERT together into a transaction
|
||||
|
@ -712,26 +712,15 @@ class Database {
|
|||
foreach($queries as $query) {
|
||||
// first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles
|
||||
$q = new Query(
|
||||
"SELECT
|
||||
"SELECT
|
||||
arsse_articles.id as id,
|
||||
(select max(id) from arsse_editions where article is arsse_articles.id) as edition,
|
||||
max(arsse_articles.modified,
|
||||
max(arsse_articles.modified,
|
||||
coalesce((select modified from arsse_marks join user on user is owner where article is arsse_articles.id),'')
|
||||
) as modified_date,
|
||||
(
|
||||
not exists(select id from arsse_marks join user on user is owner where article is arsse_articles.id)
|
||||
and exists(select * from target_values where read is 1 or starred is 1)
|
||||
) as to_insert,
|
||||
exists(
|
||||
select id from arsse_marks
|
||||
join user on user is owner
|
||||
where
|
||||
article is arsse_articles.id
|
||||
and (
|
||||
read is not coalesce((select read from target_values),read)
|
||||
or starred is not coalesce((select starred from target_values),starred)
|
||||
)
|
||||
) as to_update
|
||||
(not exists(select id from arsse_marks join user on user is owner where article is arsse_articles.id)) as to_insert,
|
||||
((select read from target_values) is not null and (select read from target_values) is not (coalesce((select read from arsse_marks join user on user is owner where article is arsse_articles.id),0)) and (not exists(select * from requested_articles) or (select max(id) from arsse_editions where article is arsse_articles.id) in (select edition from requested_articles))) as honour_read,
|
||||
((select starred from target_values) is not null and (select starred from target_values) is not (coalesce((select starred from arsse_marks join user on user is owner where article is arsse_articles.id),0))) as honour_star
|
||||
FROM arsse_articles"
|
||||
);
|
||||
// common table expression for the affected user
|
||||
|
@ -760,6 +749,32 @@ class Database {
|
|||
// otherwise add a CTE for all the user's subscriptions
|
||||
$q->setCTE("subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner)", [], [], "join subscribed_feeds on feed is subscribed_feeds.id");
|
||||
}
|
||||
if($context->editions()) {
|
||||
// if multiple specific editions have been requested, prepare a CTE to list them and their articles
|
||||
if(!$context->editions) throw new Db\ExceptionInput("tooShort", ['field' => "editions", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
|
||||
if(sizeof($context->editions) > 50) throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
|
||||
list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
|
||||
$q->setCTE(
|
||||
"requested_articles(id,edition) as (select article,id as edition from arsse_editions where edition in ($inParams))",
|
||||
$inTypes,
|
||||
$context->editions
|
||||
);
|
||||
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
|
||||
} else if($context->articles()) {
|
||||
// if multiple specific articles have been requested, prepare a CTE to list them and their articles
|
||||
if(!$context->articles) throw new Db\ExceptionInput("tooShort", ['field' => "articles", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
|
||||
if(sizeof($context->articles) > 50) throw new Db\ExceptionInput("tooLong", ['field' => "articles", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
|
||||
list($inParams, $inTypes) = $this->generateIn($context->articles, "int");
|
||||
$q->setCTE(
|
||||
"requested_articles(id,edition) as (select id,(select max(id) from arsse_editions where article is arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams))",
|
||||
$inTypes,
|
||||
$context->articles
|
||||
);
|
||||
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
|
||||
} else {
|
||||
// if neither list is specified, mock an empty table
|
||||
$q->setCTE("requested_articles(id,edition) as (select 'empty','table' where 1 is 0)");
|
||||
}
|
||||
// filter based on edition offset
|
||||
if($context->oldestEdition()) $q->setWhere("edition >= ?", "int", $context->oldestEdition);
|
||||
if($context->latestEdition()) $q->setWhere("edition <= ?", "int", $context->latestEdition);
|
||||
|
@ -768,7 +783,7 @@ class Database {
|
|||
if($context->notModifiedSince()) $q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince);
|
||||
// push the current query onto the CTE stack and execute the query we're actually interested in
|
||||
$q->pushCTE(
|
||||
"target_articles(id,edition,modified,to_insert,to_update)", // CTE table specification
|
||||
"target_articles(id,edition,modified_date,to_insert,honour_read,honour_star)", // CTE table specification
|
||||
[], // CTE types
|
||||
[], // CTE values
|
||||
$query // new query body
|
||||
|
|
|
@ -18,6 +18,8 @@ class Context {
|
|||
public $notModifiedSince;
|
||||
public $edition;
|
||||
public $article;
|
||||
public $editions;
|
||||
public $articles;
|
||||
|
||||
protected $props = [];
|
||||
|
||||
|
@ -30,6 +32,32 @@ class Context {
|
|||
return isset($this->props[$prop]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function cleanArray(array $spec): array {
|
||||
$spec = array_values($spec);
|
||||
for($a = 0; $a < sizeof($spec); $a++) {
|
||||
$id = $spec[$a];
|
||||
if(is_int($id) && $id > -1) continue;
|
||||
if(is_float($id) && !fmod($id, 1) && $id >= 0) {
|
||||
$spec[$a] = (int) $id;
|
||||
continue;
|
||||
}
|
||||
if(is_string($id)) {
|
||||
try {
|
||||
$ch1 = strval(intval($id));
|
||||
$ch2 = strval($id);
|
||||
} catch(\Throwable $e) {
|
||||
$ch1 = true;
|
||||
$ch2 = false;
|
||||
}
|
||||
if($ch1 !== $ch2 || $id < 1) $id = 0;
|
||||
} else {
|
||||
$id = 0;
|
||||
}
|
||||
$spec[$a] = (int) $id;
|
||||
}
|
||||
return array_values(array_filter($spec));
|
||||
}
|
||||
|
||||
function reverse(bool $spec = null) {
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
|
@ -84,4 +112,14 @@ class Context {
|
|||
function article(int $spec = null) {
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
|
||||
function editions(array $spec = null) {
|
||||
if($spec) $spec = $this->cleanArray($spec);
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
|
||||
function articles(array $spec = null) {
|
||||
if($spec) $spec = $this->cleanArray($spec);
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
}
|
|
@ -63,9 +63,9 @@ return [
|
|||
'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.unknownSavepointStatus' => 'Savepoint status code {0} not implemented',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Required field "{field}" of action "{action}" may not contain only whitespace',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Required field "{field}" of action "{action}" has a maximum length of {max}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooShort' => 'Required field "{field}" of action "{action}" has a minimum length of {min}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooShort' => 'Field "{field}" of action "{action}" has a minimum length of {min}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence',
|
||||
|
|
|
@ -32,6 +32,8 @@ class TestContext extends \PHPUnit\Framework\TestCase {
|
|||
'starred' => true,
|
||||
'modifiedSince' => new \DateTime(),
|
||||
'notModifiedSince' => new \DateTime(),
|
||||
'editions' => [1,2],
|
||||
'articles' => [1,2],
|
||||
];
|
||||
$times = ['modifiedSince','notModifiedSince'];
|
||||
$c = new Context;
|
||||
|
@ -48,4 +50,14 @@ class TestContext extends \PHPUnit\Framework\TestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testCleanArrayValues() {
|
||||
$methods = ["articles", "editions"];
|
||||
$in = [1, "2", 3.5, 3.0, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
||||
$out = [1,2, 3];
|
||||
$c = new Context;
|
||||
foreach($methods as $method) {
|
||||
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -518,6 +518,39 @@ trait SeriesArticle {
|
|||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkMultipleArticles() {
|
||||
Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->articles([2,4,7,20]));
|
||||
$now = $this->dateTransform(time(), "sql");
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$state['arsse_marks']['rows'][9][3] = 1;
|
||||
$state['arsse_marks']['rows'][9][4] = $now;
|
||||
$state['arsse_marks']['rows'][] = [$this->user,7,0,1,$now];
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkMultipleArticlessUnreadAndStarred() {
|
||||
Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles([2,4,7,20]));
|
||||
$now = $this->dateTransform(time(), "sql");
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$state['arsse_marks']['rows'][9][2] = 0;
|
||||
$state['arsse_marks']['rows'][9][3] = 1;
|
||||
$state['arsse_marks']['rows'][9][4] = $now;
|
||||
$state['arsse_marks']['rows'][11][2] = 0;
|
||||
$state['arsse_marks']['rows'][11][4] = $now;
|
||||
$state['arsse_marks']['rows'][] = [$this->user,7,0,1,$now];
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkTooFewMultipleArticles() {
|
||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||
Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles([]));
|
||||
}
|
||||
|
||||
function testMarkTooManyMultipleArticles() {
|
||||
$this->assertException("tooLong", "Db", "ExceptionInput");
|
||||
Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles(range(1,51)));
|
||||
}
|
||||
|
||||
function testMarkAMissingArticle() {
|
||||
$this->assertException("subjectMissing", "Db", "ExceptionInput");
|
||||
Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->article(1));
|
||||
|
@ -532,6 +565,58 @@ trait SeriesArticle {
|
|||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkMultipleEditions() {
|
||||
Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->editions([2,4,7,20]));
|
||||
$now = $this->dateTransform(time(), "sql");
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$state['arsse_marks']['rows'][9][3] = 1;
|
||||
$state['arsse_marks']['rows'][9][4] = $now;
|
||||
$state['arsse_marks']['rows'][] = [$this->user,7,0,1,$now];
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkMultipleEditionsUnread() {
|
||||
Data::$db->articleMark($this->user, ['read'=>false], (new Context)->editions([2,4,7,1001]));
|
||||
$now = $this->dateTransform(time(), "sql");
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$state['arsse_marks']['rows'][9][2] = 0;
|
||||
$state['arsse_marks']['rows'][9][4] = $now;
|
||||
$state['arsse_marks']['rows'][11][2] = 0;
|
||||
$state['arsse_marks']['rows'][11][4] = $now;
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkMultipleEditionsUnreadWithStale() {
|
||||
Data::$db->articleMark($this->user, ['read'=>false], (new Context)->editions([2,4,7,20]));
|
||||
$now = $this->dateTransform(time(), "sql");
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$state['arsse_marks']['rows'][11][2] = 0;
|
||||
$state['arsse_marks']['rows'][11][4] = $now;
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkMultipleEditionsUnreadAndStarredWithStale() {
|
||||
Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions([2,4,7,20]));
|
||||
$now = $this->dateTransform(time(), "sql");
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$state['arsse_marks']['rows'][9][3] = 1;
|
||||
$state['arsse_marks']['rows'][9][4] = $now;
|
||||
$state['arsse_marks']['rows'][11][2] = 0;
|
||||
$state['arsse_marks']['rows'][11][4] = $now;
|
||||
$state['arsse_marks']['rows'][] = [$this->user,7,0,1,$now];
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
function testMarkTooFewMultipleEditions() {
|
||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||
Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions([]));
|
||||
}
|
||||
|
||||
function testMarkTooManyMultipleEditions() {
|
||||
$this->assertException("tooLong", "Db", "ExceptionInput");
|
||||
Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions(range(1,51)));
|
||||
}
|
||||
|
||||
function testMarkAStaleEditionUnread() {
|
||||
Data::$db->articleMark($this->user, ['read'=>false], (new Context)->edition(20)); // no changes occur
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
|
|
Loading…
Reference in a new issue