From 67063d269b7f064606470d8563cb1549741c4575 Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Thu, 28 Aug 2025 19:14:01 -0700 Subject: [PATCH 1/7] Edits --- .../ComputerScienceResourceController.php | 13 ++++++++ .../Controllers/ResourceEditsController.php | 8 +++-- resources/js/Components/Form/TagSelector.vue | 14 ++++---- .../ResourceEdit/ResourceEditsFAQ.vue | 3 +- ...st.php => ComputerScienceResourceTest.php} | 32 ++++++++++++++++++- tests/Feature/ResourceEditsTest.php | 9 ++++-- 6 files changed, 64 insertions(+), 15 deletions(-) rename tests/Feature/{ComputerScienceResourceControllerTest.php => ComputerScienceResourceTest.php} (77%) diff --git a/app/Http/Controllers/ComputerScienceResourceController.php b/app/Http/Controllers/ComputerScienceResourceController.php index c93b2391..4c4f444e 100644 --- a/app/Http/Controllers/ComputerScienceResourceController.php +++ b/app/Http/Controllers/ComputerScienceResourceController.php @@ -17,6 +17,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; use Inertia\Inertia; use Throwable; @@ -127,6 +128,18 @@ public function store(StoreResourceRequest $request) return response()->json($resource); } catch (Throwable $e) { DB::rollBack(); + // Attempt to remove the uploaded image if it was stored + if (isset($path) && $path) { + try { + Storage::disk('public')->delete($path); + } catch (Throwable $removeEx) { + Log::warning('Failed to remove image after exception', [ + 'user_id' => Auth::id(), + 'image_path' => $path, + 'error' => $removeEx->getMessage(), + ]); + } + } Log::critical('Failed to create resource', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 42e732f3..8a07be81 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -122,7 +122,11 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc } if (array_key_exists('image_path', $changes)) { - // TODO: Removed code to delete photo, will be handled in a cron job + // TODO: Remove the old photo resource photo, will be handled in a cron job, + // + // photo image_url history can be viewed via activity log. + // + $destPath = null; if (isset($changes['image_path'])) { // Copy the new file from 'resource-edits' to 'resource' (do not delete the old one) @@ -131,7 +135,7 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc $newFileName = Str::random(40).'.'.$fileExtension; $destPath = 'resource/'.$newFileName; - Storage::disk('public')->copy($sourcePath, $destPath); + Storage::disk('public')->move($sourcePath, $destPath); } // Update image_path in DB diff --git a/resources/js/Components/Form/TagSelector.vue b/resources/js/Components/Form/TagSelector.vue index 5cac5deb..f90fed58 100644 --- a/resources/js/Components/Form/TagSelector.vue +++ b/resources/js/Components/Form/TagSelector.vue @@ -47,6 +47,12 @@ function updateDropdownPosition() { position: "absolute", }; } +// Watch selectedTags and update dropdown position when tags change and dropdown is open +watch(selectedTags, () => { + if (showDropdown.value) { + nextTick(updateDropdownPosition); + } +}); watch(showDropdown, (val) => { if (val) { @@ -191,14 +197,6 @@ function onKeydown(event) { searchInput.value?.blur(); return; } - - if ( - event.key === "Backspace" && - !searchQuery.value && - selectedTags.value.length > 0 - ) { - removeTag(selectedTags.value[selectedTags.value.length - 1]); - } } function onBlur() { diff --git a/resources/js/Components/Resources/ResourceEdit/ResourceEditsFAQ.vue b/resources/js/Components/Resources/ResourceEdit/ResourceEditsFAQ.vue index 99a66aff..107fcd68 100644 --- a/resources/js/Components/Resources/ResourceEdit/ResourceEditsFAQ.vue +++ b/resources/js/Components/Resources/ResourceEdit/ResourceEditsFAQ.vue @@ -113,7 +113,8 @@ import FrequentlyAskedQuestion from "@/Components/FrequentlyAskedQuestion.vue"; class="border border-gray-300 dark:border-gray-600 px-4 py-2" > Since there are no existing votes (positive or negative), the entire community can merge any edits. - The reasoning is that someone could've make a typo and needed a quick fix. + The reasoning is that someone could've made a typo and needed a quick fix. + Everyone automatically upvotes their own resources. So, you must unvote your newly created resource to reach 0 votes. diff --git a/tests/Feature/ComputerScienceResourceControllerTest.php b/tests/Feature/ComputerScienceResourceTest.php similarity index 77% rename from tests/Feature/ComputerScienceResourceControllerTest.php rename to tests/Feature/ComputerScienceResourceTest.php index aeb5cb98..8634998a 100644 --- a/tests/Feature/ComputerScienceResourceControllerTest.php +++ b/tests/Feature/ComputerScienceResourceTest.php @@ -13,7 +13,7 @@ use Tests\RequestFactories\ComputerScienceResource\StoreResourceRequestFactory; use Tests\TestCase; -class ComputerScienceResourceControllerTest extends TestCase +class ComputerScienceResourceTest extends TestCase { use RefreshDatabase; @@ -112,4 +112,34 @@ public function test_cannot_post_resource_with_invalid_fields(string $field, mix "Failed asserting that a resource with name '{$validData['name']}' was not created in the database. Invalid field: $field" ); } + + public function test_image_is_removed_if_resource_creation_fails() + { + Storage::fake('public'); + $this->actingAs($this->user); + + // Create valid form data but set an invalid field to force failure + $formData = StoreResourceRequestFactory::new()->create(); + $formData['image_file'] = UploadedFile::fake()->image('fail_image.jpg'); + $formData['name'] = str_repeat('a', 101); // Invalid name, will fail validation + + $response = $this->postJson(route('resources.store'), $formData); + $response->assertStatus(422); // Validation error + + // The image should not exist in storage + Storage::disk('public')->assertMissing('resource/' . $formData['image_file']->hashName()); + } + + public function test_model_removes_image_upon_deletion() + { + $resource = ComputerScienceResource::factory()->create(); + + $imagePath = $resource->image_path; + + Storage::disk('public')->assertExists($imagePath); + + $resource->delete(); + + Storage::disk('public')->assertMissing($imagePath); + } } diff --git a/tests/Feature/ResourceEditsTest.php b/tests/Feature/ResourceEditsTest.php index 0bb7861e..17558938 100644 --- a/tests/Feature/ResourceEditsTest.php +++ b/tests/Feature/ResourceEditsTest.php @@ -148,13 +148,13 @@ public function test_merged_edit_reflects_changes_on_original_resource(): void $resource = ComputerScienceResource::factory()->create(); $this->actingAs($this->user); - $mergeAttempts = 10; + $mergeAttempts = 5; for ($i = 0; $i < $mergeAttempts; $i++) { $changes = [ 'name' => "Resource Name Edited {$i}", 'description' => "Resource Description Changed {$i}", - 'image_file' => UploadedFile::fake()->image('resource.jpg'), + 'image_file' => UploadedFile::fake()->image("resource_{$i}.jpg"), 'page_url' => "http://{$i}.com", 'difficulty' => fake()->randomElement(config('computerScienceResource.difficulties')), 'platforms' => fake()->randomElements(config('computerScienceResource.platforms'), fake()->numberBetween(1, 3)), @@ -164,14 +164,17 @@ public function test_merged_edit_reflects_changes_on_original_resource(): void 'general_tags' => ["$i-a", "$i-b", "$i-c"], ]; - $this->makeAndApplyResourceEdits($resource->id, $changes); + $edit = $this->createResourceEdit($resource->id, $changes); + $this->approveResourceEdit($edit); // Refresh and assert $resource->refresh(); $this->assertEquals($changes['name'], $resource->name); $this->assertEquals($changes['description'], $resource->description); + Storage::disk('public')->assertExists($resource->image_path); + Storage::disk('public')->assertMissing($edit->image_path); $this->assertEquals($changes['page_url'], $resource->page_url); $this->assertEquals($changes['difficulty'], $resource->difficulty); From 2d4d7d936d80e9a9dae4483cfb4a90765c0a0d34 Mon Sep 17 00:00:00 2001 From: AllanKoder <74692833+AllanKoder@users.noreply.github.com> Date: Fri, 29 Aug 2025 02:14:24 +0000 Subject: [PATCH 2/7] Apply automatic changes --- tests/Feature/ComputerScienceResourceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/ComputerScienceResourceTest.php b/tests/Feature/ComputerScienceResourceTest.php index 8634998a..db69b737 100644 --- a/tests/Feature/ComputerScienceResourceTest.php +++ b/tests/Feature/ComputerScienceResourceTest.php @@ -127,7 +127,7 @@ public function test_image_is_removed_if_resource_creation_fails() $response->assertStatus(422); // Validation error // The image should not exist in storage - Storage::disk('public')->assertMissing('resource/' . $formData['image_file']->hashName()); + Storage::disk('public')->assertMissing('resource/'.$formData['image_file']->hashName()); } public function test_model_removes_image_upon_deletion() From 85d2e3d4830f97401d8d4028f35041fafb0c1c63 Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Thu, 28 Aug 2025 20:07:19 -0700 Subject: [PATCH 3/7] exception for resource already existing --- .../ResourceAlreadyCreatedException.php | 16 ++ .../Resources/ResourceInvalidTabException.php | 9 + .../ComputerScienceResourceController.php | 169 +++------------- .../ComputerScienceResourceService.php | 190 ++++++++++++++++++ tests/Feature/ComputerScienceResourceTest.php | 18 ++ 5 files changed, 263 insertions(+), 139 deletions(-) create mode 100644 app/Exceptions/Resources/ResourceAlreadyCreatedException.php create mode 100644 app/Exceptions/Resources/ResourceInvalidTabException.php create mode 100644 app/Services/ComputerScienceResourceService.php diff --git a/app/Exceptions/Resources/ResourceAlreadyCreatedException.php b/app/Exceptions/Resources/ResourceAlreadyCreatedException.php new file mode 100644 index 00000000..f268c8f1 --- /dev/null +++ b/app/Exceptions/Resources/ResourceAlreadyCreatedException.php @@ -0,0 +1,16 @@ +resource = $resource; + } +} diff --git a/app/Exceptions/Resources/ResourceInvalidTabException.php b/app/Exceptions/Resources/ResourceInvalidTabException.php new file mode 100644 index 00000000..abc68eba --- /dev/null +++ b/app/Exceptions/Resources/ResourceInvalidTabException.php @@ -0,0 +1,9 @@ +validated(); - - DB::beginTransaction(); try { - // Store the image onto storage - $path = null; - if (array_key_exists('image_file', $validatedData) && $imageFile = $validatedData['image_file']) { - $path = $imageFile->store('resource', 'public'); - if (! $path) { - Log::error('Failed to store image file', [ - 'user_id' => Auth::id(), - 'file_info' => $imageFile, - ]); - - $fileName = $imageFile->getClientOriginalName(); - throw new \RuntimeException( - "Could not save the image file '{$fileName}' for user ID ".Auth::id().'.' - ); - } - } - - $resource = ComputerScienceResource::create([ + $resource = $this->resourceService->createResource($validatedData); + session()->flash('success', 'Created Resource!'); + return response()->json($resource); + } + catch (ResourceAlreadyCreatedException $e) + { + Log::warning('Resource already exists', [ 'user_id' => Auth::id(), - 'name' => $validatedData['name'], - 'image_path' => $path, - 'description' => $validatedData['description'], - 'page_url' => $validatedData['page_url'], - 'platforms' => $validatedData['platforms'], - 'difficulty' => $validatedData['difficulty'], - 'pricing' => $validatedData['pricing'], + 'resource_id' => $e->resource->id ?? null, + 'name' => $e->resource->name ?? null, ]); - - // Add topics as tags - $resource->topic_tags = $validatedData['topic_tags']; - - // Add programming languages as tags (if provided) - if (isset($validatedData['programming_language_tags'])) { - $resource->programming_language_tags = $validatedData['programming_language_tags']; - } - - // Add general tags (if provided) - if (isset($validatedData['general_tags'])) { - $resource->general_tags = $validatedData['general_tags']; - } - - DB::commit(); - - $this->upvoteService->upvote('resource', $resource->id); - - Log::info('Resource created', [ - 'resource_id' => $resource->id, + session()->flash('warning', 'Resource Already Exists!'); + return response()->json($e->resource); + } + catch (Throwable $e) { + Log::error('Error creating resource', [ 'user_id' => Auth::id(), - 'name' => $resource->name, - 'slug' => $resource->slug, - 'platforms' => $resource->platforms, - ]); - - session()->flash('success', 'Created Resource!'); - - return response()->json($resource); - } catch (Throwable $e) { - DB::rollBack(); - // Attempt to remove the uploaded image if it was stored - if (isset($path) && $path) { - try { - Storage::disk('public')->delete($path); - } catch (Throwable $removeEx) { - Log::warning('Failed to remove image after exception', [ - 'user_id' => Auth::id(), - 'image_path' => $path, - 'error' => $removeEx->getMessage(), - ]); - } - } - Log::critical('Failed to create resource', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), - 'user_id' => Auth::id(), - 'data' => $validatedData, ]); - return response()->json([], 500); } } @@ -156,62 +87,22 @@ public function store(StoreResourceRequest $request) */ public function show(Request $request, string $slug, string $tab = 'reviews') { - $computerScienceResource = ComputerScienceResource::where('slug', $slug)->firstOrFail(); - // Get the review summaries - $computerScienceResource->load('reviewSummary'); - $computerScienceResource->load('user'); - - $validTabs = ['reviews', 'discussion', 'edits']; + try { + $result = $this->resourceService->getShowResourceData($request, $slug, $tab); + return Inertia::render('Resources/Show', $result); + } catch (ResourceInvalidTabException $e) { + Log::warning('Invalid resource tab requested', [ + 'user_id' => Auth::id(), + 'slug' => $slug, + 'requested_tab' => $tab, + 'error' => $e->getMessage(), + ]); - if (! in_array($tab, $validTabs)) { - // Redirect to default if invalid return redirect()->route('resources.show', [ - 'slug' => $computerScienceResource->slug, + 'slug' => $slug, 'tab' => 'reviews', - ]); + ])->with('warning', 'Invalid tab requested, redirected to reviews.'); } - - // return the resource and tab - $data = [ - 'tab' => $tab, - 'resource' => $computerScienceResource, - ]; - - $sortBy = $request->query('sort_by', 'top'); - // Load only the necessary tab data - if ($tab === 'reviews') { - $userReview = null; - if ($userId = Auth::id()) { - $userReview = ResourceReview::whereBelongsTo($computerScienceResource) - ->firstWhere('user_id', $userId); - } - - $data['userReview'] = $userReview; - - $data['reviews'] = Inertia::defer( - function () use ($computerScienceResource, $sortBy, $request) { - $query = ResourceReview::whereBelongsTo($computerScienceResource); - $query = $this->generalVotesSortingManager->applySort($query, $sortBy, ResourceReview::class); - - return $query->with('user')->paginate(10)->appends($request->query()); - } - ); - } elseif ($tab === 'edits') { - $data['resourceEdits'] = Inertia::defer( - function () use ($computerScienceResource, $sortBy, $request) { - $query = ResourceEdits::whereBelongsTo($computerScienceResource); - $query = $this->generalVotesSortingManager->applySort($query, $sortBy, ResourceEdits::class); - - return $query->with('user')->paginate(10)->appends($request->query()); - } - ); - } elseif ($tab === 'discussion') { - $data['discussion'] = Inertia::defer( - fn () => $this->commentService->getPaginatedComments('resource', $computerScienceResource->id, 0, 150, $sortBy) - ); - } - - return Inertia::render('Resources/Show', $data); } /** diff --git a/app/Services/ComputerScienceResourceService.php b/app/Services/ComputerScienceResourceService.php new file mode 100644 index 00000000..ce632a08 --- /dev/null +++ b/app/Services/ComputerScienceResourceService.php @@ -0,0 +1,190 @@ +existingConflictingResource($validatedData)) + { + throw new ResourceAlreadyCreatedException($conflictingResource); + } + + + DB::beginTransaction(); + try { + // Store the image onto storage + $path = null; + if (array_key_exists('image_file', $validatedData) && $imageFile = $validatedData['image_file']) { + $path = $imageFile->store('resource', 'public'); + if (! $path) { + Log::error('Failed to store image file', [ + 'user_id' => Auth::id(), + 'file_info' => $imageFile, + ]); + + $fileName = $imageFile->getClientOriginalName(); + throw new \RuntimeException( + "Could not save the image file '{$fileName}' for user ID " . Auth::id() . '.' + ); + } + } + + $resource = ComputerScienceResource::create([ + 'user_id' => Auth::id(), + 'name' => $validatedData['name'], + 'image_path' => $path, + 'description' => $validatedData['description'], + 'page_url' => $validatedData['page_url'], + 'platforms' => $validatedData['platforms'], + 'difficulty' => $validatedData['difficulty'], + 'pricing' => $validatedData['pricing'], + ]); + + // Add topics as tags + $resource->topic_tags = $validatedData['topic_tags']; + + // Add programming languages as tags (if provided) + if (isset($validatedData['programming_language_tags'])) { + $resource->programming_language_tags = $validatedData['programming_language_tags']; + } + + // Add general tags (if provided) + if (isset($validatedData['general_tags'])) { + $resource->general_tags = $validatedData['general_tags']; + } + + DB::commit(); + + $this->upvoteService->upvote('resource', $resource->id); + + Log::info('Resource created', [ + 'resource_id' => $resource->id, + 'user_id' => Auth::id(), + 'name' => $resource->name, + 'slug' => $resource->slug, + 'platforms' => $resource->platforms, + ]); + + return $resource; + } catch (Throwable $e) { + DB::rollBack(); + // Attempt to remove the uploaded image if it was stored + if (isset($path) && $path) { + try { + Storage::disk('public')->delete($path); + } catch (Throwable $removeEx) { + Log::warning('Failed to remove image after exception', [ + 'user_id' => Auth::id(), + 'image_path' => $path, + 'error' => $removeEx->getMessage(), + ]); + } + } + Log::critical('Failed to create resource', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'user_id' => Auth::id(), + 'data' => $validatedData, + ]); + + throw $e; + } + } + + /** + * Get all data for the resource show page, including tab logic. + * @throws Throwable + */ + public function getShowResourceData(Request $request, string $slug, string $tab = 'reviews') + { + $computerScienceResource = ComputerScienceResource::where('slug', $slug)->firstOrFail(); + $computerScienceResource->load('reviewSummary'); + $computerScienceResource->load('user'); + + $validTabs = ['reviews', 'discussion', 'edits']; + if (! in_array($tab, $validTabs)) { + throw new ResourceInvalidTabException('Invalid tab: ' . $tab); + } + + $data = [ + 'tab' => $tab, + 'resource' => $computerScienceResource, + ]; + + $sortBy = $request->query('sort_by', 'top'); + if ($tab === 'reviews') { + $userReview = null; + if ($userId = Auth::id()) { + $userReview = ResourceReview::whereBelongsTo($computerScienceResource) + ->firstWhere('user_id', $userId); + } + $data['userReview'] = $userReview; + $data['reviews'] = Inertia::defer( + function () use ($computerScienceResource, $sortBy, $request) { + $query = ResourceReview::whereBelongsTo($computerScienceResource); + $query = $this->resourceSortingManager->applySort($query, $sortBy, ResourceReview::class); + return $query->with('user')->paginate(10)->appends($request->query()); + } + ); + } elseif ($tab === 'edits') { + $data['resourceEdits'] = Inertia::defer( + function () use ($computerScienceResource, $sortBy, $request) { + $query = ResourceEdits::whereBelongsTo($computerScienceResource); + $query = $this->resourceSortingManager->applySort($query, $sortBy, ResourceEdits::class); + return $query->with('user')->paginate(10)->appends($request->query()); + } + ); + } elseif ($tab === 'discussion') { + $data['discussion'] = Inertia::defer( + fn() => $this->commentService->getPaginatedComments('resource', $computerScienceResource->id, 0, 150, $sortBy) + ); + } + return $data; + } + + + /** + * In case a user does a double submit, we have a check for that + * Checks against StoreResourceRequest + */ + private function existingConflictingResource(array $data): ?ComputerScienceResource + { + // If there is a resource with these matching properties, it is safe to say + // it is the same resource + return ComputerScienceResource::where('name', $data['name']) + ->where('page_url', $data['page_url']) + ->first(); + } +} diff --git a/tests/Feature/ComputerScienceResourceTest.php b/tests/Feature/ComputerScienceResourceTest.php index 8634998a..b2d75e0a 100644 --- a/tests/Feature/ComputerScienceResourceTest.php +++ b/tests/Feature/ComputerScienceResourceTest.php @@ -142,4 +142,22 @@ public function test_model_removes_image_upon_deletion() Storage::disk('public')->assertMissing($imagePath); } + + public function test_posting_duplicate_resource_returns_existing_resource() + { + $this->actingAs($this->user); + + // Create a resource first + $formData = StoreResourceRequestFactory::new()->create(); + $this->postJson(route('resources.store'), $formData); + + // Try to create the same resource again + $response = $this->postJson(route('resources.store'), $formData); + + $response->assertStatus(200); + + // Should return the existing resource, not create a new one + $resources = ComputerScienceResource::where('name', $formData['name'])->get(); + $this->assertCount(1, $resources); + } } From 37f7bd9b84d1e04b4daa84d0e366aa6c9d0e8c75 Mon Sep 17 00:00:00 2001 From: AllanKoder <74692833+AllanKoder@users.noreply.github.com> Date: Fri, 29 Aug 2025 03:07:45 +0000 Subject: [PATCH 4/7] Apply automatic changes --- .../Resources/ResourceInvalidTabException.php | 4 +-- .../ComputerScienceResourceController.php | 11 ++++---- .../ComputerScienceResourceService.php | 27 +++++++++---------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/app/Exceptions/Resources/ResourceInvalidTabException.php b/app/Exceptions/Resources/ResourceInvalidTabException.php index abc68eba..39e1ac19 100644 --- a/app/Exceptions/Resources/ResourceInvalidTabException.php +++ b/app/Exceptions/Resources/ResourceInvalidTabException.php @@ -4,6 +4,4 @@ use Exception; -class ResourceInvalidTabException extends Exception -{ -} +class ResourceInvalidTabException extends Exception {} diff --git a/app/Http/Controllers/ComputerScienceResourceController.php b/app/Http/Controllers/ComputerScienceResourceController.php index b3464a53..5fb8dd3a 100644 --- a/app/Http/Controllers/ComputerScienceResourceController.php +++ b/app/Http/Controllers/ComputerScienceResourceController.php @@ -60,24 +60,24 @@ public function store(StoreResourceRequest $request) try { $resource = $this->resourceService->createResource($validatedData); session()->flash('success', 'Created Resource!'); + return response()->json($resource); - } - catch (ResourceAlreadyCreatedException $e) - { + } catch (ResourceAlreadyCreatedException $e) { Log::warning('Resource already exists', [ 'user_id' => Auth::id(), 'resource_id' => $e->resource->id ?? null, 'name' => $e->resource->name ?? null, ]); session()->flash('warning', 'Resource Already Exists!'); + return response()->json($e->resource); - } - catch (Throwable $e) { + } catch (Throwable $e) { Log::error('Error creating resource', [ 'user_id' => Auth::id(), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); + return response()->json([], 500); } } @@ -89,6 +89,7 @@ public function show(Request $request, string $slug, string $tab = 'reviews') { try { $result = $this->resourceService->getShowResourceData($request, $slug, $tab); + return Inertia::render('Resources/Show', $result); } catch (ResourceInvalidTabException $e) { Log::warning('Invalid resource tab requested', [ diff --git a/app/Services/ComputerScienceResourceService.php b/app/Services/ComputerScienceResourceService.php index ce632a08..1cd7f75d 100644 --- a/app/Services/ComputerScienceResourceService.php +++ b/app/Services/ComputerScienceResourceService.php @@ -5,18 +5,16 @@ use App\Exceptions\Resources\ResourceAlreadyCreatedException; use App\Exceptions\Resources\ResourceInvalidTabException; use App\Models\ComputerScienceResource; -use App\Services\UpvoteService; -use App\Services\SortingManagers\ResourceSortingManager; -use Throwable; -use App\Models\ResourceReview; use App\Models\ResourceEdits; -use App\Services\CommentService; -use Inertia\Inertia; +use App\Models\ResourceReview; +use App\Services\SortingManagers\ResourceSortingManager; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use Inertia\Inertia; +use Throwable; class ComputerScienceResourceService { @@ -30,18 +28,14 @@ public function __construct( /** * Create a new ComputerScienceResource * - * @param array $validatedData - * @return ComputerScienceResource * @throws Throwable */ public function createResource(array $validatedData): ComputerScienceResource { - if ($conflictingResource = $this->existingConflictingResource($validatedData)) - { + if ($conflictingResource = $this->existingConflictingResource($validatedData)) { throw new ResourceAlreadyCreatedException($conflictingResource); } - DB::beginTransaction(); try { // Store the image onto storage @@ -56,7 +50,7 @@ public function createResource(array $validatedData): ComputerScienceResource $fileName = $imageFile->getClientOriginalName(); throw new \RuntimeException( - "Could not save the image file '{$fileName}' for user ID " . Auth::id() . '.' + "Could not save the image file '{$fileName}' for user ID ".Auth::id().'.' ); } } @@ -125,6 +119,7 @@ public function createResource(array $validatedData): ComputerScienceResource /** * Get all data for the resource show page, including tab logic. + * * @throws Throwable */ public function getShowResourceData(Request $request, string $slug, string $tab = 'reviews') @@ -135,7 +130,7 @@ public function getShowResourceData(Request $request, string $slug, string $tab $validTabs = ['reviews', 'discussion', 'edits']; if (! in_array($tab, $validTabs)) { - throw new ResourceInvalidTabException('Invalid tab: ' . $tab); + throw new ResourceInvalidTabException('Invalid tab: '.$tab); } $data = [ @@ -155,6 +150,7 @@ public function getShowResourceData(Request $request, string $slug, string $tab function () use ($computerScienceResource, $sortBy, $request) { $query = ResourceReview::whereBelongsTo($computerScienceResource); $query = $this->resourceSortingManager->applySort($query, $sortBy, ResourceReview::class); + return $query->with('user')->paginate(10)->appends($request->query()); } ); @@ -163,18 +159,19 @@ function () use ($computerScienceResource, $sortBy, $request) { function () use ($computerScienceResource, $sortBy, $request) { $query = ResourceEdits::whereBelongsTo($computerScienceResource); $query = $this->resourceSortingManager->applySort($query, $sortBy, ResourceEdits::class); + return $query->with('user')->paginate(10)->appends($request->query()); } ); } elseif ($tab === 'discussion') { $data['discussion'] = Inertia::defer( - fn() => $this->commentService->getPaginatedComments('resource', $computerScienceResource->id, 0, 150, $sortBy) + fn () => $this->commentService->getPaginatedComments('resource', $computerScienceResource->id, 0, 150, $sortBy) ); } + return $data; } - /** * In case a user does a double submit, we have a check for that * Checks against StoreResourceRequest From b5fc69165fc31e8dcc6de67da20eba31e61dcecc Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Thu, 28 Aug 2025 20:17:56 -0700 Subject: [PATCH 5/7] Disable button --- app/Http/Controllers/CommentController.php | 4 ++-- app/Http/Controllers/UpvoteController.php | 1 + resources/js/Pages/Resources/Create.vue | 7 ++++++- resources/js/Pages/Resources/Form/TagsFields.vue | 6 +++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index ea05c076..b07d7ab1 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -62,10 +62,10 @@ public function store(StoreCommentRequest $request) } catch (Throwable $e) { DB::rollBack(); Log::critical('Failed to save comment', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), 'validated_data' => $validatedData, 'user_id' => Auth::id(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), ]); return response()->json(['message' => 'Failed to save comment'], 500); diff --git a/app/Http/Controllers/UpvoteController.php b/app/Http/Controllers/UpvoteController.php index ebc84b5c..2593233d 100644 --- a/app/Http/Controllers/UpvoteController.php +++ b/app/Http/Controllers/UpvoteController.php @@ -29,6 +29,7 @@ public function downvote($typeKey, $id) { $result = $this->upvoteService->downvote($typeKey, $id); + // TODO: REFACTOR TO EXCEPTIONS if (isset($result['error'])) { return response()->json(['message' => $result['error']], $result['status']); } diff --git a/resources/js/Pages/Resources/Create.vue b/resources/js/Pages/Resources/Create.vue index 2902927d..3d788e24 100644 --- a/resources/js/Pages/Resources/Create.vue +++ b/resources/js/Pages/Resources/Create.vue @@ -46,6 +46,7 @@ const { isSavedToLocalStorage, hasFormContent, clearLocalStorage } = const showReset = ref(false); const stepperValue = ref("1"); const formRef = ref(null); +const isLoading = ref(false); const toast = useToast(); const scrollToForm = () => { @@ -70,13 +71,15 @@ const resetForm = () => { }; const submitForm = async () => { + isLoading.value = true; try { const response = await axios.postForm( route("resources.store"), formData ); - clearLocalStorage(); + clearLocalStorage(); + isLoading.value = false; router.visit(route("resources.show", { slug: response.data.slug })); } catch (err) { toast.add({ @@ -89,6 +92,7 @@ const submitForm = async () => { : "An error occurred while creating the resource. Please try again."), life: 10000, }); + isLoading.value = false; console.error(err); } }; @@ -161,6 +165,7 @@ const handleFormChange = (newFormData) => { @change="handleFormChange" @back="() => navigateToStep('1')" @next="submitForm" + :is-loading="isLoading" /> diff --git a/resources/js/Pages/Resources/Form/TagsFields.vue b/resources/js/Pages/Resources/Form/TagsFields.vue index acee9832..f405e9f3 100644 --- a/resources/js/Pages/Resources/Form/TagsFields.vue +++ b/resources/js/Pages/Resources/Form/TagsFields.vue @@ -14,6 +14,10 @@ const props = defineProps({ type: Object, required: true, }, + isLoading: { + type: Boolean, + required: true, + } }); const emit = defineEmits(["change", "next", "back"]); @@ -97,7 +101,7 @@ const validateAndNext = async () => { Back - + Submit From b226d9e915dc36b61c073cc8ace61a4287bb7918 Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Thu, 28 Aug 2025 20:28:43 -0700 Subject: [PATCH 6/7] nits --- .../Controllers/ResourceEditsController.php | 5 ++-- .../ComputerScienceResourceService.php | 25 ++----------------- tests/Feature/ComputerScienceResourceTest.php | 2 +- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 8a07be81..b43f6930 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -44,6 +44,7 @@ public function store(ComputerScienceResource $computerScienceResource, StoreRes $actualChanges = $this->calculateChanges($computerScienceResource, $proposedChanges); + // Add image path to the actual changes if (array_key_exists('image_file', $proposedChanges)) { $actualChanges['image_path'] = null; if (isset($proposedChanges['image_file'])) { @@ -135,6 +136,8 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc $newFileName = Str::random(40).'.'.$fileExtension; $destPath = 'resource/'.$newFileName; + + // TODO: FIGURE OUT WHAT TO DO IN CASE OF EXCEPTION IN CODE FROM LATER STEPS Storage::disk('public')->move($sourcePath, $destPath); } @@ -145,11 +148,9 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc $resource->save(); $proposedTagFields = ['topic_tags', 'programming_language_tags', 'general_tags']; - $allTags = []; foreach ($proposedTagFields as $field) { if (array_key_exists($field, $changes)) { $resource->$field = $changes[$field]; - $allTags[] = $changes[$field]; } } diff --git a/app/Services/ComputerScienceResourceService.php b/app/Services/ComputerScienceResourceService.php index 1cd7f75d..13e7cc69 100644 --- a/app/Services/ComputerScienceResourceService.php +++ b/app/Services/ComputerScienceResourceService.php @@ -41,18 +41,8 @@ public function createResource(array $validatedData): ComputerScienceResource // Store the image onto storage $path = null; if (array_key_exists('image_file', $validatedData) && $imageFile = $validatedData['image_file']) { + // TODO: FIGURE OUT WHAT TO DO IN CASE OF EXCEPTION IN CODE FROM LATER STEPS $path = $imageFile->store('resource', 'public'); - if (! $path) { - Log::error('Failed to store image file', [ - 'user_id' => Auth::id(), - 'file_info' => $imageFile, - ]); - - $fileName = $imageFile->getClientOriginalName(); - throw new \RuntimeException( - "Could not save the image file '{$fileName}' for user ID ".Auth::id().'.' - ); - } } $resource = ComputerScienceResource::create([ @@ -94,18 +84,7 @@ public function createResource(array $validatedData): ComputerScienceResource return $resource; } catch (Throwable $e) { DB::rollBack(); - // Attempt to remove the uploaded image if it was stored - if (isset($path) && $path) { - try { - Storage::disk('public')->delete($path); - } catch (Throwable $removeEx) { - Log::warning('Failed to remove image after exception', [ - 'user_id' => Auth::id(), - 'image_path' => $path, - 'error' => $removeEx->getMessage(), - ]); - } - } + Log::critical('Failed to create resource', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), diff --git a/tests/Feature/ComputerScienceResourceTest.php b/tests/Feature/ComputerScienceResourceTest.php index 21e2c4f2..bd342f9e 100644 --- a/tests/Feature/ComputerScienceResourceTest.php +++ b/tests/Feature/ComputerScienceResourceTest.php @@ -127,7 +127,7 @@ public function test_image_is_removed_if_resource_creation_fails() $response->assertStatus(422); // Validation error // The image should not exist in storage - Storage::disk('public')->assertMissing('resource/'.$formData['image_file']->hashName()); + $this->assertEmpty(Storage::disk('public')->allFiles('resource'), "Failed asserting that no image files exist in the 'resource' directory after failed resource creation."); } public function test_model_removes_image_upon_deletion() From 6560ec884bee40cba08290ecbd38672f2d413870 Mon Sep 17 00:00:00 2001 From: AllanKoder <74692833+AllanKoder@users.noreply.github.com> Date: Fri, 29 Aug 2025 03:29:11 +0000 Subject: [PATCH 7/7] Apply automatic changes --- app/Http/Controllers/ResourceEditsController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index b43f6930..3f7cca63 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -136,7 +136,6 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc $newFileName = Str::random(40).'.'.$fileExtension; $destPath = 'resource/'.$newFileName; - // TODO: FIGURE OUT WHAT TO DO IN CASE OF EXCEPTION IN CODE FROM LATER STEPS Storage::disk('public')->move($sourcePath, $destPath); }