feat: weekly task now calculates category/thread post counts

This commit is contained in:
2025-11-20 11:18:41 +02:00
parent 96a42525d3
commit 84edf8ecbe
7 changed files with 191 additions and 60 deletions

View File

@@ -56,11 +56,12 @@ The forum integrates seamlessly with your Nextcloud instance, using your existin
<nextcloud min-version="29" max-version="33"/>
</dependencies>
<background-jobs>
<job>OCA\Forum\Cron\RebuildUserStatsTask</job>
<job>OCA\Forum\Cron\RebuildStatsTask</job>
</background-jobs>
<commands>
<command>OCA\Forum\Command\TestNotifier</command>
<command>OCA\Forum\Command\RebuildUserStats</command>
<command>OCA\Forum\Command\SetRole</command>
</commands>
<navigations>
<navigation role="all">

View File

@@ -7,14 +7,14 @@ declare(strict_types=1);
namespace OCA\Forum\Command;
use OCA\Forum\Service\UserStatsService;
use OCA\Forum\Service\StatsService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RebuildUserStats extends Command {
public function __construct(
private UserStatsService $userStatsService,
private StatsService $statsService,
) {
parent::__construct();
}
@@ -28,7 +28,7 @@ class RebuildUserStats extends Command {
protected function execute(InputInterface $input, OutputInterface $output): int {
$output->writeln('<info>Rebuilding user statistics for all users...</info>');
$result = $this->userStatsService->createStatsForAllUsers();
$result = $this->statsService->rebuildAllUserStats();
$output->writeln(sprintf('Processed %d users', $result['users']));
$output->writeln(sprintf('Created %d new user stats', $result['created']));

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Forum\Cron;
use OCA\Forum\Service\StatsService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;
class RebuildStatsTask extends TimedJob {
public function __construct(
ITimeFactory $time,
private StatsService $statsService,
private LoggerInterface $logger,
) {
parent::__construct($time);
// Run once a week (604800 seconds = 7 days)
$this->setInterval(604800);
}
protected function run($arguments): void {
$this->logger->info('Starting weekly stats rebuild');
// Rebuild user stats
$userResult = $this->statsService->rebuildAllUserStats();
$this->logger->info('User stats rebuild completed', [
'users' => $userResult['users'],
'created' => $userResult['created'],
'updated' => $userResult['updated'],
]);
// Rebuild category stats
$categoryResult = $this->statsService->rebuildAllCategoryStats();
$this->logger->info('Category stats rebuild completed', [
'categories' => $categoryResult['categories'],
'updated' => $categoryResult['updated'],
]);
// Rebuild thread stats
$threadResult = $this->statsService->rebuildAllThreadStats();
$this->logger->info('Thread stats rebuild completed', [
'threads' => $threadResult['threads'],
'updated' => $threadResult['updated'],
]);
$this->logger->info('Weekly stats rebuild completed');
}
}

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Forum\Cron;
use OCA\Forum\Service\UserStatsService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;
class RebuildUserStatsTask extends TimedJob {
public function __construct(
ITimeFactory $time,
private UserStatsService $userStatsService,
private LoggerInterface $logger,
) {
parent::__construct($time);
// Run once a week (604800 seconds = 7 days)
$this->setInterval(604800);
}
protected function run($arguments): void {
$this->logger->info('Starting weekly user stats rebuild for all users');
$result = $this->userStatsService->createStatsForAllUsers();
$this->logger->info('User stats rebuild completed', [
'users' => $result['users'],
'created' => $result['created'],
'updated' => $result['updated'],
]);
}
}

View File

@@ -8,7 +8,7 @@ declare(strict_types=1);
namespace OCA\Forum\Listener;
use OCA\Forum\Db\UserStatsMapper;
use OCA\Forum\Service\UserStatsService;
use OCA\Forum\Service\StatsService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserCreatedEvent;
@@ -21,7 +21,7 @@ use Psr\Log\LoggerInterface;
class UserEventListener implements IEventListener {
public function __construct(
private UserStatsMapper $userStatsMapper,
private UserStatsService $userStatsService,
private StatsService $statsService,
private LoggerInterface $logger,
) {
}
@@ -40,7 +40,7 @@ class UserEventListener implements IEventListener {
try {
// Create user stats with zero counts for new user
$this->userStatsService->rebuildUserStats($userId);
$this->statsService->rebuildUserStats($userId);
$this->logger->info("Created user stats for new Nextcloud user: {$userId}");
} catch (\Exception $ex) {
$this->logger->error("Failed to create user stats for new user: {$userId}", [

View File

@@ -8,14 +8,14 @@ declare(strict_types=1);
namespace OCA\Forum\Migration;
use Closure;
use OCA\Forum\Service\UserStatsService;
use OCA\Forum\Service\StatsService;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2Date20251114222614 extends SimpleMigrationStep {
public function __construct(
private UserStatsService $userStatsService,
private StatsService $statsService,
) {
}
@@ -108,7 +108,7 @@ class Version2Date20251114222614 extends SimpleMigrationStep {
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
$output->info('Creating user statistics for all users...');
$result = $this->userStatsService->createStatsForAllUsers();
$result = $this->statsService->rebuildAllUserStats();
$output->info(sprintf('Processed %d users', $result['users']));
$output->info(sprintf('Created %d new user stats', $result['created']));

View File

@@ -11,7 +11,7 @@ use OCP\IDBConnection;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class UserStatsService {
class StatsService {
public function __construct(
private IDBConnection $db,
private IUserManager $userManager,
@@ -24,7 +24,7 @@ class UserStatsService {
*
* @return array{users: int, updated: int, created: int} Statistics about the creation
*/
public function createStatsForAllUsers(): array {
public function rebuildAllUserStats(): array {
// Get all user IDs from Nextcloud
$users = [];
$this->userManager->callForAllUsers(function ($user) use (&$users) {
@@ -50,16 +50,6 @@ class UserStatsService {
];
}
/**
* Rebuild user statistics from actual post and thread counts
*
* @return array{users: int, updated: int, created: int} Statistics about the rebuild
*/
public function rebuildAllUserStats(): array {
// Delegate to createStatsForAllUsers which processes all Nextcloud users
return $this->createStatsForAllUsers();
}
/**
* Rebuild statistics for a single user
*
@@ -170,4 +160,128 @@ class UserStatsService {
}
}
}
/**
* Rebuild thread and post counts for all categories
*
* @return array{categories: int, updated: int} Statistics about the rebuild
*/
public function rebuildAllCategoryStats(): array {
// Get all category IDs
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from('forum_categories');
$result = $qb->executeQuery();
$categoryIds = [];
while ($row = $result->fetch()) {
$categoryIds[] = (int)$row['id'];
}
$result->closeCursor();
$updated = 0;
foreach ($categoryIds as $categoryId) {
$this->rebuildCategoryStats($categoryId);
$updated++;
}
return [
'categories' => count($categoryIds),
'updated' => $updated,
];
}
/**
* Rebuild statistics for a single category
*
* @param int $categoryId The category ID to rebuild stats for
* @return void
*/
public function rebuildCategoryStats(int $categoryId): void {
// Count non-deleted threads in this category
$threadQb = $this->db->getQueryBuilder();
$threadQb->select($threadQb->func()->count('*', 'count'))
->from('forum_threads')
->where($threadQb->expr()->eq('category_id', $threadQb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
->andWhere($threadQb->expr()->isNull('deleted_at'));
$threadResult = $threadQb->executeQuery();
$threadCount = (int)($threadResult->fetchOne() ?? 0);
$threadResult->closeCursor();
// Count non-deleted posts in non-deleted threads in this category
$postQb = $this->db->getQueryBuilder();
$postQb->select($postQb->func()->count('*', 'count'))
->from('forum_posts', 'p')
->innerJoin('p', 'forum_threads', 't', $postQb->expr()->eq('p.thread_id', 't.id'))
->where($postQb->expr()->eq('t.category_id', $postQb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
->andWhere($postQb->expr()->isNull('p.deleted_at'))
->andWhere($postQb->expr()->isNull('t.deleted_at'));
$postResult = $postQb->executeQuery();
$postCount = (int)($postResult->fetchOne() ?? 0);
$postResult->closeCursor();
// Update category stats
$updateQb = $this->db->getQueryBuilder();
$updateQb->update('forum_categories')
->set('thread_count', $updateQb->createNamedParameter($threadCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
->set('post_count', $updateQb->createNamedParameter($postCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
->set('updated_at', $updateQb->createNamedParameter(time(), \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)));
$updateQb->executeStatement();
}
/**
* Rebuild post counts for all threads
*
* @return array{threads: int, updated: int} Statistics about the rebuild
*/
public function rebuildAllThreadStats(): array {
// Get all non-deleted thread IDs
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from('forum_threads')
->where($qb->expr()->isNull('deleted_at'));
$result = $qb->executeQuery();
$threadIds = [];
while ($row = $result->fetch()) {
$threadIds[] = (int)$row['id'];
}
$result->closeCursor();
$updated = 0;
foreach ($threadIds as $threadId) {
$this->rebuildThreadStats($threadId);
$updated++;
}
return [
'threads' => count($threadIds),
'updated' => $updated,
];
}
/**
* Rebuild statistics for a single thread
*
* @param int $threadId The thread ID to rebuild stats for
* @return void
*/
public function rebuildThreadStats(int $threadId): void {
// Count non-deleted posts in this thread
$postQb = $this->db->getQueryBuilder();
$postQb->select($postQb->func()->count('*', 'count'))
->from('forum_posts')
->where($postQb->expr()->eq('thread_id', $postQb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
->andWhere($postQb->expr()->isNull('deleted_at'));
$postResult = $postQb->executeQuery();
$postCount = (int)($postResult->fetchOne() ?? 0);
$postResult->closeCursor();
// Update thread stats
$updateQb = $this->db->getQueryBuilder();
$updateQb->update('forum_threads')
->set('post_count', $updateQb->createNamedParameter($postCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
->set('updated_at', $updateQb->createNamedParameter(time(), \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)));
$updateQb->executeStatement();
}
}