From 71535c0e414d128bf6edd7d2c5c2831f5dc0c609 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Oct 2020 07:42:02 +1100 Subject: [PATCH] Add tasks to API --- app/Factory/TaskFactory.php | 33 ++ app/Filters/TaskFilters.php | 117 ++++ app/Http/Controllers/OpenAPI/TaskSchema.php | 30 ++ app/Http/Controllers/ProjectController.php | 4 + app/Http/Controllers/TaskController.php | 504 ++++++++++++++++++ .../Requests/Project/StoreProjectRequest.php | 2 +- app/Http/Requests/Task/BulkTaskRequest.php | 39 ++ app/Http/Requests/Task/CreateTaskRequest.php | 28 + app/Http/Requests/Task/DestroyTaskRequest.php | 28 + app/Http/Requests/Task/EditTaskRequest.php | 38 ++ app/Http/Requests/Task/ShowTaskRequest.php | 28 + app/Http/Requests/Task/StoreTaskRequest.php | 85 +++ app/Http/Requests/Task/UpdateTaskRequest.php | 85 +++ app/Models/Task.php | 22 +- app/Policies/TaskPolicy.php | 26 + app/Policies/TaxRatePolicy.php | 1 + app/Providers/AuthServiceProvider.php | 3 + app/Repositories/ExpenseRepository.php | 5 + app/Repositories/TaskRepository.php | 75 +++ app/Repositories/VendorRepository.php | 4 + app/Transformers/TaskTransformer.php | 4 + routes/api.php | 6 +- tests/Feature/DocumentsApiTest.php | 20 +- tests/Feature/TaskApiTest.php | 151 ++++++ tests/MockAccountData.php | 8 + 25 files changed, 1328 insertions(+), 18 deletions(-) create mode 100644 app/Factory/TaskFactory.php create mode 100644 app/Filters/TaskFilters.php create mode 100644 app/Http/Controllers/OpenAPI/TaskSchema.php create mode 100644 app/Http/Controllers/TaskController.php create mode 100644 app/Http/Requests/Task/BulkTaskRequest.php create mode 100644 app/Http/Requests/Task/CreateTaskRequest.php create mode 100644 app/Http/Requests/Task/DestroyTaskRequest.php create mode 100644 app/Http/Requests/Task/EditTaskRequest.php create mode 100644 app/Http/Requests/Task/ShowTaskRequest.php create mode 100644 app/Http/Requests/Task/StoreTaskRequest.php create mode 100644 app/Http/Requests/Task/UpdateTaskRequest.php create mode 100644 app/Policies/TaskPolicy.php create mode 100644 app/Repositories/TaskRepository.php create mode 100644 tests/Feature/TaskApiTest.php diff --git a/app/Factory/TaskFactory.php b/app/Factory/TaskFactory.php new file mode 100644 index 000000000000..e0d43b02e94c --- /dev/null +++ b/app/Factory/TaskFactory.php @@ -0,0 +1,33 @@ +description = ''; + //$task->rate = ''; + $task->company_id = $company_id; + $task->user_id = $user_id; + $task->time_log = '[]'; + $task->is_running = false; + $task->is_deleted = false; + $task->duration = 0; + + return $task; + } +} diff --git a/app/Filters/TaskFilters.php b/app/Filters/TaskFilters.php new file mode 100644 index 000000000000..227d5b73231e --- /dev/null +++ b/app/Filters/TaskFilters.php @@ -0,0 +1,117 @@ +builder; + } + + return $this->builder->where(function ($query) use ($filter) { + $query->where('tasks.description', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value1', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value2', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value3', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value4', 'like', '%'.$filter.'%'); + }); + } + + /** + * Filters the list based on the status + * archived, active, deleted. + * + * @param string filter + * @return Illuminate\Database\Query\Builder + */ + public function status(string $filter = '') : Builder + { + if (strlen($filter) == 0) { + return $this->builder; + } + + $table = 'tasks'; + $filters = explode(',', $filter); + + return $this->builder->where(function ($query) use ($filters, $table) { + $query->whereNull($table.'.id'); + + if (in_array(parent::STATUS_ACTIVE, $filters)) { + $query->orWhereNull($table.'.deleted_at'); + } + + if (in_array(parent::STATUS_ARCHIVED, $filters)) { + $query->orWhere(function ($query) use ($table) { + $query->whereNotNull($table.'.deleted_at'); + + if (! in_array($table, ['users'])) { + $query->where($table.'.is_deleted', '=', 0); + } + }); + } + + if (in_array(parent::STATUS_DELETED, $filters)) { + $query->orWhere($table.'.is_deleted', '=', 1); + } + }); + } + + /** + * Sorts the list based on $sort. + * + * @param string sort formatted as column|asc + * @return Illuminate\Database\Query\Builder + */ + public function sort(string $sort) : Builder + { + $sort_col = explode('|', $sort); + + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + } + + /** + * Returns the base query. + * + * @param int company_id + * @return Illuminate\Database\Query\Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + } + + /** + * Filters the query by the users company ID. + * + * @param $company_id The company Id + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + return $this->builder->company(); + } +} diff --git a/app/Http/Controllers/OpenAPI/TaskSchema.php b/app/Http/Controllers/OpenAPI/TaskSchema.php new file mode 100644 index 000000000000..72c6d473ca9b --- /dev/null +++ b/app/Http/Controllers/OpenAPI/TaskSchema.php @@ -0,0 +1,30 @@ +fill($request->all()); $project->save(); + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $project); + } + return $this->itemResponse($project->fresh()); } diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php new file mode 100644 index 000000000000..0413a9e4a12f --- /dev/null +++ b/app/Http/Controllers/TaskController.php @@ -0,0 +1,504 @@ +task_repo = $task_repo; + } + + /** + * @OA\Get( + * path="/api/v1/tasks", + * operationId="getTasks", + * tags={"tasks"}, + * summary="Gets a list of tasks", + * description="Lists tasks, search and filters allow fine grained lists to be generated. + * + * Query parameters can be added to performed more fine grained filtering of the tasks, these are handled by the TaskFilters class which defines the methods available", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\Response( + * response=200, + * description="A list of tasks", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function index(TaskFilters $filters) + { + $tasks = Task::filter($filters); + + return $this->listResponse($tasks); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/tasks/{id}", + * operationId="showTask", + * tags={"tasks"}, + * summary="Shows a client", + * description="Displays a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Task Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the task object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function show(ShowTaskRequest $request, Task $task) + { + return $this->itemResponse($task); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/tasks/{id}/edit", + * operationId="editTask", + * tags={"tasks"}, + * summary="Shows a client for editting", + * description="Displays a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Task Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function edit(EditTaskRequest $request, Task $task) + { + return $this->itemResponse($task); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param App\Models\Task $task + * @return \Illuminate\Http\Response + * + * + * + * @OA\Put( + * path="/api/v1/tasks/{id}", + * operationId="updateTask", + * tags={"tasks"}, + * summary="Updates a client", + * description="Handles the updating of a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Task Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function update(UpdateTaskRequest $request, Task $task) + { + if ($request->entityIsDeleted($task)) { + return $request->disallowUpdate(); + } + + $task = $this->task_repo->save($request->all(), $task); + + return $this->itemResponse($task->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/tasks/create", + * operationId="getTasksCreate", + * tags={"tasks"}, + * summary="Gets a new blank client object", + * description="Returns a blank object with default values", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A blank client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function create(CreateTaskRequest $request) + { + $task = TaskFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($task); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * + * + * @OA\Post( + * path="/api/v1/tasks", + * operationId="storeTask", + * tags={"tasks"}, + * summary="Adds a client", + * description="Adds an client to a company", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="Returns the saved client object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function store(StoreTaskRequest $request) + { + $task = $this->task_repo->save($request->all(), TaskFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + return $this->itemResponse($task); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/tasks/{id}", + * operationId="deleteTask", + * tags={"tasks"}, + * summary="Deletes a client", + * description="Handles the deletion of a client by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Task Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns a HTTP status", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function destroy(DestroyTaskRequest $request, Task $task) + { + //may not need these destroy routes as we are using actions to 'archive/delete' + $task->delete(); + + return response()->json([], 200); + } + + /** + * Perform bulk actions on the list view. + * + * @param BulkTaskRequest $request + * @return \Illuminate\Http\Response + * + * + * @OA\Post( + * path="/api/v1/tasks/bulk", + * operationId="bulkTasks", + * tags={"tasks"}, + * summary="Performs bulk actions on an array of tasks", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="User credentials", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The Task User response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + $action = request()->input('action'); + + $ids = request()->input('ids'); + $tasks = Task::withTrashed()->find($this->transformKeys($ids)); + + $tasks->each(function ($task, $key) use ($action) { + if (auth()->user()->can('edit', $task)) { + $this->task_repo->{$action}($task); + } + }); + + return $this->listResponse(Task::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + /** + * Returns a client statement. + * + * @return [type] [description] + */ + public function statement() + { + //todo + } +} diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php index f17afb073761..ecee7100ecf6 100644 --- a/app/Http/Requests/Project/StoreProjectRequest.php +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -33,7 +33,7 @@ class StoreProjectRequest extends Request { $rules = []; - $rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); + //$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; return $rules; diff --git a/app/Http/Requests/Task/BulkTaskRequest.php b/app/Http/Requests/Task/BulkTaskRequest.php new file mode 100644 index 000000000000..a3221713cb26 --- /dev/null +++ b/app/Http/Requests/Task/BulkTaskRequest.php @@ -0,0 +1,39 @@ +user()->can(auth()->user()->isAdmin(), Task::class); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = $this->getGlobalRules(); + + /* We don't require IDs on bulk storing. */ + if ($this->action !== self::$STORE_METHOD) { + $rules['ids'] = ['required']; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Task/CreateTaskRequest.php b/app/Http/Requests/Task/CreateTaskRequest.php new file mode 100644 index 000000000000..bd485c641a9a --- /dev/null +++ b/app/Http/Requests/Task/CreateTaskRequest.php @@ -0,0 +1,28 @@ +user()->can('create', Task::class); + } +} diff --git a/app/Http/Requests/Task/DestroyTaskRequest.php b/app/Http/Requests/Task/DestroyTaskRequest.php new file mode 100644 index 000000000000..3a72fc215aec --- /dev/null +++ b/app/Http/Requests/Task/DestroyTaskRequest.php @@ -0,0 +1,28 @@ +user()->can('edit', $this->task); + } +} diff --git a/app/Http/Requests/Task/EditTaskRequest.php b/app/Http/Requests/Task/EditTaskRequest.php new file mode 100644 index 000000000000..a2a52c466f18 --- /dev/null +++ b/app/Http/Requests/Task/EditTaskRequest.php @@ -0,0 +1,38 @@ +user()->can('edit', $this->task); + } + + // public function prepareForValidation() + // { + // $input = $this->all(); + + // //$input['id'] = $this->encodePrimaryKey($input['id']); + + // $this->replace($input); + + // } +} diff --git a/app/Http/Requests/Task/ShowTaskRequest.php b/app/Http/Requests/Task/ShowTaskRequest.php new file mode 100644 index 000000000000..f285b2c95625 --- /dev/null +++ b/app/Http/Requests/Task/ShowTaskRequest.php @@ -0,0 +1,28 @@ +user()->can('view', $this->task); + } +} diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php new file mode 100644 index 000000000000..1c1a2b23d1f7 --- /dev/null +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -0,0 +1,85 @@ +user()->can('create', Task::class); + } + + public function rules() + { + $rules = []; + /* Ensure we have a client name, and that all emails are unique*/ + //$rules['name'] = 'required|min:1'; + //$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + + // $rules['number'] = new UniqueTaskNumberRule($this->all()); + + + return $rules; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + if (array_key_exists('project_id', $input) && is_string($input['project_id'])) { + $input['project_id'] = $this->decodePrimaryKey($input['project_id']); + } + + if (array_key_exists('invoice_id', $input) && is_string($input['invoice_id'])) { + $input['invoice_id'] = $this->decodePrimaryKey($input['invoice_id']); + } + + $this->replace($input); + } + + // public function messages() + // { + // // return [ + // // 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + // // //'required' => trans('validation.required', ['attribute' => 'email']), + // // 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), + // // ]; + // } +} diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php new file mode 100644 index 000000000000..9d03ab13c4f6 --- /dev/null +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -0,0 +1,85 @@ +user()->can('edit', $this->task); + } + + public function rules() + { + $rules = []; + /* Ensure we have a client name, and that all emails are unique*/ + + if ($this->input('number')) { + $rules['number'] = 'unique:tasks,number,'.$this->id.',id,company_id,'.$this->taskss->company_id; + } + + return $rules; + } + + // public function messages() + // { + // return [ + // 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + // 'email' => ctrans('validation.email', ['attribute' => 'email']), + // 'name.required' => ctrans('validation.required', ['attribute' => 'name']), + // 'required' => ctrans('validation.required', ['attribute' => 'email']), + // ]; + // } + + protected function prepareForValidation() + { + $input = $this->all(); + + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { + $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); + } + + if (array_key_exists('project_id', $input) && is_string($input['project_id'])) { + $input['project_id'] = $this->decodePrimaryKey($input['project_id']); + } + + if (array_key_exists('invoice_id', $input) && is_string($input['invoice_id'])) { + $input['invoice_id'] = $this->decodePrimaryKey($input['invoice_id']); + } + + $this->replace($input); + } +} diff --git a/app/Models/Task.php b/app/Models/Task.php index f028773f8848..9a3345f7cfb0 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\Models\Filterable; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -19,12 +20,16 @@ class Task extends BaseModel { use MakesHash; use SoftDeletes; - + use Filterable; + protected $fillable = [ 'client_id', 'invoice_id', + 'project_id', 'custom_value1', 'custom_value2', + 'custom_value3', + 'custom_value4', 'description', 'is_running', 'time_log', @@ -32,11 +37,6 @@ class Task extends BaseModel protected $touches = []; - protected $casts = [ - 'updated_at' => 'timestamp', - 'created_at' => 'timestamp', - ]; - public function getEntityType() { return self::class; @@ -66,4 +66,14 @@ class Task extends BaseModel { return $this->belongsTo(Client::class); } + + public function invoice() + { + return $this->belongsTo(Invoice::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } } diff --git a/app/Policies/TaskPolicy.php b/app/Policies/TaskPolicy.php new file mode 100644 index 000000000000..40e4ae0cb921 --- /dev/null +++ b/app/Policies/TaskPolicy.php @@ -0,0 +1,26 @@ +isAdmin(); + } +} diff --git a/app/Policies/TaxRatePolicy.php b/app/Policies/TaxRatePolicy.php index ae2c5082a4a2..e589d36153bb 100644 --- a/app/Policies/TaxRatePolicy.php +++ b/app/Policies/TaxRatePolicy.php @@ -12,6 +12,7 @@ namespace App\Policies; use App\Models\TaxRate; +use App\Models\User; /** * Class TaxRatePolicy. diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 96687e579d31..793e49d37bcb 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -29,6 +29,7 @@ use App\Models\Project; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Task; use App\Models\TaxRate; use App\Models\User; use App\Models\Vendor; @@ -51,6 +52,7 @@ use App\Policies\ProjectPolicy; use App\Policies\QuotePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; +use App\Policies\TaskPolicy; use App\Policies\TaxRatePolicy; use App\Policies\UserPolicy; use App\Policies\VendorPolicy; @@ -86,6 +88,7 @@ class AuthServiceProvider extends ServiceProvider RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class, Webhook::class => WebhookPolicy::class, + Task::class => TaskPolicy::class, TaxRate::class => TaxRatePolicy::class, User::class => UserPolicy::class, Vendor::class => VendorPolicy::class, diff --git a/app/Repositories/ExpenseRepository.php b/app/Repositories/ExpenseRepository.php index 64bfea705648..990cf0b93818 100644 --- a/app/Repositories/ExpenseRepository.php +++ b/app/Repositories/ExpenseRepository.php @@ -52,6 +52,11 @@ class ExpenseRepository extends BaseRepository $expense->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $expense); + } + // if ($expense->id_number == "" || !$expense->id_number) { // $expense->id_number = $this->getNextExpenseNumber($expense); // } //todo write tests for this and make sure that custom expense numbers also works as expected from here diff --git a/app/Repositories/TaskRepository.php b/app/Repositories/TaskRepository.php new file mode 100644 index 000000000000..2bf267e53edb --- /dev/null +++ b/app/Repositories/TaskRepository.php @@ -0,0 +1,75 @@ +fill($data); + $task->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $task); + } + + return $task; + } + + /** + * Store tasks in bulk. + * + * @param array $task + * @return task|null + */ + public function create($task): ?Task + { + return $this->save( + $task, + TaskFactory::create(auth()->user()->company()->id, auth()->user()->id) + ); + } +} diff --git a/app/Repositories/VendorRepository.php b/app/Repositories/VendorRepository.php index 2611e9bc0099..6f241f8187a5 100644 --- a/app/Repositories/VendorRepository.php +++ b/app/Repositories/VendorRepository.php @@ -75,6 +75,10 @@ class VendorRepository extends BaseRepository $data['name'] = $vendor->present()->name(); } + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $vendor); + } + return $vendor; } diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index d5b21a4eacd9..fd2cc483ae01 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -44,6 +44,10 @@ class TaskTransformer extends EntityTransformer { return [ 'id' => (string) $this->encodePrimaryKey($task->id), + 'user_id' => (string) $this->encodePrimaryKey($task->user_id), + 'assigned_user_id' => (string) $this->encodePrimaryKey($task->assigned_user_id), + 'number' => (string) $task->number ?: '', + 'start_time' => (int) $task->start_time, 'description' => $task->description ?: '', 'duration' => 0, 'created_at' => (int) $task->created_at, diff --git a/routes/api.php b/routes/api.php index 8e0449643bf7..fe48c007897f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -69,6 +69,10 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); + Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit + + Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); + Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk'); @@ -125,7 +129,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('group_settings', 'GroupSettingController'); Route::post('group_settings/bulk', 'GroupSettingController@bulk'); - Route::resource('tax_rates', 'TaxRateController'); // name = (tasks. index / create / show / update / destroy / edit + Route::resource('tax_rates', 'TaxRateController'); // name = (tax_rates. index / create / show / update / destroy / edit Route::post('tax_rates/bulk', 'TaxRateController@bulk')->name('tax_rates.bulk'); Route::post('refresh', 'Auth\LoginController@refresh'); diff --git a/tests/Feature/DocumentsApiTest.php b/tests/Feature/DocumentsApiTest.php index 1b0ad8454674..67976152f39a 100644 --- a/tests/Feature/DocumentsApiTest.php +++ b/tests/Feature/DocumentsApiTest.php @@ -140,18 +140,18 @@ class DocumentsApiTest extends TestCase } - // public function testTaskDocuments() - // { + public function testTaskDocuments() + { - // $response = $this->withHeaders([ - // 'X-API-SECRET' => config('ninja.api_secret'), - // 'X-API-TOKEN' => $this->token, - // ])->get('/api/v1/tasks'); + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks'); - // $response->assertStatus(200); - // $arr = $response->json(); - // $this->assertArrayHasKey('documents', $arr['data'][0]); + $response->assertStatus(200); + $arr = $response->json(); + $this->assertArrayHasKey('documents', $arr['data'][0]); - // } + } } diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php new file mode 100644 index 000000000000..b6ed93936754 --- /dev/null +++ b/tests/Feature/TaskApiTest.php @@ -0,0 +1,151 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testTaskPost() + { + $data = [ + 'description' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks', $data); + + $response->assertStatus(200); + } + + public function testTaskPut() + { + $data = [ + 'description' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/tasks/'.$this->encodePrimaryKey($this->task->id), $data); + + $response->assertStatus(200); + } + + public function testTaskGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks/'.$this->encodePrimaryKey($this->task->id)); + + $response->assertStatus(200); + } + + public function testTaskNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks/'.$this->encodePrimaryKey($this->task->id)); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data']['archived_at']); + } + + public function testTaskArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testTaskRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testTaskDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks/bulk?action=delete', $data); + + $arr = $response->json(); + + $this->assertTrue($arr['data'][0]['is_deleted']); + } +} diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 27acbaf2b934..87972001b69a 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -40,6 +40,7 @@ use App\Models\Project; use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringInvoice; +use App\Models\Task; use App\Models\User; use App\Models\Vendor; use App\Models\VendorContact; @@ -77,6 +78,8 @@ trait MockAccountData public $expense; + public $task; + public function makeTestData() { @@ -217,6 +220,11 @@ trait MockAccountData 'company_id' => $this->company->id, ]); + $this->task = Task::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + $gs = new GroupSetting; $gs->name = 'Test'; $gs->company_id = $this->client->company_id;