Skip to content

Commit 2ff30e2

Browse files
committed
Resource edits, and tests
1 parent 16569d2 commit 2ff30e2

18 files changed

Lines changed: 589 additions & 217 deletions

app/Events/CommentService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function getPaginatedComments(string $commentableKey, int $commentableId,
5858
]);
5959

6060
// Apply sorting on the comments
61-
$query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy, Comment::class);
61+
$query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy);
6262

6363
$rootComments = $query->get();
6464
Log::debug('Root comments retrieved', [

app/Http/Controllers/ResourceEditsController.php

Lines changed: 61 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,16 @@
66
use App\Models\ComputerScienceResource;
77
use App\Models\ResourceEdits;
88
use App\Services\ResourceEditsService;
9-
use App\Services\UpvoteService;
9+
use Illuminate\Http\Request;
1010
use Illuminate\Support\Facades\Auth;
11-
use Illuminate\Support\Facades\DB;
1211
use Illuminate\Support\Facades\Log;
13-
use Illuminate\Support\Facades\Storage;
1412
use Inertia\Inertia;
15-
use Str;
1613
use Throwable;
1714

1815
class ResourceEditsController extends Controller
1916
{
2017
public function __construct(
21-
protected ResourceEditsService $resourceEditsService,
22-
protected UpvoteService $upvoteService
18+
protected ResourceEditsService $resourceEditsService
2319
) {}
2420

2521
/**
@@ -41,38 +37,61 @@ public function create(string $slug)
4137
public function store(ComputerScienceResource $computerScienceResource, StoreResourceEditRequest $request)
4238
{
4339
$validatedData = $request->validated();
44-
$proposedChanges = $validatedData['proposed_changes'] ?? [];
45-
46-
$actualChanges = $this->resourceEditsService->calculateChanges($computerScienceResource, $proposedChanges);
47-
48-
// Add image path to the actual changes
49-
if (array_key_exists('image_file', $proposedChanges)) {
50-
$actualChanges['image_path'] = null;
51-
if (isset($proposedChanges['image_file'])) {
52-
$path = $proposedChanges['image_file']->store('resource-edits', 'public');
53-
$actualChanges['image_path'] = $path;
54-
}
55-
unset($actualChanges['image_file']);
56-
}
5740

58-
if (empty($actualChanges)) {
59-
Log::warning("Resource edit was submitted without any changes for resource ID: {$computerScienceResource->id}");
41+
try {
42+
$resourceEdit = $this->resourceEditsService->createResourceEdit($computerScienceResource, $validatedData);
43+
44+
Log::info('Resource edit created', [
45+
'resource_edit_id' => $resourceEdit->id,
46+
'resource_id' => $computerScienceResource->id,
47+
'user_id' => Auth::id(),
48+
'edit_title' => $resourceEdit->edit_title,
49+
]);
50+
51+
return redirect()->route('resource_edits.show', ['slug' => $resourceEdit->slug])
52+
->with('success', 'Edits Created!');
53+
} catch (\InvalidArgumentException $e) {
54+
Log::warning('Resource edit submitted with no changes', [
55+
'resource_id' => $computerScienceResource->id,
56+
'user_id' => Auth::id(),
57+
'error' => $e->getMessage(),
58+
]);
6059

6160
return redirect()->back()->with('warning', 'Cannot submit an edit with no changes made.');
61+
} catch (Throwable $e) {
62+
Log::critical('Failed to create resource edit', [
63+
'error' => $e->getMessage(),
64+
'trace' => $e->getTraceAsString(),
65+
'resource_id' => $computerScienceResource->id,
66+
'user_id' => Auth::id(),
67+
'data' => $validatedData,
68+
]);
69+
70+
return redirect()->back()->withErrors(['error' => 'Failed to create resource edit. Please try again.']);
6271
}
72+
}
6373

64-
$resourceEdit = ResourceEdits::create([
65-
'user_id' => Auth::id(),
66-
'computer_science_resource_id' => $computerScienceResource->id,
67-
'edit_title' => $validatedData['edit_title'],
68-
'edit_description' => $validatedData['edit_description'],
69-
'proposed_changes' => $actualChanges,
70-
]);
74+
public function index(Request $request)
75+
{
76+
try {
77+
$data = $this->resourceEditsService->getIndexData($request);
7178

72-
$this->upvoteService->upvote('edit', $resourceEdit->id);
79+
return Inertia::render('ResourceEdits/Index', $data);
80+
} catch (Throwable $e) {
81+
Log::error('Error loading resource edits index', [
82+
'user_id' => Auth::id(),
83+
'query' => $request->query(),
84+
'error' => $e->getMessage(),
85+
'trace' => $e->getTraceAsString(),
86+
]);
7387

74-
return redirect()->route('resource_edits.show', ['slug' => $resourceEdit->slug])
75-
->with('success', 'Edits Created!');
88+
// Return an empty page with an error flash so the UI can show a message
89+
session()->flash('error', 'Unable to load resource edits right now.');
90+
91+
return Inertia::render('ResourceEdits/Index', [
92+
'resource_edits' => ResourceEdits::query()->paginate(1),
93+
]);
94+
}
7695
}
7796

7897
public function show(string $slug)
@@ -87,57 +106,10 @@ public function show(string $slug)
87106
]);
88107
}
89108

90-
public function merge(ResourceEditsService $editsService, ResourceEdits $resourceEdits)
109+
public function merge(ResourceEdits $resourceEdits)
91110
{
92-
if (! $editsService->canMergeEdits($resourceEdits)) {
93-
return redirect()->back()->with('warning', 'Not enough approvals');
94-
}
95-
96-
DB::beginTransaction();
97111
try {
98-
$resource = ComputerScienceResource::findOrFail($resourceEdits->computer_science_resource_id);
99-
100-
// Go through each property in proposed_changes, and if it exists. then set the value
101-
$changes = $resourceEdits->proposed_changes;
102-
$proposedFields = ['name', 'description', 'page_url', 'platforms', 'difficulties', 'pricing'];
103-
foreach ($proposedFields as $field) {
104-
if (array_key_exists($field, $changes)) {
105-
$resource->$field = $changes[$field];
106-
}
107-
}
108-
109-
if (array_key_exists('image_path', $changes)) {
110-
if ($resource->image_path) {
111-
Storage::disk('public')->delete($resource->image_path);
112-
}
113-
$destPath = null;
114-
if (isset($changes['image_path'])) {
115-
// Move the new file from 'resource-edits' to 'resource'
116-
$sourcePath = $changes['image_path'];
117-
$fileExtension = pathinfo($sourcePath, PATHINFO_EXTENSION);
118-
$newFileName = Str::random(40).'.'.$fileExtension;
119-
$destPath = 'resource/'.$newFileName;
120-
121-
// TODO: FIGURE OUT WHAT TO DO IN CASE OF EXCEPTION IN CODE FROM LATER STEPS
122-
Storage::disk('public')->move($sourcePath, $destPath);
123-
}
124-
// Update image_path in DB
125-
$resource->image_path = $destPath;
126-
}
127-
128-
$resource->save();
129-
130-
$proposedTagFields = ['topics_tags', 'programming_languages_tags', 'general_tags'];
131-
foreach ($proposedTagFields as $field) {
132-
if (array_key_exists($field, $changes)) {
133-
$resource->$field = $changes[$field];
134-
}
135-
}
136-
137-
// Delete the edit since we successfully merged the changes
138-
$resourceEdits->delete();
139-
140-
DB::commit();
112+
$resource = $this->resourceEditsService->mergeResourceEdit($resourceEdits);
141113

142114
Log::info('Resource edit merged', [
143115
'resource_id' => $resource->id,
@@ -149,12 +121,20 @@ public function merge(ResourceEditsService $editsService, ResourceEdits $resourc
149121

150122
return redirect(route('resources.show', ['slug' => $resource->slug]))
151123
->with('success', 'Successfully Merged Changes!');
124+
} catch (\LogicException $e) {
125+
Log::warning('Insufficient approvals for resource edit merge', [
126+
'resource_edit_id' => $resourceEdits->id,
127+
'user_id' => Auth::id(),
128+
'error' => $e->getMessage(),
129+
]);
130+
131+
return redirect()->back()->with('warning', 'Not enough approvals');
152132
} catch (Throwable $e) {
153-
DB::rollBack();
154133
Log::critical('Failed to merge resource edits', [
155134
'error' => $e->getMessage(),
156135
'trace' => $e->getTraceAsString(),
157136
'resource_edit_id' => $resourceEdits->id,
137+
'user_id' => Auth::id(),
158138
]);
159139

160140
return redirect()->back()->withErrors(['error' => 'Failed to merge resource edits. Please try again.']);

app/Services/CommentService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public function getPaginatedComments(string $commentableKey, int $commentableId,
5757
]);
5858

5959
// Apply sorting on the comments
60-
$query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy, Comment::class);
60+
$query = app(GeneralVotesSortingManager::class)->applySort($query, $sortBy);
6161

6262
$rootComments = $query->get();
6363
Log::debug('Root comments retrieved', [

app/Services/ComputerScienceResourceService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public function getShowResourceData(Request $request, string $slug, string $tab
133133
function () use ($computerScienceResource, $sortBy, $request) {
134134
try {
135135
$query = ResourceReview::whereBelongsTo($computerScienceResource);
136-
$query = $this->resourceSortingManager->applySort($query, $sortBy, ResourceReview::class);
136+
$query = $this->resourceSortingManager->applySort($query, $sortBy);
137137

138138
return $query->with('user')->paginate(10)->appends($request->query());
139139
} catch (Throwable $e) {

app/Services/ResourceEditsService.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44

55
use App\Models\ComputerScienceResource;
66
use App\Models\ResourceEdits;
7+
use App\Services\SortingManagers\GeneralVotesSortingManager;
78
use App\Utilities\UrlUtilities;
9+
use Illuminate\Http\Request;
810
use Illuminate\Support\Facades\Auth;
11+
use Illuminate\Support\Facades\DB;
12+
use Illuminate\Support\Facades\Storage;
13+
use Illuminate\Support\Str;
14+
use Throwable;
915

1016
class ResourceEditsService
1117
{
18+
public function __construct(
19+
protected UpvoteService $upvoteService,
20+
protected ComputerScienceResourceFilter $filterService,
21+
protected GeneralVotesSortingManager $sortingManager,
22+
) {}
1223
/**
1324
* Determines the amount of votes needed to merge the resource edit into the
1425
*
@@ -90,4 +101,135 @@ public function normalize(array $array): array
90101

91102
return $array;
92103
}
104+
105+
/**
106+
* Create a new resource edit
107+
*
108+
* @throws Throwable
109+
*/
110+
public function createResourceEdit(ComputerScienceResource $computerScienceResource, array $validatedData): ResourceEdits
111+
{
112+
$proposedChanges = $validatedData['proposed_changes'] ?? [];
113+
114+
$actualChanges = $this->calculateChanges($computerScienceResource, $proposedChanges);
115+
116+
// Add image path to the actual changes
117+
if (array_key_exists('image_file', $proposedChanges)) {
118+
$actualChanges['image_path'] = null;
119+
if (isset($proposedChanges['image_file'])) {
120+
$path = $proposedChanges['image_file']->store('resource-edits', 'public');
121+
$actualChanges['image_path'] = $path;
122+
}
123+
unset($actualChanges['image_file']);
124+
}
125+
126+
if (empty($actualChanges)) {
127+
throw new \InvalidArgumentException('Cannot submit an edit with no changes made.');
128+
}
129+
130+
$resourceEdit = ResourceEdits::create([
131+
'user_id' => Auth::id(),
132+
'computer_science_resource_id' => $computerScienceResource->id,
133+
'edit_title' => $validatedData['edit_title'],
134+
'edit_description' => $validatedData['edit_description'],
135+
'proposed_changes' => $actualChanges,
136+
]);
137+
138+
$this->upvoteService->upvote('edit', $resourceEdit->id);
139+
140+
return $resourceEdit;
141+
}
142+
143+
/**
144+
* Merge resource edits into the original resource
145+
*
146+
* @throws Throwable
147+
*/
148+
public function mergeResourceEdit(ResourceEdits $resourceEdits): ComputerScienceResource
149+
{
150+
if (!$this->canMergeEdits($resourceEdits)) {
151+
throw new \LogicException('Not enough approvals to merge this edit.');
152+
}
153+
154+
DB::beginTransaction();
155+
try {
156+
$resource = ComputerScienceResource::findOrFail($resourceEdits->computer_science_resource_id);
157+
158+
// Get the raw proposed_changes to access image_path before it's transformed
159+
$changes = json_decode($resourceEdits->getRawOriginal('proposed_changes'), true);
160+
161+
// Go through each property in proposed_changes, and if it exists, then set the value
162+
$proposedFields = ['name', 'description', 'page_url', 'platforms', 'difficulties', 'pricing'];
163+
foreach ($proposedFields as $field) {
164+
if (array_key_exists($field, $changes)) {
165+
$resource->$field = $changes[$field];
166+
}
167+
}
168+
169+
if (array_key_exists('image_path', $changes)) {
170+
if ($resource->image_path) {
171+
Storage::disk('public')->delete($resource->image_path);
172+
}
173+
$destPath = null;
174+
if (isset($changes['image_path'])) {
175+
// Move the new file from 'resource-edits' to 'resource'
176+
$sourcePath = $changes['image_path'];
177+
$fileExtension = pathinfo($sourcePath, PATHINFO_EXTENSION);
178+
$newFileName = Str::random(40).'.'.$fileExtension;
179+
$destPath = 'resource/'.$newFileName;
180+
181+
Storage::disk('public')->move($sourcePath, $destPath);
182+
}
183+
// Update image_path in DB
184+
$resource->image_path = $destPath;
185+
}
186+
187+
$resource->save();
188+
189+
$proposedTagFields = ['topics_tags', 'programming_languages_tags', 'general_tags'];
190+
foreach ($proposedTagFields as $field) {
191+
if (array_key_exists($field, $changes)) {
192+
$resource->$field = $changes[$field];
193+
}
194+
}
195+
196+
// Delete the edit since we successfully merged the changes
197+
$resourceEdits->delete();
198+
199+
DB::commit();
200+
201+
return $resource;
202+
} catch (Throwable $e) {
203+
DB::rollBack();
204+
throw $e;
205+
}
206+
}
207+
208+
/**
209+
* Get data for the resource edits index page (filters, pagination)
210+
*
211+
* @return array{
212+
* resource_edits: \Illuminate\Contracts\Pagination\LengthAwarePaginator,
213+
* sortingType: string
214+
* }
215+
*/
216+
public function getIndexData(Request $request): array
217+
{
218+
$query = ResourceEdits::query();
219+
220+
// Apply filters and sorting through the dedicated filter service
221+
$filters = $request->query();
222+
$sortBy = $filters['sort_by'] ?? 'top';
223+
224+
$query = $this->sortingManager->applySort($query, $sortBy);
225+
226+
$resourceEdits = $query->with('user', 'computerScienceResource')
227+
->paginate(20)
228+
->appends($request->query());
229+
230+
return [
231+
'resource_edits' => $resourceEdits,
232+
'sortingType' => $sortBy,
233+
];
234+
}
93235
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<img class="w-56" src="/images/LogoTitleSide.svg"/>
3+
</template>

resources/js/Components/FrequentlyAskedQuestion.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ const isAnswerOpen = ref(false);
1111
@click="isAnswerOpen = !isAnswerOpen"
1212
class="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
1313
>
14-
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
14+
<h2 class="font-semibold text-gray-900 dark:text-white">
1515
<slot name="question"></slot>
16-
</h3>
16+
</h2>
1717
<Icon
1818
:icon="isAnswerOpen ? 'mdi:expand-less' : 'mdi:expand-more'"
1919
class="text-xl text-gray-500 dark:text-gray-400 flex-shrink-0 ml-4"

0 commit comments

Comments
 (0)