Skip to content

Commit

Permalink
Added tasks endpoints; Added more test
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Mar 21, 2024
1 parent 6d5dcd5 commit 8d3725b
Show file tree
Hide file tree
Showing 33 changed files with 810 additions and 52 deletions.
2 changes: 1 addition & 1 deletion app/Actions/Jetstream/AddOrganizationMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class AddOrganizationMember implements AddsTeamMembers
*/
public function add(User $owner, Organization $organization, string $email, ?string $role = null): void
{
Gate::forUser($owner)->authorize('addTeamMember', $organization);
Gate::forUser($owner)->authorize('addTeamMember', $organization); // TODO: refactor after owner refactoring

$this->validate($organization, $email, $role);

Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/Api/V1/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public function update(Organization $organization, Project $project, ProjectUpda
$this->checkPermission($organization, 'projects:update', $project);
$project->name = $request->input('name');
$project->color = $request->input('color');
$project->client_id = $request->input('project_id');
$project->save();

return new ProjectResource($project);
Expand Down
106 changes: 106 additions & 0 deletions app/Http/Controllers/Api/V1/TaskController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1;

use App\Http\Requests\V1\Task\TaskIndexRequest;
use App\Http\Requests\V1\Task\TaskStoreRequest;
use App\Http\Requests\V1\Task\TaskUpdateRequest;
use App\Http\Resources\V1\Task\TaskCollection;
use App\Http\Resources\V1\Task\TaskResource;
use App\Models\Organization;
use App\Models\Task;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;

class TaskController extends Controller
{
protected function checkPermission(Organization $organization, string $permission, ?Task $task = null): void
{
parent::checkPermission($organization, $permission);
if ($task !== null && $task->organization_id !== $organization->id) {
throw new AuthorizationException('Task does not belong to organization');
}
}

/**
* Get tasks
*
* @return TaskCollection<TaskResource>
*
* @throws AuthorizationException
*
* @operationId getTasks
*/
public function index(Organization $organization, TaskIndexRequest $request): TaskCollection
{
$this->checkPermission($organization, 'tasks:view');

$projectId = $request->input('project_id');

$query = Task::query()
->whereBelongsTo($organization, 'organization');

if ($projectId !== null) {
$query->where('project_id', '=', $projectId);
}

$tasks = $query->paginate();

return new TaskCollection($tasks);
}

/**
* Create task
*
* @throws AuthorizationException
*
* @operationId createTask
*/
public function store(Organization $organization, TaskStoreRequest $request): JsonResource
{
$this->checkPermission($organization, 'tasks:create');
$task = new Task();
$task->name = $request->input('name');
$task->project_id = $request->input('project_id');
$task->organization()->associate($organization);
$task->save();

return new TaskResource($task);
}

/**
* Update task
*
* @throws AuthorizationException
*
* @operationId updateTask
*/
public function update(Organization $organization, Task $task, TaskUpdateRequest $request): JsonResource
{
$this->checkPermission($organization, 'tasks:update', $task);
$task->name = $request->input('name');
$task->save();

return new TaskResource($task);
}

/**
* Delete task
*
* @throws AuthorizationException
*
* @operationId deleteTask
*/
public function destroy(Organization $organization, Task $task): JsonResponse
{
$this->checkPermission($organization, 'tasks:delete', $task);

$task->delete();

return response()
->json(null, 204);
}
}
36 changes: 36 additions & 0 deletions app/Http/Requests/V1/Task/TaskIndexRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests\V1\Task;

use App\Models\Organization;
use App\Models\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;

/**
* @property Organization $organization Organization from model binding
*/
class TaskIndexRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'project_id' => [
'uuid',
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
];
}
}
43 changes: 43 additions & 0 deletions app/Http/Requests/V1/Task/TaskStoreRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests\V1\Task;

use App\Models\Organization;
use App\Models\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;

/**
* @property Organization $organization Organization from model binding
*/
class TaskStoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'name' => [
// TODO: unique
'required',
'string',
'min:1',
'max:255',
],
'project_id' => [
'required',
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
];
}
}
42 changes: 42 additions & 0 deletions app/Http/Requests/V1/Task/TaskUpdateRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests\V1\Task;

use App\Models\Organization;
use App\Models\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;

/**
* @property Organization $organization Organization from model binding
*/
class TaskUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
*/
public function rules(): array
{
return [
'name' => [
// TODO: unique
'required',
'string',
'min:1',
'max:255',
],
'project_id' => [
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
/** @var Builder<Project> $builder */
return $builder->whereBelongsTo($this->organization, 'organization');
}),
],
];
}
}
18 changes: 18 additions & 0 deletions app/Http/Resources/V1/Task/TaskCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace App\Http\Resources\V1\Task;

use App\Http\Resources\PaginatedResourceCollection;
use Illuminate\Http\Resources\Json\ResourceCollection;

class TaskCollection extends ResourceCollection implements PaginatedResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*/
public $collects = TaskResource::class;
}
37 changes: 37 additions & 0 deletions app/Http/Resources/V1/Task/TaskResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace App\Http\Resources\V1\Task;

use App\Http\Resources\V1\BaseResource;
use App\Models\Tag;
use App\Models\Task;
use Illuminate\Http\Request;

/**
* @property Task $resource
*/
class TaskResource extends BaseResource
{
/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null>
*/
public function toArray(Request $request): array
{
return [
/** @var string $id ID */
'id' => $this->resource->id,
/** @var string $name Name */
'name' => $this->resource->name,
/** @var string $project_id ID of the project */
'project_id' => $this->resource->project_id,
/** @var string $created_at When the tag was created */
'created_at' => $this->formatDateTime($this->resource->created_at),
/** @var string $updated_at When the tag was last updated */
'updated_at' => $this->formatDateTime($this->resource->updated_at),
];
}
}
3 changes: 3 additions & 0 deletions app/Models/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;

/**
* @property string $id
* @property string $name
* @property string $project_id
* @property string $organization_id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property-read Project $project
* @property-read Organization $organization
*
Expand Down
1 change: 0 additions & 1 deletion app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Filament\Forms\Components\Section;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
Expand Down
9 changes: 9 additions & 0 deletions app/Providers/JetstreamServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ protected function configurePermissions(): void
'projects:create',
'projects:update',
'projects:delete',
'tasks:view',
'tasks:create',
'tasks:update',
'tasks:delete',
'time-entries:view:all',
'time-entries:create:all',
'time-entries:update:all',
Expand Down Expand Up @@ -87,6 +91,10 @@ protected function configurePermissions(): void
'projects:create',
'projects:update',
'projects:delete',
'tasks:view',
'tasks:create',
'tasks:update',
'tasks:delete',
'time-entries:view:all',
'time-entries:create:all',
'time-entries:update:all',
Expand All @@ -106,6 +114,7 @@ protected function configurePermissions(): void
Jetstream::role('employee', 'Employee', [
'projects:view',
'tags:view',
'tasks:view',
'time-entries:view:own',
'time-entries:create:own',
'time-entries:update:own',
Expand Down
2 changes: 1 addition & 1 deletion database/factories/OrganizationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function definition(): array
public function withOwner(?User $owner = null): self
{
return $this->state(fn (array $attributes) => [
'user_id' => $owner === null ? User::factory() : $owner,
'user_id' => $owner === null ? User::factory() : $owner->getKey(),
]);
}
}
1 change: 1 addition & 0 deletions database/factories/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function definition(): array
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'two_factor_secret' => null,
'two_factor_confirmed_at' => null,
'two_factor_recovery_codes' => null,
'remember_token' => Str::random(10),
'profile_photo_path' => null,
Expand Down
9 changes: 9 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Http\Controllers\Api\V1\OrganizationController;
use App\Http\Controllers\Api\V1\ProjectController;
use App\Http\Controllers\Api\V1\TagController;
use App\Http\Controllers\Api\V1\TaskController;
use App\Http\Controllers\Api\V1\TimeEntryController;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Expand Down Expand Up @@ -69,6 +70,14 @@
Route::delete('/organizations/{organization}/clients/{client}', [ClientController::class, 'destroy'])->name('destroy');
});

// Task routes
Route::name('tasks.')->group(static function () {
Route::get('/organizations/{organization}/tasks', [TaskController::class, 'index'])->name('index');
Route::post('/organizations/{organization}/tasks', [TaskController::class, 'store'])->name('store');
Route::put('/organizations/{organization}/tasks/{task}', [TaskController::class, 'update'])->name('update');
Route::delete('/organizations/{organization}/tasks/{task}', [TaskController::class, 'destroy'])->name('destroy');
});

// Import routes
Route::name('import.')->group(static function () {
Route::post('/organizations/{organization}/import', [ImportController::class, 'import'])->name('import');
Expand Down
Loading

0 comments on commit 8d3725b

Please sign in to comment.