diff --git a/app/Actions/Draft/FindOrCreateDraftStudy.php b/app/Actions/Draft/FindOrCreateDraftStudy.php index 71fb7059..e52fd577 100644 --- a/app/Actions/Draft/FindOrCreateDraftStudy.php +++ b/app/Actions/Draft/FindOrCreateDraftStudy.php @@ -25,6 +25,8 @@ public function execute(FileSystemObject $folder, Draft $draft, Project $project if (! $study) { $study = $this->createNewStudy($folder, $draft, $project, $validation); $this->updateFolderWithStudy($folder, $study); + } else { + $this->reassignStudyToProject($study, $project); } return $study; @@ -66,4 +68,20 @@ private function updateFolderWithStudy(FileSystemObject $folder, Study $study): $folder->study_id = $study->id; $folder->save(); } + + /** + * Keep an existing study on the project this process run is using. + */ + private function reassignStudyToProject(Study $study, Project $project): void + { + if ($study->project_id === $project->id) { + return; + } + + $study->project_id = $project->id; + $study->save(); + + $study->sample()->update(['project_id' => $project->id]); + $study->datasets()->update(['project_id' => $project->id]); + } } diff --git a/app/Actions/Draft/ProcessDraft.php b/app/Actions/Draft/ProcessDraft.php index 79fc4bf9..8aacb639 100644 --- a/app/Actions/Draft/ProcessDraft.php +++ b/app/Actions/Draft/ProcessDraft.php @@ -15,7 +15,6 @@ use App\Models\Validation; use App\Support\Draft\HifsaPdfResolver; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\DB; @@ -36,7 +35,7 @@ public function __construct( /** * Process draft and create project structure. */ - public function execute(Request $request, Draft $draft, User $user): Response|JsonResponse|RedirectResponse + public function execute(Request $request, Draft $draft, User $user): Response|JsonResponse { [$user_id, $team_id, $team] = $user->getUserTeamData(); @@ -94,7 +93,7 @@ private function prepareDraft(Request $request, Draft $draft): void */ public function createOrUpdateProject(Draft $draft, int $user_id, int $team_id, $user, $team): Project { - $project = Project::where('draft_id', $draft->id)->first(); + $project = $this->resolveDraftProject($draft); if (! $project) { $project = $this->createNewProject($draft, $user_id, $team_id, $user, $team); @@ -105,6 +104,40 @@ public function createOrUpdateProject(Draft $draft, int $user_id, int $team_id, return $project; } + /** + * Pick the existing project for this draft. + * + * A draft can accumulate more than one project row (retries, concurrent + * process requests). `first()` without an order then often returns an + * empty newer project while studies still belong to an older one, so + * finalizeProcessing sees zero studies and redirects. + */ + public function resolveDraftProject(Draft $draft): ?Project + { + $studyProjectId = Study::query() + ->where('draft_id', $draft->id) + ->whereNotNull('project_id') + ->value('project_id'); + + if ($studyProjectId) { + $project = Project::query()->find($studyProjectId); + + if ($project) { + if ($project->draft_id !== $draft->id) { + $project->draft_id = $draft->id; + $project->save(); + } + + return $project; + } + } + + return Project::query() + ->where('draft_id', $draft->id) + ->orderBy('id') + ->first(); + } + /** * Create a new project from draft. */ @@ -363,7 +396,7 @@ public function createDatasetFromOrphanedFile(FileSystemObject $folder, Study $s /** * Finalize processing and return response. */ - public function finalizeProcessing(Draft $draft, Project $project): Response|JsonResponse|RedirectResponse + public function finalizeProcessing(Draft $draft, Project $project): Response|JsonResponse { $draft->save(); @@ -371,8 +404,15 @@ public function finalizeProcessing(Draft $draft, Project $project): Response|Jso $this->hifsaPdfResolver->persistCsvData($studies); $studies = $this->hifsaPdfResolver->enrichStudies($studies, $draft); - if (count($studies) == 0) { - return redirect()->back()->withErrors(['studies' => 'nmrXiv requires raw or processed raw instrument output files. If you data is from a single sample organise all the files in one folder and click proceed. If you have multiple samples, group your data in subfolders with each subfolder corresponding to a sample. Thank you.']); + if ($studies->isEmpty()) { + $message = 'nmrXiv requires raw or processed raw instrument output files. If you data is from a single sample organise all the files in one folder and click proceed. If you have multiple samples, group your data in subfolders with each subfolder corresponding to a sample. Thank you.'; + + return response()->json([ + 'message' => $message, + 'errors' => [ + 'studies' => [$message], + ], + ], 422); } Log::info('Finalizing processing for draft '.$draft->id); diff --git a/app/Actions/Project/PublishEmbargoProject.php b/app/Actions/Project/PublishEmbargoProject.php index 07868045..89336b56 100644 --- a/app/Actions/Project/PublishEmbargoProject.php +++ b/app/Actions/Project/PublishEmbargoProject.php @@ -103,7 +103,7 @@ private function validateForPublication(Project $project, bool $restoreReleaseDa $project->release_date = now()->startOfDay()->toDateString(); $project->save(); - $validation->process(); + $validation->process(project: $project); $publishAttemptValidation = $validation->fresh(); if (! $publishAttemptValidation['report']['project']['status']) { @@ -113,7 +113,7 @@ private function validateForPublication(Project $project, bool $restoreReleaseDa $project->refresh(); if ($project->validation) { - $project->validation->process(); + $project->validation->process(project: $project); } } diff --git a/app/Actions/Project/UpdateProject.php b/app/Actions/Project/UpdateProject.php index fa1fd6be..c886584c 100644 --- a/app/Actions/Project/UpdateProject.php +++ b/app/Actions/Project/UpdateProject.php @@ -149,7 +149,7 @@ public function update(Project $project, array $input) } } } - $validation->process(); + $validation->process(project: $project); $project = $project->fresh(); diff --git a/app/Http/Controllers/DraftController.php b/app/Http/Controllers/DraftController.php index 8757bb28..8b71f921 100644 --- a/app/Http/Controllers/DraftController.php +++ b/app/Http/Controllers/DraftController.php @@ -16,7 +16,6 @@ use App\Support\ProvisionalDoi; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; @@ -67,10 +66,8 @@ public function all(Request $request): JsonResponse /** * Process draft and convert to project structure. - * - * @return Response|JsonResponse|RedirectResponse */ - public function process(Request $request, Draft $draft) + public function process(Request $request, Draft $draft): Response|JsonResponse { $this->authorize('updateDraft', $draft); @@ -176,7 +173,7 @@ public function complete(Request $request, Draft $draft): JsonResponse { $this->authorize('updateDraft', $draft); - $project = Project::where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if (! $project) { return response()->json([ @@ -186,10 +183,12 @@ public function complete(Request $request, Draft $draft): JsonResponse } $validation = $project->validation; - $validation->process(); + $validation->process(project: $project); + + $project->load(['studies.datasets', 'owner', 'citations', 'authors', 'tags']); return response()->json([ - 'project' => Project::with(['studies.datasets', 'owner', 'citations', 'authors', 'tags'])->where('draft_id', $draft->id)->first(), + 'project' => $project, 'validation' => $validation, ]); } @@ -201,7 +200,7 @@ public function info(Request $request, Draft $draft): JsonResponse { $this->authorize('updateDraft', $draft); - $project = Project::where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if (! $project) { return response()->json([ @@ -268,7 +267,7 @@ public function status(Request $request, Draft $draft): JsonResponse { $this->authorize('updateDraft', $draft); - $project = Project::where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if (! $project) { return response()->json([ @@ -321,7 +320,7 @@ public function storeProvisionalDoi(Request $request, Draft $draft): JsonRespons $payload = DB::transaction(function () use ($draft, $user, $user_id, $team_id, $team): array { Draft::query()->whereKey($draft->id)->lockForUpdate()->firstOrFail(); - $project = Project::query()->where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if (! $project) { $project = $this->processDraft->createNewProject($draft, $user_id, $team_id, $user, $team); @@ -357,7 +356,7 @@ public function destroyProvisionalDoi(Request $request, Draft $draft): Response { $this->authorize('updateDraft', $draft); - $project = Project::query()->where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if (! $project) { return response()->noContent(); diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 0a6b7822..92eac14a 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -349,7 +349,7 @@ public function validation(Request $request, Project $project) } } - $validation->process(); + $validation->process(project: $project); return Inertia::render('Project/Validation', [ 'project' => $project->load('projectInvitations', 'tags', 'authors', 'citations', 'fundingReferences'), @@ -377,7 +377,7 @@ public function validationReport(Request $request, Project $project) } } - $validation->process(); + $validation->process(project: $project); return $validation->fresh(); } @@ -409,7 +409,7 @@ public function publish(Request $request, Project $project, PublishProject $publ } $project->release_date = $request->get('release_date'); - $validation->process(); + $validation->process(project: $project); $validation = $validation->fresh(); if ($validation['report']['project']['status']) { $project->status = 'queued'; @@ -467,7 +467,7 @@ public function publish(Request $request, Project $project, PublishProject $publ $validation = $project->validation; if ($validation) { - $validation->process(forceSamplesMode: true); + $validation->process(forceSamplesMode: true, project: $project); $validation = $validation->fresh(); } @@ -562,7 +562,7 @@ protected function publishValidationHintsFromReport(?array $report): array $citations = $project['citations'] ?? null; if (is_string($citations) && str_starts_with($citations, 'false|')) { - $hints[] = 'Add a DOI to every citation, or choose a future release date if you are not ready to publish immediately.'; + $hints[] = 'Add at least one citation before publishing in project mode.'; } $labels = [ diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index 09286c2f..ac703908 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; +use App\Actions\Draft\ProcessDraft; use App\Models\Draft; -use App\Models\Project; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -12,6 +12,8 @@ class UploadController extends Controller { + public function __construct(private ProcessDraft $processDraft) {} + public function upload(Request $request) { $draftId = $request->get('draft_id'); @@ -20,7 +22,7 @@ public function upload(Request $request) $draft = Draft::find((int) $draftId); if ($draft && $request->user()?->can('updateDraft', $draft)) { - $project = Project::where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if ($project && $project->status !== 'draft') { return redirect()->route('publish', ['draft' => $draft->id]); @@ -39,7 +41,7 @@ public function publish(Request $request, Draft $draft) { $this->authorize('updateDraft', $draft); - $project = Project::where('draft_id', $draft->id)->first(); + $project = $this->processDraft->resolveDraftProject($draft); if (! $project) { return redirect()->route('upload', ['draft_id' => $draft->id]); @@ -50,11 +52,23 @@ public function publish(Request $request, Draft $draft) } $validation = $project->validation; - $validation->process(); + $validation->process(project: $project); + + $project->load([ + 'studies.datasets', + 'studies.sample.molecules', + 'studies.sample.mixtureComposition.components.molecule', + 'owner', + 'citations', + 'fundingReferences', + 'authors', + 'tags', + 'license', + ]); return Inertia::render('Publish', [ 'draft' => $draft, - 'project' => Project::with(['studies.datasets', 'studies.sample.molecules', 'studies.sample.mixtureComposition.components.molecule', 'owner', 'citations', 'fundingReferences', 'authors', 'tags', 'license'])->where('draft_id', $draft->id)->first(), + 'project' => $project, 'validation' => $validation, ]); } diff --git a/app/Jobs/ProcessFiles.php b/app/Jobs/ProcessFiles.php index a43ca437..c5b5d745 100644 --- a/app/Jobs/ProcessFiles.php +++ b/app/Jobs/ProcessFiles.php @@ -2,9 +2,9 @@ namespace App\Jobs; +use App\Actions\Draft\ProcessDraft; use App\Models\Draft; use App\Models\FileSystemObject; -use App\Models\Project; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; @@ -119,7 +119,7 @@ private function updateFileSystemObjectStatus(FileSystemObject $fsObject): bool */ private function handleMissingFilesRestored(): void { - $project = Project::where('draft_id', $this->draft->id)->first(); + $project = app(ProcessDraft::class)->resolveDraftProject($this->draft); if (! $project) { Log::warning('No project found for draft '.$this->draft->id); diff --git a/app/Jobs/ValidateAndSubmitELNDraft.php b/app/Jobs/ValidateAndSubmitELNDraft.php index a15c2c5e..4ee566b3 100644 --- a/app/Jobs/ValidateAndSubmitELNDraft.php +++ b/app/Jobs/ValidateAndSubmitELNDraft.php @@ -76,7 +76,7 @@ public function handle(): void // Process validation $validation = $project->validation; - $validation->process(); + $validation->process(project: $project); $validation = $validation->fresh(); $status = true; diff --git a/app/Models/Validation.php b/app/Models/Validation.php index 3ab56f48..ef965b38 100644 --- a/app/Models/Validation.php +++ b/app/Models/Validation.php @@ -82,9 +82,28 @@ public function project(): HasOne return $this->hasOne(Project::class); } - public function process(bool $forceSamplesMode = false): void + public function projects(): HasMany { - if (! $project = $this->project) { + return $this->hasMany(Project::class); + } + + /** + * Project this validation should score when several project rows share validation_id. + */ + public function associatedProject(): ?Project + { + return $this->projects() + ->withCount('studies') + ->orderByDesc('studies_count') + ->orderBy('id') + ->first(); + } + + public function process(bool $forceSamplesMode = false, ?Project $project = null): void + { + $project ??= $this->associatedProject(); + + if (! $project) { return; } @@ -151,14 +170,16 @@ public function process(bool $forceSamplesMode = false): void 'id' => $study->id, ]; + $moleculeIds = $study->sample?->molecules?->pluck('id')->toArray() ?? []; + $values = [ 'title' => $study->name, 'description' => $study->description, 'keywords' => $study->tags->pluck('id')->toArray(), - 'composition' => $study->sample->molecules->pluck('id')->toArray(), + 'composition' => $moleculeIds, 'nmrium_info' => $study->has_nmrium ? $study->has_nmrium : null, 'sample' => $study->sample, - 'molecules' => $study->sample->molecules->pluck('id')->toArray(), + 'molecules' => $moleculeIds, ]; $study_rules = $rules['study']; @@ -256,14 +277,14 @@ public function process(bool $forceSamplesMode = false): void array_push($studiesValidation, $studyReport); } - // Validate citations + // Validate citations. The associated article DOI is optional. $citations = $project->citations; $citationsValidation = []; $citationsStatus = $citations && $citations->isNotEmpty(); - $shouldValidateCitationDoi = ! $samplesMode; + $shouldAnnotateCitationDoi = ! $samplesMode; - if ($shouldValidateCitationDoi && $project->release_date) { - $shouldValidateCitationDoi = Carbon::parse($project->release_date)->lessThanOrEqualTo(now()); + if ($shouldAnnotateCitationDoi && $project->release_date) { + $shouldAnnotateCitationDoi = Carbon::parse($project->release_date)->lessThanOrEqualTo(now()); } if ($citations && $citations->isNotEmpty()) { @@ -273,18 +294,9 @@ public function process(bool $forceSamplesMode = false): void 'id' => $citation->id, ]; - if ($shouldValidateCitationDoi) { - // Check if DOI is present only for current/past release date projects. - $hasDoi = is_string($citation->doi) && trim($citation->doi) !== ''; - - if ($hasDoi) { - $citationReport['doi'] = 'true|required'; - } else { - $citationReport['doi'] = 'false|required'; - $citationsStatus = false; // Citation validation failed - } - - $citationReport['status'] = $hasDoi; + if ($shouldAnnotateCitationDoi) { + $citationReport['doi'] = 'true|optional'; + $citationReport['status'] = true; } else { $citationReport['doi'] = $samplesMode ? 'true|skipped-samples-mode' diff --git a/tests/Feature/Draft/ProcessDraftProjectReuseTest.php b/tests/Feature/Draft/ProcessDraftProjectReuseTest.php new file mode 100644 index 00000000..ccc02ff2 --- /dev/null +++ b/tests/Feature/Draft/ProcessDraftProjectReuseTest.php @@ -0,0 +1,146 @@ +user = User::factory()->withPersonalTeam()->create(); + $this->team = $this->user->currentTeam; + $this->draft = Draft::factory()->create([ + 'name' => 'Reuse Draft Project', + 'owner_id' => $this->user->id, + 'team_id' => $this->team->id, + ]); + } + + public function test_resolve_draft_project_prefers_the_project_that_already_has_studies(): void + { + $emptyProject = $this->makeProject('Empty sibling project'); + $studyProject = $this->makeProject('Project with studies'); + + $folder = $this->makeStudyFolder('sample-a'); + $this->makeStudy($studyProject, $folder, 'sample-a'); + + $resolved = app(ProcessDraft::class)->resolveDraftProject($this->draft); + + $this->assertNotNull($resolved); + $this->assertSame($studyProject->id, $resolved->id); + $this->assertNotSame($emptyProject->id, $resolved->id); + } + + public function test_process_returns_existing_studies_when_another_empty_project_shares_the_draft(): void + { + Bus::fake(); + + $this->makeProject('Empty sibling project'); + $studyProject = $this->makeProject('Project with studies'); + + $folder = $this->makeStudyFolder('sample-a'); + $this->makeStudy($studyProject, $folder, 'sample-a'); + + $response = $this->actingAs($this->user) + ->postJson('/dashboard/drafts/'.$this->draft->id.'/process', [ + 'name' => $this->draft->name, + ]); + + $response->assertOk() + ->assertJsonPath('project.id', $studyProject->id) + ->assertJsonCount(1, 'studies'); + + Bus::assertDispatched(ArchiveStudy::class); + } + + public function test_process_returns_json_validation_error_instead_of_redirect_when_no_studies(): void + { + FileSystemObject::factory()->file()->rootLevel()->create([ + 'name' => 'notes.txt', + 'draft_id' => $this->draft->id, + 'status' => 'present', + ]); + + $response = $this->actingAs($this->user) + ->postJson('/dashboard/drafts/'.$this->draft->id.'/process', [ + 'name' => $this->draft->name, + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['studies']); + } + + private function makeProject(string $name): Project + { + $validation = Validation::factory()->create(); + + return Project::factory()->create([ + 'name' => $name, + 'owner_id' => $this->user->id, + 'team_id' => $this->team->id, + 'draft_id' => $this->draft->id, + 'license_id' => null, + 'validation_id' => $validation->id, + ]); + } + + private function makeStudyFolder(string $name): FileSystemObject + { + return FileSystemObject::factory()->directory()->rootLevel()->create([ + 'name' => $name, + 'draft_id' => $this->draft->id, + 'model_type' => 'study', + 'status' => 'present', + 'has_children' => true, + ]); + } + + private function makeStudy(Project $project, FileSystemObject $folder, string $name): Study + { + $study = Study::factory()->create([ + 'name' => $name, + 'project_id' => $project->id, + 'team_id' => $this->team->id, + 'owner_id' => $this->user->id, + 'draft_id' => $this->draft->id, + 'fs_id' => $folder->id, + 'license_id' => null, + ]); + + $folder->update([ + 'study_id' => $study->id, + 'project_id' => $project->id, + ]); + + Sample::factory()->create([ + 'name' => $name.'_sample', + 'study_id' => $study->id, + 'project_id' => $project->id, + ]); + + return $study; + } +} diff --git a/tests/Feature/Project/ProjectValidationTest.php b/tests/Feature/Project/ProjectValidationTest.php index 2f9aa820..de4ae030 100644 --- a/tests/Feature/Project/ProjectValidationTest.php +++ b/tests/Feature/Project/ProjectValidationTest.php @@ -200,4 +200,36 @@ public function test_validation_processes_empty_project() $this->assertNotNull($validation->report); $this->assertIsArray($validation->report); } + + public function test_validation_report_includes_studies_when_an_empty_sibling_shares_validation(): void + { + $validation = Validation::factory()->create(); + $this->project->validation()->associate($validation); + $this->project->save(); + + Project::factory()->create([ + 'validation_id' => $validation->id, + 'name' => 'Empty sibling', + 'owner_id' => $this->user->id, + 'team_id' => $this->user->personalTeam()->id, + ]); + + $study = Study::factory()->for($this->project)->create([ + 'name' => 'NMR sample', + 'owner_id' => $this->user->id, + 'validation_id' => $validation->id, + ]); + + Sample::factory()->create([ + 'study_id' => $study->id, + 'project_id' => $this->project->id, + ]); + + $response = $this->actingAs($this->user) + ->get(route('project.validation', $this->project)); + + $response->assertStatus(200) + ->assertJsonPath('report.project.studies.0.id', $study->id) + ->assertJsonPath('report.project.studies.0.name', 'NMR sample'); + } } diff --git a/tests/Feature/Project/PublishEmbargoProjectValidationTest.php b/tests/Feature/Project/PublishEmbargoProjectValidationTest.php index fbe0c032..e7737bcb 100644 --- a/tests/Feature/Project/PublishEmbargoProjectValidationTest.php +++ b/tests/Feature/Project/PublishEmbargoProjectValidationTest.php @@ -56,7 +56,7 @@ public function test_failed_publish_now_restores_validation_report_for_original_ $response->assertStatus(422); $response->assertJsonPath( 'validation.report.project.citations_detail.0.doi', - 'false|required' + 'true|optional' ); $project->refresh(); diff --git a/tests/Feature/Project/PublishProjectTest.php b/tests/Feature/Project/PublishProjectTest.php index 0e0a54dc..3356ce79 100644 --- a/tests/Feature/Project/PublishProjectTest.php +++ b/tests/Feature/Project/PublishProjectTest.php @@ -126,7 +126,7 @@ public function test_project_publication_updates_validation_status() } #[Test] - public function citations_without_doi_fail_validation(): void + public function citations_without_doi_pass_validation(): void { $this->project->draft->update(['project_enabled' => true]); @@ -144,12 +144,10 @@ public function citations_without_doi_fail_validation(): void $validation->process(); - // Check that validation report shows citation without DOI - $this->assertFalse($validation->report['project']['status']); - $this->assertEquals('false|required', $validation->report['project']['citations']); + $this->assertEquals('true|required', $validation->report['project']['citations']); $this->assertNotEmpty($validation->report['project']['citations_detail']); - $this->assertEquals(false, $validation->report['project']['citations_detail'][0]['status']); - $this->assertEquals('false|required', $validation->report['project']['citations_detail'][0]['doi']); + $this->assertEquals(true, $validation->report['project']['citations_detail'][0]['status']); + $this->assertEquals('true|optional', $validation->report['project']['citations_detail'][0]['doi']); } #[Test] @@ -175,7 +173,7 @@ public function citations_with_doi_pass_validation(): void $this->assertEquals('true|required', $validation->report['project']['citations']); $this->assertNotEmpty($validation->report['project']['citations_detail']); $this->assertEquals(true, $validation->report['project']['citations_detail'][0]['status']); - $this->assertEquals('true|required', $validation->report['project']['citations_detail'][0]['doi']); + $this->assertEquals('true|optional', $validation->report['project']['citations_detail'][0]['doi']); } #[Test] diff --git a/tests/Feature/UploadTest.php b/tests/Feature/UploadTest.php index 92ee00c5..2a41f17b 100644 --- a/tests/Feature/UploadTest.php +++ b/tests/Feature/UploadTest.php @@ -5,6 +5,8 @@ use App\Models\Draft; use App\Models\FileSystemObject; use App\Models\Project; +use App\Models\Sample; +use App\Models\Study; use App\Models\User; use App\Models\Validation; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -227,6 +229,53 @@ public function test_publish_renders_for_authorized_user(): void $response->assertStatus(200); } + public function test_publish_uses_the_draft_project_that_has_studies_not_a_later_empty_sibling(): void + { + $validation = Validation::factory()->create(); + $draft = Draft::factory()->create([ + 'owner_id' => $this->user->id, + 'team_id' => $this->user->currentTeam->id, + ]); + + $ownedProject = Project::factory()->create([ + 'draft_id' => $draft->id, + 'owner_id' => $this->user->id, + 'team_id' => $this->user->currentTeam->id, + 'validation_id' => $validation->id, + 'license_id' => null, + ]); + + $study = Study::factory()->create([ + 'name' => 'sample-a', + 'project_id' => $ownedProject->id, + 'team_id' => $this->user->currentTeam->id, + 'owner_id' => $this->user->id, + 'draft_id' => $draft->id, + 'license_id' => null, + ]); + Sample::factory()->create([ + 'name' => 'sample-a_sample', + 'study_id' => $study->id, + 'project_id' => $ownedProject->id, + ]); + + $otherUser = User::factory()->withPersonalTeam()->create(); + Project::factory()->create([ + 'draft_id' => $draft->id, + 'owner_id' => $otherUser->id, + 'team_id' => $otherUser->currentTeam->id, + 'license_id' => null, + ]); + + $response = $this->actingAs($this->user) + ->get('/publish/'.$draft->id); + + $response->assertStatus(200); + + $page = $this->inertiaPageFromResponse($response); + $this->assertSame($ownedProject->id, $page['props']['project']['id']); + } + public function test_publish_processes_project_validation(): void { $validation = Validation::factory()->create(); diff --git a/tests/Unit/Models/ValidationModelTest.php b/tests/Unit/Models/ValidationModelTest.php index 18e372ef..bb976f25 100644 --- a/tests/Unit/Models/ValidationModelTest.php +++ b/tests/Unit/Models/ValidationModelTest.php @@ -417,6 +417,45 @@ public function test_process_method_validates_project_fields() $this->assertArrayHasKey('project', $validation->report); } + public function test_process_scores_studies_when_an_earlier_empty_project_shares_validation(): void + { + $validation = new Validation; + $validation->save(); + + Project::factory()->create([ + 'validation_id' => $validation->id, + 'name' => 'Empty sibling', + ]); + + $project = Project::factory()->create([ + 'validation_id' => $validation->id, + 'name' => 'Project with studies', + ]); + + $study = Study::factory()->create([ + 'validation_id' => $validation->id, + 'project_id' => $project->id, + 'name' => 'Sample A', + ]); + + Sample::factory()->create([ + 'study_id' => $study->id, + 'project_id' => $project->id, + ]); + + config(['validations.default' => 'v1']); + config(['validations.v1.project' => []]); + config(['validations.v1.study' => []]); + config(['validations.v1.dataset' => []]); + + $validation->process(); + $validation->refresh(); + + $this->assertCount(1, $validation->report['project']['studies']); + $this->assertSame($study->id, $validation->report['project']['studies'][0]['id']); + $this->assertSame('Sample A', $validation->report['project']['studies'][0]['name']); + } + public function test_process_method_handles_project_with_studies() { $validation = new Validation;