Skip to content

Commit 52059db

Browse files
committed
#11326 Implement backend infrastructure for managing and moderating public comments
1 parent e6cfd21 commit 52059db

File tree

12 files changed

+1068
-5
lines changed

12 files changed

+1068
-5
lines changed

api/v1/comments/UserCommentController.php

Lines changed: 475 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace PKP\API\v1\comments\resources;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Resources\Json\JsonResource;
7+
use PKP\user\User;
8+
use PKP\userComment\UserComment;
9+
10+
class UserCommentReportResource extends JsonResource
11+
{
12+
public function toArray(Request $request)
13+
{
14+
/*** @var UserComment $model */
15+
$model = $this;
16+
/** @var User $user */
17+
$user = $model->user;
18+
19+
return [
20+
'id' => $this->id,
21+
'userCommentId' => $this->userCommentId,
22+
'userId' => $this->userId,
23+
'userName' => $user->getFullName(),
24+
'userOrcidDisplayValue' => $user->getOrcidDisplayValue(),
25+
'note' => $this->note,
26+
'createdAt' => $this->createdAt,
27+
'updatedAt' => $this->updatedAt,
28+
];
29+
}
30+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace PKP\API\v1\comments\resources;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Resources\Json\JsonResource;
7+
use PKP\user\User;
8+
use PKP\userComment\UserComment;
9+
10+
class UserCommentResource extends JsonResource
11+
{
12+
public function toArray(Request $request)
13+
{
14+
$model = $this; /*** @var UserComment $model */
15+
$user = $model->user; /** @var User $user */
16+
17+
$query = $request->query();
18+
$results = [
19+
'id' => $model->id,
20+
'contextId' => $model->contextId,
21+
'commentText' => $model->commentText,
22+
'createdAt' => $model->createdAt,
23+
'isVisible' => $model->isVisible,
24+
'isReported' => $model->reports->isNotEmpty(),
25+
'publicationId' => $model->publicationId,
26+
'updatedAt' => $model->updatedAt,
27+
'userId' => $model->userId,
28+
'userName' => $user->getFullName(),
29+
'userOrcidDisplayValue' => $user->getOrcidDisplayValue(),
30+
];
31+
32+
if (key_exists('includeReports', $query)) {
33+
$reports = $model->reports;
34+
$results['reports'] = UserCommentReportResource::collection($reports);
35+
}
36+
37+
return $results;
38+
}
39+
}

classes/core/PKPBaseController.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
2121
use Illuminate\Foundation\Bus\DispatchesJobs;
2222
use Illuminate\Foundation\Validation\ValidatesRequests;
23+
use Illuminate\Http\JsonResponse;
2324
use Illuminate\Http\Request;
25+
use Illuminate\Http\Response;
26+
use Illuminate\Pagination\LengthAwarePaginator;
2427
use Illuminate\Routing\Controller;
2528
use Illuminate\Routing\Route;
2629
use PKP\security\authorization\AllowedHostsPolicy;
@@ -110,12 +113,12 @@ public static function getRouteController(?Request $request = null): ?static
110113
if (!$requestedRoute = static::getRequestedRoute($request)) {
111114
return null;
112115
}
113-
116+
114117
$calledRouteController = (new ReflectionFunction($requestedRoute->action['uses']))->getClosureThis();
115118

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

583586
return true;
584587
}
588+
589+
/**
590+
* Formats and returns an HTTP response for a LengthAwarePaginator paginated collection.
591+
*
592+
* @param LengthAwarePaginator $pagination - The paginated collection
593+
*
594+
*/
595+
public function paginatedResponse(LengthAwarePaginator $pagination): JsonResponse
596+
{
597+
return response()->json([
598+
'data' => $pagination->values(),
599+
'total' => $pagination->total(),
600+
'pagination' => [
601+
'lastPage' => $pagination->lastPage(),
602+
'currentPage' => $pagination->currentPage(),
603+
],
604+
], Response::HTTP_OK);
605+
}
585606
}

classes/facades/Repo.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@
4444
use PKP\query\Repository as QueryRepository;
4545
use PKP\ror\Repository as RorRepository;
4646
use PKP\stageAssignment\Repository as StageAssignmentRepository;
47+
use PKP\submission\reviewer\recommendation\Repository as ReviewerRecommendationRepository;
4748
use PKP\submissionFile\Repository as SubmissionFileRepository;
4849
use PKP\user\interest\Repository as UserInterestRepository;
50+
use PKP\userComment\Repository as UserCommentRepository;
4951
use PKP\userGroup\Repository as UserGroupRepository;
50-
use PKP\submission\reviewer\recommendation\Repository as ReviewerRecommendationRepository;
5152

5253
class Repo
5354
{
@@ -170,4 +171,8 @@ public static function reviewerRecommendation(): ReviewerRecommendationRepositor
170171
{
171172
return app(ReviewerRecommendationRepository::class);
172173
}
174+
public static function userComment(): UserCommentRepository
175+
{
176+
return app(UserCommentRepository::class);
177+
}
173178
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/**
4+
* @file I11325_UserComments.php
5+
*
6+
* Copyright (c) 2014-2020 Simon Fraser University
7+
* Copyright (c) 2000-2020 John Willinsky
8+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
9+
*
10+
* @class UserCommentsSchemaMigration
11+
*
12+
* @brief Describe database table structures.
13+
*/
14+
15+
namespace PKP\migration\upgrade\v3_6_0;
16+
17+
use Illuminate\Database\Migrations\Migration;
18+
use Illuminate\Database\Schema\Blueprint;
19+
use Illuminate\Support\Facades\Schema;
20+
21+
class I11325_UserComments extends Migration
22+
{
23+
/**
24+
* Run the migrations.
25+
*/
26+
public function up(): void
27+
{
28+
Schema::create('user_comments', function (Blueprint $table) {
29+
$table->bigInteger('user_comment_id')->autoIncrement();
30+
$table->bigInteger('user_id')->comment('ID of the user that made the comment.');
31+
$table->bigInteger('context_id');
32+
$table->bigInteger('publication_id')->nullable();
33+
$table->text('comment_text')->comment('The comment text.');
34+
$table->boolean('is_approved')->default(false);
35+
$table->timestamps();
36+
37+
$table->foreign('user_id')
38+
->references('user_id')
39+
->on('users')
40+
->onDelete('cascade')
41+
->onUpdate('cascade');
42+
});
43+
44+
Schema::create('user_comment_reports', function (Blueprint $table) {
45+
$table->bigIncrements('user_comment_report_id');
46+
$table->unsignedBigInteger('user_comment_id');
47+
$table->unsignedBigInteger('user_id');
48+
$table->text('note');
49+
$table->timestamps();
50+
51+
// Foreign key constraints
52+
$table->foreign('user_comment_id')
53+
->references('user_comment_id')
54+
->on('user_comments')
55+
->onDelete('cascade')
56+
->onUpdate('cascade');
57+
58+
$table->foreign('user_id')
59+
->references('user_id')
60+
->on('users')
61+
->onDelete('cascade')
62+
->onUpdate('cascade');
63+
});
64+
65+
Schema::create('user_comment_settings', function (Blueprint $table) {
66+
$table->bigInteger('user_comment_setting_id')->autoIncrement();
67+
$table->bigInteger('user_comment_id');
68+
$table->string('locale', 14)->default('');
69+
$table->string('setting_name', 255);
70+
$table->longText('setting_value')->nullable();
71+
72+
$table->foreign('user_comment_id')
73+
->references('user_comment_id')
74+
->on('user_comments')
75+
->onDelete('cascade')
76+
->onUpdate('cascade');
77+
});
78+
}
79+
80+
/**
81+
* Reverse the migration.
82+
*/
83+
public function down(): void
84+
{
85+
Schema::drop('user_comment_reports');
86+
Schema::drop('user_comment_settings');
87+
Schema::drop('user_comments');
88+
}
89+
}

classes/services/PKPSchemaService.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class PKPSchemaService
5353
public const SCHEMA_USER_GROUP = 'userGroup';
5454
public const SCHEMA_EVENT_LOG = 'eventLog';
5555
public const SCHEMA_EMAIL_LOG = 'emailLog';
56+
public const SCHEMA_USER_COMMENT = 'userComment';
5657

5758
/** @var array cache of schemas that have been loaded */
5859
private $_schemas = [];
@@ -75,6 +76,8 @@ class PKPSchemaService
7576
* @hook Schema::get::before::
7677
* @hook Schema::get::before::
7778
* @hook Schema::get::before::
79+
* @hook Schema::get::before::
80+
* @hook Schema::get::before::
7881
*/
7982
public function get($schemaName, $forceReload = false)
8083
{
@@ -269,7 +272,13 @@ public function getPropsByAttributeOrigin(string $schemaName, string $attributeO
269272
public function groupPropsByOrigin(string $schemaName, bool $excludeReadOnly = false): array
270273
{
271274
$schema = $this->get($schemaName);
272-
$propsByOrigin = [];
275+
// Initialize the array with empty arrays for each origin
276+
$propsByOrigin = [
277+
Schema::ATTRIBUTE_ORIGIN_MAIN => [],
278+
Schema::ATTRIBUTE_ORIGIN_COMPOSED => [],
279+
Schema::ATTRIBUTE_ORIGIN_SETTINGS => [],
280+
];
281+
273282
foreach ($schema->properties as $propName => $propSchema) {
274283
if (empty($propSchema->origin)) {
275284
continue;

classes/userComment/Repository.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace PKP\userComment;
4+
5+
use APP\core\Request;
6+
use Illuminate\Database\Eloquent\Builder;
7+
use Illuminate\Pagination\LengthAwarePaginator;
8+
use PKP\API\v1\comments\resources\UserCommentReportResource;
9+
use PKP\API\v1\comments\resources\UserCommentResource;
10+
use PKP\core\SettingsBuilder;
11+
use PKP\user\User;
12+
use PKP\userComment\relationships\userCommentReport\UserCommentReport;
13+
14+
class Repository
15+
{
16+
protected Request $request;
17+
protected int $perPage;
18+
private const DEFAULT_ITEMS_PER_PAGE = 50;
19+
20+
public function __construct(Request $request)
21+
{
22+
$this->request = $request;
23+
$context = $request->getContext();
24+
$this->perPage = $context->getData('itemsPerPage') ?? self::DEFAULT_ITEMS_PER_PAGE;
25+
}
26+
27+
28+
/**
29+
* @param UserComment $userComment - The user comment to report
30+
* @param User $user - The user reporting the comment
31+
* @param string $note - The note to attach to the report
32+
*
33+
* @return int - The ID of the new report
34+
*/
35+
public function addReport(UserComment $userComment, User $user, string $note): int
36+
{
37+
$report = UserCommentReport::query()->create([
38+
'userCommentId' => $userComment->id,
39+
'userId' => $user->getId(),
40+
'note' => $note,
41+
]);
42+
43+
return $report->id;
44+
}
45+
46+
/**
47+
* Accepts a model query builder and returns a paginated collection of the data.
48+
*
49+
* @param SettingsBuilder|Builder $model - The model query builder to paginate. The underlying model of the builder must be either UserComment or UserCommentReport.
50+
*/
51+
public function getPaginatedData(SettingsBuilder|Builder $model): LengthAwarePaginator
52+
{
53+
$resourceClass = match (true) {
54+
$model->getModel() instanceof UserComment => UserCommentResource::class,
55+
$model->getModel() instanceof UserCommentReport => UserCommentReportResource::class,
56+
default => throw new \InvalidArgumentException('Unsupported model type: ' . get_class($model->getModel())),
57+
};
58+
59+
$currentPage = LengthAwarePaginator::resolveCurrentPage();
60+
$sanitizedPage = $currentPage - 1;
61+
$offsetRows = $this->perPage * $sanitizedPage;
62+
$total = $model->count();
63+
64+
$data = $model
65+
->orderBy('created_at', 'DESC')
66+
->skip($offsetRows)
67+
->take($this->perPage)
68+
->get();
69+
70+
return new LengthAwarePaginator(
71+
$resourceClass::collection($data),
72+
$total,
73+
$this->perPage
74+
);
75+
}
76+
77+
public function setPage(int $page): self
78+
{
79+
LengthAwarePaginator::currentPageResolver(fn () => $page);
80+
return $this;
81+
}
82+
83+
/**
84+
* Set the number of items per page.
85+
*
86+
* @param int $perPage - The number of items per page
87+
*/
88+
public function setPerPage(int $perPage): self
89+
{
90+
$this->perPage = $perPage;
91+
return $this;
92+
}
93+
}

0 commit comments

Comments
 (0)