Merge pull request #4146 from nextcloud/unread-comments-folder

Allow getting the unread comment count for an entire folder at once
This commit is contained in:
Morris Jobke 2017-04-10 13:21:39 -05:00 committed by GitHub
commit a045f3c4d7
9 changed files with 311 additions and 168 deletions

View File

@ -42,6 +42,10 @@ class CommentPropertiesPlugin extends ServerPlugin {
/** @var IUserSession */ /** @var IUserSession */
private $userSession; private $userSession;
private $cachedUnreadCount = [];
private $cachedFolders = [];
public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) {
$this->commentsManager = $commentsManager; $this->commentsManager = $commentsManager;
$this->userSession = $userSession; $this->userSession = $userSession;
@ -79,6 +83,18 @@ class CommentPropertiesPlugin extends ServerPlugin {
return; return;
} }
// need prefetch ?
if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
&& $propFind->getDepth() !== 0
&& !is_null($propFind->getStatus(self::PROPERTY_NAME_UNREAD))
) {
$unreadCounts = $this->commentsManager->getNumberOfUnreadCommentsForFolder($node->getId(), $this->userSession->getUser());
$this->cachedFolders[] = $node->getPath();
foreach ($unreadCounts as $id => $count) {
$this->cachedUnreadCount[$id] = $count;
}
}
$propFind->handle(self::PROPERTY_NAME_COUNT, function() use ($node) { $propFind->handle(self::PROPERTY_NAME_COUNT, function() use ($node) {
return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId())); return $this->commentsManager->getNumberOfCommentsForObject('files', strval($node->getId()));
}); });
@ -88,7 +104,20 @@ class CommentPropertiesPlugin extends ServerPlugin {
}); });
$propFind->handle(self::PROPERTY_NAME_UNREAD, function() use ($node) { $propFind->handle(self::PROPERTY_NAME_UNREAD, function() use ($node) {
if (isset($this->cachedUnreadCount[$node->getId()])) {
return $this->cachedUnreadCount[$node->getId()];
} else {
list($parentPath,) = \Sabre\Uri\split($node->getPath());
if ($parentPath === '') {
$parentPath = '/';
}
// if we already cached the folder this file is in we know there are no shares for this file
if (array_search($parentPath, $this->cachedFolders) === false) {
return $this->getUnreadCount($node); return $this->getUnreadCount($node);
} else {
return 0;
}
}
}); });
} }

View File

@ -21,9 +21,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
* *
*/ */
namespace OC\Comments; namespace OC\Comments;
use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use OCP\Comments\CommentsEvent; use OCP\Comments\CommentsEvent;
use OCP\Comments\IComment; use OCP\Comments\IComment;
use OCP\Comments\ICommentsEventHandler; use OCP\Comments\ICommentsEventHandler;
@ -401,6 +403,40 @@ class Manager implements ICommentsManager {
return intval($data[0]); return intval($data[0]);
} }
/**
* Get the number of unread comments for all files in a folder
*
* @param int $folderId
* @param IUser $user
* @return array [$fileId => $unreadCount]
*/
public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
$qb = $this->dbConn->getQueryBuilder();
$query = $qb->select('fileid', $qb->createFunction(
'COUNT(' . $qb->getColumnName('c.id') . ')')
)->from('comments', 'c')
->innerJoin('c', 'filecache', 'f', $qb->expr()->andX(
$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
$qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT))
))
->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
$qb->expr()->eq('m.object_id', 'c.object_id'),
$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID()))
))
->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)))
->andWhere($qb->expr()->orX(
$qb->expr()->gt('c.creation_timestamp', 'marker_datetime'),
$qb->expr()->isNull('marker_datetime')
))
->groupBy('f.fileid');
$resultStatement = $query->execute();
return array_map(function ($count) {
return (int)$count;
}, $resultStatement->fetchAll(\PDO::FETCH_KEY_PAIR));
}
/** /**
* creates a new comment and returns it. At this point of time, it is not * creates a new comment and returns it. At this point of time, it is not
* saved in the used data storage. Use save() after setting other fields * saved in the used data storage. Use save() after setting other fields

View File

@ -73,7 +73,7 @@ class QuoteHelper {
return $string; return $string;
} }
return $alias . '.`' . $columnName . '`'; return '`' . $alias . '`.`' . $columnName . '`';
} }
return '`' . $string . '`'; return '`' . $string . '`';

View File

@ -23,6 +23,8 @@
*/ */
namespace OCP\Comments; namespace OCP\Comments;
use OCP\IUser;
/** /**
* Interface ICommentsManager * Interface ICommentsManager
* *
@ -125,6 +127,16 @@ interface ICommentsManager {
*/ */
public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null); public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null);
/**
* Get the number of unread comments for all files in a folder
*
* @param int $folderId
* @param IUser $user
* @return array [$fileId => $unreadCount]
* @since 12.0.0
*/
public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user);
/** /**
* creates a new comment and returns it. At this point of time, it is not * creates a new comment and returns it. At this point of time, it is not
* saved in the used data storage. Use save() after setting other fields * saved in the used data storage. Use save() after setting other fields

View File

@ -1,6 +1,7 @@
<?php <?php
namespace Test\Comments; namespace Test\Comments;
use OCP\IUser;
/** /**
* Class FakeManager * Class FakeManager
@ -44,4 +45,6 @@ class FakeManager implements \OCP\Comments\ICommentsManager {
public function registerDisplayNameResolver($type, \Closure $closure) {} public function registerDisplayNameResolver($type, \Closure $closure) {}
public function resolveDisplayName($type, $id) {} public function resolveDisplayName($type, $id) {}
public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {}
} }

View File

@ -6,7 +6,9 @@ use OC\Comments\Comment;
use OCP\Comments\CommentsEvent; use OCP\Comments\CommentsEvent;
use OCP\Comments\ICommentsEventHandler; use OCP\Comments\ICommentsEventHandler;
use OCP\Comments\ICommentsManager; use OCP\Comments\ICommentsManager;
use OCP\IDBConnection;
use OCP\IUser; use OCP\IUser;
use Test\Files\Storage\DummyUser;
use Test\TestCase; use Test\TestCase;
/** /**
@ -15,23 +17,30 @@ use Test\TestCase;
* @group DB * @group DB
*/ */
class ManagerTest extends TestCase { class ManagerTest extends TestCase {
/** @var IDBConnection */
private $connection;
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
$sql = \OC::$server->getDatabaseConnection()->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*comments`'); $this->connection = \OC::$server->getDatabaseConnection();
\OC::$server->getDatabaseConnection()->prepare($sql)->execute();
$sql = $this->connection->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*comments`');
$this->connection->prepare($sql)->execute();
} }
protected function addDatabaseEntry($parentId, $topmostParentId, $creationDT = null, $latestChildDT = null) { protected function addDatabaseEntry($parentId, $topmostParentId, $creationDT = null, $latestChildDT = null, $objectId = null) {
if (is_null($creationDT)) { if (is_null($creationDT)) {
$creationDT = new \DateTime(); $creationDT = new \DateTime();
} }
if (is_null($latestChildDT)) { if (is_null($latestChildDT)) {
$latestChildDT = new \DateTime('yesterday'); $latestChildDT = new \DateTime('yesterday');
} }
if (is_null($objectId)) {
$objectId = 'file64';
}
$qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); $qb = $this->connection->getQueryBuilder();
$qb $qb
->insert('comments') ->insert('comments')
->values([ ->values([
@ -45,7 +54,7 @@ class ManagerTest extends TestCase {
'creation_timestamp' => $qb->createNamedParameter($creationDT, 'datetime'), 'creation_timestamp' => $qb->createNamedParameter($creationDT, 'datetime'),
'latest_child_timestamp' => $qb->createNamedParameter($latestChildDT, 'datetime'), 'latest_child_timestamp' => $qb->createNamedParameter($latestChildDT, 'datetime'),
'object_type' => $qb->createNamedParameter('files'), 'object_type' => $qb->createNamedParameter('files'),
'object_id' => $qb->createNamedParameter('file64'), 'object_id' => $qb->createNamedParameter($objectId),
]) ])
->execute(); ->execute();
@ -297,6 +306,52 @@ class ManagerTest extends TestCase {
$this->assertSame($amount, 4); $this->assertSame($amount, 4);
} }
public function testGetNumberOfUnreadCommentsForFolder() {
// 2 comment for 1111 with 1 before read marker
// 2 comments for 1112 with no read marker
// 1 comment for 1113 before read marker
// 1 comment for 1114 with no read marker
$this->addDatabaseEntry(0, 0, null, null, '1112');
for ($i = 1; $i < 5; $i++) {
$this->addDatabaseEntry(0, 0, null, null, '111' . $i);
}
$this->addDatabaseEntry(0, 0, (new \DateTime())->modify('-2 days'), null, '1111');
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->will($this->returnValue('comment_test'));
$manager = $this->getManager();
$manager->setReadMark('files', '1111', (new \DateTime())->modify('-1 days'), $user);
$manager->setReadMark('files', '1113', (new \DateTime()), $user);
$query = $this->connection->getQueryBuilder();
$query->insert('filecache')
->values([
'fileid' => $query->createParameter('fileid'),
'parent' => $query->createNamedParameter(1000),
'size' => $query->createNamedParameter(10),
'mtime' => $query->createNamedParameter(10),
'storage_mtime' => $query->createNamedParameter(10),
'path' => $query->createParameter('path'),
'path_hash' => $query->createParameter('path'),
]);
for ($i = 1111; $i < 1115; $i++) {
$query->setParameter('path', 'path_' . $i);
$query->setParameter('fileid', $i);
$query->execute();
}
$amount = $manager->getNumberOfUnreadCommentsForFolder(1000, $user);
$this->assertEquals([
'1111' => 1,
'1112' => 2,
'1114' => 1,
], $amount);
}
public function invalidCreateArgsProvider() { public function invalidCreateArgsProvider() {
return [ return [
['', 'aId-1', 'oType-1', 'oId-1'], ['', 'aId-1', 'oType-1', 'oId-1'],
@ -643,8 +698,12 @@ class ManagerTest extends TestCase {
->method('handle'); ->method('handle');
$manager = $this->getManager(); $manager = $this->getManager();
$manager->registerEventHandler(function () use ($handler1) {return $handler1; }); $manager->registerEventHandler(function () use ($handler1) {
$manager->registerEventHandler(function () use ($handler2) {return $handler2; }); return $handler1;
});
$manager->registerEventHandler(function () use ($handler2) {
return $handler2;
});
$comment = new Comment(); $comment = new Comment();
$comment $comment
@ -724,7 +783,9 @@ class ManagerTest extends TestCase {
public function testResolveDisplayNameDirtyResolver() { public function testResolveDisplayNameDirtyResolver() {
$manager = $this->getManager(); $manager = $this->getManager();
$planetClosure = function() { return null; }; $planetClosure = function () {
return null;
};
$manager->registerDisplayNameResolver('planet', $planetClosure); $manager->registerDisplayNameResolver('planet', $planetClosure);
$this->assertTrue(is_string($manager->resolveDisplayName('planet', 'neptune'))); $this->assertTrue(is_string($manager->resolveDisplayName('planet', 'neptune')));
@ -736,7 +797,9 @@ class ManagerTest extends TestCase {
public function testResolveDisplayNameInvalidType() { public function testResolveDisplayNameInvalidType() {
$manager = $this->getManager(); $manager = $this->getManager();
$planetClosure = function() { return null; }; $planetClosure = function () {
return null;
};
$manager->registerDisplayNameResolver('planet', $planetClosure); $manager->registerDisplayNameResolver('planet', $planetClosure);
$this->assertTrue(is_string($manager->resolveDisplayName(1337, 'neptune'))); $this->assertTrue(is_string($manager->resolveDisplayName(1337, 'neptune')));

View File

@ -1200,7 +1200,7 @@ class QueryBuilderTest extends \Test\TestCase {
public function dataGetColumnName() { public function dataGetColumnName() {
return [ return [
['column', '', '`column`'], ['column', '', '`column`'],
['column', 'a', 'a.`column`'], ['column', 'a', '`a`.`column`'],
]; ];
} }

View File

@ -65,7 +65,7 @@ class QuoteHelperTest extends \Test\TestCase {
public function dataQuoteColumnNames() { public function dataQuoteColumnNames() {
return [ return [
// Single case // Single case
['d.column', 'd.`column`'], ['d.column', '`d`.`column`'],
['column', '`column`'], ['column', '`column`'],
[new Literal('literal'), 'literal'], [new Literal('literal'), 'literal'],
[new Literal(1), '1'], [new Literal(1), '1'],