From be68983fb965273dcc9be60f2ffc1abdd7ea488e Mon Sep 17 00:00:00 2001 From: Mohamed Alsharaf Date: Mon, 18 Jul 2016 19:29:51 +1200 Subject: [PATCH] Feature: allow to create a more restricted project visibility Fixes #123 #121 #116 Project can be set to internal only. This means users with role of "User" can only see issue created by them. This is to allow manager and developer to work on internal issues that should not be visible to users/clients of the project. --- app/Form/Project.php | 6 +- app/Http/Controllers/ProjectController.php | 19 ++++-- app/Http/Middleware/Permission.php | 11 +++- app/Model/Project.php | 70 +++++++++++++++++++++- app/Model/Project/Issue.php | 12 ++++ app/Model/Traits/Project/CountTrait.php | 4 +- app/Model/Traits/Project/FilterTrait.php | 16 +++++ app/Model/Traits/Project/QueryTrait.php | 1 + app/Model/Traits/User/QueryTrait.php | 16 ++++- app/Model/User.php | 2 +- resources/lang/en/tinyissue.php | 1 + resources/views/projects/index.blade.php | 10 +--- 12 files changed, 148 insertions(+), 20 deletions(-) diff --git a/app/Form/Project.php b/app/Form/Project.php index 7fe37c3d8..abe7fb83a 100644 --- a/app/Form/Project.php +++ b/app/Form/Project.php @@ -58,7 +58,11 @@ public function fields() 'private' => [ 'type' => 'select', 'label' => 'visibility', - 'options' => [ProjectModel::PRIVATE_YES => trans('tinyissue.private'), ProjectModel::PRIVATE_NO => trans('tinyissue.public')], + 'options' => [ + ProjectModel::INTERNAL_YES => trans('tinyissue.internal'), + ProjectModel::PRIVATE_YES => trans('tinyissue.private'), + ProjectModel::PRIVATE_NO => trans('tinyissue.public'), + ], ], 'default_assignee' => [ 'type' => 'hidden', diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 424c5b774..9d9d47e88 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -11,6 +11,7 @@ namespace Tinyissue\Http\Controllers; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Tinyissue\Form\FilterIssue as FilterForm; use Tinyissue\Form\Note as NoteForm; @@ -39,15 +40,20 @@ public function getIndex(Project $project) { $activities = $project->activities() ->with('activity', 'issue', 'user', 'assignTo', 'comment', 'note') - ->orderBy('created_at', 'DESC') - ->take(10) - ->get(); + ->orderBy('users_activity.created_at', 'DESC') + ->take(10); + + // Internal project and logged user can see created only + if ($project->isPrivateInternal() && $this->auth->user()->isUser()) { + $activities->join('projects_issues', 'projects_issues.id', '=', 'item_id'); + $activities->where('created_by', '=', $this->auth->user()->id); + } return view('project.index', [ 'tabs' => $this->projectMainViewTabs($project, 'index'), 'project' => $project, 'active' => 'activity', - 'activities' => $activities, + 'activities' => $activities->get(), 'sidebar' => 'project', ]); } @@ -64,8 +70,9 @@ public function getIndex(Project $project) */ public function getIssues(FilterForm $filterForm, Request $request, Project $project, $status = Issue::STATUS_OPEN) { - $active = $status == Issue::STATUS_OPEN ? 'open_issue' : 'closed_issue'; - $issues = $project->listIssues($status, $request->all()); + $request['created_by'] = auth()->user()->id; + $active = $status == Issue::STATUS_OPEN ? 'open_issue' : 'closed_issue'; + $issues = $project->listIssues($status, $request->all()); return view('project.index', [ 'tabs' => $this->projectMainViewTabs($project, 'issues', $issues, $status), diff --git a/app/Http/Middleware/Permission.php b/app/Http/Middleware/Permission.php index c1fabf4ac..667bb3769 100644 --- a/app/Http/Middleware/Permission.php +++ b/app/Http/Middleware/Permission.php @@ -63,6 +63,7 @@ public function handle(Request $request, Closure $next) $user = $this->auth->user(); /** @var ProjectModel|null $project */ $project = $request->route()->getParameter('project'); + $issue = $request->route()->getParameter('issue'); // Check if user has the permission // & if the user can access the current context (e.g. is one of the project users) @@ -70,8 +71,14 @@ public function handle(Request $request, Closure $next) && in_array($permission, $this->publicAccess) && $project instanceof ProjectModel && !$project->isPrivate()) { // Ignore we are ok to view issues in public project - } elseif (!$this->auth->guest() - && (!$user->permission($permission) || !$user->permissionInContext($request->route()))) { + } elseif ( + !$this->auth->guest() + && ( + !$user->permission($permission) || + !$user->permissionInContext($request->route()) || + ($project instanceof ProjectModel && $project->isPrivateInternal() && $user->isUser() && $issue && !$issue->isCreatedBy($user)) + ) + ) { abort(401); } diff --git a/app/Model/Project.php b/app/Model/Project.php index 54e0c0be0..d9ecb95d8 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -23,6 +23,7 @@ * @property string $name * @property int $status * @property int $default_assignee + * @property int $private * @property Project\Issue[] $issues * @property int $openIssuesCount * @property int $closedIssuesCount @@ -37,6 +38,13 @@ class Project extends Model Traits\Project\CrudTrait, Traits\Project\QueryTrait; + /** + * Project private & user role can see their own issues only. + * + * @var int + */ + const INTERNAL_YES = 2; + /** * Project not public to view and create issue. * @@ -93,6 +101,28 @@ class Project extends Model */ protected $fillable = ['name', 'default_assignee', 'status', 'private']; + /** + * List of HTML classes for each status. + * + * @var array + */ + protected $attrClassNames = [ + self::PRIVATE_NO => 'note', + self::PRIVATE_YES => 'info', + self::INTERNAL_YES => 'primary', + ]; + + /** + * List of statuses names. + * + * @var array + */ + protected $statusesNames = [ + self::PRIVATE_NO => 'public', + self::PRIVATE_YES => 'private', + self::INTERNAL_YES => 'internal', + ]; + /** * Generate a URL for the active project. * @@ -209,6 +239,44 @@ public function isMember($userId) */ public function isPrivate() { - return $this->private === true; + return $this->private === self::PRIVATE_YES; + } + + /** + * Whether or not the project is private internal. + * + * @return bool + */ + public function isPrivateInternal() + { + return $this->private === self::INTERNAL_YES; + } + + /** + * Returns project status as string name. + * + * @return string + */ + public function getStatusAsName() + { + if (array_key_exists((int) $this->private, $this->statusesNames)) { + return $this->statusesNames[(int) $this->private]; + } + + return ''; + } + + /** + * Returns the class name to be used for project status. + * + * @return string + */ + public function getStatusClass() + { + if (array_key_exists((int) $this->private, $this->attrClassNames)) { + return $this->attrClassNames[(int) $this->private]; + } + + return ''; } } diff --git a/app/Model/Project/Issue.php b/app/Model/Project/Issue.php index ca0e6987a..cae3e7614 100644 --- a/app/Model/Project/Issue.php +++ b/app/Model/Project/Issue.php @@ -218,4 +218,16 @@ public function canUserViewQuote(Model\User $user = null) return false; } + + /** + * Whether or not a user is the creator of the issue. + * + * @param Model\User $user + * + * @return bool + */ + public function isCreatedBy(Model\User $user) + { + return $this->created_by === $user->id; + } } diff --git a/app/Model/Traits/Project/CountTrait.php b/app/Model/Traits/Project/CountTrait.php index 51c005f7d..16c60ca03 100644 --- a/app/Model/Traits/Project/CountTrait.php +++ b/app/Model/Traits/Project/CountTrait.php @@ -35,7 +35,9 @@ trait CountTrait */ public function countPrivateProjects() { - return $this->where('private', '=', Project::PRIVATE_YES)->count(); + return $this + ->where('private', '=', Project::PRIVATE_YES) + ->orWhere('private', '=', Project::INTERNAL_YES)->count(); } /** diff --git a/app/Model/Traits/Project/FilterTrait.php b/app/Model/Traits/Project/FilterTrait.php index 903e3911c..a8f1b88ef 100644 --- a/app/Model/Traits/Project/FilterTrait.php +++ b/app/Model/Traits/Project/FilterTrait.php @@ -78,4 +78,20 @@ public function filterTitleOrBody(Relations\HasMany $query, $keyword) }); } } + + /** + * Filter by created by. + * + * @param Relations\HasMany $query + * @param int $userId + * @param bool $enabled + * + * @return void + */ + public function filterCreatedBy(Relations\HasMany $query, $userId, $enabled = false) + { + if (true === $enabled && $userId > 0) { + $query->where('created_by', '=', $userId); + } + } } diff --git a/app/Model/Traits/Project/QueryTrait.php b/app/Model/Traits/Project/QueryTrait.php index 63dc03008..bf655ef17 100644 --- a/app/Model/Traits/Project/QueryTrait.php +++ b/app/Model/Traits/Project/QueryTrait.php @@ -106,6 +106,7 @@ public function listIssues($status = Project\Issue::STATUS_OPEN, array $filter = $this->filterTitleOrBody($query, array_get($filter, 'keyword')); $this->filterTags($query, array_get($filter, 'tag_status')); $this->filterTags($query, array_get($filter, 'tag_type')); + $this->filterCreatedBy($query, array_get($filter, 'created_by'), $this->isPrivateInternal()); // Sort if ($sortBy == 'updated') { diff --git a/app/Model/Traits/User/QueryTrait.php b/app/Model/Traits/User/QueryTrait.php index ac6825fd3..bcc963572 100644 --- a/app/Model/Traits/User/QueryTrait.php +++ b/app/Model/Traits/User/QueryTrait.php @@ -54,7 +54,21 @@ public function projectsWidthActivities($status = Project::STATUS_OPEN) ->with([ 'activities' => function (Relations\Relation $query) { $query->with('activity', 'issue', 'user', 'assignTo', 'comment', 'note'); - $query->orderBy('created_at', 'DESC'); + $query->orderBy('users_activity.created_at', 'DESC'); + + // For logged users with role User, show issues that are created by them in internal projects + // of issue create by any for other project statuses + if (auth()->user()->isUser()) { + $query->join('projects_issues', 'projects_issues.id', '=', 'item_id'); + $query->join('projects', 'projects.id', '=', 'parent_id'); + $query->where(function (Eloquent\Builder $query) { + $query->where(function (Eloquent\Builder $query) { + $query->where('created_by', '=', auth()->user()->id); + $query->where('private', '=', Project::INTERNAL_YES); + }); + $query->orWhere('private', '<>', Project::INTERNAL_YES); + }); + } }, ]); } diff --git a/app/Model/User.php b/app/Model/User.php index f76eb740c..155c81c58 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -275,6 +275,6 @@ public function isBlocked() */ public function isUser() { - return $this->role->role === Role::ROLE_USER; + return $this->exists && $this->role->role === Role::ROLE_USER; } } diff --git a/resources/lang/en/tinyissue.php b/resources/lang/en/tinyissue.php index 2df8b5360..1d45a8d5f 100644 --- a/resources/lang/en/tinyissue.php +++ b/resources/lang/en/tinyissue.php @@ -232,4 +232,5 @@ 'your_created_issues_description' => 'Issues that are created by you', 'issue_created_by_you' => 'Issue Created By You', 'readonly_issue_message' => 'The issue is in read only status.', + 'internal' => 'Internal', ]; diff --git a/resources/views/projects/index.blade.php b/resources/views/projects/index.blade.php index 039220b39..c84aa4bbe 100644 --- a/resources/views/projects/index.blade.php +++ b/resources/views/projects/index.blade.php @@ -41,13 +41,9 @@ {{ $project->openIssuesCount }} @lang('tinyissue.open_issue' . ($project->openIssuesCount <= 1? '' : 's')) @if(!Auth::guest()) - - @if($project->private) - @lang('tinyissue.private') - @else - @lang('tinyissue.public') - @endif - + + @lang('tinyissue.' . $project->getStatusAsName()) + @endif @endforeach