From b66f38db4d39a182986a25ceda699dd6806826f6 Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Fri, 29 Aug 2025 14:10:03 -0700 Subject: [PATCH 1/5] Normalization and admin board --- .../Resources/ComputerScienceResource.php | 4 ++ .../Controllers/ResourceEditsController.php | 23 +--------- app/Models/ComputerScienceResource.php | 8 ++++ app/Models/ResourceEdits.php | 8 ++++ .../ComputerScienceResourceService.php | 10 +++-- app/Services/DataNormalizationService.php | 6 +++ app/Services/ResourceEditsService.php | 42 +++++++++++++++++++ app/Utilities/UrlUtilities.php | 14 +++++++ tests/Feature/ComputerScienceResourceTest.php | 3 +- 9 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 app/Utilities/UrlUtilities.php diff --git a/app/Filament/Admin/Resources/ComputerScienceResource.php b/app/Filament/Admin/Resources/ComputerScienceResource.php index 4d305e3c..a58068ca 100644 --- a/app/Filament/Admin/Resources/ComputerScienceResource.php +++ b/app/Filament/Admin/Resources/ComputerScienceResource.php @@ -44,6 +44,9 @@ public static function form(Form $form): Form Forms\Components\TextInput::make('slug') ->maxLength(255), + Forms\Components\TextInput::make('page_url') + ->maxLength(255), + Forms\Components\Select::make('user_id') ->relationship('user', 'name') ->required(), @@ -84,6 +87,7 @@ public static function table(Table $table): Table ->description(fn (ModelsComputerScienceResource $resource): string => $resource->user->id)->wrap(), TextColumn::make('name')->searchable() ->description(fn (ModelsComputerScienceResource $resource): string => $resource->description)->wrap(), + TextColumn::make('page_url')->searchable()->wrap()->copyable(), TextColumn::make('topic_tags') ->label('topics_tags') ->badge() diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index ef23c0ed..2114be77 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -18,7 +18,7 @@ class ResourceEditsController extends Controller { public function __construct( - private DataNormalizationService $dataNormalizationService + protected ResourceEditsService $resourceEditsService ) {} /** @@ -42,7 +42,7 @@ public function store(ComputerScienceResource $computerScienceResource, StoreRes $validatedData = $request->validated(); $proposedChanges = $validatedData['proposed_changes'] ?? []; - $actualChanges = $this->calculateChanges($computerScienceResource, $proposedChanges); + $actualChanges = $this->resourceEditsService->calculateChanges($computerScienceResource, $proposedChanges); // Add image path to the actual changes if (array_key_exists('image_file', $proposedChanges)) { @@ -72,25 +72,6 @@ public function store(ComputerScienceResource $computerScienceResource, StoreRes ->with('success', 'Edits Created!'); } - /** - * Calculate the actual differences between the proposed changes and the original resource. - */ - private function calculateChanges(ComputerScienceResource $resource, array $proposedChanges): array - { - $actualChanges = []; - $normalizedProposed = $this->dataNormalizationService->normalize($proposedChanges); - $normalizedOriginal = $this->dataNormalizationService->normalize($resource->toArray()); - - foreach ($normalizedProposed as $key => $value) { - if (! array_key_exists($key, $normalizedOriginal) || $normalizedOriginal[$key] !== $value) { - // Use the original value from the request, not the normalized one, for file uploads. - $actualChanges[$key] = $proposedChanges[$key]; - } - } - - return $actualChanges; - } - public function show(string $slug) { $resourceEdits = ResourceEdits::where('slug', $slug)->firstOrFail(); diff --git a/app/Models/ComputerScienceResource.php b/app/Models/ComputerScienceResource.php index 045e9e67..5433f2c1 100644 --- a/app/Models/ComputerScienceResource.php +++ b/app/Models/ComputerScienceResource.php @@ -6,6 +6,7 @@ use App\Observers\ComputerScienceResourceObserver; use App\Traits\HasComments; use App\Traits\HasVotes; +use App\Utilities\UrlUtilities; use Cviebrock\EloquentSluggable\Sluggable; use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -89,6 +90,13 @@ protected function imageUrl(): Attribute ); } + protected function pageUrl(): Attribute + { + return Attribute::make( + set: fn ($value) => UrlUtilities::normalize($value) + ); + } + /** * Get the review summary relationship. */ diff --git a/app/Models/ResourceEdits.php b/app/Models/ResourceEdits.php index b3dc4736..4d9b9d9d 100644 --- a/app/Models/ResourceEdits.php +++ b/app/Models/ResourceEdits.php @@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Auth; +use App\Utilities\UrlUtilities; use Illuminate\Support\Facades\Storage; use ShiftOneLabs\LaravelCascadeDeletes\CascadesDeletes; use Spatie\Activitylog\LogOptions; @@ -89,6 +90,13 @@ protected function proposedChanges(): Attribute return $changes; }, + set: function ($value) { + // TODO: HANDLE THIS IN THE REQUEST FORM. + if (array_key_exists('page_url', $value) && is_string($value['page_url'])) { + $value['page_url'] = UrlUtilities::normalize($value['page_url']); + } + return json_encode($value); + } ); } diff --git a/app/Services/ComputerScienceResourceService.php b/app/Services/ComputerScienceResourceService.php index d6ea772b..4f5a72f1 100644 --- a/app/Services/ComputerScienceResourceService.php +++ b/app/Services/ComputerScienceResourceService.php @@ -8,6 +8,7 @@ use App\Models\ResourceEdits; use App\Models\ResourceReview; use App\Services\SortingManagers\ResourceSortingManager; +use App\Utilities\UrlUtilities; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -109,7 +110,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 = [ @@ -144,7 +145,7 @@ function () use ($computerScienceResource, $sortBy, $request) { ); } 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) ); } @@ -157,6 +158,9 @@ function () use ($computerScienceResource, $sortBy, $request) { */ private function existingConflictingResource(array $data): ?ComputerScienceResource { - return ComputerScienceResource::where('page_url', $data['page_url'])->first(); + return ComputerScienceResource::where( + 'page_url', + UrlUtilities::normalize($data['page_url']), + )->first(); } } diff --git a/app/Services/DataNormalizationService.php b/app/Services/DataNormalizationService.php index a4f6530d..d4bc5381 100644 --- a/app/Services/DataNormalizationService.php +++ b/app/Services/DataNormalizationService.php @@ -1,6 +1,7 @@ = $neededApprovals; } + + /** + * Calculate the actual differences between the proposed changes and the original resource. + */ + public function calculateChanges(ComputerScienceResource $resource, array $proposedChanges): array + { + $actualChanges = []; + $normalizedProposed = $this->normalize($proposedChanges); + $normalizedOriginal = $this->normalize($resource->toArray()); + + foreach ($normalizedProposed as $key => $value) { + if (! array_key_exists($key, $normalizedOriginal) || $normalizedOriginal[$key] !== $value) { + // Use the original value from the request, not the normalized one, for file uploads. + $actualChanges[$key] = $proposedChanges[$key]; + } + } + + return $actualChanges; + } + + /** + * Normalize an array by sorting keys and values for consistent comparison, including page_url normalization. + */ + public function normalize(array $array): array + { + ksort($array); + + // Normalize page_url if present + if (array_key_exists('page_url', $array) && is_string($array['page_url'])) { + $array['page_url'] = UrlUtilities::normalize($array['page_url']); + } + + foreach ($array as &$value) { + if (is_array($value)) { + sort($value); + } + } + + return $array; + } } diff --git a/app/Utilities/UrlUtilities.php b/app/Utilities/UrlUtilities.php new file mode 100644 index 00000000..7ac19cbc --- /dev/null +++ b/app/Utilities/UrlUtilities.php @@ -0,0 +1,14 @@ +create(); $this->postJson(route('resources.store'), $formData); - // Try to create the same resource again + // Try to create the same resource again, with a slighly different URL + $formData['page_url'] = $formData['page_url'] . " "; $response = $this->postJson(route('resources.store'), $formData); $response->assertStatus(200); From bc02ff30cd6bdca33525e566c65cf1197e58af68 Mon Sep 17 00:00:00 2001 From: AllanKoder <74692833+AllanKoder@users.noreply.github.com> Date: Fri, 29 Aug 2025 21:10:25 +0000 Subject: [PATCH 2/5] Apply automatic changes --- app/Http/Controllers/ResourceEditsController.php | 1 - app/Models/ResourceEdits.php | 3 ++- app/Services/ComputerScienceResourceService.php | 4 ++-- app/Services/DataNormalizationService.php | 1 + app/Services/ResourceEditsService.php | 2 +- tests/Feature/ComputerScienceResourceTest.php | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php index 2114be77..2b6ff2bf 100644 --- a/app/Http/Controllers/ResourceEditsController.php +++ b/app/Http/Controllers/ResourceEditsController.php @@ -5,7 +5,6 @@ use App\Http\Requests\StoreResourceEdit; use App\Models\ComputerScienceResource; use App\Models\ResourceEdits; -use App\Services\DataNormalizationService; use App\Services\ResourceEditsService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; diff --git a/app/Models/ResourceEdits.php b/app/Models/ResourceEdits.php index 4d9b9d9d..5252793c 100644 --- a/app/Models/ResourceEdits.php +++ b/app/Models/ResourceEdits.php @@ -6,6 +6,7 @@ use App\Services\ResourceEditsService; use App\Traits\HasComments; use App\Traits\HasVotes; +use App\Utilities\UrlUtilities; use Cviebrock\EloquentSluggable\Sluggable; use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -14,7 +15,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Auth; -use App\Utilities\UrlUtilities; use Illuminate\Support\Facades\Storage; use ShiftOneLabs\LaravelCascadeDeletes\CascadesDeletes; use Spatie\Activitylog\LogOptions; @@ -95,6 +95,7 @@ protected function proposedChanges(): Attribute if (array_key_exists('page_url', $value) && is_string($value['page_url'])) { $value['page_url'] = UrlUtilities::normalize($value['page_url']); } + return json_encode($value); } ); diff --git a/app/Services/ComputerScienceResourceService.php b/app/Services/ComputerScienceResourceService.php index 4f5a72f1..af3b2250 100644 --- a/app/Services/ComputerScienceResourceService.php +++ b/app/Services/ComputerScienceResourceService.php @@ -110,7 +110,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 = [ @@ -145,7 +145,7 @@ function () use ($computerScienceResource, $sortBy, $request) { ); } 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) ); } diff --git a/app/Services/DataNormalizationService.php b/app/Services/DataNormalizationService.php index d4bc5381..47f6e8e4 100644 --- a/app/Services/DataNormalizationService.php +++ b/app/Services/DataNormalizationService.php @@ -1,6 +1,7 @@ postJson(route('resources.store'), $formData); // Try to create the same resource again, with a slighly different URL - $formData['page_url'] = $formData['page_url'] . " "; + $formData['page_url'] = $formData['page_url'].' '; $response = $this->postJson(route('resources.store'), $formData); $response->assertStatus(200); From e529b2e790935ea9b61993c6710ab9ee41331dbf Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Fri, 29 Aug 2025 14:10:52 -0700 Subject: [PATCH 3/5] removed todo --- app/Models/ResourceEdits.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Models/ResourceEdits.php b/app/Models/ResourceEdits.php index 4d9b9d9d..357bb64d 100644 --- a/app/Models/ResourceEdits.php +++ b/app/Models/ResourceEdits.php @@ -91,7 +91,6 @@ protected function proposedChanges(): Attribute return $changes; }, set: function ($value) { - // TODO: HANDLE THIS IN THE REQUEST FORM. if (array_key_exists('page_url', $value) && is_string($value['page_url'])) { $value['page_url'] = UrlUtilities::normalize($value['page_url']); } From 3f9dfb5d44c49a504913e09fe835ef677706fd3b Mon Sep 17 00:00:00 2001 From: Allan Kong <74692833+AllanKoder@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:14:23 -0700 Subject: [PATCH 4/5] Update tests/Feature/ComputerScienceResourceTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 2177e3ab..8030391a 100644 --- a/tests/Feature/ComputerScienceResourceTest.php +++ b/tests/Feature/ComputerScienceResourceTest.php @@ -154,7 +154,7 @@ public function test_posting_duplicate_resource_returns_existing_resource() $formData = StoreResourceRequestFactory::new()->create(); $this->postJson(route('resources.store'), $formData); - // Try to create the same resource again, with a slighly different URL + // Try to create the same resource again, with a slightly different URL $formData['page_url'] = $formData['page_url'].' '; $response = $this->postJson(route('resources.store'), $formData); From 46e2e746dc86def545036cef2dd90ed9ec73fae3 Mon Sep 17 00:00:00 2001 From: Allan Kong Date: Fri, 29 Aug 2025 14:17:15 -0700 Subject: [PATCH 5/5] deleted DataNormalizationService --- app/Services/DataNormalizationService.php | 37 ----------------------- 1 file changed, 37 deletions(-) delete mode 100644 app/Services/DataNormalizationService.php diff --git a/app/Services/DataNormalizationService.php b/app/Services/DataNormalizationService.php deleted file mode 100644 index 47f6e8e4..00000000 --- a/app/Services/DataNormalizationService.php +++ /dev/null @@ -1,37 +0,0 @@ -normalize($array1) === $this->normalize($array2); - } -}