diff --git a/appinfo/info.xml b/appinfo/info.xml index 56a40f5..7d39139 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -56,11 +56,12 @@ The forum integrates seamlessly with your Nextcloud instance, using your existin - OCA\Forum\Cron\RebuildUserStatsTask + OCA\Forum\Cron\RebuildStatsTask OCA\Forum\Command\TestNotifier OCA\Forum\Command\RebuildUserStats + OCA\Forum\Command\SetRole diff --git a/lib/Command/RebuildUserStats.php b/lib/Command/RebuildUserStats.php index 19b58f5..264dbdb 100644 --- a/lib/Command/RebuildUserStats.php +++ b/lib/Command/RebuildUserStats.php @@ -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('Rebuilding user statistics for all users...'); - $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'])); diff --git a/lib/Cron/RebuildStatsTask.php b/lib/Cron/RebuildStatsTask.php new file mode 100644 index 0000000..80cd98c --- /dev/null +++ b/lib/Cron/RebuildStatsTask.php @@ -0,0 +1,54 @@ + +// 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'); + } +} diff --git a/lib/Cron/RebuildUserStatsTask.php b/lib/Cron/RebuildUserStatsTask.php deleted file mode 100644 index 5097a72..0000000 --- a/lib/Cron/RebuildUserStatsTask.php +++ /dev/null @@ -1,38 +0,0 @@ - -// 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'], - ]); - } -} diff --git a/lib/Listener/UserEventListener.php b/lib/Listener/UserEventListener.php index 3c77516..32f5a80 100644 --- a/lib/Listener/UserEventListener.php +++ b/lib/Listener/UserEventListener.php @@ -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}", [ diff --git a/lib/Migration/Version2Date20251114222614.php b/lib/Migration/Version2Date20251114222614.php index d136a01..af274a6 100644 --- a/lib/Migration/Version2Date20251114222614.php +++ b/lib/Migration/Version2Date20251114222614.php @@ -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'])); diff --git a/lib/Service/UserStatsService.php b/lib/Service/StatsService.php similarity index 58% rename from lib/Service/UserStatsService.php rename to lib/Service/StatsService.php index 2d4203f..fb03d16 100644 --- a/lib/Service/UserStatsService.php +++ b/lib/Service/StatsService.php @@ -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(); + } }