Browse Source

Update 2016-07-09.18.14

Add Feature Tasks with TDD
pull/1/head
Nafies Luthfi 10 years ago
parent
commit
da856e3fff
  1. 24
      app/Entities/Projects/FeaturePresenter.php
  2. 10
      app/Entities/Projects/FeaturesRepository.php
  3. 2
      app/Entities/Projects/ProjectsRepository.php
  4. 22
      app/Entities/Projects/Task.php
  5. 35
      app/Entities/Projects/TasksRepository.php
  6. 20
      app/Http/Controllers/Projects/FeaturesController.php
  7. 52
      app/Http/Controllers/Projects/TasksController.php
  8. 34
      app/Http/Requests/Tasks/CreateRequest.php
  9. 34
      app/Http/Requests/Tasks/DeleteRequest.php
  10. 35
      app/Http/Requests/Tasks/UpdateRequest.php
  11. 8
      app/Http/routes/projects.php
  12. 4
      app/Providers/AuthServiceProvider.php
  13. 13
      database/factories/ModelFactory.php
  14. 3
      public/assets/css/app.css
  15. 2
      public/assets/css/app.css.map
  16. 1
      resources/assets/sass/app.scss
  17. 1
      resources/lang/id/feature.php
  18. 22
      resources/lang/id/task.php
  19. 6
      resources/views/features/partials/breadcrumb.blade.php
  20. 5
      resources/views/features/partials/feature-show.blade.php
  21. 103
      resources/views/features/partials/feature-tasks.blade.php
  22. 8
      resources/views/features/show.blade.php
  23. 15
      resources/views/projects/features.blade.php
  24. 115
      tests/ManageTasksTest.php

24
app/Entities/Projects/FeaturePresenter.php

@ -0,0 +1,24 @@
<?php
namespace App\Entities\Projects;
use Laracasts\Presenter\Presenter;
class FeaturePresenter extends Presenter
{
public function workerNameAndEmail()
{
return $this->worker_id ? $this->worker->name . ' (' . $this->worker->email . ')' : '-';
}
public function projectLink()
{
return link_to_route('projects.show', $this->project->name, [$this->project_id]);
}
public function projectFeaturesLink()
{
return link_to_route('projects.features', trans('project.features'), [$this->project_id]);
}
}

10
app/Entities/Projects/FeaturesRepository.php

@ -27,4 +27,14 @@ class FeaturesRepository extends BaseRepository
$featureData['project_id'] = $projectId;
return $this->storeArray($featureData);
}
public function getTasksByFeatureId($featureId)
{
return Task::whereFeatureId($featureId)->get();
}
public function requireTaskById($taskId)
{
return Task::findOrFail($taskId);
}
}

2
app/Entities/Projects/ProjectsRepository.php

@ -80,6 +80,6 @@ class ProjectsRepository extends BaseRepository
public function getProjectFeatures($projectId)
{
return Feature::whereProjectId($projectId)->with('worker')->get();
return Feature::whereProjectId($projectId)->with('worker','tasks')->get();
}
}

22
app/Entities/Projects/Task.php

@ -0,0 +1,22 @@
<?php
namespace App\Entities\Projects;
use App\Entities\Projects\TaskPresenter;
use App\Entities\Users\User;
use Illuminate\Database\Eloquent\Model;
use Laracasts\Presenter\PresentableTrait;
class Task extends Model {
use PresentableTrait;
protected $presenter = TaskPresenter::class;
protected $guarded = ['id','created_at','updated_at'];
public function feature()
{
return $this->belongsTo(Feature::class, 'project_id');
}
}

35
app/Entities/Projects/TasksRepository.php

@ -0,0 +1,35 @@
<?php
namespace App\Entities\Projects;
use App\Entities\BaseRepository;
use App\Entities\Projects\Feature;
/**
* Tasks Repository Class
*/
class TasksRepository extends BaseRepository
{
protected $model;
public function __construct(Task $model)
{
parent::__construct($model);
}
public function requireFeatureById($featureId)
{
return Feature::findOrFail($featureId);
}
public function createTask($taskData, $featureId)
{
$taskData['feature_id'] = $featureId;
return $this->storeArray($taskData);
}
public function getTasksByFeatureId($featureId)
{
return Task::whereTaskId($featureId)->get();
}
}

20
app/Http/Controllers/Projects/FeaturesController.php

@ -33,10 +33,20 @@ class FeaturesController extends Controller {
return redirect()->route('projects.features', $feature->project_id);
}
public function show($featureId)
public function show(Request $req, $featureId)
{
$editableTask = null;
$feature = $this->repo->requireById($featureId);
return view('features.show', compact('feature'));
if ($req->get('action') == 'task_edit' && $req->has('task_id')) {
$editableTask = $this->repo->requireTaskById($req->get('task_id'));
}
if ($req->get('action') == 'task_delete' && $req->has('task_id')) {
$editableTask = $this->repo->requireTaskById($req->get('task_id'));
}
return view('features.show', compact('feature','editableTask'));
}
public function edit($featureId)
@ -75,10 +85,4 @@ class FeaturesController extends Controller {
return redirect()->route('projects.features', $projectId);
}
public function tasks($featureId)
{
$feature = $this->repo->requireById($featureId);
return view('features.features', compact('feature'));
}
}

52
app/Http/Controllers/Projects/TasksController.php

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Projects;
use App\Http\Requests\Tasks\CreateRequest;
use App\Http\Requests\Tasks\UpdateRequest;
use App\Http\Requests\Tasks\DeleteRequest;
use App\Http\Controllers\Controller;
use App\Entities\Projects\TasksRepository;
use Illuminate\Http\Request;
class TasksController extends Controller {
private $repo;
public function __construct(TasksRepository $repo)
{
$this->repo = $repo;
}
public function store(CreateRequest $req, $featureId)
{
$feature = $this->repo->createTask($req->except('_token'), $featureId);
flash()->success(trans('task.created'));
return redirect()->route('features.show', $featureId);
}
public function update(UpdateRequest $req, $taskId)
{
$task = $this->repo->update($req->except(['_method','_token']), $taskId);
flash()->success(trans('task.updated'));
return redirect()->route('features.show', $task->feature_id);
}
public function destroy(DeleteRequest $req, $taskId)
{
$task = $this->repo->requireById($taskId);
$featureId = $task->feature_id;
if ($taskId == $req->get('task_id'))
{
$task->delete();
flash()->success(trans('task.deleted'));
}
else
flash()->error(trans('task.undeleted'));
return redirect()->route('features.show', $featureId);
}
}

34
app/Http/Requests/Tasks/CreateRequest.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Tasks;
use App\Entities\Projects\Feature;
use App\Http\Requests\Request;
class CreateRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$feature = Feature::findOrFail($this->segment(2));
return auth()->user()->can('manage_feature', $feature);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:60',
'description' => 'max:255'
];
}
}

34
app/Http/Requests/Tasks/DeleteRequest.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Tasks;
use App\Entities\Projects\Feature;
use App\Http\Requests\Request;
class DeleteRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$feature = Feature::findOrFail($this->get('feature_id'));
return auth()->user()->can('manage_feature', $feature);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'task_id' => 'required',
'feature_id' => 'required',
];
}
}

35
app/Http/Requests/Tasks/UpdateRequest.php

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Tasks;
use App\Entities\Projects\Feature;
use App\Http\Requests\Request;
class UpdateRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$feature = Feature::findOrFail($this->get('feature_id'));
return auth()->user()->can('manage_feature', $feature);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:60',
'description' => 'max:255',
'progress' => 'required|numeric|max:100',
];
}
}

8
app/Http/routes/projects.php

@ -16,4 +16,12 @@ Route::group(['middleware' => ['web','role:admin'], 'namespace' => 'Projects'],
Route::post('projects/{id}/features', ['as'=>'features.store', 'uses'=>'FeaturesController@store']);
Route::get('features/{id}/delete', ['as'=>'features.delete', 'uses'=>'FeaturesController@delete']);
Route::resource('features','FeaturesController',['except' => ['index','create','store']]);
/**
* Tasks Routes
*/
Route::get('features/{id}/tasks/create', ['as'=>'tasks.create', 'uses'=>'TasksController@create']);
Route::post('features/{id}/tasks', ['as'=>'tasks.store', 'uses'=>'TasksController@store']);
Route::patch('task/{id}', ['as'=>'tasks.update', 'uses'=>'TasksController@update']);
Route::delete('task/{id}', ['as'=>'tasks.destroy', 'uses'=>'TasksController@destroy']);
});

4
app/Providers/AuthServiceProvider.php

@ -38,6 +38,10 @@ class AuthServiceProvider extends ServiceProvider
return $user->id == $project->owner_id;
});
$gate->define('manage_feature', function ($user, $feature) {
return $user->id == $feature->worker_id;
});
}

13
database/factories/ModelFactory.php

@ -3,6 +3,7 @@
use App\Entities\Payments\Payment;
use App\Entities\Projects\Feature;
use App\Entities\Projects\Project;
use App\Entities\Projects\Task;
use App\Entities\Subscriptions\Subscription;
use App\Entities\Users\User;
@ -103,4 +104,16 @@ $factory->define(Feature::class, function (Faker\Generator $faker) {
return factory(User::class)->create()->id;
},
];
});
$factory->define(Task::class, function (Faker\Generator $faker) {
return [
'feature_id' => function() {
return factory(Feature::class)->create()->id;
},
'name' => $faker->sentence(3),
'description' => $faker->paragraph,
'progress' => rand(40,100),
];
});

3
public/assets/css/app.css

@ -6646,7 +6646,8 @@ table.dataTable thead .sorting:after {
* For details, see http://www.apache.org/licenses/LICENSE-2.0.
*/
ul, ol {
margin-bottom: 0; }
margin-bottom: 0;
padding-left: 20px; }
.breadcrumb {
margin-top: 15px; }

2
public/assets/css/app.css.map
File diff suppressed because it is too large
View File

1
resources/assets/sass/app.scss

@ -16,6 +16,7 @@ body {
ul, ol {
margin-bottom: 0;
padding-left: 20px;
}
#wrapper {

1
resources/lang/id/feature.php

@ -21,6 +21,7 @@ return [
'progress' => 'Progress',
'worker' => 'Pekerja',
'price' => 'Nilai Fitur',
'tasks' => 'Daftar Task',
'price_total' => 'Nilai Fitur Total',
'empty' => 'Belum ada Fitur',
'back_to_index' => 'Kembali ke daftar Fitur',

22
resources/lang/id/task.php

@ -0,0 +1,22 @@
<?php
return [
'task' => 'Task',
'tasks' => 'Daftar Task',
'name' => 'Nama Task',
'create' => 'Input Task Baru',
'created' => 'Input Task baru telah berhasil.',
'show' => 'Detail Task',
'edit' => 'Edit Task',
'update' => 'Update Task',
'updated' => 'Update data Task telah berhasil.',
'delete' => 'Hapus Task',
'deleted' => 'Hapus data Task telah berhasil.',
'undeleted' => 'Data Task gagal dihapus.',
'search' => 'Cari Task',
'found' => 'Task ditemukan',
'not_found' => 'Task tidak ditemukan',
'progress' => 'Progress',
'empty' => 'Belum ada Task',
'back_to_index' => 'Kembali ke daftar Task',
];

6
resources/views/features/partials/breadcrumb.blade.php

@ -0,0 +1,6 @@
<ul class="breadcrumb hidden-print">
<li>{{ link_to_route('projects.index',trans('project.projects')) }}</li>
<li>{{ $feature->present()->projectLink }}</li>
<li>{{ $feature->present()->projectFeaturesLink }}</li>
<li class="active">{{ isset($title) ? $title : $feature->name }}</li>
</ul>

5
resources/views/features/partials/feature-show.blade.php

@ -5,9 +5,10 @@
<tbody>
<tr><th>{{ trans('feature.name') }}</th><td>{{ $feature->name }}</td></tr>
<tr><th>{{ trans('feature.price') }}</th><td>{{ formatRp($feature->price) }}</td></tr>
<tr><th>{{ trans('feature.tasks_count') }}</th><td>10</td></tr>
<tr><th>{{ trans('feature.progress') }}</th><td>100%</td></tr>
<tr><th>{{ trans('feature.tasks_count') }}</th><td>{{ $feature->tasks->count() }}</td></tr>
<tr><th>{{ trans('feature.progress') }}</th><td>{{ formatDecimal($feature->tasks->avg('progress')) }}%</td></tr>
<tr><th>{{ trans('feature.worker') }}</th><td>{{ $feature->worker->name }}</td></tr>
<tr><th>{{ trans('feature.description') }}</th><td>{!! nl2br($feature->description) !!}</td></tr>
</tbody>
</table>
</div>

103
resources/views/features/partials/feature-tasks.blade.php

@ -0,0 +1,103 @@
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ trans('feature.tasks') }}</h3></div>
<table class="table table-condensed">
<thead>
<th>{{ trans('app.table_no') }}</th>
<th>{{ trans('task.name') }}</th>
<th>{{ trans('app.description') }}</th>
<th class="text-center">{{ trans('task.progress') }}</th>
<th>{{ trans('app.action') }}</th>
</thead>
<tbody>
@forelse($feature->tasks as $key => $task)
<tr>
<td>{{ 1 + $key }}</td>
<td>{{ $task->name }}</td>
<td>{{ $task->description }}</td>
<td class="text-center">{{ $task->progress }} %</td>
<td>
{{ link_to_route('features.show', trans('task.edit'), [
$feature->id,
'action' => 'task_edit',
'task_id' => $task->id
],['class' => 'btn btn-warning btn-xs']) }}
{{ link_to_route('features.show', trans('task.delete'), [
$feature->id,
'action' => 'task_delete',
'task_id' => $task->id
],['class' => 'btn btn-danger btn-xs']) }}
</td>
</tr>
@empty
<tr><td colspan="5">{{ trans('task.empty') }}</td></tr>
@endforelse
</tbody>
<tfoot>
<tr>
<th class="text-right" colspan="3">Total</th>
<th class="text-center">{{ formatDecimal($feature->tasks->avg('progress')) }} %</th>
<th></th>
</tr>
</tfoot>
</table>
</div>
@if (Request::has('action') == false)
{!! Form::open(['route' => ['tasks.store', $feature->id]])!!}
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ trans('task.create') }}</h3></div>
<div class="panel-body">
<div class="row">
<div class="col-sm-4">
{!! FormField::text('name') !!}
</div>
<div class="col-sm-8">
{!! FormField::text('description') !!}
</div>
</div>
{!! Form::submit(trans('task.create'), ['class' => 'btn btn-primary']) !!}
{!! Form::close() !!}
</div>
</div>
@endif
@if (Request::get('action') == 'task_edit' && $editableTask)
{!! Form::model($editableTask, ['route' => ['tasks.update', $editableTask->id],'method' =>'patch'])!!}
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ trans('task.edit') }}</h3></div>
<div class="panel-body">
<div class="row">
<div class="col-sm-4">
{!! FormField::text('name') !!}
</div>
<div class="col-sm-6">
{!! FormField::text('description') !!}
</div>
<div class="col-sm-2">
{!! FormField::text('progress', ['addon' => ['after' => '%']]) !!}
</div>
</div>
{!! Form::hidden('feature_id', $editableTask->feature_id) !!}
{!! Form::submit(trans('task.update'), ['class' => 'btn btn-warning']) !!}
{!! link_to_route('features.show', trans('app.cancel'), [$feature->id], ['class' => 'btn btn-default']) !!}
{!! Form::close() !!}
</div>
</div>
@endif
@if (Request::get('action') == 'task_delete' && $editableTask)
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ trans('task.delete') }}</h3></div>
<div class="panel-body">
{{ trans('app.delete_confirm') }}
{!! link_to_route('features.show', trans('app.cancel'), [$feature->id], ['class' => 'btn btn-default']) !!}
<div class="pull-right">
{!! FormField::delete([
'route'=>['tasks.destroy',$task->id]],
trans('app.delete_confirm_button'),
['class'=>'btn btn-danger'],
[
'task_id' => $task->id,
'feature_id' => $task->feature_id,
]) !!}
</div>
</div>
</div>
@endif

8
resources/views/features/show.blade.php

@ -3,13 +3,15 @@
@section('title', trans('feature.show'))
@section('content')
@include('features.partials.breadcrumb')
<h1 class="page-header">{{ $feature->name }} <small>{{ trans('feature.show') }}</small></h1>
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
@include('features.partials.feature-show')
</div>
<div class="col-sm-6">
@include('projects.partials.project-show', ['project' => $feature->project])
<div class="col-sm-8">
@include('features.partials.feature-tasks')
</div>
</div>
@endsection

15
resources/views/projects/features.blade.php

@ -24,9 +24,18 @@
@forelse($features as $key => $feature)
<tr>
<td>{{ 1 + $key }}</td>
<td>{{ $feature->name }}</td>
<td class="text-center">{{ $feature->tasks_count = rand(3, 7) }}</td>
<td class="text-center">{{ $feature->progress = rand(70, 100) }} %</td>
<td>
{{ $feature->name }}
@if ($feature->tasks->isEmpty() == false)
<ul>
@foreach($feature->tasks as $task)
<li style="cursor:pointer" title="{{ $task->description }}">{{ $task->name }}</li>
@endforeach
</ul>
@endif
</td>
<td class="text-center">{{ $feature->tasks_count = $feature->tasks->count() }}</td>
<td class="text-center">{{ formatDecimal($feature->progress = $feature->tasks->avg('progress')) }} %</td>
<td class="text-right">{{ formatRp($feature->price) }}</td>
<td>{{ $feature->worker->name }}</td>
<td>{!! link_to_route('features.show', trans('app.show'),[$feature->id],['class' => 'btn btn-info btn-xs']) !!}</td>

115
tests/ManageTasksTest.php

@ -0,0 +1,115 @@
<?php
use App\Entities\Projects\Feature;
use App\Entities\Projects\Project;
use App\Entities\Projects\Task;
use App\Entities\Users\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ManageTasksTest extends TestCase
{
use DatabaseTransactions;
/** @test */
public function admin_can_entry_task()
{
$user = factory(User::class)->create();
$user->assignRole('admin');
$this->actingAs($user);
$feature = factory(Feature::class)->create(['worker_id' => $user->id]);
$this->visit('features/' . $feature->id);
$this->seePageIs('features/' . $feature->id);
$this->see(trans('feature.tasks'));
$this->see(trans('task.create'));
// Fill Form
$this->type('Nama Task Baru','name');
$this->type('Ipsam magnam laboriosam distinctio officia facere sapiente eius corporis','description');
$this->press(trans('task.create'));
$this->seePageIs('features/' . $feature->id);
$this->see(trans('task.created'));
$this->seeInDatabase('tasks', [
'name' => 'Nama Task Baru',
'progress' => 0,
'feature_id' => $feature->id
]);
}
/** @test */
public function admin_can_edit_task_data()
{
$user = factory(User::class)->create();
$user->assignRole('admin');
$user->assignRole('worker');
$this->actingAs($user);
$feature = factory(Feature::class)->create(['worker_id' => $user->id]);
$task = factory(Task::class)->create(['feature_id' => $feature->id]);
$this->visit('features/' . $feature->id);
$this->click(trans('task.edit'));
$this->seePageIs('features/' . $feature->id . '?action=task_edit&task_id=' . $task->id);
$this->see(trans('task.edit'));
$this->see(trans('task.update'));
// Fill Form
$this->type('Nama Task Edit','name');
$this->type(77,'progress');
$this->press(trans('task.update'));
$this->seePageIs('features/' . $feature->id);
$this->see(trans('task.updated'));
$this->seeInDatabase('tasks', [
'name' => 'Nama Task Edit',
'progress' => 77,
'feature_id' => $feature->id
]);
}
/** @test */
public function admin_can_delete_a_task()
{
$user = factory(User::class)->create();
$user->assignRole('admin');
$user->assignRole('worker');
$this->actingAs($user);
$feature = factory(Feature::class)->create(['worker_id' => $user->id]);
$task = factory(Task::class)->create(['feature_id' => $feature->id]);
$this->visit('features/' . $feature->id);
$this->click(trans('task.delete'));
$this->see(trans('app.delete_confirm_button'));
$this->press(trans('app.delete_confirm_button'));
$this->seePageIs('features/' . $feature->id);
$this->see(trans('task.deleted'));
}
/** @test */
public function admin_can_see_all_tasks()
{
$user = factory(User::class)->create();
$user->assignRole('admin');
$user->assignRole('worker');
$this->actingAs($user);
$feature = factory(Feature::class)->create(['worker_id' => $user->id]);
$tasks = factory(Task::class, 10)->create(['feature_id' => $feature->id]);
$this->assertEquals(10, $tasks->count());
$this->visit('features/' . $feature->id);
$this->see($tasks[1]->name);
$this->see($tasks[1]->progress);
$this->see($tasks[1]->description);
$this->see($tasks[9]->name);
$this->see($tasks[9]->progress);
$this->see($tasks[9]->description);
}
}
Loading…
Cancel
Save