mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-17 17:28:02 +00:00
refactor: user stats -> forum users
This commit is contained in:
@@ -40,8 +40,8 @@ class RebuildAllStats extends Command {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$output->writeln('<info>Starting full stats rebuild...</info>');
|
||||
|
||||
// Rebuild user stats
|
||||
$output->writeln('Rebuilding user stats...');
|
||||
// Rebuild forum users
|
||||
$output->writeln('Rebuilding forum users...');
|
||||
$userResult = $this->statsService->rebuildAllUserStats();
|
||||
$output->writeln(sprintf(
|
||||
' <comment>Users processed: %d, created: %d, updated: %d</comment>',
|
||||
|
||||
@@ -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('<info>User statistics rebuilt successfully!</info>');
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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<Http::STATUS_OK, list<array<string, mixed>>, 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<Http::STATUS_OK, array<string, mixed>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
|
||||
*
|
||||
* 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<Http::STATUS_CREATED, array<string, mixed>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
@@ -14,22 +14,22 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/**
|
||||
* @template-extends QBMapper<UserStats>
|
||||
* @template-extends QBMapper<ForumUser>
|
||||
*/
|
||||
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<UserStats>
|
||||
* @return array<ForumUser>
|
||||
*/
|
||||
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<string> $userIds
|
||||
* @return array<UserStats>
|
||||
* @return array<ForumUser>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
65
lib/Migration/Version9Date20251129000000.php
Normal file
65
lib/Migration/Version9Date20251129000000.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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<string, mixed> Default preference values */
|
||||
@@ -37,14 +37,14 @@ class UserPreferencesService {
|
||||
self::PREF_SIGNATURE,
|
||||
];
|
||||
|
||||
/** @var array<string> Keys stored in user_stats table instead of config */
|
||||
private const USER_STATS_KEYS = [
|
||||
/** @var array<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
16
openapi.json
16
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<UserStats | null>(null)
|
||||
const currentForumUser = ref<ForumUser | null>(null)
|
||||
const loading = ref<boolean>(false)
|
||||
const error = ref<string | null>(null)
|
||||
const loaded = ref<boolean>(false)
|
||||
|
||||
export function useCurrentUser() {
|
||||
const fetchCurrentUser = async (force = false): Promise<UserStats | null> => {
|
||||
const fetchCurrentUser = async (force = false): Promise<ForumUser | null> => {
|
||||
// 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<UserStats>('/users/me')
|
||||
currentUserStats.value = response.data
|
||||
const response = await ocs.get<ForumUser>('/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<UserStats | null> => {
|
||||
const refresh = async (): Promise<ForumUser | null> => {
|
||||
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<UserStats | null>,
|
||||
currentForumUser: currentForumUser as Ref<ForumUser | null>,
|
||||
loading: loading as Ref<boolean>,
|
||||
error: error as Ref<string | null>,
|
||||
loaded: loaded as Ref<boolean>,
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface Post {
|
||||
}>
|
||||
}
|
||||
|
||||
export interface UserStats {
|
||||
export interface ForumUser {
|
||||
userId: string
|
||||
postCount: number
|
||||
threadCount: number
|
||||
|
||||
@@ -60,23 +60,23 @@
|
||||
<div class="user-info">
|
||||
<h2 class="user-name">{{ displayName }}</h2>
|
||||
<div class="user-meta">
|
||||
<span v-if="userStats && userStats.createdAt" class="meta-item">
|
||||
<span v-if="forumUser && forumUser.createdAt" class="meta-item">
|
||||
<span class="meta-label">{{ strings.firstPost }}</span>
|
||||
<NcDateTime :timestamp="userStats.createdAt * 1000" />
|
||||
<NcDateTime :timestamp="forumUser.createdAt * 1000" />
|
||||
</span>
|
||||
<span v-if="userStats && userStats.createdAt" class="meta-divider">·</span>
|
||||
<span v-if="forumUser && forumUser.createdAt" class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{
|
||||
strings.threadsLabel(userStats?.threadCount || 0)
|
||||
strings.threadsLabel(forumUser?.threadCount || 0)
|
||||
}}</span>
|
||||
<span class="meta-value">{{ userStats?.threadCount || 0 }}</span>
|
||||
<span class="meta-value">{{ forumUser?.threadCount || 0 }}</span>
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{
|
||||
strings.repliesLabel(userStats?.postCount || 0)
|
||||
strings.repliesLabel(forumUser?.postCount || 0)
|
||||
}}</span>
|
||||
<span class="meta-value">{{ userStats?.postCount || 0 }}</span>
|
||||
<span class="meta-value">{{ forumUser?.postCount || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user