From 4f5aef9624e88cd2ff87a1fe7a37b7092749bf22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Fri, 11 Apr 2025 18:10:05 +0200 Subject: [PATCH 1/2] Adding batch ID to the assignment entity, so we can mark them as checked by plagiarism-detection tools. --- .../presenters/PlagiarismPresenter.php | 42 +++++++++++-- app/model/entity/Assignment.php | 17 +++++ app/model/view/AssignmentViewFactory.php | 1 + migrations/Version20250411155056.php | 35 +++++++++++ tests/Presenters/PlagiarismPresenter.phpt | 62 ++++++++++++++++--- 5 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 migrations/Version20250411155056.php diff --git a/app/V1Module/presenters/PlagiarismPresenter.php b/app/V1Module/presenters/PlagiarismPresenter.php index 9965d478d..d1c647219 100644 --- a/app/V1Module/presenters/PlagiarismPresenter.php +++ b/app/V1Module/presenters/PlagiarismPresenter.php @@ -18,6 +18,7 @@ use App\Model\Entity\PlagiarismDetectedSimilarity; use App\Model\Entity\PlagiarismDetectedSimilarFile; use App\Model\Entity\SolutionFile; +use App\Model\Repository\Assignments; use App\Model\Repository\AssignmentSolutions; use App\Model\Repository\PlagiarismDetectionBatches; use App\Model\Repository\PlagiarismDetectedSimilarities; @@ -32,6 +33,12 @@ */ class PlagiarismPresenter extends BasePresenter { + /** + * @var Assignments + * @inject + */ + public $assignments; + /** * @var AssignmentSolutions * @inject @@ -168,15 +175,38 @@ public function checkUpdateBatch(string $id): void * Update detection bath record. At the moment, only the uploadCompletedAt can be changed. * @POST */ - #[Post("uploadCompleted", new VBool(), "Whether the upload of the batch data is completed or not.")] + #[Post( + "uploadCompleted", + new VBool(), + "Whether the upload of the batch data is completed or not.", + required: false + )] + #[Post( + "assignments", + new VArray(new VUuid()), + "List of assignment IDs to be marked as 'checked' by this batch.", + required: false + )] #[Path("id", new VUuid(), "Identification of the detection batch", required: true)] public function actionUpdateBatch(string $id): void { - $req = $this->getRequest(); - $uploadCompleted = filter_var($req->getPost("uploadCompleted"), FILTER_VALIDATE_BOOLEAN); $batch = $this->detectionBatches->findOrThrow($id); - $batch->setUploadCompleted($uploadCompleted); - $this->detectionBatches->persist($batch); + $req = $this->getRequest(); + + $uploadCompleted = $req->getPost("uploadCompleted"); + if ($uploadCompleted !== null) { + $uploadCompleted = filter_var($uploadCompleted, FILTER_VALIDATE_BOOLEAN); + $batch->setUploadCompleted($uploadCompleted); + } + + $assignments = $req->getPost("assignments") ?? []; + foreach ($assignments as $assignmentId) { + $assignment = $this->assignments->findOrThrow($assignmentId); + $assignment->setPlagiarismBatch($batch); + $this->assignments->persist($assignment, false); // no flush + } + + $this->detectionBatches->persist($batch); // and flush $this->sendSuccessResponse($batch); } @@ -281,7 +311,7 @@ public function actionAddSimilarities(string $id, string $solutionId): void } try { - $detectedFile = new PlagiarismDetectedSimilarFile( + new PlagiarismDetectedSimilarFile( $detectedSimilarity, $similarSolution, $similarFile, diff --git a/app/model/entity/Assignment.php b/app/model/entity/Assignment.php index 03cd6683d..fc41cc01d 100644 --- a/app/model/entity/Assignment.php +++ b/app/model/entity/Assignment.php @@ -544,6 +544,13 @@ public function syncWithExercise() $this->syncedAt = new DateTime(); } + /** + * @var PlagiarismDetectionBatch|null + * @ORM\ManyToOne(targetEntity="PlagiarismDetectionBatch") + * Refers to last plagiarism detection batch which checked solutions of this assignment. + */ + protected $plagiarismBatch = null; + /* * Accessors */ @@ -702,4 +709,14 @@ public function setMaxPointsDeadlineInterpolation(bool $interpolation = true): v { $this->maxPointsDeadlineInterpolation = $interpolation; } + + public function getPlagiarismBatch(): ?PlagiarismDetectionBatch + { + return $this->plagiarismBatch; + } + + public function setPlagiarismBatch(?PlagiarismDetectionBatch $batch = null) + { + $this->plagiarismBatch = $batch; + } } diff --git a/app/model/view/AssignmentViewFactory.php b/app/model/view/AssignmentViewFactory.php index a63923ac3..cebc7e07c 100644 --- a/app/model/view/AssignmentViewFactory.php +++ b/app/model/view/AssignmentViewFactory.php @@ -116,6 +116,7 @@ function (LocalizedExercise $text) use ($assignment) { ], "solutionFilesLimit" => $assignment->getSolutionFilesLimit(), "solutionSizeLimit" => $assignment->getSolutionSizeLimit(), + "plagiarismBatchId" => $assignment->getPlagiarismBatch()?->getId(), "permissionHints" => PermissionHints::get($this->assignmentAcl, $assignment) ]; } diff --git a/migrations/Version20250411155056.php b/migrations/Version20250411155056.php new file mode 100644 index 000000000..8efd06991 --- /dev/null +++ b/migrations/Version20250411155056.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE assignment ADD plagiarism_batch_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE assignment ADD CONSTRAINT FK_30C544BA5B4CC7BF FOREIGN KEY (plagiarism_batch_id) REFERENCES plagiarism_detection_batch (id)'); + $this->addSql('CREATE INDEX IDX_30C544BA5B4CC7BF ON assignment (plagiarism_batch_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE assignment DROP FOREIGN KEY FK_30C544BA5B4CC7BF'); + $this->addSql('DROP INDEX IDX_30C544BA5B4CC7BF ON assignment'); + $this->addSql('ALTER TABLE assignment DROP plagiarism_batch_id'); + } +} diff --git a/tests/Presenters/PlagiarismPresenter.phpt b/tests/Presenters/PlagiarismPresenter.phpt index f6a449d00..6cafec774 100644 --- a/tests/Presenters/PlagiarismPresenter.phpt +++ b/tests/Presenters/PlagiarismPresenter.phpt @@ -158,6 +158,52 @@ class TestPlagiarismPresenter extends Tester\TestCase Assert::true($batch->getUploadCompletedAt() === null); } + public function testBatchSetCompletedAndMarkAssignments() + { + PresenterTestHelper::loginDefaultAdmin($this->container); + + $batch = current(array_filter($this->presenter->detectionBatches->findAll(), function ($b) { + return $b->getUploadCompletedAt() === null; + })); + Assert::notNull($batch); + + $assignments = []; + $otherAssignments = []; + foreach ($this->presenter->assignments->findAll() as $assignment) { + if ($assignment->isExam()) { + $otherAssignments[] = $assignment; + } else { + $assignments[] = $assignment; + } + } + Assert::count(2, $assignments); + Assert::count(1, $otherAssignments); + + $payload = PresenterTestHelper::performPresenterRequest( + $this->presenter, + 'V1:PlagiarismPresenter', + 'POST', + ['action' => 'updateBatch', 'id' => $batch->getId()], + [ + 'assignments' => array_map(function ($a) { + return $a->getId(); + }, $assignments) + ] + ); + Assert::equal($batch->getId(), $payload->getId()); + Assert::true($payload->getUploadCompletedAt() === null); + + foreach ($assignments as $assignment) { + $this->presenter->assignments->refresh($assignment); + Assert::equal($batch->getId(), $assignment->getPlagiarismBatch()?->getId()); + } + + foreach ($otherAssignments as $assignment) { + $this->presenter->assignments->refresh($assignment); + Assert::null($assignment->getPlagiarismBatch()); + } + } + public function testGetSimilarities() { PresenterTestHelper::loginDefaultAdmin($this->container); @@ -237,16 +283,16 @@ class TestPlagiarismPresenter extends Tester\TestCase 'fileEntry' => $similarity->getFileEntry(), 'fragments' => [ [ - [ 'offset' => 42, 'length' => 54 ], - [ 'offset' => 42, 'length' => 54 ], + ['offset' => 42, 'length' => 54], + ['offset' => 42, 'length' => 54], ], [ - [ 'offset' => 420, 'length' => 540 ], - [ 'offset' => 420, 'length' => 540 ], + ['offset' => 420, 'length' => 540], + ['offset' => 420, 'length' => 540], ], [ - [ 'offset' => 4200, 'length' => 1024 ], - [ 'offset' => 4200, 'length' => 1024 ], + ['offset' => 4200, 'length' => 1024], + ['offset' => 4200, 'length' => 1024], ], ] ]], @@ -476,8 +522,8 @@ class TestPlagiarismPresenter extends Tester\TestCase 'fileEntry' => $similarity->getFileEntry(), 'fragments' => [ [ - [ 'off' => 42, 'length' => 54 ], - [ 'offset' => 42, 'len' => 54 ], + ['off' => 42, 'length' => 54], + ['offset' => 42, 'len' => 54], ], ] ]], From 8f5b6a936dcbc218ce5f725d2e3acb967f767d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Fri, 11 Apr 2025 18:25:07 +0200 Subject: [PATCH 2/2] Changing the view to report batch-completion time rather than ID. --- app/model/view/AssignmentViewFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/model/view/AssignmentViewFactory.php b/app/model/view/AssignmentViewFactory.php index cebc7e07c..992269f22 100644 --- a/app/model/view/AssignmentViewFactory.php +++ b/app/model/view/AssignmentViewFactory.php @@ -116,7 +116,7 @@ function (LocalizedExercise $text) use ($assignment) { ], "solutionFilesLimit" => $assignment->getSolutionFilesLimit(), "solutionSizeLimit" => $assignment->getSolutionSizeLimit(), - "plagiarismBatchId" => $assignment->getPlagiarismBatch()?->getId(), + "plagiarismCheckedAt" => $assignment->getPlagiarismBatch()?->getUploadCompletedAt()?->getTimestamp(), "permissionHints" => PermissionHints::get($this->assignmentAcl, $assignment) ]; }