diff --git a/lib/Command/RebuildAllStats.php b/lib/Command/RebuildAllStats.php index 4f75377..4cd13f3 100644 --- a/lib/Command/RebuildAllStats.php +++ b/lib/Command/RebuildAllStats.php @@ -40,8 +40,8 @@ class RebuildAllStats extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Starting full stats rebuild...'); - // Rebuild user stats - $output->writeln('Rebuilding user stats...'); + // Rebuild forum users + $output->writeln('Rebuilding forum users...'); $userResult = $this->statsService->rebuildAllUserStats(); $output->writeln(sprintf( ' Users processed: %d, created: %d, updated: %d', diff --git a/lib/Command/RebuildUserStats.php b/lib/Command/RebuildUserStats.php index 264dbdb..d2ae5b6 100644 --- a/lib/Command/RebuildUserStats.php +++ b/lib/Command/RebuildUserStats.php @@ -31,8 +31,8 @@ class RebuildUserStats extends Command { $result = $this->statsService->rebuildAllUserStats(); $output->writeln(sprintf('Processed %d users', $result['users'])); - $output->writeln(sprintf('Created %d new user stats', $result['created'])); - $output->writeln(sprintf('Updated %d existing user stats', $result['updated'])); + $output->writeln(sprintf('Created %d new forum users', $result['created'])); + $output->writeln(sprintf('Updated %d existing forum users', $result['updated'])); $output->writeln('User statistics rebuilt successfully!'); return 0; diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index b1fa72a..95b4d6b 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -9,10 +9,10 @@ namespace OCA\Forum\Controller; use OCA\Forum\Attribute\RequirePermission; use OCA\Forum\Db\CategoryMapper; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Db\UserRoleMapper; -use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\AdminSettingsService; use OCA\Forum\Service\UserService; use OCP\AppFramework\Http; @@ -30,7 +30,7 @@ class AdminController extends OCSController { public function __construct( string $appName, IRequest $request, - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private UserService $userService, private ThreadMapper $threadMapper, private PostMapper $postMapper, @@ -62,20 +62,20 @@ class AdminController extends OCSController { } // Get total counts - $totalUsers = $this->userStatsMapper->countAll(); + $totalUsers = $this->forumUserMapper->countAll(); $totalThreads = $this->threadMapper->countAll(); $totalPosts = $this->postMapper->countAll(); $totalCategories = $this->categoryMapper->countAll(); // Get recent activity (last 7 days) $weekAgo = time() - (7 * 24 * 60 * 60); - $recentUsers = $this->userStatsMapper->countSince($weekAgo); + $recentUsers = $this->forumUserMapper->countSince($weekAgo); $recentThreads = $this->threadMapper->countSince($weekAgo); $recentPosts = $this->postMapper->countSince($weekAgo); // Get top contributors (users with most posts) - $topContributorsAllTime = $this->userStatsMapper->getTopContributors(5); - $topContributorsRecent = $this->userStatsMapper->getTopContributorsSince($weekAgo, 5); + $topContributorsAllTime = $this->forumUserMapper->getTopContributors(5); + $topContributorsRecent = $this->forumUserMapper->getTopContributorsSince($weekAgo, 5); return new DataResponse([ 'totals' => [ @@ -110,11 +110,11 @@ class AdminController extends OCSController { #[ApiRoute(verb: 'GET', url: '/api/admin/users')] public function users(): DataResponse { try { - // Get all user stats indexed by userId for quick lookup - $allStats = $this->userStatsMapper->findAll(); - $statsByUserId = []; - foreach ($allStats as $stats) { - $statsByUserId[$stats->getUserId()] = $stats; + // Get all forum users indexed by userId for quick lookup + $allForumUsers = $this->forumUserMapper->findAll(); + $forumUsersByUserId = []; + foreach ($allForumUsers as $forumUser) { + $forumUsersByUserId[$forumUser->getUserId()] = $forumUser; } // Collect all user IDs first @@ -126,20 +126,20 @@ class AdminController extends OCSController { // Enrich all users at once for performance (includes roles) $enrichedUserData = $this->userService->enrichMultipleUsers($userIds); - // Build final user list with forum stats + // Build final user list with forum user data $enrichedUsers = []; foreach ($userIds as $userId) { $userInfo = $enrichedUserData[$userId]; - $stats = $statsByUserId[$userId] ?? null; + $forumUser = $forumUsersByUserId[$userId] ?? null; $userData = [ 'userId' => $userId, 'displayName' => $userInfo['displayName'], - 'postCount' => $stats ? $stats->getPostCount() : 0, - 'threadCount' => $stats ? $stats->getThreadCount() : 0, - 'createdAt' => $stats ? $stats->getCreatedAt() : 0, - 'updatedAt' => $stats ? $stats->getUpdatedAt() : 0, - 'deletedAt' => $stats ? $stats->getDeletedAt() : null, + 'postCount' => $forumUser ? $forumUser->getPostCount() : 0, + 'threadCount' => $forumUser ? $forumUser->getThreadCount() : 0, + 'createdAt' => $forumUser ? $forumUser->getCreatedAt() : 0, + 'updatedAt' => $forumUser ? $forumUser->getUpdatedAt() : 0, + 'deletedAt' => $forumUser ? $forumUser->getDeletedAt() : null, 'isDeleted' => $userInfo['isDeleted'], 'roles' => $userInfo['roles'], ]; diff --git a/lib/Controller/ForumUserController.php b/lib/Controller/ForumUserController.php index 84ef362..245377b 100644 --- a/lib/Controller/ForumUserController.php +++ b/lib/Controller/ForumUserController.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace OCA\Forum\Controller; -use OCA\Forum\Db\UserStatsMapper; +use OCA\Forum\Db\ForumUserMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -20,14 +20,14 @@ use OCP\IUserSession; use Psr\Log\LoggerInterface; /** - * Controller for user statistics - * Note: User stats are automatically created on first post/thread + * Controller for forum users + * Note: Forum users are automatically created on first post/thread */ class ForumUserController extends OCSController { public function __construct( string $appName, IRequest $request, - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private IUserSession $userSession, private LoggerInterface $logger, ) { @@ -35,34 +35,34 @@ class ForumUserController extends OCSController { } /** - * Get all user statistics + * Get all forum users * * @return DataResponse>, array{}> * - * 200: User statistics returned + * 200: Forum users returned */ #[NoAdminRequired] #[PublicPage] #[ApiRoute(verb: 'GET', url: '/api/users')] public function index(): DataResponse { try { - $users = $this->userStatsMapper->findAll(); + $users = $this->forumUserMapper->findAll(); return new DataResponse(array_map(fn ($u) => $u->jsonSerialize(), $users)); } catch (\Exception $e) { - $this->logger->error('Error fetching user stats: ' . $e->getMessage()); - return new DataResponse(['error' => 'Failed to fetch user stats'], Http::STATUS_INTERNAL_SERVER_ERROR); + $this->logger->error('Error fetching forum users: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to fetch forum users'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** - * Get user statistics by Nextcloud user ID - * Special case: use "me" to get current user stats + * Get forum user by Nextcloud user ID + * Special case: use "me" to get current forum user * * @param string $userId Nextcloud user ID or "me" for current user * @return DataResponse, array{}>|DataResponse * - * 200: User stats returned - * 404: User has no stats (hasn't posted yet) or guest user accessing "me" + * 200: Forum user returned + * 404: Forum user not found (user has not posted yet) or guest user accessing "me" */ #[NoAdminRequired] #[PublicPage] @@ -73,40 +73,40 @@ class ForumUserController extends OCSController { if ($userId === 'me') { $currentUser = $this->userSession->getUser(); if (!$currentUser) { - // Guest users have no stats - return 404 like a user who hasn't posted yet - return new DataResponse(['error' => 'User stats not found'], Http::STATUS_NOT_FOUND); + // Guest users have no forum profile - return 404 like a user who hasn't posted yet + return new DataResponse(['error' => 'Forum user not found'], Http::STATUS_NOT_FOUND); } $userId = $currentUser->getUID(); } - $stats = $this->userStatsMapper->find($userId); - return new DataResponse($stats->jsonSerialize()); + $forumUser = $this->forumUserMapper->find($userId); + return new DataResponse($forumUser->jsonSerialize()); } catch (DoesNotExistException $e) { - return new DataResponse(['error' => 'User stats not found'], Http::STATUS_NOT_FOUND); + return new DataResponse(['error' => 'Forum user not found'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { - $this->logger->error('Error fetching user stats: ' . $e->getMessage()); - return new DataResponse(['error' => 'Failed to fetch user stats'], Http::STATUS_INTERNAL_SERVER_ERROR); + $this->logger->error('Error fetching forum user: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to fetch forum user'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** - * Create user stats - * Note: This is typically not needed as stats are auto-created on first post + * Create forum user + * Note: This is typically not needed as forum users are auto-created on first post * * @param string $userId Nextcloud user ID * @return DataResponse, array{}> * - * 201: User stats created + * 201: Forum user created */ #[NoAdminRequired] #[ApiRoute(verb: 'POST', url: '/api/users')] public function create(string $userId): DataResponse { try { - $stats = $this->userStatsMapper->createOrUpdate($userId); - return new DataResponse($stats->jsonSerialize(), Http::STATUS_CREATED); + $forumUser = $this->forumUserMapper->createOrUpdate($userId); + return new DataResponse($forumUser->jsonSerialize(), Http::STATUS_CREATED); } catch (\Exception $e) { - $this->logger->error('Error creating user stats: ' . $e->getMessage()); - return new DataResponse(['error' => 'Failed to create user stats'], Http::STATUS_INTERNAL_SERVER_ERROR); + $this->logger->error('Error creating forum user: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to create forum user'], Http::STATUS_INTERNAL_SERVER_ERROR); } } } diff --git a/lib/Controller/PostController.php b/lib/Controller/PostController.php index 540925a..c1edc11 100644 --- a/lib/Controller/PostController.php +++ b/lib/Controller/PostController.php @@ -10,12 +10,12 @@ namespace OCA\Forum\Controller; use OCA\Forum\Attribute\RequirePermission; use OCA\Forum\Db\BBCodeMapper; use OCA\Forum\Db\CategoryMapper; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\Post; use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\ReactionMapper; use OCA\Forum\Db\ReadMarkerMapper; use OCA\Forum\Db\ThreadMapper; -use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\BBCodeService; use OCA\Forum\Service\NotificationService; use OCA\Forum\Service\PermissionService; @@ -39,7 +39,7 @@ class PostController extends OCSController { private PostMapper $postMapper, private ThreadMapper $threadMapper, private CategoryMapper $categoryMapper, - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private ReactionMapper $reactionMapper, private BBCodeService $bbCodeService, private BBCodeMapper $bbCodeMapper, @@ -364,12 +364,12 @@ class PostController extends OCSController { // Don't fail the request if thread update fails } - // Update user stats post count (auto-creates stats if needed) + // Update forum user post count (auto-creates forum user if needed) try { - $this->userStatsMapper->incrementPostCount($user->getUID()); + $this->forumUserMapper->incrementPostCount($user->getUID()); } catch (\Exception $e) { - $this->logger->warning('Failed to update user stats post count: ' . $e->getMessage()); - // Don't fail the request if stats update fails + $this->logger->warning('Failed to update forum user post count: ' . $e->getMessage()); + // Don't fail the request if forum user update fails } // Update the category's post count @@ -506,18 +506,18 @@ class PostController extends OCSController { // Don't fail the request if thread update fails } - // Update user stats - decrement post count, and thread count if it's the first post + // Update forum user - decrement post count, and thread count if it's the first post try { if ($post->getIsFirstPost()) { // First post: decrement thread count only - $this->userStatsMapper->decrementThreadCount($post->getAuthorId()); + $this->forumUserMapper->decrementThreadCount($post->getAuthorId()); } else { // Reply post: decrement post count only - $this->userStatsMapper->decrementPostCount($post->getAuthorId()); + $this->forumUserMapper->decrementPostCount($post->getAuthorId()); } } catch (\Exception $e) { - $this->logger->warning('Failed to update user stats after post deletion: ' . $e->getMessage()); - // Don't fail the request if stats update fails + $this->logger->warning('Failed to update forum user after post deletion: ' . $e->getMessage()); + // Don't fail the request if forum user update fails } // Update category post count (only for reply posts, not first posts) diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php index 8c887fe..c3e154b 100644 --- a/lib/Controller/ThreadController.php +++ b/lib/Controller/ThreadController.php @@ -9,12 +9,12 @@ namespace OCA\Forum\Controller; use OCA\Forum\Attribute\RequirePermission; use OCA\Forum\Db\CategoryMapper; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\Post; use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\Thread; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Db\ThreadSubscriptionMapper; -use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\PermissionService; use OCA\Forum\Service\ThreadEnrichmentService; use OCA\Forum\Service\UserPreferencesService; @@ -37,7 +37,7 @@ class ThreadController extends OCSController { private ThreadMapper $threadMapper, private CategoryMapper $categoryMapper, private PostMapper $postMapper, - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private ThreadSubscriptionMapper $threadSubscriptionMapper, private ThreadEnrichmentService $threadEnrichmentService, private UserPreferencesService $userPreferencesService, @@ -331,11 +331,11 @@ class ThreadController extends OCSController { $this->logger->warning('Failed to update category counts: ' . $e->getMessage()); } - // Update user stats (thread count only, first post doesn't count) + // Update forum user (thread count only, first post doesn't count) try { - $this->userStatsMapper->incrementThreadCount($user->getUID()); + $this->forumUserMapper->incrementThreadCount($user->getUID()); } catch (\Exception $e) { - $this->logger->warning('Failed to update user stats: ' . $e->getMessage()); + $this->logger->warning('Failed to update forum user: ' . $e->getMessage()); } // Auto-subscribe the thread creator to receive notifications (if preference is enabled) @@ -603,16 +603,16 @@ class ThreadController extends OCSController { // Don't fail the request if category update fails } - // Update author's user stats (decrement thread count and all posts in this thread) + // Update author's forum user (decrement thread count and all posts in this thread) try { - $this->userStatsMapper->decrementThreadCount($thread->getAuthorId()); + $this->forumUserMapper->decrementThreadCount($thread->getAuthorId()); // Decrement post count by the number of posts in this thread if ($thread->getPostCount() > 0) { - $this->userStatsMapper->decrementPostCount($thread->getAuthorId(), $thread->getPostCount()); + $this->forumUserMapper->decrementPostCount($thread->getAuthorId(), $thread->getPostCount()); } } catch (\Exception $e) { - $this->logger->warning('Failed to update user stats after thread deletion: ' . $e->getMessage()); - // Don't fail the request if stats update fails + $this->logger->warning('Failed to update forum user after thread deletion: ' . $e->getMessage()); + // Don't fail the request if forum user update fails } return new DataResponse([ diff --git a/lib/Cron/RebuildStatsTask.php b/lib/Cron/RebuildStatsTask.php index 80cd98c..fd8a013 100644 --- a/lib/Cron/RebuildStatsTask.php +++ b/lib/Cron/RebuildStatsTask.php @@ -27,9 +27,9 @@ class RebuildStatsTask extends TimedJob { protected function run($arguments): void { $this->logger->info('Starting weekly stats rebuild'); - // Rebuild user stats + // Rebuild forum users $userResult = $this->statsService->rebuildAllUserStats(); - $this->logger->info('User stats rebuild completed', [ + $this->logger->info('Forum users rebuild completed', [ 'users' => $userResult['users'], 'created' => $userResult['created'], 'updated' => $userResult['updated'], diff --git a/lib/Db/UserStats.php b/lib/Db/ForumUser.php similarity index 97% rename from lib/Db/UserStats.php rename to lib/Db/ForumUser.php index 22fb4b1..dda1633 100644 --- a/lib/Db/UserStats.php +++ b/lib/Db/ForumUser.php @@ -30,7 +30,7 @@ use OCP\AppFramework\Db\Entity; * @method int getUpdatedAt() * @method void setUpdatedAt(int $updatedAt) */ -class UserStats extends Entity implements JsonSerializable { +class ForumUser extends Entity implements JsonSerializable { public $id; protected string $userId = ''; protected int $postCount = 0; diff --git a/lib/Db/UserStatsMapper.php b/lib/Db/ForumUserMapper.php similarity index 77% rename from lib/Db/UserStatsMapper.php rename to lib/Db/ForumUserMapper.php index a9befe9..ac97410 100644 --- a/lib/Db/UserStatsMapper.php +++ b/lib/Db/ForumUserMapper.php @@ -14,22 +14,22 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; /** - * @template-extends QBMapper + * @template-extends QBMapper */ -class UserStatsMapper extends QBMapper { +class ForumUserMapper extends QBMapper { public function __construct( IDBConnection $db, ) { - parent::__construct($db, Application::tableName('forum_user_stats'), UserStats::class); + parent::__construct($db, Application::tableName('forum_users'), ForumUser::class); } /** - * Find user stats by user ID + * Find forum user by user ID * * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws DoesNotExistException */ - public function find(string $userId): UserStats { + public function find(string $userId): ForumUser { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) @@ -40,9 +40,9 @@ class UserStatsMapper extends QBMapper { } /** - * Find all user stats + * Find all forum users * - * @return array + * @return array */ public function findAll(): array { $qb = $this->db->getQueryBuilder(); @@ -53,10 +53,10 @@ class UserStatsMapper extends QBMapper { } /** - * Find user stats by multiple user IDs + * Find forum users by multiple user IDs * * @param array $userIds - * @return array + * @return array */ public function findByUserIds(array $userIds): array { if (empty($userIds)) { @@ -71,45 +71,45 @@ class UserStatsMapper extends QBMapper { } /** - * Create or update user stats (upsert pattern) - * This is used when we need to ensure stats exist for a user + * Create or update forum user (upsert pattern) + * This is used when we need to ensure a forum user record exists */ - public function createOrUpdate(string $userId): UserStats { + public function createOrUpdate(string $userId): ForumUser { try { return $this->find($userId); } catch (DoesNotExistException $e) { - $stats = new UserStats(); - $stats->setUserId($userId); - $stats->setPostCount(0); - $stats->setThreadCount(0); - $stats->setCreatedAt(time()); - $stats->setUpdatedAt(time()); - /** @var UserStats */ - return $this->insert($stats); + $user = new ForumUser(); + $user->setUserId($userId); + $user->setPostCount(0); + $user->setThreadCount(0); + $user->setCreatedAt(time()); + $user->setUpdatedAt(time()); + /** @var ForumUser */ + return $this->insert($user); } } /** * Increment post count for a user - * Auto-creates stats if user doesn't exist + * Auto-creates record if user doesn't exist */ public function incrementPostCount(string $userId, int $amount = 1): void { - $stats = $this->createOrUpdate($userId); - $stats->setPostCount($stats->getPostCount() + $amount); - $stats->setLastPostAt(time()); - $stats->setUpdatedAt(time()); - $this->update($stats); + $user = $this->createOrUpdate($userId); + $user->setPostCount($user->getPostCount() + $amount); + $user->setLastPostAt(time()); + $user->setUpdatedAt(time()); + $this->update($user); } /** * Increment thread count for a user - * Auto-creates stats if user doesn't exist + * Auto-creates record if user doesn't exist */ public function incrementThreadCount(string $userId, int $amount = 1): void { - $stats = $this->createOrUpdate($userId); - $stats->setThreadCount($stats->getThreadCount() + $amount); - $stats->setUpdatedAt(time()); - $this->update($stats); + $user = $this->createOrUpdate($userId); + $user->setThreadCount($user->getThreadCount() + $amount); + $user->setUpdatedAt(time()); + $this->update($user); } /** @@ -117,12 +117,12 @@ class UserStatsMapper extends QBMapper { */ public function decrementPostCount(string $userId, int $amount = 1): void { try { - $stats = $this->find($userId); - $stats->setPostCount(max(0, $stats->getPostCount() - $amount)); - $stats->setUpdatedAt(time()); - $this->update($stats); + $user = $this->find($userId); + $user->setPostCount(max(0, $user->getPostCount() - $amount)); + $user->setUpdatedAt(time()); + $this->update($user); } catch (DoesNotExistException $e) { - // User stats don't exist, nothing to decrement + // User record doesn't exist, nothing to decrement } } @@ -131,12 +131,12 @@ class UserStatsMapper extends QBMapper { */ public function decrementThreadCount(string $userId, int $amount = 1): void { try { - $stats = $this->find($userId); - $stats->setThreadCount(max(0, $stats->getThreadCount() - $amount)); - $stats->setUpdatedAt(time()); - $this->update($stats); + $user = $this->find($userId); + $user->setThreadCount(max(0, $user->getThreadCount() - $amount)); + $user->setUpdatedAt(time()); + $this->update($user); } catch (DoesNotExistException $e) { - // User stats don't exist, nothing to decrement + // User record doesn't exist, nothing to decrement } } @@ -291,20 +291,20 @@ class UserStatsMapper extends QBMapper { */ public function markDeleted(string $userId): void { try { - $stats = $this->find($userId); - $stats->setDeletedAt(time()); - $stats->setUpdatedAt(time()); - $this->update($stats); + $user = $this->find($userId); + $user->setDeletedAt(time()); + $user->setUpdatedAt(time()); + $this->update($user); } catch (DoesNotExistException $e) { - // User has no stats, create a record marking them as deleted - $stats = new UserStats(); - $stats->setUserId($userId); - $stats->setPostCount(0); - $stats->setThreadCount(0); - $stats->setDeletedAt(time()); - $stats->setCreatedAt(time()); - $stats->setUpdatedAt(time()); - $this->insert($stats); + // User has no record, create one marking them as deleted + $user = new ForumUser(); + $user->setUserId($userId); + $user->setPostCount(0); + $user->setThreadCount(0); + $user->setDeletedAt(time()); + $user->setCreatedAt(time()); + $user->setUpdatedAt(time()); + $this->insert($user); } } } diff --git a/lib/Listener/UserEventListener.php b/lib/Listener/UserEventListener.php index fc3792f..7c3ff7c 100644 --- a/lib/Listener/UserEventListener.php +++ b/lib/Listener/UserEventListener.php @@ -7,8 +7,8 @@ declare(strict_types=1); namespace OCA\Forum\Listener; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\RoleMapper; -use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\StatsService; use OCA\Forum\Service\UserRoleService; use OCP\AppFramework\Db\DoesNotExistException; @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; */ class UserEventListener implements IEventListener { public function __construct( - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private StatsService $statsService, private UserRoleService $userRoleService, private RoleMapper $roleMapper, @@ -44,11 +44,11 @@ class UserEventListener implements IEventListener { $userId = $user->getUID(); try { - // Create user stats with zero counts for new user + // Create forum user record with zero counts for new user $this->statsService->rebuildUserStats($userId); - $this->logger->info("Created user stats for new Nextcloud user: {$userId}"); + $this->logger->info("Created forum user record for new Nextcloud user: {$userId}"); } catch (\Exception $ex) { - $this->logger->error("Failed to create user stats for new user: {$userId}", [ + $this->logger->error("Failed to create forum user record for new user: {$userId}", [ 'exception' => $ex->getMessage(), ]); } @@ -72,13 +72,13 @@ class UserEventListener implements IEventListener { $userId = $user->getUID(); try { - // Soft delete: mark stats as deleted if they exist - // Stats only exist if user has posted, so this may not find anything - $this->userStatsMapper->markDeleted($userId); - $this->logger->info("Soft-deleted user stats for Nextcloud user: {$userId}"); + // Soft delete: mark forum user as deleted if record exists + // Record only exists if user has posted, so this may not find anything + $this->forumUserMapper->markDeleted($userId); + $this->logger->info("Soft-deleted forum user record for Nextcloud user: {$userId}"); } catch (\Exception $ex) { - // If stats don't exist, that's fine - user never posted - $this->logger->debug("No user stats found to delete for Nextcloud user: {$userId}"); + // If record doesn't exist, that's fine - user never posted + $this->logger->debug("No forum user record found to delete for Nextcloud user: {$userId}"); } } } diff --git a/lib/Migration/SeedHelper.php b/lib/Migration/SeedHelper.php index dfe8628..98fa5e3 100644 --- a/lib/Migration/SeedHelper.php +++ b/lib/Migration/SeedHelper.php @@ -1025,10 +1025,10 @@ class SeedHelper { } } - // Create user stats for the admin user if it does not exist + // Create forum user for the admin user if it does not exist $qb = $db->getQueryBuilder(); $qb->select('user_id') - ->from('forum_user_stats') + ->from('forum_users') ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($adminUserId))); $result = $qb->executeQuery(); $statsExists = $result->fetch(); @@ -1036,7 +1036,7 @@ class SeedHelper { if (!$statsExists) { $qb = $db->getQueryBuilder(); - $qb->insert('forum_user_stats') + $qb->insert('forum_users') ->values([ 'user_id' => $qb->createNamedParameter($adminUserId), 'post_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT), @@ -1049,7 +1049,7 @@ class SeedHelper { } else { // Update existing stats to increment thread count $qb = $db->getQueryBuilder(); - $qb->update('forum_user_stats') + $qb->update('forum_users') ->set('thread_count', $qb->func()->add('thread_count', $qb->createNamedParameter(1, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))) ->set('last_post_at', $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)) ->set('updated_at', $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)) diff --git a/lib/Migration/Version2Date20251114222614.php b/lib/Migration/Version2Date20251114222614.php index ec2c54e..b3b5c3b 100644 --- a/lib/Migration/Version2Date20251114222614.php +++ b/lib/Migration/Version2Date20251114222614.php @@ -8,14 +8,17 @@ declare(strict_types=1); namespace OCA\Forum\Migration; use Closure; -use OCA\Forum\Service\StatsService; use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUserManager; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; class Version2Date20251114222614 extends SimpleMigrationStep { public function __construct( - private StatsService $statsService, + private IDBConnection $db, + private IUserManager $userManager, ) { } @@ -108,30 +111,162 @@ class Version2Date20251114222614 extends SimpleMigrationStep { public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { $output->info('Creating user statistics for all users...'); - $result = $this->statsService->rebuildAllUserStats(); + $result = $this->rebuildAllUserStatsLegacy(); $output->info(sprintf('Processed %d users', $result['users'])); - $output->info(sprintf('Created %d new user stats', $result['created'])); - $output->info(sprintf('Updated %d existing user stats', $result['updated'])); + $output->info(sprintf('Created %d new forum users', $result['created'])); + $output->info(sprintf('Updated %d existing forum users', $result['updated'])); $output->info('User statistics created successfully!'); // Subscribe thread authors to their threads $this->subscribeAuthorsToThreads($output); } + /** + * Rebuild user stats using the old table name (forum_user_stats) + * This is needed because Version8 hasn't renamed the table yet + */ + private function rebuildAllUserStatsLegacy(): array { + // Get all user IDs from Nextcloud + $users = []; + $this->userManager->callForAllUsers(function ($user) use (&$users) { + $users[] = $user->getUID(); + }); + + $updated = 0; + $created = 0; + + foreach ($users as $userId) { + $wasCreated = $this->rebuildUserStatsLegacy($userId); + if ($wasCreated) { + $created++; + } else { + $updated++; + } + } + + return [ + 'users' => count($users), + 'updated' => $updated, + 'created' => $created, + ]; + } + + /** + * Rebuild stats for a single user using the old table name + */ + private function rebuildUserStatsLegacy(string $userId): bool { + // Count non-deleted threads created by this user + $threadQb = $this->db->getQueryBuilder(); + $threadQb->select($threadQb->func()->count('*', 'count')) + ->from('forum_threads') + ->where($threadQb->expr()->eq('author_id', $threadQb->createNamedParameter($userId))) + ->andWhere($threadQb->expr()->isNull('deleted_at')); + $threadResult = $threadQb->executeQuery(); + $threadCount = (int)($threadResult->fetchOne() ?? 0); + $threadResult->closeCursor(); + + // Count non-deleted posts created by this user (from non-deleted threads) + // Exclude is_first_post posts as they are counted as threads + $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('p.author_id', $postQb->createNamedParameter($userId))) + ->andWhere($postQb->expr()->isNull('p.deleted_at')) + ->andWhere($postQb->expr()->isNull('t.deleted_at')) + ->andWhere($postQb->expr()->eq('p.is_first_post', $postQb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); + $postResult = $postQb->executeQuery(); + $postCount = (int)($postResult->fetchOne() ?? 0); + $postResult->closeCursor(); + + // Get the timestamp of the last non-deleted post (from non-deleted threads) + $lastPostQb = $this->db->getQueryBuilder(); + $lastPostQb->select('p.created_at') + ->from('forum_posts', 'p') + ->innerJoin('p', 'forum_threads', 't', $lastPostQb->expr()->eq('p.thread_id', 't.id')) + ->where($lastPostQb->expr()->eq('p.author_id', $lastPostQb->createNamedParameter($userId))) + ->andWhere($lastPostQb->expr()->isNull('p.deleted_at')) + ->andWhere($lastPostQb->expr()->isNull('t.deleted_at')) + ->orderBy('p.created_at', 'DESC') + ->setMaxResults(1); + $lastPostResult = $lastPostQb->executeQuery(); + $lastPostAt = $lastPostResult->fetchOne(); + $lastPostResult->closeCursor(); + + // Check if forum user record already exists (using OLD table name) + $checkQb = $this->db->getQueryBuilder(); + $checkQb->select('user_id') + ->from('forum_user_stats') // OLD table name! + ->where($checkQb->expr()->eq('user_id', $checkQb->createNamedParameter($userId))); + $checkResult = $checkQb->executeQuery(); + $exists = $checkResult->fetch(); + $checkResult->closeCursor(); + + $timestamp = time(); + + if ($exists) { + // Update existing record (using OLD table name) + $updateQb = $this->db->getQueryBuilder(); + $updateQb->update('forum_user_stats') // OLD table name! + ->set('thread_count', $updateQb->createNamedParameter($threadCount, IQueryBuilder::PARAM_INT)) + ->set('post_count', $updateQb->createNamedParameter($postCount, IQueryBuilder::PARAM_INT)) + ->set('updated_at', $updateQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) + ->where($updateQb->expr()->eq('user_id', $updateQb->createNamedParameter($userId))); + + if ($lastPostAt) { + $updateQb->set('last_post_at', $updateQb->createNamedParameter((int)$lastPostAt, IQueryBuilder::PARAM_INT)); + } + + $updateQb->executeStatement(); + return false; + } else { + // Create new record (using OLD table name) + $insertQb = $this->db->getQueryBuilder(); + $insertQb->insert('forum_user_stats') // OLD table name! + ->values([ + 'user_id' => $insertQb->createNamedParameter($userId), + 'thread_count' => $insertQb->createNamedParameter($threadCount, IQueryBuilder::PARAM_INT), + 'post_count' => $insertQb->createNamedParameter($postCount, IQueryBuilder::PARAM_INT), + 'last_post_at' => $insertQb->createNamedParameter($lastPostAt ? (int)$lastPostAt : null, IQueryBuilder::PARAM_INT), + 'created_at' => $insertQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT), + 'updated_at' => $insertQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT), + ]); + + try { + $insertQb->executeStatement(); + return true; + } catch (\Exception $e) { + // If insert fails (race condition), try updating instead + $updateQb = $this->db->getQueryBuilder(); + $updateQb->update('forum_user_stats') // OLD table name! + ->set('thread_count', $updateQb->createNamedParameter($threadCount, IQueryBuilder::PARAM_INT)) + ->set('post_count', $updateQb->createNamedParameter($postCount, IQueryBuilder::PARAM_INT)) + ->set('updated_at', $updateQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) + ->where($updateQb->expr()->eq('user_id', $updateQb->createNamedParameter($userId))); + + if ($lastPostAt) { + $updateQb->set('last_post_at', $updateQb->createNamedParameter((int)$lastPostAt, IQueryBuilder::PARAM_INT)); + } + + $updateQb->executeStatement(); + return false; + } + } + } + /** * Subscribe all thread authors to their threads */ private function subscribeAuthorsToThreads(IOutput $output): void { $output->info('Subscribing thread authors to their threads...'); - $db = \OC::$server->get(\OCP\IDBConnection::class); $timestamp = time(); $subscribed = 0; try { // Get all threads with their authors - $qb = $db->getQueryBuilder(); + $qb = $this->db->getQueryBuilder(); $qb->select('id', 'author_id') ->from('forum_threads'); $result = $qb->executeQuery(); @@ -143,10 +278,10 @@ class Version2Date20251114222614 extends SimpleMigrationStep { $authorId = $thread['author_id']; // Check if author is already subscribed - $qb = $db->getQueryBuilder(); + $qb = $this->db->getQueryBuilder(); $qb->select('id') ->from('forum_thread_subs') - ->where($qb->expr()->eq('thread_id', $qb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))) + ->where($qb->expr()->eq('thread_id', $qb->createNamedParameter($threadId, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($authorId))); $result = $qb->executeQuery(); $exists = $result->fetch(); @@ -154,12 +289,12 @@ class Version2Date20251114222614 extends SimpleMigrationStep { if (!$exists) { // Subscribe the author to their thread - $qb = $db->getQueryBuilder(); + $qb = $this->db->getQueryBuilder(); $qb->insert('forum_thread_subs') ->values([ - 'thread_id' => $qb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT), + 'thread_id' => $qb->createNamedParameter($threadId, IQueryBuilder::PARAM_INT), 'user_id' => $qb->createNamedParameter($authorId), - 'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT), + 'created_at' => $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT), ]) ->executeStatement(); $subscribed++; diff --git a/lib/Migration/Version7Date20251124120000.php b/lib/Migration/Version7Date20251124120000.php index 15606a3..9cdcad2 100644 --- a/lib/Migration/Version7Date20251124120000.php +++ b/lib/Migration/Version7Date20251124120000.php @@ -78,7 +78,7 @@ class Version7Date20251124120000 extends SimpleMigrationStep { public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { $this->migrateConfigValues($output); $this->updateExistingRoleFlags($output); - SeedHelper::seedAll($output); + // Note: SeedHelper::seedAll() is called in Version9 after table rename } /** diff --git a/lib/Migration/Version9Date20251129000000.php b/lib/Migration/Version9Date20251129000000.php new file mode 100644 index 0000000..9cc576d --- /dev/null +++ b/lib/Migration/Version9Date20251129000000.php @@ -0,0 +1,65 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Forum\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Version 9 Migration: + * - Rename forum_user_stats to forum_users + * - Run seed (creates initial data if not exists) + */ +class Version9Date20251129000000 extends SimpleMigrationStep { + public function __construct( + private IDBConnection $db, + private IConfig $config, + ) { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + // Rename forum_user_stats to forum_users using raw SQL + // ISchemaWrapper doesn't have renameTable method + $platform = $this->db->getDatabasePlatform(); + $prefix = $this->config->getSystemValueString('dbtableprefix', 'oc_'); + + $oldTable = $prefix . 'forum_user_stats'; + $newTable = $prefix . 'forum_users'; + + // Check if old table exists and new table doesn't + $schema = $schemaClosure(); + if ($schema->hasTable('forum_user_stats') && !$schema->hasTable('forum_users')) { + $output->info('Renaming forum_user_stats to forum_users...'); + + // Use platform-specific rename syntax + $platformName = $platform->getName(); + if ($platformName === 'mysql' || $platformName === 'mariadb') { + $this->db->executeStatement("RENAME TABLE `{$oldTable}` TO `{$newTable}`"); + } elseif ($platformName === 'postgresql') { + $this->db->executeStatement("ALTER TABLE \"{$oldTable}\" RENAME TO \"{$newTable}\""); + } else { + // SQLite and others + $this->db->executeStatement("ALTER TABLE \"{$oldTable}\" RENAME TO \"{$newTable}\""); + } + + $output->info('Table renamed successfully'); + } + + // Run seed after table rename (SeedHelper uses forum_users table) + SeedHelper::seedAll($output); + } +} diff --git a/lib/Service/StatsService.php b/lib/Service/StatsService.php index 1e13709..58d8863 100644 --- a/lib/Service/StatsService.php +++ b/lib/Service/StatsService.php @@ -95,10 +95,10 @@ class StatsService { $lastPostAt = $lastPostResult->fetchOne(); $lastPostResult->closeCursor(); - // Check if user stats already exist + // Check if forum user record already exists $checkQb = $this->db->getQueryBuilder(); - $checkQb->select('id') - ->from('forum_user_stats') + $checkQb->select('user_id') + ->from('forum_users') ->where($checkQb->expr()->eq('user_id', $checkQb->createNamedParameter($userId))); $checkResult = $checkQb->executeQuery(); $exists = $checkResult->fetch(); @@ -107,9 +107,9 @@ class StatsService { $timestamp = time(); if ($exists) { - // Update existing stats + // Update existing record $updateQb = $this->db->getQueryBuilder(); - $updateQb->update('forum_user_stats') + $updateQb->update('forum_users') ->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($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)) @@ -122,9 +122,9 @@ class StatsService { $updateQb->executeStatement(); return false; } else { - // Create new stats + // Create new record $insertQb = $this->db->getQueryBuilder(); - $insertQb->insert('forum_user_stats') + $insertQb->insert('forum_users') ->values([ 'user_id' => $insertQb->createNamedParameter($userId), 'thread_count' => $insertQb->createNamedParameter($threadCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT), @@ -139,13 +139,13 @@ class StatsService { return true; } catch (\Exception $e) { // If insert fails (race condition), try updating instead - $this->logger->warning('Failed to create user stats, attempting update', [ + $this->logger->warning('Failed to create forum user record, attempting update', [ 'userId' => $userId, 'exception' => $e->getMessage(), ]); $updateQb = $this->db->getQueryBuilder(); - $updateQb->update('forum_user_stats') + $updateQb->update('forum_users') ->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($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)) diff --git a/lib/Service/UserPreferencesService.php b/lib/Service/UserPreferencesService.php index 12362fe..4b9a337 100644 --- a/lib/Service/UserPreferencesService.php +++ b/lib/Service/UserPreferencesService.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace OCA\Forum\Service; use OCA\Forum\AppInfo\Application; -use OCA\Forum\Db\UserStatsMapper; +use OCA\Forum\Db\ForumUserMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IConfig; use Psr\Log\LoggerInterface; @@ -20,7 +20,7 @@ class UserPreferencesService { /** Preference key for upload directory path */ public const PREF_UPLOAD_DIRECTORY = 'upload_directory'; - /** Preference key for user signature (stored in user_stats table) */ + /** Preference key for user signature (stored in forum_users table) */ public const PREF_SIGNATURE = 'signature'; /** @var array Default preference values */ @@ -37,14 +37,14 @@ class UserPreferencesService { self::PREF_SIGNATURE, ]; - /** @var array Keys stored in user_stats table instead of config */ - private const USER_STATS_KEYS = [ + /** @var array Keys stored in forum_users table instead of config */ + private const FORUM_USER_KEYS = [ self::PREF_SIGNATURE, ]; public function __construct( private IConfig $config, - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private LoggerInterface $logger, ) { } @@ -78,9 +78,9 @@ class UserPreferencesService { throw new \InvalidArgumentException("Invalid preference key: $key"); } - // Handle keys stored in user_stats table - if (in_array($key, self::USER_STATS_KEYS, true)) { - return $this->getUserStatsValue($userId, $key); + // Handle keys stored in forum_users table + if (in_array($key, self::FORUM_USER_KEYS, true)) { + return $this->getForumUserValue($userId, $key); } $default = self::DEFAULTS[$key] ?? null; @@ -127,9 +127,9 @@ class UserPreferencesService { throw new \InvalidArgumentException("Invalid preference key: $key"); } - // Handle keys stored in user_stats table - if (in_array($key, self::USER_STATS_KEYS, true)) { - $this->setUserStatsValue($userId, $key, $value); + // Handle keys stored in forum_users table + if (in_array($key, self::FORUM_USER_KEYS, true)) { + $this->setForumUserValue($userId, $key, $value); return; } @@ -170,17 +170,17 @@ class UserPreferencesService { } /** - * Get a value from user_stats table + * Get a value from forum_users table * * @param string $userId The user ID * @param string $key The preference key * @return mixed The value */ - private function getUserStatsValue(string $userId, string $key): mixed { + private function getForumUserValue(string $userId, string $key): mixed { try { - $stats = $this->userStatsMapper->find($userId); + $forumUser = $this->forumUserMapper->find($userId); return match ($key) { - self::PREF_SIGNATURE => $stats->getSignature() ?? '', + self::PREF_SIGNATURE => $forumUser->getSignature() ?? '', default => self::DEFAULTS[$key] ?? null, }; } catch (DoesNotExistException $e) { @@ -189,21 +189,21 @@ class UserPreferencesService { } /** - * Set a value in user_stats table + * Set a value in forum_users table * * @param string $userId The user ID * @param string $key The preference key * @param mixed $value The value to set */ - private function setUserStatsValue(string $userId, string $key, mixed $value): void { - $stats = $this->userStatsMapper->createOrUpdate($userId); + private function setForumUserValue(string $userId, string $key, mixed $value): void { + $forumUser = $this->forumUserMapper->createOrUpdate($userId); match ($key) { - self::PREF_SIGNATURE => $stats->setSignature((string)$value), + self::PREF_SIGNATURE => $forumUser->setSignature((string)$value), default => null, }; - $stats->setUpdatedAt(time()); - $this->userStatsMapper->update($stats); + $forumUser->setUpdatedAt(time()); + $this->forumUserMapper->update($forumUser); } } diff --git a/lib/Service/UserService.php b/lib/Service/UserService.php index 5311449..f7099e8 100644 --- a/lib/Service/UserService.php +++ b/lib/Service/UserService.php @@ -8,9 +8,9 @@ declare(strict_types=1); namespace OCA\Forum\Service; use OCA\Forum\Db\BBCodeMapper; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\RoleMapper; use OCA\Forum\Db\UserRoleMapper; -use OCA\Forum\Db\UserStatsMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IL10N; use OCP\IUserManager; @@ -22,7 +22,7 @@ use OCP\IUserManager; class UserService { public function __construct( private IUserManager $userManager, - private UserStatsMapper $userStatsMapper, + private ForumUserMapper $forumUserMapper, private RoleMapper $roleMapper, private UserRoleMapper $userRoleMapper, private BBCodeMapper $bbCodeMapper, @@ -47,7 +47,7 @@ class UserService { /** * Check if a user has been deleted - * Checks both Nextcloud user existence and user_stats deleted_at flag + * Checks both Nextcloud user existence and forum_users deleted_at flag */ public function isUserDeleted(string $userId): bool { // First check if user exists in Nextcloud @@ -56,12 +56,12 @@ class UserService { return true; } - // Check if marked as deleted in user_stats + // Check if marked as deleted in forum_users try { - $stats = $this->userStatsMapper->find($userId); - return $stats->getDeletedAt() !== null; + $forumUser = $this->forumUserMapper->find($userId); + return $forumUser->getDeletedAt() !== null; } catch (DoesNotExistException $e) { - // No stats record, user is not deleted (just hasn't posted yet) + // No forum user record, user is not deleted (just hasn't posted yet) return false; } } @@ -92,12 +92,12 @@ class UserService { } } - // Get signature from user stats + // Get signature from forum user $signatureRaw = null; $signature = null; try { - $stats = $this->userStatsMapper->find($userId); - $signatureRaw = $stats->getSignature(); + $forumUser = $this->forumUserMapper->find($userId); + $signatureRaw = $forumUser->getSignature(); if ($signatureRaw !== null && $signatureRaw !== '') { // Parse BBCode in signature if ($bbcodes === null) { @@ -106,7 +106,7 @@ class UserService { $signature = $this->bbCodeService->parse($signatureRaw, $bbcodes); } } catch (DoesNotExistException $e) { - // No stats record, no signature + // No forum user record, no signature } return [ @@ -135,7 +135,7 @@ class UserService { $rolesMap = $this->fetchRolesForUsers($userIds); } - // Fetch all user stats at once for signatures + // Fetch all forum users at once for signatures $signaturesMap = $this->fetchSignaturesForUsers($userIds); // Fetch BBCodes once for parsing all signatures (if not provided) @@ -237,12 +237,12 @@ class UserService { $signaturesMap[$userId] = null; } - // Fetch all user stats for these users - $userStats = $this->userStatsMapper->findByUserIds($userIds); + // Fetch all forum users for these users + $forumUsers = $this->forumUserMapper->findByUserIds($userIds); // Extract signatures - foreach ($userStats as $stats) { - $signaturesMap[$stats->getUserId()] = $stats->getSignature(); + foreach ($forumUsers as $forumUser) { + $signaturesMap[$forumUser->getUserId()] = $forumUser->getSignature(); } return $signaturesMap; diff --git a/openapi.json b/openapi.json index 920d6e4..a9f0179 100644 --- a/openapi.json +++ b/openapi.json @@ -3116,7 +3116,7 @@ "/ocs/v2.php/apps/forum/api/users": { "get": { "operationId": "forum_user-index", - "summary": "Get all user statistics", + "summary": "Get all forum users", "tags": [ "forum_user" ], @@ -3143,7 +3143,7 @@ ], "responses": { "200": { - "description": "User statistics returned", + "description": "Forum users returned", "content": { "application/json": { "schema": { @@ -3182,7 +3182,7 @@ }, "post": { "operationId": "forum_user-create", - "summary": "Create user stats Note: This is typically not needed as stats are auto-created on first post", + "summary": "Create forum user Note: This is typically not needed as forum users are auto-created on first post", "tags": [ "forum_user" ], @@ -3227,7 +3227,7 @@ ], "responses": { "201": { - "description": "User stats created", + "description": "Forum user created", "content": { "application/json": { "schema": { @@ -3293,7 +3293,7 @@ "/ocs/v2.php/apps/forum/api/users/{userId}": { "get": { "operationId": "forum_user-show", - "summary": "Get user statistics by Nextcloud user ID Special case: use \"me\" to get current user stats", + "summary": "Get forum user by Nextcloud user ID Special case: use \"me\" to get current forum user", "tags": [ "forum_user" ], @@ -3329,7 +3329,7 @@ ], "responses": { "200": { - "description": "User stats returned", + "description": "Forum user returned", "content": { "application/json": { "schema": { @@ -3362,7 +3362,7 @@ } }, "404": { - "description": "User has no stats (hasn't posted yet) or guest user accessing \"me\"", + "description": "Forum user not found (user has not posted yet) or guest user accessing \"me\"", "content": { "application/json": { "schema": { @@ -8997,7 +8997,7 @@ "tags": [ { "name": "forum_user", - "description": "Controller for user statistics Note: User stats are automatically created on first post/thread" + "description": "Controller for forum users Note: Forum users are automatically created on first post/thread" } ] } diff --git a/src/composables/useCurrentUser.ts b/src/composables/useCurrentUser.ts index 9686c23..c6580ee 100644 --- a/src/composables/useCurrentUser.ts +++ b/src/composables/useCurrentUser.ts @@ -1,38 +1,38 @@ import { ref, computed, type Ref } from 'vue' import { ocs } from '@/axios' -import type { UserStats } from '@/types' +import type { ForumUser } from '@/types' import { getCurrentUser } from '@nextcloud/auth' -const currentUserStats = ref(null) +const currentForumUser = ref(null) const loading = ref(false) const error = ref(null) const loaded = ref(false) export function useCurrentUser() { - const fetchCurrentUser = async (force = false): Promise => { + const fetchCurrentUser = async (force = false): Promise => { // Don't refetch if already loaded unless forced if (loaded.value && !force) { - return currentUserStats.value + return currentForumUser.value } try { loading.value = true error.value = null - const response = await ocs.get('/users/me') - currentUserStats.value = response.data + const response = await ocs.get('/users/me') + currentForumUser.value = response.data loaded.value = true - return currentUserStats.value + return currentForumUser.value } catch (e: unknown) { - // If 404, user stats don't exist yet (user hasn't posted) - this is OK + // If 404, forum user doesn't exist yet (user hasn't posted) - this is OK const err = e as { response?: { status?: number } } if (err?.response?.status === 404) { - console.debug('User stats not found - user has not posted yet') - currentUserStats.value = null + console.debug('Forum user not found - user has not posted yet') + currentForumUser.value = null loaded.value = true return null } - console.error('Failed to fetch current user stats', e) + console.error('Failed to fetch current forum user', e) error.value = (e as Error).message || 'Failed to load user information' return null } finally { @@ -40,12 +40,12 @@ export function useCurrentUser() { } } - const refresh = async (): Promise => { + const refresh = async (): Promise => { return fetchCurrentUser(true) } const clear = (): void => { - currentUserStats.value = null + currentForumUser.value = null loaded.value = false error.value = null } @@ -60,7 +60,7 @@ export function useCurrentUser() { ) return { - currentUserStats: currentUserStats as Ref, + currentForumUser: currentForumUser as Ref, loading: loading as Ref, error: error as Ref, loaded: loaded as Ref, diff --git a/src/types/models.ts b/src/types/models.ts index 6b948b6..75a83fe 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -80,7 +80,7 @@ export interface Post { }> } -export interface UserStats { +export interface ForumUser { userId: string postCount: number threadCount: number diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue index f530444..36677fd 100644 --- a/src/views/ProfileView.vue +++ b/src/views/ProfileView.vue @@ -60,23 +60,23 @@ @@ -170,7 +170,7 @@ import PageWrapper from '@/components/PageWrapper.vue' import ThreadCard from '@/components/ThreadCard.vue' import ArrowLeftIcon from '@icons/ArrowLeft.vue' import RefreshIcon from '@icons/Refresh.vue' -import type { UserStats, Thread, Post } from '@/types' +import type { ForumUser, Thread, Post } from '@/types' import { ocs } from '@/axios' import { t, n } from '@nextcloud/l10n' import { getCurrentUser } from '@nextcloud/auth' @@ -195,7 +195,7 @@ export default defineComponent({ loading: false, loadingThreads: false, loadingPosts: false, - userStats: null as UserStats | null, + forumUser: null as ForumUser | null, displayName: '', threads: [] as Thread[], posts: [] as Post[], @@ -252,13 +252,13 @@ export default defineComponent({ // Load user stats (may not exist if user hasn't posted) try { const userResponse = await ocs.get(`/users/${this.userId}`) - this.userStats = userResponse.data + this.forumUser = userResponse.data } catch (err: any) { // 404 is OK - user hasn't posted yet if (err.response?.status !== 404) { throw err } - this.userStats = null + this.forumUser = null } // Load both tabs on initial load for accurate counts diff --git a/tests/unit/Controller/ForumUserControllerTest.php b/tests/unit/Controller/ForumUserControllerTest.php index 4500cf7..dfc6f2f 100644 --- a/tests/unit/Controller/ForumUserControllerTest.php +++ b/tests/unit/Controller/ForumUserControllerTest.php @@ -6,8 +6,8 @@ namespace OCA\Forum\Tests\Controller; use OCA\Forum\AppInfo\Application; use OCA\Forum\Controller\ForumUserController; -use OCA\Forum\Db\UserStats; -use OCA\Forum\Db\UserStatsMapper; +use OCA\Forum\Db\ForumUser; +use OCA\Forum\Db\ForumUserMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\IRequest; @@ -18,21 +18,21 @@ use Psr\Log\LoggerInterface; class ForumUserControllerTest extends TestCase { private ForumUserController $controller; - private UserStatsMapper $userStatsMapper; + private ForumUserMapper $forumUserMapper; private IUserSession $userSession; private LoggerInterface $logger; private IRequest $request; protected function setUp(): void { $this->request = $this->createMock(IRequest::class); - $this->userStatsMapper = $this->createMock(UserStatsMapper::class); + $this->forumUserMapper = $this->createMock(ForumUserMapper::class); $this->userSession = $this->createMock(IUserSession::class); $this->logger = $this->createMock(LoggerInterface::class); $this->controller = new ForumUserController( Application::APP_ID, $this->request, - $this->userStatsMapper, + $this->forumUserMapper, $this->userSession, $this->logger ); @@ -42,7 +42,7 @@ class ForumUserControllerTest extends TestCase { $user1 = $this->createForumUser(1, 'user1', 10); $user2 = $this->createForumUser(2, 'user2', 25); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('findAll') ->willReturn([$user1, $user2]); @@ -58,7 +58,7 @@ class ForumUserControllerTest extends TestCase { $nextcloudUserId = 'user1'; $user = $this->createForumUser(1, $nextcloudUserId, 10); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('find') ->with($nextcloudUserId) ->willReturn($user); @@ -74,15 +74,15 @@ class ForumUserControllerTest extends TestCase { public function testShowReturnsNotFoundWhenUserDoesNotExist(): void { $nextcloudUserId = 'non-existent-user'; - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('find') ->with($nextcloudUserId) - ->willThrowException(new DoesNotExistException('User stats not found')); + ->willThrowException(new DoesNotExistException('Forum user not found')); $response = $this->controller->show($nextcloudUserId); $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); - $this->assertEquals(['error' => 'User stats not found'], $response->getData()); + $this->assertEquals(['error' => 'Forum user not found'], $response->getData()); } public function testShowWithMeReturnsCurrentUserSuccessfully(): void { @@ -93,7 +93,7 @@ class ForumUserControllerTest extends TestCase { $user->method('getUID')->willReturn($nextcloudUserId); $this->userSession->method('getUser')->willReturn($user); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('find') ->with($nextcloudUserId) ->willReturn($forumUser); @@ -111,7 +111,7 @@ class ForumUserControllerTest extends TestCase { $response = $this->controller->show('me'); $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); - $this->assertEquals(['error' => 'User stats not found'], $response->getData()); + $this->assertEquals(['error' => 'Forum user not found'], $response->getData()); } public function testShowWithMeReturnsNotFoundWhenForumUserDoesNotExist(): void { @@ -121,22 +121,22 @@ class ForumUserControllerTest extends TestCase { $user->method('getUID')->willReturn($nextcloudUserId); $this->userSession->method('getUser')->willReturn($user); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('find') ->with($nextcloudUserId) - ->willThrowException(new DoesNotExistException('User stats not found')); + ->willThrowException(new DoesNotExistException('Forum user not found')); $response = $this->controller->show('me'); $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); - $this->assertEquals(['error' => 'User stats not found'], $response->getData()); + $this->assertEquals(['error' => 'Forum user not found'], $response->getData()); } public function testCreateForumUserSuccessfully(): void { $nextcloudUserId = 'new-user'; $createdUser = $this->createForumUser(1, $nextcloudUserId, 0); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('createOrUpdate') ->with($nextcloudUserId) ->willReturn($createdUser); @@ -149,13 +149,13 @@ class ForumUserControllerTest extends TestCase { $this->assertEquals(0, $data['postCount']); } - private function createForumUser(int $id, string $userId, int $postCount): UserStats { - $userStats = new UserStats(); - $userStats->setId($id); - $userStats->setUserId($userId); - $userStats->setPostCount($postCount); - $userStats->setCreatedAt(time()); - $userStats->setUpdatedAt(time()); - return $userStats; + private function createForumUser(int $id, string $userId, int $postCount): ForumUser { + $forumUser = new ForumUser(); + $forumUser->setId($id); + $forumUser->setUserId($userId); + $forumUser->setPostCount($postCount); + $forumUser->setCreatedAt(time()); + $forumUser->setUpdatedAt(time()); + return $forumUser; } } diff --git a/tests/unit/Controller/PostControllerTest.php b/tests/unit/Controller/PostControllerTest.php index 91704d7..6cfc597 100644 --- a/tests/unit/Controller/PostControllerTest.php +++ b/tests/unit/Controller/PostControllerTest.php @@ -10,6 +10,8 @@ use OCA\Forum\Db\BBCode; use OCA\Forum\Db\BBCodeMapper; use OCA\Forum\Db\Category; use OCA\Forum\Db\CategoryMapper; +use OCA\Forum\Db\ForumUser; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\Post; use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\Reaction; @@ -18,8 +20,6 @@ use OCA\Forum\Db\ReadMarker; use OCA\Forum\Db\ReadMarkerMapper; use OCA\Forum\Db\Thread; use OCA\Forum\Db\ThreadMapper; -use OCA\Forum\Db\UserStats; -use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\BBCodeService; use OCA\Forum\Service\NotificationService; use OCA\Forum\Service\PermissionService; @@ -38,7 +38,7 @@ class PostControllerTest extends TestCase { private PostMapper $postMapper; private ThreadMapper $threadMapper; private CategoryMapper $categoryMapper; - private UserStatsMapper $userStatsMapper; + private ForumUserMapper $forumUserMapper; private ReactionMapper $reactionMapper; private BBCodeService $bbCodeService; private BBCodeMapper $bbCodeMapper; @@ -56,7 +56,7 @@ class PostControllerTest extends TestCase { $this->postMapper = $this->createMock(PostMapper::class); $this->threadMapper = $this->createMock(ThreadMapper::class); $this->categoryMapper = $this->createMock(CategoryMapper::class); - $this->userStatsMapper = $this->createMock(UserStatsMapper::class); + $this->forumUserMapper = $this->createMock(ForumUserMapper::class); $this->reactionMapper = $this->createMock(ReactionMapper::class); $this->bbCodeService = $this->createMock(BBCodeService::class); $this->bbCodeMapper = $this->createMock(BBCodeMapper::class); @@ -83,7 +83,7 @@ class PostControllerTest extends TestCase { $this->postMapper, $this->threadMapper, $this->categoryMapper, - $this->userStatsMapper, + $this->forumUserMapper, $this->reactionMapper, $this->bbCodeService, $this->bbCodeMapper, @@ -226,7 +226,7 @@ class PostControllerTest extends TestCase { $this->userSession->method('getUser')->willReturn($user); // Mock forum user - $forumUser = new UserStats(); + $forumUser = new ForumUser(); $forumUser->setId(1); $forumUser->setUserId($userId); $forumUser->setPostCount(0); @@ -279,8 +279,8 @@ class PostControllerTest extends TestCase { ->method('update') ->willReturn($category); - // Mock user stats increment (void methods, no return value expectations) - $this->userStatsMapper->expects($this->once()) + // Mock forum user increment (void methods, no return value expectations) + $this->forumUserMapper->expects($this->once()) ->method('incrementPostCount') ->with($userId); @@ -596,7 +596,7 @@ class PostControllerTest extends TestCase { return $updatedCategory; }); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('incrementPostCount') ->with($userId); @@ -672,7 +672,7 @@ class PostControllerTest extends TestCase { return $updatedCategory; }); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('decrementPostCount') ->with($userId); @@ -741,11 +741,11 @@ class PostControllerTest extends TestCase { ->method('update'); // First post deletion should decrement thread count, not post count - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('decrementThreadCount') ->with($userId); - $this->userStatsMapper->expects($this->never()) + $this->forumUserMapper->expects($this->never()) ->method('decrementPostCount'); $response = $this->controller->destroy($postId); diff --git a/tests/unit/Controller/ThreadControllerTest.php b/tests/unit/Controller/ThreadControllerTest.php index b67e834..8f98c7a 100644 --- a/tests/unit/Controller/ThreadControllerTest.php +++ b/tests/unit/Controller/ThreadControllerTest.php @@ -8,13 +8,13 @@ use OCA\Forum\AppInfo\Application; use OCA\Forum\Controller\ThreadController; use OCA\Forum\Db\Category; use OCA\Forum\Db\CategoryMapper; +use OCA\Forum\Db\ForumUser; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\Post; use OCA\Forum\Db\PostMapper; use OCA\Forum\Db\Thread; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Db\ThreadSubscriptionMapper; -use OCA\Forum\Db\UserStats; -use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\PermissionService; use OCA\Forum\Service\ThreadEnrichmentService; use OCA\Forum\Service\UserPreferencesService; @@ -32,7 +32,7 @@ class ThreadControllerTest extends TestCase { private ThreadMapper $threadMapper; private CategoryMapper $categoryMapper; private PostMapper $postMapper; - private UserStatsMapper $userStatsMapper; + private ForumUserMapper $forumUserMapper; private ThreadSubscriptionMapper $threadSubscriptionMapper; private ThreadEnrichmentService $threadEnrichmentService; private UserPreferencesService $userPreferencesService; @@ -47,7 +47,7 @@ class ThreadControllerTest extends TestCase { $this->threadMapper = $this->createMock(ThreadMapper::class); $this->categoryMapper = $this->createMock(CategoryMapper::class); $this->postMapper = $this->createMock(PostMapper::class); - $this->userStatsMapper = $this->createMock(UserStatsMapper::class); + $this->forumUserMapper = $this->createMock(ForumUserMapper::class); $this->threadSubscriptionMapper = $this->createMock(ThreadSubscriptionMapper::class); $this->threadEnrichmentService = $this->createMock(ThreadEnrichmentService::class); $this->userPreferencesService = $this->createMock(UserPreferencesService::class); @@ -73,7 +73,7 @@ class ThreadControllerTest extends TestCase { $this->threadMapper, $this->categoryMapper, $this->postMapper, - $this->userStatsMapper, + $this->forumUserMapper, $this->threadSubscriptionMapper, $this->threadEnrichmentService, $this->userPreferencesService, @@ -222,14 +222,14 @@ class ThreadControllerTest extends TestCase { $user->method('getUID')->willReturn($userId); $this->userSession->method('getUser')->willReturn($user); - $forumUser = new UserStats(); + $forumUser = new ForumUser(); $forumUser->setUserId($userId); $forumUser->setPostCount(10); - // Mock user stats increment methods (first post doesn't count, only thread count increments) - $this->userStatsMapper->expects($this->never()) + // Mock forum user increment methods (first post doesn't count, only thread count increments) + $this->forumUserMapper->expects($this->never()) ->method('incrementPostCount'); - $this->userStatsMapper->expects($this->once()) + $this->forumUserMapper->expects($this->once()) ->method('incrementThreadCount'); // Mock thread subscription @@ -311,7 +311,7 @@ class ThreadControllerTest extends TestCase { $this->assertEquals(['error' => 'User not authenticated'], $response->getData()); } - public function testCreateThreadReturnsForbiddenWhenUserStatsNotRegistered(): void { + public function testCreateThreadSucceedsEvenWhenForumUserUpdateFails(): void { $categoryId = 1; $title = 'New Thread'; $content = 'Initial post content'; @@ -321,11 +321,11 @@ class ThreadControllerTest extends TestCase { $user->method('getUID')->willReturn($userId); $this->userSession->method('getUser')->willReturn($user); - // Mock user stats methods to throw exceptions (simulating user stats failure) + // Mock forum user methods to throw exceptions (simulating forum user update failure) // The controller catches these and just logs warnings, so thread creation should still succeed - $this->userStatsMapper->method('incrementPostCount') + $this->forumUserMapper->method('incrementPostCount') ->willThrowException(new \Exception('Failed to increment post count')); - $this->userStatsMapper->method('incrementThreadCount') + $this->forumUserMapper->method('incrementThreadCount') ->willThrowException(new \Exception('Failed to increment thread count')); // Mock thread subscription @@ -348,7 +348,7 @@ class ThreadControllerTest extends TestCase { $response = $this->controller->create($categoryId, $title, $content); - // Thread creation should succeed even if user stats fail (they're in a try-catch) + // Thread creation should succeed even if forum user update fails (they're in a try-catch) $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); } diff --git a/tests/unit/Service/UserPreferencesServiceTest.php b/tests/unit/Service/UserPreferencesServiceTest.php index 8fe011f..c05c5ad 100644 --- a/tests/unit/Service/UserPreferencesServiceTest.php +++ b/tests/unit/Service/UserPreferencesServiceTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace OCA\Forum\Tests\Service; use OCA\Forum\AppInfo\Application; -use OCA\Forum\Db\UserStatsMapper; +use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Service\UserPreferencesService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IConfig; @@ -15,21 +15,21 @@ use Psr\Log\LoggerInterface; class UserPreferencesServiceTest extends TestCase { private UserPreferencesService $service; private IConfig $config; - private UserStatsMapper $userStatsMapper; + private ForumUserMapper $forumUserMapper; private LoggerInterface $logger; protected function setUp(): void { $this->config = $this->createMock(IConfig::class); - $this->userStatsMapper = $this->createMock(UserStatsMapper::class); + $this->forumUserMapper = $this->createMock(ForumUserMapper::class); $this->logger = $this->createMock(LoggerInterface::class); - // By default, mock no user stats (no signature) - $this->userStatsMapper->method('find') + // By default, mock no forum user (no signature) + $this->forumUserMapper->method('find') ->willThrowException(new DoesNotExistException('')); $this->service = new UserPreferencesService( $this->config, - $this->userStatsMapper, + $this->forumUserMapper, $this->logger ); } @@ -37,7 +37,7 @@ class UserPreferencesServiceTest extends TestCase { public function testGetAllPreferencesReturnsAllPreferences(): void { $userId = 'user1'; - // Only config-based preferences (signature is from user_stats) + // Only config-based preferences (signature is from forum_users) $this->config->expects($this->exactly(2)) ->method('getUserValue') ->willReturnCallback(function ($uid, $appId, $key, $default) use ($userId) {