Skip to content

pkp/pkp-lib#11326 Implement backend infrastructure for managing and moderating public comments #11410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
491 changes: 491 additions & 0 deletions api/v1/comments/UserCommentController.php

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions api/v1/comments/resources/UserCommentReportResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* @file api/v1/comments/resources/UserCommentReportResource.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2005 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class UserCommentReportResource
*
* @ingroup api_v1_comments
*
* @brief Class for mapping HTTP output values for user comment reports.
*/

namespace PKP\API\v1\comments\resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use PKP\user\User;

class UserCommentReportResource extends JsonResource
{
public function toArray(Request $request): array
{
/** @var User $user */
$user = $this->user;

return [
'id' => $this->id,
'userCommentId' => $this->userCommentId,
'userId' => $this->userId,
'userName' => $user->getFullName(),
'userOrcidDisplayValue' => $user->getOrcidDisplayValue(),
'note' => $this->note,
'createdAt' => $this->createdAt,
'updatedAt' => $this->updatedAt,
];
}
}
50 changes: 50 additions & 0 deletions api/v1/comments/resources/UserCommentResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/**
* @file api/v1/comments/resources/UserCommentResource.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2005 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class UserCommentResource
*
* @ingroup api_v1_comments
*
* @brief Class for mapping HTTP output values for user comments.
*/

namespace PKP\API\v1\comments\resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use PKP\user\User;

class UserCommentResource extends JsonResource
{
public function toArray(Request $request): array
{
$user = $this->user; /** @var User $user */
$requestQueryParams = $request->query();

$results = [
'id' => $this->id,
'contextId' => $this->contextId,
'commentText' => $this->commentText,
'createdAt' => $this->createdAt,
'updatedAt' => $this->updatedAt,
'isApproved' => $this->isApproved,
'isReported' => $this->reports->isNotEmpty(),
'publicationId' => $this->publicationId,
'userId' => $this->userId,
'userName' => $user->getFullName(),
'userOrcidDisplayValue' => $user->getOrcidDisplayValue(),
];

if (key_exists('includeReports', $requestQueryParams)) {
$results['reports'] = UserCommentReportResource::collection($this->reports);
}

return $results;
}
}
26 changes: 23 additions & 3 deletions classes/core/PKPBaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Routing\Controller;
use Illuminate\Routing\Route;
use PKP\security\authorization\AllowedHostsPolicy;
Expand Down Expand Up @@ -110,12 +112,12 @@ public static function getRouteController(?Request $request = null): ?static
if (!$requestedRoute = static::getRequestedRoute($request)) {
return null;
}

$calledRouteController = (new ReflectionFunction($requestedRoute->action['uses']))->getClosureThis();

// When the routes are added to router as a closure/callable from other section like from a
// When the routes are added to router as a closure/callable from other section like from a
// plugin through the hook, the resolved called route class may not be an instance of
// `PKPBaseController` and we need to resolve the current controller instance from
// `PKPBaseController` and we need to resolve the current controller instance from
// `APIHandler::getApiController` method
if ($calledRouteController instanceof self) {
return $calledRouteController;
Expand Down Expand Up @@ -582,4 +584,22 @@ protected function _validateStatDates(array $params, string $dateStartParam = 'd

return true;
}

/**
* Format and returns paginated response data.
*
* @param LengthAwarePaginator $pagination - The object with paginated data and metadata.
*
*/
public function formatPaginatedResponseData(LengthAwarePaginator $pagination): array
{
return [
'data' => $pagination->values(),
'total' => $pagination->total(),
'pagination' => [
'lastPage' => $pagination->lastPage(),
'currentPage' => $pagination->currentPage(),
]
];
}
}
34 changes: 27 additions & 7 deletions classes/core/PKPString.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,24 +332,44 @@ public static function getWordCount(string $str): int

/**
* Map the specific HTML tags in title/ sub title for JATS schema compability
*
* @see https://jats.nlm.nih.gov/publishing/0.4/xsd/JATS-journalpublishing0.xsd
*
* @param string $htmlTitle The submission title/sub title as in HTML
* @return string
*/
public static function mapTitleHtmlTagsToXml(string $htmlTitle): string
{
$mappings = [
'<b>' => '<bold>',
'</b>' => '</bold>',
'<i>' => '<italic>',
'</i>' => '</italic>',
'<u>' => '<underline>',
'</u>' => '</underline>',
'<b>' => '<bold>',
'</b>' => '</bold>',
'<i>' => '<italic>',
'</i>' => '</italic>',
'<u>' => '<underline>',
'</u>' => '</underline>',
];

return str_replace(array_keys($mappings), array_values($mappings), $htmlTitle);
}

/**
* Does a strict conversion of a string to a boolean value.
*
* @param string $value The value to convert.
*
* @return bool|null Returns true if the string is "true", false if it is "false", and null otherwise.
*/
public static function strictConvertToBoolean(string $value): ?bool
{
$lower = strtolower($value);
if ($lower === 'true') {
return true;
}
if ($lower === 'false') {
return false;
}

return null;
}
}

if (!PKP_STRICT_MODE) {
Expand Down
8 changes: 7 additions & 1 deletion classes/facades/Repo.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@
use PKP\query\Repository as QueryRepository;
use PKP\ror\Repository as RorRepository;
use PKP\stageAssignment\Repository as StageAssignmentRepository;
use PKP\submission\reviewer\recommendation\Repository as ReviewerRecommendationRepository;
use PKP\submissionFile\Repository as SubmissionFileRepository;
use PKP\user\interest\Repository as UserInterestRepository;
use PKP\userComment\Repository as UserCommentRepository;
use PKP\userGroup\Repository as UserGroupRepository;
use PKP\submission\reviewer\recommendation\Repository as ReviewerRecommendationRepository;

class Repo
{
Expand Down Expand Up @@ -170,4 +171,9 @@ public static function reviewerRecommendation(): ReviewerRecommendationRepositor
{
return app(ReviewerRecommendationRepository::class);
}

public static function userComment(): UserCommentRepository
{
return app(UserCommentRepository::class);
}
}
88 changes: 88 additions & 0 deletions classes/migration/upgrade/v3_6_0/I11326_UserComments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/**
* @file I11326_UserComments.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I11325_UserComments
*
* @brief Migration to add table structures for user comments.
*/

namespace PKP\migration\upgrade\v3_6_0;

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class I11326_UserComments extends Migration
{
/**
* Run the migration.
*/
public function up(): void
{
Schema::create('user_comments', function (Blueprint $table) {
$table->bigInteger('user_comment_id')->autoIncrement()->comment('Primary key.');
$table->bigInteger('user_id')->comment('ID of the user that made the comment.');
$table->bigInteger('context_id')->comment('ID of the context (e.g., journal) the comment belongs to.');
$table->bigInteger('publication_id')->nullable()->comment('ID of the publication that the comment belongs to.');
$table->text('comment_text')->comment('The comment text.');
$table->boolean('is_approved')->default(false)->comment('Boolean indicating if the comment is approved.');
$table->timestamps();

$table->foreign('user_id')
->references('user_id')
->on('users')
->onDelete('cascade')
->onUpdate('cascade');
});

Schema::create('user_comment_reports', function (Blueprint $table) {
$table->bigInteger('user_comment_report_id')->autoIncrement()->comment('Primary key.');
$table->bigInteger('user_comment_id')->comment('ID of the user comment that the reported was created for.');
$table->bigInteger('user_id')->comment('ID of the user that made the report.');
$table->text('note')->comment('Reason for the report.');
$table->timestamps();

$table->foreign('user_comment_id')
->references('user_comment_id')
->on('user_comments')
->onDelete('cascade')
->onUpdate('cascade');

$table->foreign('user_id')
->references('user_id')
->on('users')
->onDelete('cascade')
->onUpdate('cascade');
});

Schema::create('user_comment_settings', function (Blueprint $table) {
$table->bigInteger('user_comment_setting_id')->autoIncrement()->comment('Primary key.');
$table->bigInteger('user_comment_id')->comment('ID of the user comment that the setting belongs to.');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->longText('setting_value')->nullable();

$table->foreign('user_comment_id')
->references('user_comment_id')
->on('user_comments')
->onDelete('cascade')
->onUpdate('cascade');
});
}

/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('user_comment_reports');
Schema::drop('user_comment_settings');
Schema::drop('user_comments');
}
}
94 changes: 94 additions & 0 deletions classes/userComment/Repository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/**
* @file classes/userComment/Repository.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @ingroup userComment
*
* @brief A repository to manage comments.
*/

namespace PKP\userComment;

use APP\core\Request;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use PKP\API\v1\comments\resources\UserCommentReportResource;
use PKP\API\v1\comments\resources\UserCommentResource;
use PKP\core\SettingsBuilder;
use PKP\user\User;
use PKP\userComment\relationships\UserCommentReport;

class Repository
{
private Request $request;
private int $perPage;

public function __construct(Request $request)
{
$this->request = $request;
$context = $request->getContext();
$this->perPage = $context->getData('itemsPerPage');
}

/**
* @param UserComment $userComment - The user comment to add the report to
* @param User $user - The user reporting the comment
* @param string $note - The note to attach to the report
*
* @return int - The ID of the newly created report
*/
public function addReport(UserComment $userComment, User $user, string $note): int
{
$report = UserCommentReport::query()->create([
'userCommentId' => $userComment->id,
'userId' => $user->getId(),
'note' => $note,
]);

return $report->id;
}

/**
* Accepts a model query builder and returns a paginated collection of the data.
*
* @param SettingsBuilder|Builder $query - The query builder used to get the paginated records. The underlying model of the builder must be either UserComment or UserCommentReport.
*/
public function getPaginatedData(SettingsBuilder|Builder $query): LengthAwarePaginator
{
$resourceClass = match (true) {
$query->getModel() instanceof UserComment => UserCommentResource::class,
$query->getModel() instanceof UserCommentReport => UserCommentReportResource::class,
default => throw new \InvalidArgumentException('Unsupported model type: ' . get_class($query->getModel())),
};

$currentPage = LengthAwarePaginator::resolveCurrentPage();
$sanitizedPage = $currentPage - 1;
$offsetRows = $this->perPage * $sanitizedPage;
$total = $query->count();

$data = $query
->orderBy('created_at', 'DESC')
->skip($offsetRows)
->take($this->perPage)
->get();

return new LengthAwarePaginator(
$resourceClass::collection($data),
$total,
$this->perPage
);
}

public function setPage(int $page): static
{
LengthAwarePaginator::currentPageResolver(fn () => $page);
return $this;
}
}
Loading