Browse Source

Merge branch 'project-issue'

pull/37/head
Nafies Luthfi 7 years ago
parent
commit
9cea1f4208
  1. 62
      app/Entities/Projects/Issue.php
  2. 39
      app/Entities/Projects/IssueStatus.php
  3. 35
      app/Entities/Projects/Priority.php
  4. 11
      app/Entities/Projects/Project.php
  5. 83
      app/Http/Controllers/Issues/CommentController.php
  6. 26
      app/Http/Controllers/Issues/OptionController.php
  7. 116
      app/Http/Controllers/Projects/IssueController.php
  8. 29
      app/Policies/Projects/IssuePolicy.php
  9. 1
      app/Providers/AppServiceProvider.php
  10. 1
      app/Providers/AuthServiceProvider.php
  11. 12
      composer.lock
  12. 21
      database/factories/IssueFactory.php
  13. 38
      database/migrations/2019_03_03_210017_create_issues_table.php
  14. 1
      resources/lang/de/app.php
  15. 57
      resources/lang/de/issue.php
  16. 1
      resources/lang/de/project.php
  17. 1
      resources/lang/en/app.php
  18. 58
      resources/lang/en/issue.php
  19. 1
      resources/lang/en/project.php
  20. 1
      resources/lang/id/app.php
  21. 2
      resources/lang/id/comment.php
  22. 57
      resources/lang/id/issue.php
  23. 1
      resources/lang/id/project.php
  24. 32
      resources/views/projects/issues/create.blade.php
  25. 54
      resources/views/projects/issues/edit.blade.php
  26. 67
      resources/views/projects/issues/index.blade.php
  27. 54
      resources/views/projects/issues/partials/comment-section.blade.php
  28. 65
      resources/views/projects/issues/show.blade.php
  29. 3
      resources/views/projects/partials/nav-tabs.blade.php
  30. 23
      routes/web/projects.php
  31. 106
      tests/Feature/Projects/IssueCommentsTest.php
  32. 219
      tests/Feature/Projects/ProjectIssuesTest.php
  33. 99
      tests/Unit/Models/IssueTest.php
  34. 11
      tests/Unit/Models/ProjectTest.php
  35. 34
      tests/Unit/Policies/IssuePolicyTest.php

62
app/Entities/Projects/Issue.php

@ -0,0 +1,62 @@
<?php
namespace App\Entities\Projects;
use App\Entities\Users\User;
use App\Entities\Projects\Comment;
use App\Entities\Projects\Project;
use Illuminate\Database\Eloquent\Model;
class Issue extends Model
{
protected $fillable = [
'project_id', 'title', 'body', 'priority_id', 'pic_id', 'creator_id',
];
public function project()
{
return $this->belongsTo(Project::class);
}
public function pic()
{
return $this->belongsTo(User::class)->withDefault(['name' => __('issue.no_pic')]);
}
public function creator()
{
return $this->belongsTo(User::class);
}
public function getPriorityAttribute()
{
return Priority::getNameById($this->priority_id);
}
public function getPriorityLabelAttribute()
{
$classColor = Priority::getColorById($this->priority_id);
return '<span class="label label-'.$classColor.'">'.$this->priority.'</span>';
}
public function getStatusAttribute()
{
return IssueStatus::getNameById($this->status_id);
}
public function getStatusLabelAttribute()
{
return '<span class="badge">'.$this->status.'</span>';
}
/**
* Issue has many comments relation.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}

39
app/Entities/Projects/IssueStatus.php

@ -0,0 +1,39 @@
<?php
namespace App\Entities\Projects;
use App\Entities\ReferenceAbstract;
class IssueStatus extends ReferenceAbstract
{
protected static $lists = [
0 => 'open',
1 => 'resolved',
2 => 'closed',
3 => 'on_hold',
4 => 'invalid',
];
protected static $colors = [
0 => 'yellow',
1 => 'green',
2 => 'primary',
3 => 'default',
4 => 'warning',
];
public static function getNameById($singleId)
{
return trans('issue.'.static::getById($singleId));
}
public static function toArray()
{
$lists = [];
foreach (static::$lists as $key => $value) {
$lists[$key] = trans('issue.'.$value);
}
return $lists;
}
}

35
app/Entities/Projects/Priority.php

@ -0,0 +1,35 @@
<?php
namespace App\Entities\Projects;
use App\Entities\ReferenceAbstract;
class Priority extends ReferenceAbstract
{
protected static $lists = [
1 => 'minor',
2 => 'major',
3 => 'critical',
];
protected static $colors = [
1 => 'info',
2 => 'warning',
3 => 'danger',
];
public static function getNameById($singleId)
{
return trans('issue.'.static::getById($singleId));
}
public static function toArray()
{
$lists = [];
foreach (static::$lists as $key => $value) {
$lists[$key] = trans('issue.'.$value);
}
return $lists;
}
}

11
app/Entities/Projects/Project.php

@ -3,6 +3,7 @@
namespace App\Entities\Projects;
use DB;
use App\Entities\Projects\Issue;
use App\Entities\Invoices\Invoice;
use App\Entities\Payments\Payment;
use App\Entities\Partners\Customer;
@ -264,4 +265,14 @@ class Project extends Model
return parent::delete();
}
/**
* Project has many Issues relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function issues()
{
return $this->hasMany(Issue::class);
}
}

83
app/Http/Controllers/Issues/CommentController.php

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Issues;
use Illuminate\Http\Request;
use App\Entities\Projects\Issue;
use App\Entities\Projects\Comment;
use App\Http\Controllers\Controller;
class CommentController extends Controller
{
/**
* Store a new comment in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Entities\Projects\Issue $issue
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request, Issue $issue)
{
$this->authorize('comment-on', $issue);
$newComment = $request->validate([
'body' => 'required|string|max:255',
]);
$issue->comments()->create([
'body' => $newComment['body'],
'creator_id' => auth()->id(),
]);
$issue->touch();
flash(__('comment.created'), 'success');
return back();
}
/**
* Update the specified comment.
*
* @param \Illuminate\Http\Request $request
* @param \App\Entities\Projects\Issue $issue
* @param \App\Entities\Projects\Comment $comment
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Issue $issue, Comment $comment)
{
$this->authorize('update', $comment);
$commentData = $request->validate([
'body' => 'required|string|max:255',
]);
$comment->update($commentData);
flash(__('comment.updated'), 'success');
return redirect()->route('projects.issues.show', [$issue->project, $issue]);
}
/**
* Remove the specified comment.
*
* @param \App\Entities\Projects\Issue $issue
* @param \\App\Entities\Projects\Comment $comment
* @return \Illuminate\Routing\Redirector
*/
public function destroy(Issue $issue, Comment $comment)
{
$this->authorize('delete', $comment);
request()->validate([
'comment_id' => 'required|exists:comments,id',
]);
if (request('comment_id') == $comment->id && $comment->delete()) {
flash(__('comment.deleted'), 'warning');
return redirect()->route('projects.issues.show', [$issue->project, $issue]);
}
flash(__('comment.undeleted'), 'error');
return back();
}
}

26
app/Http/Controllers/Issues/OptionController.php

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Issues;
use Illuminate\Http\Request;
use App\Entities\Projects\Issue;
use App\Http\Controllers\Controller;
class OptionController extends Controller
{
public function update(Request $request, Issue $issue)
{
$issueData = $request->validate([
'priority_id' => 'required|in:1,2,3',
'status_id' => 'required|in:0,1,2,3,4',
'pic_id' => 'nullable|exists:users,id',
]);
$issue->priority_id = $issueData['priority_id'];
$issue->status_id = $issueData['status_id'];
$issue->pic_id = $issueData['pic_id'];
$issue->save();
flash(__('issue.updated'), 'success');
return back();
}
}

116
app/Http/Controllers/Projects/IssueController.php

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Projects;
use App\Entities\Users\User;
use Illuminate\Http\Request;
use App\Entities\Projects\Issue;
use App\Entities\Projects\Comment;
use App\Entities\Projects\Project;
use App\Entities\Projects\Priority;
use App\Http\Controllers\Controller;
use App\Entities\Projects\IssueStatus;
class IssueController extends Controller
{
public function index(Project $project)
{
$issueQuery = $project->issues()
->orderBy('updated_at', 'desc')
->with(['pic', 'creator'])
->withCount(['comments']);
if ($priorityId = request('priority_id')) {
$issueQuery->where('priority_id', $priorityId);
}
if ($statusId = request('status_id')) {
$issueQuery->where('status_id', $priorityId);
}
$issues = $issueQuery->get();
return view('projects.issues.index', compact('project', 'issues'));
}
public function create(Project $project)
{
$users = User::pluck('name', 'id');
$priorities = Priority::toArray();
return view('projects.issues.create', compact('project', 'users', 'priorities'));
}
public function store(Request $request, Project $project)
{
$issueData = $request->validate([
'title' => 'required|max:60',
'body' => 'required|max:255',
'priority_id' => 'required|in:1,2,3',
'pic_id' => 'nullable|exists:users,id',
]);
Issue::create([
'project_id' => $project->id,
'creator_id' => auth()->id(),
'title' => $issueData['title'],
'body' => $issueData['body'],
'priority_id' => $issueData['priority_id'],
'pic_id' => $issueData['pic_id'],
]);
flash(__('issue.created'), 'success');
return redirect()->route('projects.issues.index', $project);
}
public function show(Project $project, Issue $issue)
{
$editableComment = null;
$priorities = Priority::toArray();
$statuses = IssueStatus::toArray();
$users = User::pluck('name', 'id');
$comments = $issue->comments()->with('creator')->get();
if (request('action') == 'comment-edit' && request('comment_id') != null) {
$editableComment = Comment::find(request('comment_id'));
}
return view('projects.issues.show', compact(
'project', 'issue', 'users', 'statuses', 'priorities', 'comments',
'editableComment'
));
}
public function edit(Project $project, Issue $issue)
{
return view('projects.issues.edit', compact('project', 'issue'));
}
public function update(Request $request, Project $project, Issue $issue)
{
$issueData = $request->validate([
'title' => 'required|max:60',
'body' => 'required|max:255',
]);
$issue->title = $issueData['title'];
$issue->body = $issueData['body'];
$issue->save();
flash(__('issue.updated'), 'success');
return redirect()->route('projects.issues.show', [$project, $issue]);
}
public function destroy(Request $request, Project $project, Issue $issue)
{
$request->validate(['issue_id' => 'required']);
if ($request->get('issue_id') == $issue->id && $issue->delete()) {
flash(__('issue.deleted'), 'warning');
return redirect()->route('projects.issues.index', $project);
}
flash(__('issue.undeleted'), 'danger');
return back();
}
}

29
app/Policies/Projects/IssuePolicy.php

@ -0,0 +1,29 @@
<?php
namespace App\Policies\Projects;
use App\Entities\Users\User;
use App\Entities\Projects\Issue;
use Illuminate\Auth\Access\HandlesAuthorization;
class IssuePolicy
{
use HandlesAuthorization;
public function create(User $user, Issue $issue)
{
return true;
}
/**
* Determine whether the user can add comment to an issue.
*
* @param \App\Entities\Users\User $user
* @param \App\Entities\Projects\Issue $issue
* @return bool
*/
public function commentOn(User $user, Issue $issue)
{
return true;
}
}

1
app/Providers/AppServiceProvider.php

@ -22,6 +22,7 @@ class AppServiceProvider extends ServiceProvider
Relation::morphMap([
'projects' => 'App\Entities\Projects\Project',
'issues' => 'App\Entities\Projects\Issue',
'jobs' => 'App\Entities\Projects\Job',
]);
}

1
app/Providers/AuthServiceProvider.php

@ -18,6 +18,7 @@ class AuthServiceProvider extends ServiceProvider
'App\Entities\Projects\Project' => 'App\Policies\Projects\ProjectPolicy',
'App\Entities\Projects\Comment' => 'App\Policies\Projects\CommentPolicy',
'App\Entities\Projects\Job' => 'App\Policies\Projects\JobPolicy',
'App\Entities\Projects\Issue' => 'App\Policies\Projects\IssuePolicy',
'App\Entities\Projects\Task' => 'App\Policies\Projects\TaskPolicy',
'App\Entities\Payments\Payment' => 'App\Policies\PaymentPolicy',
'App\Entities\Users\User' => 'App\Policies\UserPolicy',

12
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a298d20cfcff4c54b1cce7bb8db3515e",
"content-hash": "935f96036f78f878a265f90061c07ece",
"packages": [
{
"name": "backup-manager/backup-manager",
@ -1230,16 +1230,16 @@
},
{
"name": "luthfi/formfield",
"version": "1.0.4",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/nafiesl/FormField.git",
"reference": "59e3ae44a0e04ae6eb6ff66a7f95f29acc5ea109"
"reference": "38d206d7b5e1e7893b67b06689d70305b998f0a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nafiesl/FormField/zipball/59e3ae44a0e04ae6eb6ff66a7f95f29acc5ea109",
"reference": "59e3ae44a0e04ae6eb6ff66a7f95f29acc5ea109",
"url": "https://api.github.com/repos/nafiesl/FormField/zipball/38d206d7b5e1e7893b67b06689d70305b998f0a1",
"reference": "38d206d7b5e1e7893b67b06689d70305b998f0a1",
"shasum": ""
},
"require": {
@ -1278,7 +1278,7 @@
}
],
"description": "Laravel Form Field the extension of Laravelcollective Form for Laravel 5.3 and newer with Twitter Bootstrap 3",
"time": "2018-09-16T11:40:29+00:00"
"time": "2019-03-12T15:08:48+00:00"
},
{
"name": "monolog/monolog",

21
database/factories/IssueFactory.php

@ -0,0 +1,21 @@
<?php
use App\Entities\Users\User;
use Faker\Generator as Faker;
use App\Entities\Projects\Issue;
use App\Entities\Projects\Project;
$factory->define(Issue::class, function (Faker $faker) {
return [
'project_id' => function () {
return factory(Project::class)->create()->id;
},
'title' => $faker->words(3, true),
'body' => $faker->sentences(3, true),
'creator_id' => function () {
return factory(User::class)->create()->id;
},
'status_id' => 0,
'priority_id' => 1,
];
});

38
database/migrations/2019_03_03_210017_create_issues_table.php

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateIssuesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('issues', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('project_id');
$table->string('title', 60);
$table->string('body');
$table->unsignedInteger('creator_id');
$table->unsignedTinyInteger('priority_id');
$table->unsignedInteger('pic_id')->nullable();
$table->unsignedTinyInteger('status_id')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('issues');
}
}

1
resources/lang/de/app.php

@ -17,6 +17,7 @@ return [
'total' => 'gesamt',
'count' => 'Summe',
'remark' => 'Remark',
'last_update' => 'Last Update',
// Action
'add' => 'Hinzufügen',

57
resources/lang/de/issue.php

@ -0,0 +1,57 @@
<?php
return [
// Labels
'issue' => 'Issue',
'list' => 'Issue List',
'search' => 'Search Issue',
'search_text' => 'Title ...',
'all' => 'All Issue',
'select' => 'Select Issue',
'detail' => 'Issue Detail',
'not_found' => 'Issue not found.',
'empty' => 'Issue is empty.',
'back_to_show' => 'Back to Issue Detail',
'back_to_index' => 'Back to Issue List',
'no_pic' => 'No issue PIC',
// Actions
'create' => 'Create new Issue',
'created' => 'A new Issue has been created.',
'show' => 'View Issue Detail',
'edit' => 'Edit Issue',
'update' => 'Update Issue',
'updated' => 'Issue data has been updated.',
'delete' => 'Delete Issue',
'delete_confirm' => 'Are you sure to delete this Issue?',
'deleted' => 'Issue has been deleted.',
'undeleted' => 'Issue not deleted.',
'undeleteable' => 'Issue data cannot be deleted.',
'assign_pic' => 'Assign PIC',
'select_pic' => 'Select a PIC',
'pic_assigned' => 'Issue PIC has been assigned.',
'pic_removed' => 'Issue PIC has been removed.',
// Attributes
'title' => 'Issue Title',
'body' => 'Issue Description',
// Relations
'project' => 'Issue Project',
'pic' => 'Issue PIC',
'creator' => 'Issue Creator',
// Priority
'minor' => 'Minor',
'major' => 'Major',
'critical' => 'Critical',
'all_priority' => 'All Priority',
// Statuses
'open' => 'Open',
'resolved' => 'Resolved',
'closed' => 'Closed',
'on_hold' => 'On Hold',
'invalid' => 'Invalid',
'all_status' => 'All Status',
];

1
resources/lang/de/project.php

@ -71,6 +71,7 @@ return [
'subscriptions' => 'Abonnements',
'status' => 'Projektstatus',
'payments' => 'Zahlungen',
'issues' => 'Issues',
// Statuses
'planned' => 'geplant',

1
resources/lang/en/app.php

@ -17,6 +17,7 @@ return [
'total' => 'Total',
'count' => 'Count',
'remark' => 'Remark',
'last_update' => 'Last Update',
// Action
'add' => 'Add',

58
resources/lang/en/issue.php

@ -0,0 +1,58 @@
<?php
return [
// Labels
'issue' => 'Issue',
'list' => 'Issue List',
'search' => 'Search Issue',
'search_text' => 'Title ...',
'all' => 'All Issue',
'select' => 'Select Issue',
'detail' => 'Issue Detail',
'not_found' => 'Issue not found.',
'empty' => 'Issue is empty.',
'back_to_show' => 'Back to Issue Detail',
'back_to_index' => 'Back to Issue List',
'no_pic' => 'No issue PIC',
// Actions
'create' => 'Create new Issue',
'created' => 'A new Issue has been created.',
'show' => 'View Issue Detail',
'edit' => 'Edit Issue',
'update' => 'Update Issue',
'updated' => 'Issue data has been updated.',
'delete' => 'Delete Issue',
'delete_confirm' => 'Are you sure to delete this Issue?',
'deleted' => 'Issue has been deleted.',
'undeleted' => 'Issue not deleted.',
'undeleteable' => 'Issue data cannot be deleted.',
'assign_pic' => 'Assign PIC',
'select_pic' => 'Select a PIC',
'pic_assigned' => 'Issue PIC has been assigned.',
'pic_removed' => 'Issue PIC has been removed.',
// Attributes
'title' => 'Issue Title',
'body' => 'Issue Description',
// Relations
'project' => 'Issue Project',
'pic' => 'Issue PIC',
'creator' => 'Issue Creator',
// Priority
'priority' => 'Priority',
'minor' => 'Minor',
'major' => 'Major',
'critical' => 'Critical',
'all_priority' => 'All Priority',
// Statuses
'open' => 'Open',
'resolved' => 'Resolved',
'closed' => 'Closed',
'on_hold' => 'On Hold',
'invalid' => 'Invalid',
'all_status' => 'All Status',
];

1
resources/lang/en/project.php

@ -71,6 +71,7 @@ return [
'subscriptions' => 'Subscriptions',
'status' => 'Project Status',
'payments' => 'Payments',
'issues' => 'Issues',
// Statuses
'planned' => 'Planned',

1
resources/lang/id/app.php

@ -17,6 +17,7 @@ return [
'total' => 'Total',
'count' => 'Jumlah',
'remark' => 'Keterangan',
'last_update' => 'Update',
// Action
'add' => 'Tambah',

2
resources/lang/id/comment.php

@ -12,7 +12,7 @@ return [
'created' => 'Input Komentar berhasil.',
'edit' => 'Edit Komentar',
'update' => 'Update Komentar',
'updated' => 'Update data Komentar telah berhasil.',
'updated' => 'Update Komentar berhasil.',
'delete' => 'Hapus Komentar',
'delete_confirm' => 'Anda yakin akan menghapus Komentar ini?',
'deleted' => 'Komentar berhasil dihapus.',

57
resources/lang/id/issue.php

@ -0,0 +1,57 @@
<?php
return [
// Labels
'issue' => 'Issue',
'list' => 'Daftar Issue',
'search' => 'Cari Issue',
'search_text' => 'Nama ...',
'all' => 'Semua Issue',
'select' => 'Pilih Issue',
'detail' => 'Detail Issue',
'not_found' => 'Issue tidak ditemukan.',
'empty' => 'Belum ada Issue',
'back_to_show' => 'Kembali ke detail Issue',
'back_to_index' => 'Kembali ke daftar Issue',
'no_pic' => 'Belum ada PIC',
// Actions
'create' => 'Input Issue Baru',
'created' => 'Input Issue baru telah berhasil.',
'show' => 'Lihat Detail Issue',
'edit' => 'Edit Issue',
'update' => 'Update Issue',
'updated' => 'Update data Issue telah berhasil.',
'delete' => 'Hapus Issue',
'delete_confirm' => 'Anda yakin akan menghapus Issue ini?',
'deleted' => 'Hapus data Issue telah berhasil.',
'undeleted' => 'Data Issue gagal dihapus.',
'undeleteable' => 'Data Issue tidak dapat dihapus.',
'assign_pic' => 'Tugaskan PIC',
'select_pic' => 'Pilih PIC',
'pic_assigned' => 'PIC telah ditugaskan.',
'pic_removed' => 'PIC telah dihapus.',
// Attributes
'title' => 'Judul Issue',
'body' => 'Deskripsi Issue',
// Relations
'project' => 'Project Issue',
'pic' => 'PIC Issue',
'creator' => 'Pembuat Issue',
// Priority
'minor' => 'Minor',
'major' => 'Major',
'critical' => 'Critical',
'all_priority' => 'Semua Priority',
// Statuses
'open' => 'Open',
'resolved' => 'Selesai',
'closed' => 'Ditutup',
'on_hold' => 'Ditunda',
'invalid' => 'Tidak Valid',
'all_status' => 'Semua Status',
];

1
resources/lang/id/project.php

@ -71,6 +71,7 @@ return [
'subscriptions' => 'Langganan',
'status' => 'Status Project',
'payments' => 'Pembayaran',
'issues' => 'Issue',
// Statuses
'planned' => 'Rencana',

32
resources/views/projects/issues/create.blade.php

@ -0,0 +1,32 @@
@extends('layouts.project')
@section('subtitle', __('issue.create'))
@section('action-buttons')
@can('create', new App\Entities\Projects\Issue)
{!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!}
@endcan
@endsection
@section('content-project')
<div class="row">
<div class="col-sm-6 col-sm-offset-2">
{{ Form::open(['route' => ['projects.issues.store', $project]]) }}
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ __('issue.create') }}</h3></div>
<div class="panel-body">
{!! FormField::text('title', ['label' => __('issue.title')]) !!}
{!! FormField::textarea('body', ['label' => __('issue.body')]) !!}
{!! FormField::radios('priority_id', $priorities, ['label' => __('issue.priority'), 'placeholder' => false]) !!}
{!! FormField::select('pic_id', $users, ['label' => __('issue.pic')]) !!}
</div>
<div class="panel-footer">
{{ Form::submit(__('issue.create'), ['class' => 'btn btn-success']) }}
{{ link_to_route('projects.issues.index', __('app.cancel'), $project, ['class' => 'btn btn-default']) }}
</div>
</div>
{{ Form::close() }}
</div>
</div>
@endsection

54
resources/views/projects/issues/edit.blade.php

@ -0,0 +1,54 @@
@extends('layouts.project')
@section('subtitle', __('issue.update'))
@section('action-buttons')
@can('create', new App\Entities\Projects\Issue)
{!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!}
@endcan
@endsection
@section('content-project')
<div class="row">
<div class="col-sm-6 col-sm-offset-2">
@if (request('action') == 'delete' && $issue)
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ __('issue.delete') }}</h3></div>
<div class="panel-body">
<label class="control-label text-primary">{{ __('issue.title') }}</label>
<p>{{ $issue->title }}</p>
<label class="control-label text-primary">{{ __('issue.body') }}</label>
<p>{{ $issue->body }}</p>
{!! $errors->first('issue_id', '<span class="form-error small">:message</span>') !!}
</div>
<hr style="margin:0">
<div class="panel-body text-danger">{{ __('issue.delete_confirm') }}</div>
<div class="panel-footer">
{!! FormField::delete(
['route' => ['projects.issues.destroy', $project, $issue]],
__('app.delete_confirm_button'),
['id' => 'delete-issue-'.$issue->id, 'class' => 'btn btn-danger'],
['issue_id' => $issue->id]
) !!}
{{ link_to_route('projects.issues.edit', __('app.cancel'), [$project, $issue], ['class' => 'btn btn-default']) }}
</div>
</div>
@else
{{ Form::model($issue, ['route' => ['projects.issues.update', $project, $issue], 'method' => 'patch']) }}
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ __('issue.update') }}</h3></div>
<div class="panel-body">
{!! FormField::text('title', ['label' => __('issue.title')]) !!}
{!! FormField::textarea('body', ['label' => __('issue.body')]) !!}
</div>
<div class="panel-footer">
{{ Form::submit(__('issue.update'), ['class' => 'btn btn-success']) }}
{{ link_to_route('projects.issues.show', __('app.cancel'), [$project, $issue], ['class' => 'btn btn-default']) }}
{{ link_to_route('projects.issues.edit', __('app.delete'), [$project, $issue, 'action' => 'delete'], ['id' => 'delete-issue-'.$issue->id, 'class' => 'btn btn-danger pull-right']) }}
</div>
</div>
{{ Form::close() }}
@endif
</div>
</div>
@endsection

67
resources/views/projects/issues/index.blade.php

@ -0,0 +1,67 @@
@inject('priorities', 'App\Entities\Projects\Priority')
@inject('issueStatuses', 'App\Entities\Projects\IssueStatus')
@extends('layouts.project')
@section('subtitle', __('project.issues'))
@section('action-buttons')
{{ Form::open(['method' => 'get', 'class' => 'form-inline', 'style' => 'display:inline']) }}
{!! FormField::select('priority_id', $priorities::toArray(), ['label' => false, 'placeholder' => __('issue.all_priority'), 'value' => request('priority_id')]) !!}
{!! FormField::select('status_id', $issueStatuses::toArray(), ['label' => false, 'placeholder' => __('issue.all_status'), 'value' => request('status_id')]) !!}
{{ Form::submit(__('app.filter'), ['class' => 'btn btn-info']) }}
@if (request(['priority_id', 'status_id']))
{{ link_to_route('projects.issues.index', __('app.reset'), $project, ['class' => 'btn btn-default']) }}
@endif
{{ Form::close() }}
@can('create', new App\Entities\Projects\Issue)
{!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!}
@endcan
@endsection
@section('content-project')
<div id="project-issues" class="panel panel-default table-responsive">
<div class="panel-heading">
<h3 class="panel-title">{{ __('project.issues') }}</h3>
</div>
<table class="table table-condensed table-striped">
<thead>
<th>{{ __('app.table_no') }}</th>
<th>{{ __('issue.title') }}</th>
<th>{{ __('issue.priority') }}</th>
<th>{{ __('app.status') }}</th>
<th class="text-center">{{ __('comment.comment') }}</th>
<th>{{ __('issue.pic') }}</th>
<th>{{ __('issue.creator') }}</th>
<th>{{ __('app.last_update') }}</th>
<th class="text-center">{{ __('app.action') }}</th>
</thead>
<tbody>
@forelse($issues as $key => $issue)
@php
$no = 1 + $key;
@endphp
<tr id="{{ $issue->id }}">
<td>{{ $no }}</td>
<td>{{ $issue->title }}</td>
<td>{!! $issue->priority_label !!}</td>
<td>{!! $issue->status_label !!}</td>
<td class="text-center">{{ $issue->comments_count }}</td>
<td>{{ $issue->pic->name }}</td>
<td>{{ $issue->creator->name }}</td>
<td>{{ $issue->updated_at->diffForHumans() }}</td>
<td class="text-center">
{{ link_to_route(
'projects.issues.show',
__('app.show'),
[$project, $issue],
['title' => __('issue.show')]
) }}
</td>
</tr>
@empty
<tr><td colspan="9">{{ __('issue.not_found') }}</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection

54
resources/views/projects/issues/partials/comment-section.blade.php

@ -0,0 +1,54 @@
@foreach($comments as $comment)
<div class="alert alert-warning">
<legend style="font-size: 14px;margin-bottom: 10px;">
<span class="label label-default pull-right">{{ $comment->time_display }}</span>
<strong>{{ $comment->creator->name }}</strong>
</legend>
<div class="pull-right">
@can('update', $comment)
{{ link_to_route('projects.issues.show', __('app.edit'), [$project, $issue, 'action' => 'comment-edit', 'comment_id' => $comment->id], ['id' => 'edit-comment-'.$comment->id, 'class' => 'small', 'title' => __('comment.edit')]) }}
@endcan
@can('delete', $comment)
{!! FormField::delete(
['route' => ['issues.comments.destroy', $issue, $comment], 'class' => ''],
'&times;',
['class' => 'btn-link', 'id' => 'delete-comment-'.$comment->id],
['comment_id' => $comment->id]
) !!}
@endcan
</div>
{!! nl2br($comment->body) !!}
</div>
@endforeach
@can('comment-on', $issue)
{{ Form::open(['route' => ['issues.comments.store', $issue]]) }}
{!! FormField::textarea('body', ['required' => true, 'label' => false, 'placeholder' => __('comment.create_text')]) !!}
{{ Form::submit(__('comment.create'), ['class' => 'btn btn-success pull-right']) }}
{{ Form::close() }}
<div class="clearfix"></div><br>
@endcan
@if (Request::get('action') == 'comment-edit' && $editableComment)
<div id="commentModal" class="modal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
{{ link_to_route('projects.issues.show', '&times;', [$issue->project, $issue], ['class' => 'close']) }}
<h4 class="modal-title">{{ __('comment.edit') }}</h4>
</div>
{!! Form::model($editableComment, ['route' => ['issues.comments.update', $issue, $editableComment],'method' => 'patch']) !!}
<div class="modal-body">
{!! FormField::textarea('body', ['label' => __('comment.body')]) !!}
{{ Form::hidden('page', request('page')) }}
</div>
<div class="modal-footer">
{!! Form::submit(__('comment.update'), ['class' => 'btn btn-success']) !!}
{{ link_to_route('projects.issues.show', __('app.cancel'), [$project->issue, $issue] + request(['page']), ['class' => 'btn btn-default']) }}
</div>
{!! Form::close() !!}
</div>
</div>
</div>
@endif

65
resources/views/projects/issues/show.blade.php

@ -0,0 +1,65 @@
@extends('layouts.project')
@section('subtitle', __('issue.detail'))
@section('action-buttons')
@can('create', new App\Entities\Projects\Issue)
{!! html_link_to_route('projects.issues.create', __('issue.create'), $project, ['class' => 'btn btn-success', 'icon' => 'plus']) !!}
@endcan
@endsection
@section('content-project')
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<div class="pull-right">{!! $issue->status_label !!}</div>
{{ __('issue.detail') }}
</h3>
</div>
<table class="table table-condensed">
<tbody>
<tr><th class="col-md-4">{{ __('issue.title') }}</th><td class="col-md-8">{{ $issue->title }}</td></tr>
<tr><th>{{ __('issue.body') }}</th><td>{{ $issue->body }}</td></tr>
<tr><th>{{ __('issue.priority') }}</th><td>{!! $issue->priority_label !!}</td></tr>
<tr><th>{{ __('issue.pic') }}</th><td>{{ $issue->pic->name }}</td></tr>
<tr><th>{{ __('app.created_by') }}</th><td>{{ $issue->creator->name }}</td></tr>
</tbody>
</table>
<div class="panel-footer">
{{ link_to_route('projects.issues.edit', __('issue.edit'), [$project, $issue], ['id' => 'edit-issue-'.$issue->id, 'class' => 'btn btn-warning']) }}
{{ link_to_route('projects.issues.index', __('issue.back_to_index'), [$project], ['class' => 'btn btn-default pull-right']) }}
</div>
</div>
<hr>
@include('projects.issues.partials.comment-section')
</div>
<div class="col-md-6">
{{ Form::model($issue, ['route' => ['issues.options.update', $issue], 'method' => 'patch']) }}
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">{{ __('app.action') }}</h3></div>
<div class="panel-body">
{!! FormField::radios('priority_id', $priorities, ['label' => __('issue.priority')]) !!}
{!! FormField::radios('status_id', $statuses, ['label' => __('app.status')]) !!}
{!! FormField::select('pic_id', $users, ['label' => __('issue.assign_pic'), 'placeholder' => __('issue.select_pic')]) !!}
</div>
<div class="panel-footer">
{{ Form::submit(__('issue.update'), ['class' => 'btn btn-success']) }}
</div>
</div>
{{ Form::close() }}
</div>
</div>
@endsection
@section('script')
<script>
(function () {
$('#commentModal').modal({
show: true,
backdrop: 'static',
});
})();
</script>
@endsection

3
resources/views/projects/partials/nav-tabs.blade.php

@ -8,6 +8,9 @@
{!! link_to_route('projects.jobs.index', __('project.jobs').' ('.$project->jobs->count().')', $project) !!}
</li>
@endcan
<li class="{{ Request::segment(3) == 'issues' ? 'active' : '' }}">
{!! link_to_route('projects.issues.index', __('project.issues').' ('.$project->issues->count().')', $project) !!}
</li>
@can('view-comments', $project)
<li class="{{ Request::segment(3) == 'comments' ? 'active' : '' }}">
{!! link_to_route('projects.comments.index', __('comment.list').' ('.$project->comments->count().')', $project) !!}

23
routes/web/projects.php

@ -46,6 +46,17 @@ Route::group(['middleware' => ['auth'], 'namespace' => 'Projects'], function ()
Route::delete('projects/{project}/comments/{comment}', 'CommentsController@destroy')->name('projects.comments.destroy');
/*
* Project Issues Routes
*/
Route::get('projects/{project}/issues', 'IssueController@index')->name('projects.issues.index');
Route::get('projects/{project}/issues/create', 'IssueController@create')->name('projects.issues.create');
Route::post('projects/{project}/issues', 'IssueController@store')->name('projects.issues.store');
Route::get('projects/{project}/issues/{issue}', 'IssueController@show')->name('projects.issues.show');
Route::get('projects/{project}/issues/{issue}/edit', 'IssueController@edit')->name('projects.issues.edit');
Route::patch('projects/{project}/issues/{issue}', 'IssueController@update')->name('projects.issues.update');
Route::delete('projects/{project}/issues/{issue}', 'IssueController@destroy')->name('projects.issues.destroy');
/*
* Tasks Routes
*/
Route::get('jobs/{job}/tasks/create', ['as' => 'tasks.create', 'uses' => 'TasksController@create']);
@ -89,3 +100,15 @@ Route::group(['middleware' => ['auth']], function () {
Route::patch('jobs/{job}/comments/{comment}', 'Jobs\CommentsController@update')->name('jobs.comments.update');
Route::delete('jobs/{job}/comments/{comment}', 'Jobs\CommentsController@destroy')->name('jobs.comments.destroy');
});
/**
* Issue Options Routes
*/
Route::patch('issues/{issue}/options', 'Issues\OptionController@update')->name('issues.options.update');
/**
* Issue Comments Routes
*/
Route::post('issues/{issue}/comments', 'Issues\CommentController@store')->name('issues.comments.store');
Route::patch('issues/{issue}/comments/{comment}', 'Issues\CommentController@update')->name('issues.comments.update');
Route::delete('issues/{issue}/comments/{comment}', 'Issues\CommentController@destroy')->name('issues.comments.destroy');

106
tests/Feature/Projects/IssueCommentsTest.php

@ -0,0 +1,106 @@
<?php
namespace Tests\Feature\Projects;
use Tests\TestCase;
use App\Entities\Projects\Issue;
use App\Entities\Projects\Comment;
use Illuminate\Foundation\Testing\RefreshDatabase;
class IssueCommentsTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function user_can_view_issue_comments()
{
$this->adminUserSigningIn();
$issue = factory(Issue::class)->create();
$comment = factory(Comment::class)->create([
'commentable_type' => 'issues',
'commentable_id' => $issue->id,
'body' => 'This is issue comment.',
]);
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->seeText('This is issue comment.');
}
/** @test */
public function admin_can_add_comment_to_an_issue()
{
$admin = $this->adminUserSigningIn();
$issue = factory(Issue::class)->create();
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->submitForm(__('comment.create'), [
'body' => 'First comment.',
]);
$this->seePageIs(route('projects.issues.show', [$issue->project, $issue]));
$this->see(__('comment.created'));
$this->seeInDatabase('comments', [
'commentable_type' => 'issues',
'commentable_id' => $issue->id,
'body' => 'First comment.',
'creator_id' => $admin->id,
]);
}
/** @test */
public function user_can_edit_an_issue_comment()
{
$this->adminUserSigningIn();
$issue = factory(Issue::class)->create();
$comment = factory(Comment::class)->create([
'commentable_type' => 'issues',
'commentable_id' => $issue->id,
'body' => 'This is issue comment.',
]);
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->seeElement('a', ['id' => 'edit-comment-'.$comment->id]);
$this->click('edit-comment-'.$comment->id);
$this->seeRouteIs('projects.issues.show', [$issue->project, $issue, 'action' => 'comment-edit', 'comment_id' => $comment->id]);
$this->submitForm(__('comment.update'), [
'body' => 'Edited comment.',
]);
$this->seePageIs(route('projects.issues.show', [$issue->project, $issue]));
$this->see(__('comment.updated'));
$this->seeInDatabase('comments', [
'id' => $comment->id,
'commentable_type' => 'issues',
'commentable_id' => $issue->id,
'body' => 'Edited comment.',
]);
}
/** @test */
public function user_can_delete_an_issue_comment()
{
$this->adminUserSigningIn();
$issue = factory(Issue::class)->create();
$comment = factory(Comment::class)->create([
'commentable_type' => 'issues',
'commentable_id' => $issue->id,
'body' => 'This is issue comment.',
]);
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->seeElement('button', ['id' => 'delete-comment-'.$comment->id]);
$this->press('delete-comment-'.$comment->id);
$this->seePageIs(route('projects.issues.show', [$issue->project, $issue]));
$this->see(__('comment.deleted'));
$this->dontSeeInDatabase('comments', [
'id' => $comment->id,
]);
}
}

219
tests/Feature/Projects/ProjectIssuesTest.php

@ -0,0 +1,219 @@
<?php
namespace Tests\Feature\Projects;
use Tests\TestCase;
use App\Entities\Projects\Issue;
use App\Entities\Projects\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ProjectIssuesTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function user_can_view_project_issues()
{
$this->adminUserSigningIn();
$project = factory(Project::class)->create();
$issue = factory(Issue::class)->create([
'project_id' => $project->id,
'title' => 'The issue title.',
'body' => 'This is a project issue body.',
]);
$this->visitRoute('projects.issues.index', $project);
$this->seeRouteIs('projects.issues.index', $project);
$this->seeText('The issue title.');
}
/** @test */
public function admin_can_add_issue_to_a_project()
{
$admin = $this->adminUserSigningIn();
$project = factory(Project::class)->create();
$this->visitRoute('projects.issues.create', $project);
$this->submitForm(__('issue.create'), [
'title' => 'First Issue.',
'body' => 'First Issue description.',
'priority_id' => 1,
'pic_id' => $admin->id,
]);
$this->seePageIs(route('projects.issues.index', $project));
$this->see(__('issue.created'));
$this->seeInDatabase('issues', [
'project_id' => $project->id,
'title' => 'First Issue.',
'body' => 'First Issue description.',
'priority_id' => 1,
'pic_id' => $admin->id,
'creator_id' => $admin->id,
]);
}
/** @test */
public function user_can_view_an_issue_detail()
{
$this->adminUserSigningIn();
$project = factory(Project::class)->create();
$issue = factory(Issue::class)->create([
'project_id' => $project->id,
'title' => 'The issue title.',
'body' => 'This is a project issue body.',
]);
$this->visitRoute('projects.issues.show', [$project, $issue]);
$this->seeText($issue->title);
$this->seeText($issue->body);
}
/** @test */
public function user_can_edit_issue()
{
$this->adminUserSigningIn();
$project = factory(Project::class)->create();
$issue = factory(Issue::class)->create([
'project_id' => $project->id,
'title' => 'The issue title.',
'body' => 'This is a project issue body.',
]);
$this->visitRoute('projects.issues.show', [$project, $issue]);
$this->seeElement('a', ['id' => 'edit-issue-'.$issue->id]);
$this->click('edit-issue-'.$issue->id);
$this->seeRouteIs('projects.issues.edit', [$project, $issue]);
$this->submitForm(__('issue.update'), [
'title' => 'First Issue.',
'body' => 'This is a project issue body.',
]);
$this->seePageIs(route('projects.issues.show', [$project, $issue]));
$this->see(__('issue.updated'));
$this->seeInDatabase('issues', [
'id' => $issue->id,
'project_id' => $project->id,
'title' => 'First Issue.',
'body' => 'This is a project issue body.',
]);
}
/** @test */
public function user_can_delete_issue()
{
$this->adminUserSigningIn();
$project = factory(Project::class)->create();
$issue = factory(Issue::class)->create([
'project_id' => $project->id,
]);
$this->visitRoute('projects.issues.edit', [$project, $issue]);
$this->seeElement('a', ['id' => 'delete-issue-'.$issue->id]);
$this->click('delete-issue-'.$issue->id);
$this->seePageIs(route('projects.issues.edit', [$project, $issue, 'action' => 'delete']));
$this->seeElement('button', ['id' => 'delete-issue-'.$issue->id]);
$this->press('delete-issue-'.$issue->id);
$this->seePageIs(route('projects.issues.index', $project));
$this->seeText(__('issue.deleted'));
$this->dontSeeInDatabase('issues', [
'id' => $issue->id,
]);
}
/** @test */
public function user_can_assign_someone_to_an_issue_as_pic()
{
$this->adminUserSigningIn();
$worker = $this->createUser('worker');
$issue = factory(Issue::class)->create();
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->submitForm(__('issue.update'), [
'pic_id' => $worker->id,
]);
$this->seeRouteIs('projects.issues.show', [$issue->project, $issue]);
$this->seeText(__('issue.updated'));
$this->seeInDatabase('issues', [
'id' => $issue->id,
'pic_id' => $worker->id,
]);
}
/** @test */
public function user_can_remove_pic_assignment()
{
$this->adminUserSigningIn();
$worker = $this->createUser('worker');
$issue = factory(Issue::class)->create(['pic_id' => $worker->id]);
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->submitForm(__('issue.update'), [
'pic_id' => null,
]);
$this->seeRouteIs('projects.issues.show', [$issue->project, $issue]);
$this->seeText(__('issue.updated'));
$this->seeInDatabase('issues', [
'id' => $issue->id,
'pic_id' => null,
]);
}
/** @test */
public function user_can_change_issue_status()
{
$this->adminUserSigningIn();
$worker = $this->createUser('worker');
$issue = factory(Issue::class)->create();
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->submitForm(__('issue.update'), [
'status_id' => 2, // resolved
'pic_id' => $worker->id,
]);
$this->seeRouteIs('projects.issues.show', [$issue->project, $issue]);
$this->seeText(__('issue.updated'));
$this->seeInDatabase('issues', [
'id' => $issue->id,
'pic_id' => $worker->id,
'status_id' => 2, // resolved
]);
}
/** @test */
public function user_can_change_issue_priority()
{
$this->adminUserSigningIn();
$worker = $this->createUser('worker');
$issue = factory(Issue::class)->create();
$this->visitRoute('projects.issues.show', [$issue->project, $issue]);
$this->submitForm(__('issue.update'), [
'priority_id' => 2, // major
'status_id' => 2, // resolved
'pic_id' => $worker->id,
]);
$this->seeRouteIs('projects.issues.show', [$issue->project, $issue]);
$this->seeText(__('issue.updated'));
$this->seeInDatabase('issues', [
'id' => $issue->id,
'pic_id' => $worker->id,
'priority_id' => 2, // major
'status_id' => 2, // resolved
]);
}
}

99
tests/Unit/Models/IssueTest.php

@ -0,0 +1,99 @@
<?php
namespace Tests\Unit\Models;
use Tests\TestCase;
use App\Entities\Users\User;
use App\Entities\Projects\Issue;
use App\Entities\Projects\Comment;
use App\Entities\Projects\Project;
use Illuminate\Support\Collection;
use App\Entities\Projects\Priority;
use Illuminate\Foundation\Testing\RefreshDatabase;
class IssueTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function an_issue_has_belongs_to_project_relation()
{
$issue = factory(Issue::class)->make();
$this->assertInstanceOf(Project::class, $issue->project);
$this->assertEquals($issue->project_id, $issue->project->id);
}
/** @test */
public function an_issue_has_belongs_to_pic_relation()
{
$pic = $this->createUser('worker');
$issue = factory(Issue::class)->make(['pic_id' => $pic->id]);
$this->assertInstanceOf(User::class, $issue->pic);
$this->assertEquals($issue->pic_id, $issue->pic->id);
}
/** @test */
public function issue_pic_name_has_default_value()
{
$issue = factory(Issue::class)->make(['pic_id' => null]);
$this->assertEquals(__('issue.no_pic'), $issue->pic->name);
}
/** @test */
public function an_issue_has_belongs_to_creator_relation()
{
$issue = factory(Issue::class)->make();
$this->assertInstanceOf(User::class, $issue->creator);
$this->assertEquals($issue->creator_id, $issue->creator->id);
}
/** @test */
public function an_issue_has_status_attribute()
{
$issue = factory(Issue::class)->make();
$this->assertEquals(__('issue.open'), $issue->status);
}
/** @test */
public function an_issue_has_status_label_attribute()
{
$issue = factory(Issue::class)->make();
$this->assertEquals('<span class="badge">'.$issue->status.'</span>', $issue->status_label);
}
/** @test */
public function an_issue_has_priority_attribute()
{
$issue = factory(Issue::class)->make();
$this->assertEquals(__('issue.minor'), $issue->priority);
}
/** @test */
public function an_issue_has_priority_label_attribute()
{
$issue = factory(Issue::class)->make();
$colorClass = Priority::getColorById($issue->priority_id);
$this->assertEquals('<span class="label label-'.$colorClass.'">'.$issue->priority.'</span>', $issue->priority_label);
}
/** @test */
public function an_issue_has_many_comments_relation()
{
$issue = factory(Issue::class)->create();
$comment = factory(Comment::class)->create([
'commentable_type' => 'issues',
'commentable_id' => $issue->id,
]);
$this->assertInstanceOf(Collection::class, $issue->comments);
$this->assertInstanceOf(Comment::class, $issue->comments->first());
}
}

11
tests/Unit/Models/ProjectTest.php

@ -6,6 +6,7 @@ use Tests\TestCase;
use App\Entities\Projects\Job;
use App\Entities\Projects\File;
use App\Entities\Projects\Task;
use App\Entities\Projects\Issue;
use App\Entities\Invoices\Invoice;
use App\Entities\Payments\Payment;
use App\Entities\Projects\Comment;
@ -350,4 +351,14 @@ class ProjectTest extends TestCase
'project_id' => $project->id,
]);
}
/** @test */
public function a_project_has_many_issues_relation()
{
$project = factory(Project::class)->create();
$issue = factory(Issue::class)->create(['project_id' => $project->id]);
$this->assertInstanceOf(Collection::class, $project->issues);
$this->assertInstanceOf(Issue::class, $project->issues->first());
}
}

34
tests/Unit/Policies/IssuePolicyTest.php

@ -0,0 +1,34 @@
<?php
namespace Tests\Unit\Policies;
use Tests\TestCase;
use App\Entities\Projects\Issue;
use Illuminate\Foundation\Testing\RefreshDatabase;
/**
* Issue Policy Test.
*
* @author Nafies Luthfi <nafiesl@gmail.com>
*/
class IssuePolicyTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function admin_can_create_issue()
{
$admin = $this->createUser('admin');
$this->assertTrue($admin->can('create', new Issue()));
}
/** @test */
public function admin_can_add_comment_to_an_issue()
{
$admin = $this->createUser('admin');
$issue = factory(Issue::class)->create();
$this->assertTrue($admin->can('comment-on', $issue));
}
}
Loading…
Cancel
Save