diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php
index b07d7ab1..ca9f1dfc 100644
--- a/app/Http/Controllers/CommentController.php
+++ b/app/Http/Controllers/CommentController.php
@@ -6,7 +6,7 @@
use App\Http\Resources\CommentResource;
use App\Http\Resources\UserResource;
use App\Services\CommentService;
-use App\Services\ModelResolverService;
+use App\Services\UpvoteService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@@ -17,8 +17,9 @@
class CommentController extends Controller
{
- public function __construct(protected ModelResolverService $modelResolver,
+ public function __construct(
protected CommentService $commentService,
+ protected UpvoteService $upvoteService,
) {}
/**
@@ -42,6 +43,8 @@ public function store(StoreCommentRequest $request)
'depth' => $comment->depth,
]);
+ $this->upvoteService->upvote('comment', $comment->id);
+
DB::commit();
return response()->json([
diff --git a/app/Http/Controllers/ComputerScienceResourceController.php b/app/Http/Controllers/ComputerScienceResourceController.php
index 5fb38cc7..676a192e 100644
--- a/app/Http/Controllers/ComputerScienceResourceController.php
+++ b/app/Http/Controllers/ComputerScienceResourceController.php
@@ -8,8 +8,10 @@
use App\Models\ComputerScienceResource;
use App\Services\ComputerScienceResourceFilter;
use App\Services\ComputerScienceResourceService;
+use App\Services\UpvoteService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
use Throwable;
@@ -18,6 +20,7 @@ class ComputerScienceResourceController extends Controller
{
public function __construct(
protected ComputerScienceResourceService $resourceService,
+ protected UpvoteService $upvoteService,
protected ComputerScienceResourceFilter $filterService
) {}
@@ -63,7 +66,21 @@ public function store(StoreResourceRequest $request)
{
$validatedData = $request->validated();
try {
+ DB::beginTransaction();
$resource = $this->resourceService->createResource($validatedData);
+
+ Log::info('Resource created', [
+ 'resource_id' => $resource->id,
+ 'user_id' => Auth::id(),
+ 'name' => $resource->name,
+ 'slug' => $resource->slug,
+ 'platforms' => $resource->platforms,
+ ]);
+
+ $this->upvoteService->upvote('resource', $resource->id);
+
+ DB::commit();
+
session()->flash('success', 'Created Resource!');
return response()->json($resource);
@@ -77,10 +94,13 @@ public function store(StoreResourceRequest $request)
return response()->json($e->resource);
} catch (Throwable $e) {
- Log::error('Error creating resource', [
- 'user_id' => Auth::id(),
+ DB::rollBack();
+
+ Log::critical('Failed to create resource', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
+ 'user_id' => Auth::id(),
+ 'data' => $validatedData,
]);
return response()->json([], 500);
diff --git a/app/Http/Controllers/ResourceEditsController.php b/app/Http/Controllers/ResourceEditsController.php
index f0dcbc94..b69fe3fd 100644
--- a/app/Http/Controllers/ResourceEditsController.php
+++ b/app/Http/Controllers/ResourceEditsController.php
@@ -6,12 +6,10 @@
use App\Models\ComputerScienceResource;
use App\Models\ResourceEdits;
use App\Services\ResourceEditsService;
+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 Str;
use Throwable;
class ResourceEditsController extends Controller
@@ -39,36 +37,61 @@ public function create(string $slug)
public function store(ComputerScienceResource $computerScienceResource, StoreResourceEditRequest $request)
{
$validatedData = $request->validated();
- $proposedChanges = $validatedData['proposed_changes'] ?? [];
-
- $actualChanges = $this->resourceEditsService->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'])) {
- $path = $proposedChanges['image_file']->store('resource-edits', 'public');
- $actualChanges['image_path'] = $path;
- }
- unset($actualChanges['image_file']);
- }
- if (empty($actualChanges)) {
- Log::warning("Resource edit was submitted without any changes for resource ID: {$computerScienceResource->id}");
+ try {
+ $resourceEdit = $this->resourceEditsService->createResourceEdit($computerScienceResource, $validatedData);
+
+ Log::info('Resource edit created', [
+ 'resource_edit_id' => $resourceEdit->id,
+ 'resource_id' => $computerScienceResource->id,
+ 'user_id' => Auth::id(),
+ 'edit_title' => $resourceEdit->edit_title,
+ ]);
+
+ return redirect()->route('resource_edits.show', ['slug' => $resourceEdit->slug])
+ ->with('success', 'Edits Created!');
+ } catch (\InvalidArgumentException $e) {
+ Log::warning('Resource edit submitted with no changes', [
+ 'resource_id' => $computerScienceResource->id,
+ 'user_id' => Auth::id(),
+ 'error' => $e->getMessage(),
+ ]);
return redirect()->back()->with('warning', 'Cannot submit an edit with no changes made.');
+ } catch (Throwable $e) {
+ Log::critical('Failed to create resource edit', [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ 'resource_id' => $computerScienceResource->id,
+ 'user_id' => Auth::id(),
+ 'data' => $validatedData,
+ ]);
+
+ return redirect()->back()->withErrors(['error' => 'Failed to create resource edit. Please try again.']);
}
+ }
- $resourceEdit = ResourceEdits::create([
- 'user_id' => Auth::id(),
- 'computer_science_resource_id' => $computerScienceResource->id,
- 'edit_title' => $validatedData['edit_title'],
- 'edit_description' => $validatedData['edit_description'],
- 'proposed_changes' => $actualChanges,
- ]);
+ public function index(Request $request)
+ {
+ try {
+ $data = $this->resourceEditsService->getIndexData($request);
- return redirect()->route('resource_edits.show', ['slug' => $resourceEdit->slug])
- ->with('success', 'Edits Created!');
+ return Inertia::render('ResourceEdits/Index', $data);
+ } catch (Throwable $e) {
+ Log::error('Error loading resource edits index', [
+ 'user_id' => Auth::id(),
+ 'query' => $request->query(),
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+
+ // Return an empty page with an error flash so the UI can show a message
+ session()->flash('error', 'Unable to load resource edits right now.');
+
+ return Inertia::render('ResourceEdits/Index', [
+ 'resource_edits' => ResourceEdits::query()->paginate(1),
+ ]);
+ }
}
public function show(string $slug)
@@ -83,57 +106,10 @@ public function show(string $slug)
]);
}
- public function merge(ResourceEditsService $editsService, ResourceEdits $resourceEdits)
+ public function merge(ResourceEdits $resourceEdits)
{
- if (! $editsService->canMergeEdits($resourceEdits)) {
- return redirect()->back()->with('warning', 'Not enough approvals');
- }
-
- DB::beginTransaction();
try {
- $resource = ComputerScienceResource::findOrFail($resourceEdits->computer_science_resource_id);
-
- // Go through each property in proposed_changes, and if it exists. then set the value
- $changes = $resourceEdits->proposed_changes;
- $proposedFields = ['name', 'description', 'page_url', 'platforms', 'difficulties', 'pricing'];
- foreach ($proposedFields as $field) {
- if (array_key_exists($field, $changes)) {
- $resource->$field = $changes[$field];
- }
- }
-
- if (array_key_exists('image_path', $changes)) {
- if ($resource->image_path) {
- Storage::disk('public')->delete($resource->image_path);
- }
- $destPath = null;
- if (isset($changes['image_path'])) {
- // Move the new file from 'resource-edits' to 'resource'
- $sourcePath = $changes['image_path'];
- $fileExtension = pathinfo($sourcePath, PATHINFO_EXTENSION);
- $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);
- }
- // Update image_path in DB
- $resource->image_path = $destPath;
- }
-
- $resource->save();
-
- $proposedTagFields = ['topics_tags', 'programming_languages_tags', 'general_tags'];
- foreach ($proposedTagFields as $field) {
- if (array_key_exists($field, $changes)) {
- $resource->$field = $changes[$field];
- }
- }
-
- // Delete the edit since we successfully merged the changes
- $resourceEdits->delete();
-
- DB::commit();
+ $resource = $this->resourceEditsService->mergeResourceEdit($resourceEdits);
Log::info('Resource edit merged', [
'resource_id' => $resource->id,
@@ -145,12 +121,20 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc
return redirect(route('resources.show', ['slug' => $resource->slug]))
->with('success', 'Successfully Merged Changes!');
+ } catch (\LogicException $e) {
+ Log::warning('Insufficient approvals for resource edit merge', [
+ 'resource_edit_id' => $resourceEdits->id,
+ 'user_id' => Auth::id(),
+ 'error' => $e->getMessage(),
+ ]);
+
+ return redirect()->back()->with('warning', 'Not enough approvals');
} catch (Throwable $e) {
- DB::rollBack();
Log::critical('Failed to merge resource edits', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'resource_edit_id' => $resourceEdits->id,
+ 'user_id' => Auth::id(),
]);
return redirect()->back()->withErrors(['error' => 'Failed to merge resource edits. Please try again.']);
diff --git a/app/Http/Controllers/ResourceReviewController.php b/app/Http/Controllers/ResourceReviewController.php
index e19b459e..c3125cdd 100644
--- a/app/Http/Controllers/ResourceReviewController.php
+++ b/app/Http/Controllers/ResourceReviewController.php
@@ -5,11 +5,16 @@
use App\Http\Requests\StoreResourceReviewRequest;
use App\Models\ComputerScienceResource;
use App\Models\ResourceReview;
+use App\Services\UpvoteService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class ResourceReviewController extends Controller
{
+ public function __construct(
+ protected UpvoteService $upvoteService,
+ ) {}
+
// Store the review on the resource
public function store(StoreResourceReviewRequest $request, ComputerScienceResource $computerScienceResource)
{
@@ -58,6 +63,8 @@ public function store(StoreResourceReviewRequest $request, ComputerScienceResour
'review_id' => $review->id,
]);
+ $this->upvoteService->upvote('review', $review->id);
+
return response()->json($review);
}
diff --git a/app/Http/Requests/StoreCommentRequest.php b/app/Http/Requests/StoreCommentRequest.php
index 90e9e01b..f773a992 100644
--- a/app/Http/Requests/StoreCommentRequest.php
+++ b/app/Http/Requests/StoreCommentRequest.php
@@ -2,21 +2,12 @@
namespace App\Http\Requests;
-use App\Services\ModelResolverService;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class StoreCommentRequest extends FormRequest
{
- protected $modelResolver;
-
- public function __construct(ModelResolverService $modelResolver)
- {
- parent::__construct();
- $this->modelResolver = $modelResolver;
- }
-
/**
* Determine if the user is authorized to make this request.
*/
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index bbf1ad4e..22bc02b6 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -2,6 +2,7 @@
namespace App\Providers;
+use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -22,6 +23,13 @@ public function register(): void
*/
public function boot(): void
{
- //
+ Relation::enforceMorphMap([
+ 'resource' => \App\Models\ComputerScienceResource::class,
+ 'review' => \App\Models\ResourceReview::class,
+ 'comment' => \App\Models\Comment::class,
+ 'edit' => \App\Models\ResourceEdits::class,
+ 'user' => \App\Models\User::class,
+ // Add other model types here
+ ]);
}
}
diff --git a/app/Providers/ModelResolverServiceProvider.php b/app/Providers/ModelResolverServiceProvider.php
deleted file mode 100644
index 291d8832..00000000
--- a/app/Providers/ModelResolverServiceProvider.php
+++ /dev/null
@@ -1,27 +0,0 @@
-app->singleton(ModelResolverService::class, function ($app) {
- return new ModelResolverService;
- });
- }
-
- /**
- * Bootstrap services.
- */
- public function boot(): void
- {
- //
- }
-}
diff --git a/app/Services/CommentService.php b/app/Services/CommentService.php
index 016489c0..d0d59f7a 100644
--- a/app/Services/CommentService.php
+++ b/app/Services/CommentService.php
@@ -8,6 +8,7 @@
use App\Services\SortingManagers\GeneralVotesSortingManager;
use Exception;
use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
@@ -16,13 +17,6 @@
class CommentService
{
- protected $modelResolver;
-
- public function __construct(ModelResolverService $resolver)
- {
- $this->modelResolver = $resolver;
- }
-
/**
* Get paginated comments with custom logic.
*
@@ -46,7 +40,7 @@ public function getPaginatedComments(string $commentableKey, int $commentableId,
'sort_by' => ['required', 'string'],
]);
- $commentableType = $this->modelResolver->getModelClass($commentableKey);
+ $commentableType = Relation::getMorphedModel($commentableKey);
Log::debug('Getting paginated comments', [
'commentable_type' => $commentableType,
'commentable_id' => $commentableId,
@@ -57,13 +51,13 @@ public function getPaginatedComments(string $commentableKey, int $commentableId,
// Get the root comments:
$query = Comment::where([
- 'commentable_type' => $commentableType,
+ 'commentable_type' => $commentableKey,
'commentable_id' => $commentableId,
'depth' => 1,
]);
// Apply sorting on the comments
- $query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy, Comment::class);
+ $query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy);
$rootComments = $query->get();
Log::debug('Root comments retrieved', [
@@ -167,17 +161,18 @@ public function createComment(array $validatedData): Comment
$comment->content = $validatedData['content'];
$comment->user_id = Auth::id();
- $commentableType = $this->modelResolver->getModelClass($validatedData['commentable_key']);
+ $commentableKey = $validatedData['commentable_key'];
+ $commentableModel = Relation::getMorphedModel($commentableKey);
$commentableId = $validatedData['commentable_id'];
// Ensure that the model exists
- $model = $this->modelResolver->resolve($validatedData['commentable_key'], $commentableId);
+ $model = $commentableModel::find($commentableId);
if (! $model) {
throw new NotFoundHttpException;
}
// Set the commentable type
- $comment->commentable_type = $commentableType;
+ $comment->commentable_type = $commentableKey;
$comment->commentable_id = $commentableId;
// Top level comment
@@ -209,7 +204,7 @@ public function createComment(array $validatedData): Comment
Validator::validate(
[
'commentable_id' => $commentableId,
- 'commentable_type' => $commentableType,
+ 'commentable_type' => $commentableKey,
'depth' => $new_comment_depth,
'replies_count' => $replies_count,
],
diff --git a/app/Services/ComputerScienceResourceService.php b/app/Services/ComputerScienceResourceService.php
index 7ad4b5ad..22827945 100644
--- a/app/Services/ComputerScienceResourceService.php
+++ b/app/Services/ComputerScienceResourceService.php
@@ -12,9 +12,7 @@
use App\Utilities\UrlUtilities;
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;
@@ -65,64 +63,39 @@ public function createResource(array $validatedData): ComputerScienceResource
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']) {
- // TODO: FIGURE OUT WHAT TO DO IN CASE OF EXCEPTION IN CODE FROM LATER STEPS
- $path = $imageFile->store('resource', 'public');
- }
-
- $resource = ComputerScienceResource::create([
- 'user_id' => Auth::id(),
- 'name' => $validatedData['name'],
- 'image_path' => $path,
- 'description' => $validatedData['description'],
- 'page_url' => $validatedData['page_url'],
- 'platforms' => $validatedData['platforms'],
- 'difficulties' => $validatedData['difficulties'],
- 'pricing' => $validatedData['pricing'],
- ]);
-
- // Add topics as tags
- $resource->topics_tags = $validatedData['topics_tags'];
-
- // Add programming languages as tags (if provided)
- if (isset($validatedData['programming_languages_tags'])) {
- $resource->programming_languages_tags = $validatedData['programming_languages_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);
+ // 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');
+ }
- Log::info('Resource created', [
- 'resource_id' => $resource->id,
- 'user_id' => Auth::id(),
- 'name' => $resource->name,
- 'slug' => $resource->slug,
- 'platforms' => $resource->platforms,
- ]);
+ $resource = ComputerScienceResource::create([
+ 'user_id' => Auth::id(),
+ 'name' => $validatedData['name'],
+ 'image_path' => $path,
+ 'description' => $validatedData['description'],
+ 'page_url' => $validatedData['page_url'],
+ 'platforms' => $validatedData['platforms'],
+ 'difficulties' => $validatedData['difficulties'],
+ 'pricing' => $validatedData['pricing'],
+ ]);
+
+ // Add topics as tags
+ $resource->topics_tags = $validatedData['topics_tags'];
+
+ // Add programming languages as tags (if provided)
+ if (isset($validatedData['programming_languages_tags'])) {
+ $resource->programming_languages_tags = $validatedData['programming_languages_tags'];
+ }
- return $resource;
- } catch (Throwable $e) {
- DB::rollBack();
+ // Add general tags (if provided)
+ if (isset($validatedData['general_tags'])) {
+ $resource->general_tags = $validatedData['general_tags'];
+ }
- Log::critical('Failed to create resource', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'user_id' => Auth::id(),
- 'data' => $validatedData,
- ]);
+ return $resource;
- throw $e;
- }
}
/**
@@ -160,7 +133,7 @@ public function getShowResourceData(Request $request, string $slug, string $tab
function () use ($computerScienceResource, $sortBy, $request) {
try {
$query = ResourceReview::whereBelongsTo($computerScienceResource);
- $query = $this->resourceSortingManager->applySort($query, $sortBy, ResourceReview::class);
+ $query = $this->resourceSortingManager->applySort($query, $sortBy);
return $query->with('user')->paginate(10)->appends($request->query());
} catch (Throwable $e) {
diff --git a/app/Services/ModelResolverService.php b/app/Services/ModelResolverService.php
deleted file mode 100644
index fd1d966b..00000000
--- a/app/Services/ModelResolverService.php
+++ /dev/null
@@ -1,38 +0,0 @@
- \App\Models\ComputerScienceResource::class,
- 'review' => \App\Models\ResourceReview::class,
- 'comment' => \App\Models\Comment::class,
- 'edit' => \App\Models\ResourceEdits::class,
- // Add other model types here
- ];
-
- /**
- * Finds the model that exists for the given key and id
- *
- * @param $key, the colloquial name for the key
- * @param $id, the id for the key
- *
- * returns null if no model exists, otherwise, it will return the model
- */
- public function resolve($key, $id)
- {
- $modelClass = $this->getModelClass($key);
-
- if (! $modelClass) {
- return null;
- }
-
- return $modelClass::find($id);
- }
-
- public function getModelClass($key)
- {
- return $this->models[$key] ?? null;
- }
-}
diff --git a/app/Services/ResourceEditsService.php b/app/Services/ResourceEditsService.php
index 97dc8c56..7283205f 100644
--- a/app/Services/ResourceEditsService.php
+++ b/app/Services/ResourceEditsService.php
@@ -4,11 +4,22 @@
use App\Models\ComputerScienceResource;
use App\Models\ResourceEdits;
+use App\Services\SortingManagers\GeneralVotesSortingManager;
use App\Utilities\UrlUtilities;
+use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use Throwable;
class ResourceEditsService
{
+ public function __construct(
+ protected UpvoteService $upvoteService,
+ protected GeneralVotesSortingManager $sortingManager,
+ ) {}
+
/**
* Determines the amount of votes needed to merge the resource edit into the
*
@@ -90,4 +101,135 @@ public function normalize(array $array): array
return $array;
}
+
+ /**
+ * Create a new resource edit
+ *
+ * @throws Throwable
+ */
+ public function createResourceEdit(ComputerScienceResource $computerScienceResource, array $validatedData): ResourceEdits
+ {
+ $proposedChanges = $validatedData['proposed_changes'] ?? [];
+
+ $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'])) {
+ $path = $proposedChanges['image_file']->store('resource-edits', 'public');
+ $actualChanges['image_path'] = $path;
+ }
+ unset($actualChanges['image_file']);
+ }
+
+ if (empty($actualChanges)) {
+ throw new \InvalidArgumentException('Cannot submit an edit with no changes made.');
+ }
+
+ $resourceEdit = ResourceEdits::create([
+ 'user_id' => Auth::id(),
+ 'computer_science_resource_id' => $computerScienceResource->id,
+ 'edit_title' => $validatedData['edit_title'],
+ 'edit_description' => $validatedData['edit_description'],
+ 'proposed_changes' => $actualChanges,
+ ]);
+
+ $this->upvoteService->upvote('edit', $resourceEdit->id);
+
+ return $resourceEdit;
+ }
+
+ /**
+ * Merge resource edits into the original resource
+ *
+ * @throws Throwable
+ */
+ public function mergeResourceEdit(ResourceEdits $resourceEdits): ComputerScienceResource
+ {
+ if (! $this->canMergeEdits($resourceEdits)) {
+ throw new \LogicException('Not enough approvals to merge this edit.');
+ }
+
+ DB::beginTransaction();
+ try {
+ $resource = ComputerScienceResource::findOrFail($resourceEdits->computer_science_resource_id);
+
+ // Get the raw proposed_changes to access image_path before it's transformed
+ $changes = json_decode($resourceEdits->getRawOriginal('proposed_changes'), true);
+
+ // Go through each property in proposed_changes, and if it exists, then set the value
+ $proposedFields = ['name', 'description', 'page_url', 'platforms', 'difficulties', 'pricing'];
+ foreach ($proposedFields as $field) {
+ if (array_key_exists($field, $changes)) {
+ $resource->$field = $changes[$field];
+ }
+ }
+
+ if (array_key_exists('image_path', $changes)) {
+ if ($resource->image_path) {
+ Storage::disk('public')->delete($resource->image_path);
+ }
+ $destPath = null;
+ if (isset($changes['image_path'])) {
+ // Move the new file from 'resource-edits' to 'resource'
+ $sourcePath = $changes['image_path'];
+ $fileExtension = pathinfo($sourcePath, PATHINFO_EXTENSION);
+ $newFileName = Str::random(40).'.'.$fileExtension;
+ $destPath = 'resource/'.$newFileName;
+
+ Storage::disk('public')->move($sourcePath, $destPath);
+ }
+ // Update image_path in DB
+ $resource->image_path = $destPath;
+ }
+
+ $resource->save();
+
+ $proposedTagFields = ['topics_tags', 'programming_languages_tags', 'general_tags'];
+ foreach ($proposedTagFields as $field) {
+ if (array_key_exists($field, $changes)) {
+ $resource->$field = $changes[$field];
+ }
+ }
+
+ // Delete the edit since we successfully merged the changes
+ $resourceEdits->delete();
+
+ DB::commit();
+
+ return $resource;
+ } catch (Throwable $e) {
+ DB::rollBack();
+ throw $e;
+ }
+ }
+
+ /**
+ * Get data for the resource edits index page (filters, pagination)
+ *
+ * @return array{
+ * resource_edits: \Illuminate\Contracts\Pagination\LengthAwarePaginator,
+ * sortingType: string
+ * }
+ */
+ public function getIndexData(Request $request): array
+ {
+ $query = ResourceEdits::query();
+
+ // Apply filters and sorting through the dedicated filter service
+ $filters = $request->query();
+ $sortBy = $filters['sort_by'] ?? 'top';
+
+ $query = $this->sortingManager->applySort($query, $sortBy);
+
+ $resourceEdits = $query->with('user', 'computerScienceResource')
+ ->paginate(20)
+ ->appends($request->query());
+
+ return [
+ 'resource_edits' => $resourceEdits,
+ 'sortingType' => $sortBy,
+ ];
+ }
}
diff --git a/app/Services/UpvoteService.php b/app/Services/UpvoteService.php
index 06aa32e6..9f166901 100644
--- a/app/Services/UpvoteService.php
+++ b/app/Services/UpvoteService.php
@@ -2,6 +2,7 @@
namespace App\Services;
+use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -10,8 +11,6 @@
class UpvoteService
{
- public function __construct(protected ModelResolverService $modelResolver) {}
-
/**
* Upvote a Model
*/
@@ -28,7 +27,7 @@ public function upvote($typeKey, $id)
DB::beginTransaction();
try {
- $model = $this->modelResolver->resolve($typeKey, $id);
+ $model = Relation::getMorphedModel($typeKey)::find($id);
if (! $model) {
Log::warning('Upvote failed: Model not found', [
@@ -86,7 +85,7 @@ public function downvote($typeKey, $id)
DB::beginTransaction();
try {
- $model = $this->modelResolver->resolve($typeKey, $id);
+ $model = Relation::getMorphedModel($typeKey)::find($id);
if (! $model) {
Log::warning('Downvote failed: Model not found', [
diff --git a/app/SortingStrategies/VoteSortingStrategy.php b/app/SortingStrategies/VoteSortingStrategy.php
index f2ad7c7f..46c53a6b 100644
--- a/app/SortingStrategies/VoteSortingStrategy.php
+++ b/app/SortingStrategies/VoteSortingStrategy.php
@@ -15,12 +15,12 @@ public static function supports(string $sortBy): bool
public static function apply(Builder $query, string $sortBy): Builder
{
$table = $query->getModel()->getTable();
- $modelClass = get_class($query->getModel());
+ $morphClass = $query->getModel()->getMorphClass();
// Join on polymorphic relationship
- $query->join('upvote_summaries', function ($join) use ($table, $modelClass) {
+ $query->join('upvote_summaries', function ($join) use ($table, $morphClass) {
$join->on('upvote_summaries.upvotable_id', '=', "{$table}.id")
- ->where('upvote_summaries.upvotable_type', '=', $modelClass);
+ ->where('upvote_summaries.upvotable_type', '=', $morphClass);
})->select("{$table}.*");
switch ($sortBy) {
diff --git a/app/Traits/HasComments.php b/app/Traits/HasComments.php
index 38041116..49bd43b7 100644
--- a/app/Traits/HasComments.php
+++ b/app/Traits/HasComments.php
@@ -16,7 +16,7 @@ trait HasComments
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'commentable_id', 'id')
- ->where('commentable_type', static::class);
+ ->where('commentable_type', $this->getMorphClass());
}
/**
@@ -25,7 +25,7 @@ public function comments(): HasMany
public function commentsCountRelationship(): HasOne
{
return $this->hasOne(CommentsCount::class, 'commentable_id', 'id')
- ->where('commentable_type', static::class);
+ ->where('commentable_type', $this->getMorphClass());
}
/**
diff --git a/app/Traits/HasVotes.php b/app/Traits/HasVotes.php
index 101aed57..4de3b34e 100644
--- a/app/Traits/HasVotes.php
+++ b/app/Traits/HasVotes.php
@@ -20,7 +20,7 @@ protected static function bootHasVotes()
static::created(function ($model) {
UpvoteSummary::firstOrCreate([
'upvotable_id' => $model->id,
- 'upvotable_type' => get_class($model),
+ 'upvotable_type' => $model->getMorphClass(),
]);
});
}
@@ -78,14 +78,14 @@ public function upvoteSummary(): MorphOne
public function upvote($userId): array
{
$currentVote = $this->getVoteValue($userId);
- $modelType = get_class($this);
+ $modelType = $this->getMorphClass();
$modelId = $this->id;
if ($currentVote > 0) {
UpvoteProcessed::dispatch($modelType, $modelId, $currentVote, 0);
return [
- 'model' => $this->deleteVote($userId),
+ 'model' => $this->vote($userId, 0),
// The value of the user vote (-n,0,n)
'userVote' => 0,
// What the change of votes of the model after the user voted
@@ -108,14 +108,14 @@ public function upvote($userId): array
public function downvote($userId): array
{
$currentVote = $this->getVoteValue($userId);
- $modelType = get_class($this);
+ $modelType = $this->getMorphClass();
$modelId = $this->id;
if ($currentVote < 0) {
UpvoteProcessed::dispatch($modelType, $modelId, $currentVote, 0);
return [
- 'model' => $this->deleteVote($userId),
+ 'model' => $this->vote($userId, 0),
'userVote' => 0,
'changeFromVote' => 0 - $currentVote,
];
diff --git a/bootstrap/providers.php b/bootstrap/providers.php
index 3137717e..9a019dc5 100644
--- a/bootstrap/providers.php
+++ b/bootstrap/providers.php
@@ -5,6 +5,5 @@
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\JetstreamServiceProvider::class,
- App\Providers\ModelResolverServiceProvider::class,
App\Providers\SocialstreamServiceProvider::class,
];
diff --git a/database/factories/CommentFactory.php b/database/factories/CommentFactory.php
index 6cc2bf2d..0f80ae3b 100644
--- a/database/factories/CommentFactory.php
+++ b/database/factories/CommentFactory.php
@@ -4,8 +4,8 @@
use App\Models\Comment;
use App\Models\User;
-use App\Services\ModelResolverService;
use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Database\Eloquent\Relations\Relation;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment>
@@ -23,8 +23,7 @@ public function definition(): array
{
// Pick a random commentable type from config.
$commentableKey = $this->faker->randomElement(['comment', 'resource']);
- $modelResolver = app(ModelResolverService::class);
- $modelClass = $modelResolver->getModelClass($commentableKey);
+ $modelClass = Relation::getMorphedModel($commentableKey);
// Use an existing user or create one.
$user = User::inRandomOrder()->first() ?? User::factory()->create();
@@ -47,7 +46,7 @@ public function definition(): array
// For non-comment targets, fetch or create the commentable model.
$commenting = $modelClass::inRandomOrder()->first() ?? $modelClass::factory()->create();
$commentableId = $commenting->id;
- $commentableType = $modelClass;
+ $commentableType = $commentableKey;
// For non-comment targets we always create a top-level comment.
$parentCommentId = null;
diff --git a/database/migrations/2025_12_25_000000_convert_morph_types_to_aliases.php b/database/migrations/2025_12_25_000000_convert_morph_types_to_aliases.php
new file mode 100644
index 00000000..22ff55fe
--- /dev/null
+++ b/database/migrations/2025_12_25_000000_convert_morph_types_to_aliases.php
@@ -0,0 +1,157 @@
+ 'resource',
+ 'App\\Models\\ResourceReview' => 'review',
+ 'App\\Models\\Comment' => 'comment',
+ 'App\\Models\\ResourceEdits' => 'edit',
+ 'App\\Models\\User' => 'user',
+ ];
+
+ // Update comments table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('comments')
+ ->where('commentable_type', $className)
+ ->update(['commentable_type' => $alias]);
+ }
+
+ // Update comments_counts table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('comments_counts')
+ ->where('commentable_type', $className)
+ ->update(['commentable_type' => $alias]);
+ }
+
+ // Update upvotes table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('upvotes')
+ ->where('upvotable_type', $className)
+ ->update(['upvotable_type' => $alias]);
+ }
+
+ // Update upvote_summaries table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('upvote_summaries')
+ ->where('upvotable_type', $className)
+ ->update(['upvotable_type' => $alias]);
+ }
+
+ // Update notifications table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('notifications')
+ ->where('notifiable_type', $className)
+ ->update(['notifiable_type' => $alias]);
+ }
+
+ // Update personal_access_tokens table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('personal_access_tokens')
+ ->where('tokenable_type', $className)
+ ->update(['tokenable_type' => $alias]);
+ }
+
+ // Update activity_log table (subject and causer)
+ foreach ($morphMap as $className => $alias) {
+ DB::table('activity_log')
+ ->where('subject_type', $className)
+ ->update(['subject_type' => $alias]);
+
+ DB::table('activity_log')
+ ->where('causer_type', $className)
+ ->update(['causer_type' => $alias]);
+ }
+
+ // Update taggables table
+ foreach ($morphMap as $className => $alias) {
+ DB::table('taggables')
+ ->where('taggable_type', $className)
+ ->update(['taggable_type' => $alias]);
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ // Define the reverse mapping from morph aliases to full class names
+ $reverseMorphMap = [
+ 'resource' => 'App\\Models\\ComputerScienceResource',
+ 'review' => 'App\\Models\\ResourceReview',
+ 'comment' => 'App\\Models\\Comment',
+ 'edit' => 'App\\Models\\ResourceEdits',
+ 'user' => 'App\\Models\\User',
+ ];
+
+ // Reverse update comments table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('comments')
+ ->where('commentable_type', $alias)
+ ->update(['commentable_type' => $className]);
+ }
+
+ // Reverse update comments_counts table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('comments_counts')
+ ->where('commentable_type', $alias)
+ ->update(['commentable_type' => $className]);
+ }
+
+ // Reverse update upvotes table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('upvotes')
+ ->where('upvotable_type', $alias)
+ ->update(['upvotable_type' => $className]);
+ }
+
+ // Reverse update upvote_summaries table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('upvote_summaries')
+ ->where('upvotable_type', $alias)
+ ->update(['upvotable_type' => $className]);
+ }
+
+ // Reverse update notifications table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('notifications')
+ ->where('notifiable_type', $alias)
+ ->update(['notifiable_type' => $className]);
+ }
+
+ // Reverse update personal_access_tokens table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('personal_access_tokens')
+ ->where('tokenable_type', $alias)
+ ->update(['tokenable_type' => $className]);
+ }
+
+ // Reverse update activity_log table (subject and causer)
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('activity_log')
+ ->where('subject_type', $alias)
+ ->update(['subject_type' => $className]);
+
+ DB::table('activity_log')
+ ->where('causer_type', $alias)
+ ->update(['causer_type' => $className]);
+ }
+
+ // Reverse update taggables table
+ foreach ($reverseMorphMap as $alias => $className) {
+ DB::table('taggables')
+ ->where('taggable_type', $alias)
+ ->update(['taggable_type' => $className]);
+ }
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index b97a5938..9f476072 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "html",
+ "name": "ComputerScienceResources.com",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/resources/js/Components/ApplicationTitleLogo.vue b/resources/js/Components/ApplicationTitleLogo.vue
new file mode 100644
index 00000000..9b27e06d
--- /dev/null
+++ b/resources/js/Components/ApplicationTitleLogo.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources/js/Components/FrequentlyAskedQuestion.vue b/resources/js/Components/FrequentlyAskedQuestion.vue
index 3c00ca35..6aff7966 100644
--- a/resources/js/Components/FrequentlyAskedQuestion.vue
+++ b/resources/js/Components/FrequentlyAskedQuestion.vue
@@ -11,9 +11,9 @@ const isAnswerOpen = ref(false);
@click="isAnswerOpen = !isAnswerOpen"
class="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
-
+ {{ edit.edit_description }} +
++ Review and vote on community-proposed changes to resources +
+