diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml index d3ba4d08..981a7e65 100644 --- a/config/packages/kimai.yaml +++ b/config/packages/kimai.yaml @@ -75,20 +75,20 @@ kimai: # PERMISSIONS # -------------------------------------------------------------------------------- permissions: - # mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"]) + # mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and @SETS"]) sets: - ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity','permissions_activity'] - ACTIVITIES_ALL_TEAMLEAD: ['view_teamlead_activity','edit_teamlead_activity','budget_teamlead_activity','permissions_teamlead_activity'] - ACTIVITIES_ALL_TEAM: ['view_team_activity','edit_team_activity','budget_team_activity'] - ACTIVITIES_TEAMLEAD: ['view_teamlead_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity'] - PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project','comments_project','comments_create_project','details_project'] - PROJECTS_ALL_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] - PROJECTS_ALL_TEAM: ['view_team_project','edit_team_project','budget_team_project','comments_team_project','comments_create_team_project','details_team_project'] - PROJECTS_TEAMLEAD: ['view_teamlead_project','budget_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] - CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer','comments_customer','comments_create_customer','details_customer'] - CUSTOMERS_ALL_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','permissions_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] - CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer'] - CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] + ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','time_activity','delete_activity','permissions_activity'] + ACTIVITIES_ALL_TEAMLEAD: ['view_teamlead_activity','edit_teamlead_activity','budget_teamlead_activity','time_teamlead_activity','permissions_teamlead_activity'] + ACTIVITIES_ALL_TEAM: ['view_team_activity','edit_team_activity','budget_team_activity','time_team_activity'] + ACTIVITIES_TEAMLEAD: ['view_teamlead_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity','time_teamlead_activity'] + PROJECTS: ['view_project','create_project','edit_project','budget_project','time_project','delete_project','permissions_project','comments_project','comments_create_project','details_project'] + PROJECTS_ALL_TEAMLEAD: ['view_teamlead_project','edit_teamlead_project','budget_teamlead_project','time_teamlead_project','permissions_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] + PROJECTS_ALL_TEAM: ['view_team_project','edit_team_project','budget_team_project','time_team_project','comments_team_project','comments_create_team_project','details_team_project'] + PROJECTS_TEAMLEAD: ['view_teamlead_project','budget_teamlead_project','time_teamlead_project','comments_teamlead_project','comments_create_teamlead_project','details_teamlead_project'] + CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','time_customer','delete_customer','permissions_customer','comments_customer','comments_create_customer','details_customer'] + CUSTOMERS_ALL_TEAMLEAD: ['view_teamlead_customer','edit_teamlead_customer','budget_teamlead_customer','time_teamlead_customer','permissions_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] + CUSTOMERS_ALL_TEAM: ['view_team_customer','edit_team_customer','budget_team_customer','time_team_customer','comments_team_customer','comments_create_team_customer','details_team_customer'] + CUSTOMERS_TEAMLEAD: ['view_teamlead_customer','budget_teamlead_customer','time_teamlead_customer','comments_teamlead_customer','comments_create_teamlead_customer','details_teamlead_customer'] INVOICE: ['view_invoice','create_invoice'] INVOICE_ADMIN: ['manage_invoice_template'] INVOICE_ALL: ['delete_invoice'] @@ -106,7 +106,7 @@ kimai: LOCKDOWN: ['lockdown_grace_timesheet','lockdown_override_timesheet'] REPORTING: ['view_reporting','view_other_reporting'] # some single default definitions for roles - SINGLE_USER: ['view_team_member','budget_team_project'] + SINGLE_USER: ['view_team_member','time_team_project'] SINGLE_TEAMLEAD: ['view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile','view_team_member'] SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile','view_team_member','view_all_data'] SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile','view_team_member','upload_invoice_template','view_all_data'] diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index 25b3ffd4..69571e06 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -212,6 +212,7 @@ class ActivityController extends BaseApiController $form = $this->createForm(ActivityApiEditForm::class, $activity, [ 'include_budget' => $this->isGranted('budget', $activity), + 'include_time' => $this->isGranted('time', $activity), ]); $form->submit($request->request->all()); @@ -276,6 +277,7 @@ class ActivityController extends BaseApiController $form = $this->createForm(ActivityApiEditForm::class, $activity, [ 'include_budget' => $this->isGranted('budget', $activity), + 'include_time' => $this->isGranted('time', $activity), ]); $form->setData($activity); diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index 5c324702..912d8f9d 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -182,6 +182,7 @@ class CustomerController extends BaseApiController $form = $this->createForm(CustomerApiEditForm::class, $customer, [ 'include_budget' => $this->isGranted('budget', $customer), + 'include_time' => $this->isGranted('time', $customer), ]); $form->submit($request->request->all()); @@ -246,6 +247,7 @@ class CustomerController extends BaseApiController $form = $this->createForm(CustomerApiEditForm::class, $customer, [ 'include_budget' => $this->isGranted('budget', $customer), + 'include_time' => $this->isGranted('time', $customer), ]); $form->setData($customer); diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 64b1acb6..610e0f5e 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -226,6 +226,7 @@ class ProjectController extends BaseApiController 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'date_format' => self::DATE_FORMAT, 'include_budget' => $this->isGranted('budget', $project), + 'include_time' => $this->isGranted('time', $project), ]); $form->submit($request->request->all()); @@ -292,6 +293,7 @@ class ProjectController extends BaseApiController 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'date_format' => self::DATE_FORMAT, 'include_budget' => $this->isGranted('budget', $project), + 'include_time' => $this->isGranted('time', $project), ]); $form->setData($project); diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index f50d36be..5349d06d 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -133,7 +133,7 @@ final class ActivityController extends AbstractController $rates = $rateRepository->getRatesForActivity($activity); } - if ($this->isGranted('budget', $activity)) { + if ($this->isGranted('budget', $activity) || $this->isGranted('time', $activity)) { $stats = $statisticService->getBudgetStatisticModel($activity, $now); } @@ -424,7 +424,8 @@ final class ActivityController extends AbstractController 'action' => $url, 'method' => 'POST', 'currency' => $currency, - 'include_budget' => $this->isGranted('budget', $activity) + 'include_budget' => $this->isGranted('budget', $activity), + 'include_time' => $this->isGranted('time', $activity), ]); } } diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index 4f33f83a..736caca5 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -315,7 +315,7 @@ final class CustomerController extends AbstractController $timezone = new \DateTimeZone($customer->getTimezone()); } - if ($this->isGranted('budget', $customer)) { + if ($this->isGranted('budget', $customer) || $this->isGranted('time', $customer)) { $stats = $statisticService->getBudgetStatisticModel($customer, $now); } @@ -530,7 +530,8 @@ final class CustomerController extends AbstractController return $this->createForm(CustomerEditForm::class, $customer, [ 'action' => $url, 'method' => 'POST', - 'include_budget' => $this->isGranted('budget', $customer) + 'include_budget' => $this->isGranted('budget', $customer), + 'include_time' => $this->isGranted('time', $customer), ]); } } diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 3902926c..b1918e3c 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -333,7 +333,7 @@ final class ProjectController extends AbstractController $rates = $rateRepository->getRatesForProject($project); } - if ($this->isGranted('budget', $project)) { + if ($this->isGranted('budget', $project) || $this->isGranted('time', $project)) { $stats = $statisticService->getBudgetStatisticModel($project, $now); } @@ -563,6 +563,7 @@ final class ProjectController extends AbstractController 'currency' => $currency, 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), 'include_budget' => $this->isGranted('budget', $project), + 'include_time' => $this->isGranted('time', $project), 'time_increment' => 15, ]); } diff --git a/src/Controller/Reporting/ProjectDateRangeController.php b/src/Controller/Reporting/ProjectDateRangeController.php index b0802f32..57d52a8c 100644 --- a/src/Controller/Reporting/ProjectDateRangeController.php +++ b/src/Controller/Reporting/ProjectDateRangeController.php @@ -22,7 +22,7 @@ final class ProjectDateRangeController extends AbstractController { /** * @Route(path="/reporting/project_daterange", name="report_project_daterange", methods={"GET","POST"}) - * @Security("is_granted('view_reporting') and is_granted('budget_project')") + * @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/Controller/Reporting/ProjectDetailsController.php b/src/Controller/Reporting/ProjectDetailsController.php index c0438fa2..9085a22d 100644 --- a/src/Controller/Reporting/ProjectDetailsController.php +++ b/src/Controller/Reporting/ProjectDetailsController.php @@ -22,7 +22,7 @@ final class ProjectDetailsController extends AbstractController { /** * @Route(path="/reporting/project_details", name="report_project_details", methods={"GET"}) - * @Security("is_granted('view_reporting') and (is_granted('details_project') or is_granted('details_teamlead_project') or is_granted('details_team_project'))") + * @Security("is_granted('view_reporting') and is_granted('details', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/Controller/Reporting/ProjectInactiveController.php b/src/Controller/Reporting/ProjectInactiveController.php index fe6451c6..fd6d82ae 100644 --- a/src/Controller/Reporting/ProjectInactiveController.php +++ b/src/Controller/Reporting/ProjectInactiveController.php @@ -21,7 +21,7 @@ final class ProjectInactiveController extends AbstractController { /** * @Route(path="/reporting/project_inactive", name="report_project_inactive", methods={"GET","POST"}) - * @Security("is_granted('view_reporting') and is_granted('budget_project')") + * @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/Controller/Reporting/ProjectViewController.php b/src/Controller/Reporting/ProjectViewController.php index 4a0f36de..f1c9b4a9 100644 --- a/src/Controller/Reporting/ProjectViewController.php +++ b/src/Controller/Reporting/ProjectViewController.php @@ -21,7 +21,7 @@ final class ProjectViewController extends AbstractController { /** * @Route(path="/reporting/project_view", name="report_project_view", methods={"GET","POST"}) - * @Security("is_granted('view_reporting') and is_granted('budget_project')") + * @Security("is_granted('view_reporting') and is_granted('budget_any', 'project')") */ public function __invoke(Request $request, ProjectStatisticService $service) { diff --git a/src/EventSubscriber/Actions/CustomerSubscriber.php b/src/EventSubscriber/Actions/CustomerSubscriber.php index fd892033..1b911798 100644 --- a/src/EventSubscriber/Actions/CustomerSubscriber.php +++ b/src/EventSubscriber/Actions/CustomerSubscriber.php @@ -78,7 +78,7 @@ class CustomerSubscriber extends AbstractActionsSubscriber $event->addDelete($this->path('admin_customer_delete', ['id' => $customer->getId()])); } - if ($this->isGranted('view_reporting') && $this->isGranted('budget_project')) { + if ($this->isGranted('view_reporting') && $this->isGranted('budget_any', 'project')) { $event->addAction('report_project_view', ['url' => $this->path('report_project_view', ['customer' => $customer->getId()]), 'icon' => 'reporting', 'translation_domain' => 'reporting']); } } diff --git a/src/Form/ActivityEditForm.php b/src/Form/ActivityEditForm.php index d4d16bf4..7a0aec6a 100644 --- a/src/Form/ActivityEditForm.php +++ b/src/Form/ActivityEditForm.php @@ -97,6 +97,7 @@ class ActivityEditForm extends AbstractType 'customer' => false, 'currency' => Customer::DEFAULT_CURRENCY, 'include_budget' => false, + 'include_time' => false, 'attr' => [ 'data-form-event' => 'kimai.activityUpdate' ], diff --git a/src/Form/CustomerEditForm.php b/src/Form/CustomerEditForm.php index 3cb57bdb..e25cb97a 100644 --- a/src/Form/CustomerEditForm.php +++ b/src/Form/CustomerEditForm.php @@ -114,6 +114,7 @@ class CustomerEditForm extends AbstractType 'csrf_token_id' => 'admin_customer_edit', 'currency' => Customer::DEFAULT_CURRENCY, 'include_budget' => false, + 'include_time' => false, 'attr' => [ 'data-form-event' => 'kimai.customerUpdate' ], diff --git a/src/Form/EntityFormTrait.php b/src/Form/EntityFormTrait.php index e7705381..8b3331e1 100644 --- a/src/Form/EntityFormTrait.php +++ b/src/Form/EntityFormTrait.php @@ -26,22 +26,30 @@ trait EntityFormTrait { $this->addColor($builder); - if ($options['include_budget']) { - $builder - ->add('budget', MoneyType::class, [ - 'empty_data' => '0.00', - 'label' => 'label.budget', - 'required' => false, - 'currency' => $options['currency'], - ]) - ->add('timeBudget', DurationType::class, [ - 'empty_data' => 0, - 'label' => 'label.timeBudget', - 'icon' => 'clock', - 'required' => false, - ]) - ->add('budgetType', BudgetType::class) - ; + $showMoney = $options['include_budget']; + $showTime = $options['include_time']; + $showBudget = $showMoney || $showTime; + + if ($showMoney) { + $builder->add('budget', MoneyType::class, [ + 'empty_data' => '0.00', + 'label' => 'label.budget', + 'required' => false, + 'currency' => $options['currency'], + ]); + } + + if ($showTime) { + $builder->add('timeBudget', DurationType::class, [ + 'empty_data' => 0, + 'label' => 'label.timeBudget', + 'icon' => 'clock', + 'required' => false, + ]); + } + + if ($showBudget) { + $builder->add('budgetType', BudgetType::class); } $builder->add('metaFields', MetaFieldsCollectionType::class); diff --git a/src/Form/ProjectEditForm.php b/src/Form/ProjectEditForm.php index 602c2109..d12eae95 100644 --- a/src/Form/ProjectEditForm.php +++ b/src/Form/ProjectEditForm.php @@ -112,6 +112,7 @@ class ProjectEditForm extends AbstractType 'currency' => Customer::DEFAULT_CURRENCY, 'date_format' => null, 'include_budget' => false, + 'include_time' => false, 'timezone' => date_default_timezone_get(), 'time_increment' => 1, 'attr' => [ diff --git a/src/Reporting/ReportingService.php b/src/Reporting/ReportingService.php index be8dcbaa..97f4a15d 100644 --- a/src/Reporting/ReportingService.php +++ b/src/Reporting/ReportingService.php @@ -42,25 +42,28 @@ final class ReportingService $event = new ReportingEvent($user); if ($this->security->isGranted('view_reporting')) { + $showBudget = $this->security->isGranted('budget_any', 'project'); + $details = $this->security->isGranted('details', 'project'); + $viewOther = $this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet'); $event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week', 'user')); $event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month', 'user')); $event->addReport(new Report('year_by_user', 'report_user_year', 'report_user_year', 'user')); - if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) { + if ($viewOther) { $event->addReport(new Report('weekly_users_list', 'report_weekly_users', 'report_weekly_users', 'users')); $event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users', 'users')); $event->addReport(new Report('yearly_users_list', 'report_yearly_users', 'report_yearly_users', 'users')); } - if ($this->security->isGranted('budget_project')) { + if ($showBudget) { $event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project')); } - if ($this->security->isGranted('details_project') || $this->security->isGranted('details_teamlead_project') || $this->security->isGranted('details_team_project')) { + if ($details) { $event->addReport(new Report('project_details', 'report_project_details', 'report_project_details', 'project')); } - if ($this->security->isGranted('budget_project')) { + if ($showBudget) { $event->addReport(new Report('daterange_projects', 'report_project_daterange', 'report_project_daterange', 'project')); $event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project')); } - if ($this->security->isGranted('view_other_reporting') && $this->security->isGranted('view_other_timesheet')) { + if ($viewOther) { $event->addReport(new Report('report_customer_monthly_projects', 'report_customer_monthly_projects', 'report_customer_monthly_projects', 'customer')); } diff --git a/src/Validator/Constraints/TimesheetBudgetUsed.php b/src/Validator/Constraints/TimesheetBudgetUsed.php index 8ff7bc96..2dcdebb2 100644 --- a/src/Validator/Constraints/TimesheetBudgetUsed.php +++ b/src/Validator/Constraints/TimesheetBudgetUsed.php @@ -14,4 +14,5 @@ final class TimesheetBudgetUsed extends TimesheetConstraint // same messages, so we can re-use the validation translation! public $messageRate = 'The budget is completely used.'; public $messageTime = 'The budget is completely used.'; + public $messagePermission = 'Sorry, the budget is used up.'; } diff --git a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php index 8a7e7d3b..f94bec0b 100644 --- a/src/Validator/Constraints/TimesheetBudgetUsedValidator.php +++ b/src/Validator/Constraints/TimesheetBudgetUsedValidator.php @@ -20,6 +20,7 @@ use App\Timesheet\RateServiceInterface; use App\Utils\Duration; use App\Utils\LocaleHelper; use DateTime; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -32,8 +33,9 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator private $timesheetRepository; private $rateService; private $configuration; + private $security; - public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService) + public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService, AuthorizationCheckerInterface $security) { $this->configuration = $configuration; $this->customerStatisticService = $customerStatisticService; @@ -41,6 +43,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator $this->activityStatisticService = $activityStatisticService; $this->timesheetRepository = $timesheetRepository; $this->rateService = $rateService; + $this->security = $security; } /** @@ -181,7 +184,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator $free = $budget - $rate; $free = max($free, 0); - $this->context->buildViolation($constraint->messageRate) + $message = $constraint->messageRate; + if (!$this->security->isGranted('budget_money', $field)) { + $message = $constraint->messagePermission; + } + + $this->context->buildViolation($message) ->atPath($field) ->setTranslationDomain('validators') ->setParameters([ @@ -200,7 +208,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator $free = $budget - $duration; $free = max($free, 0); - $this->context->buildViolation($constraint->messageTime) + $message = $constraint->messageTime; + if (!$this->security->isGranted('budget_time', $field)) { + $message = $constraint->messagePermission; + } + + $this->context->buildViolation($message) ->atPath($field) ->setTranslationDomain('validators') ->setParameters([ diff --git a/src/Voter/ActivityVoter.php b/src/Voter/ActivityVoter.php index 57bd2cbe..5cc1f946 100644 --- a/src/Voter/ActivityVoter.php +++ b/src/Voter/ActivityVoter.php @@ -28,6 +28,7 @@ final class ActivityVoter extends Voter 'view', 'edit', 'budget', + 'time', 'delete', 'permissions', ]; diff --git a/src/Voter/CustomerVoter.php b/src/Voter/CustomerVoter.php index dedcd832..a6f5072d 100644 --- a/src/Voter/CustomerVoter.php +++ b/src/Voter/CustomerVoter.php @@ -29,6 +29,7 @@ final class CustomerVoter extends Voter 'create', 'edit', 'budget', + 'time', 'delete', 'permissions', 'comments', diff --git a/src/Voter/EntityMultiRoleVoter.php b/src/Voter/EntityMultiRoleVoter.php new file mode 100644 index 00000000..77cbed14 --- /dev/null +++ b/src/Voter/EntityMultiRoleVoter.php @@ -0,0 +1,121 @@ +permissionManager = $permissionManager; + } + + /** + * @param string $attribute + * @param Activity|Project|Customer|string $subject + * @return bool + */ + protected function supports($attribute, $subject) + { + if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) { + return false; + } + + if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) { + return true; + } + + if ($subject instanceof Activity || $subject instanceof Project || $subject instanceof Customer) { + return true; + } + + return false; + } + + /** + * @param string $attribute + * @param Activity|Project|Customer|string $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + $user = $token->getUser(); + + if (!$user instanceof User) { + return false; + } + + $suffix = null; + + if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) { + $suffix = $subject; + } elseif ($subject instanceof Activity) { + $suffix = 'activity'; + } elseif ($subject instanceof Project) { + $suffix = 'project'; + } elseif ($subject instanceof Customer) { + $suffix = 'customer'; + } + + if ($suffix === null) { + return false; + } + + $permissions = []; + + if ($attribute === 'details') { + $permissions[] = 'details'; + } + + if ($attribute === 'budget_money' || $attribute === 'budget_any') { + $permissions[] = 'budget'; + $permissions[] = 'budget_teamlead'; + $permissions[] = 'budget_team'; + } + + if ($attribute === 'budget_time' || $attribute === 'budget_any') { + $permissions[] = 'time'; + $permissions[] = 'time_teamlead'; + $permissions[] = 'time_team'; + } + foreach ($permissions as $permission) { + if ($this->permissionManager->hasRolePermission($user, $permission . '_' . $suffix)) { + return true; + } + } + + return false; + } +} diff --git a/src/Voter/ProjectVoter.php b/src/Voter/ProjectVoter.php index 66a24ef0..40902827 100644 --- a/src/Voter/ProjectVoter.php +++ b/src/Voter/ProjectVoter.php @@ -28,6 +28,7 @@ final class ProjectVoter extends Voter 'view', 'edit', 'budget', + 'time', 'delete', 'permissions', 'comments', diff --git a/src/Widget/Type/UserTeamProjects.php b/src/Widget/Type/UserTeamProjects.php index 510b3cb4..1fd27ede 100644 --- a/src/Widget/Type/UserTeamProjects.php +++ b/src/Widget/Type/UserTeamProjects.php @@ -83,7 +83,10 @@ class UserTeamProjects extends SimpleWidget implements AuthorizedWidget, UserWid */ public function getPermissions(): array { - return ['budget_team_project', 'budget_teamlead_project', 'budget_project']; + return [ + 'budget_team_project', 'budget_teamlead_project', 'budget_project', + 'time_team_project', 'time_teamlead_project', 'time_project', + ]; } public function setUser(User $user): void diff --git a/templates/activity/edit.html.twig b/templates/activity/edit.html.twig index f15bb27d..5cb55124 100644 --- a/templates/activity/edit.html.twig +++ b/templates/activity/edit.html.twig @@ -28,18 +28,22 @@ {% if form.project is defined %} {{ form_row(form.project) }} {% endif %} - {% if form.budget is defined %} -
| {{ 'label.timeBudget'|trans }} | -- {% if entity.timeBudget > 0 %} - {{ entity.timeBudget|duration }} - {% else %} - - - {% endif %} - | -- {% if entity.timeBudget > 0 %} - 100% - {% else %} - - - {% endif %} - | -
| {{ durationTrans|trans }} | -{{ stats.duration|duration }} | -{{ totalPercentReached|number_format(2) }}% | -
| {{ 'label.billable'|trans }} | -{{ stats.durationBillable|duration }} | -{{ percentReached|number_format(2) }}% | -
| - {% if totalPercentReached < 100 %} - {{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.durationBillable)|duration}) }} - {% else %} - {{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }} - {% endif %} - | -||
|---|---|---|
| {{ 'label.budget'|trans }} | -- {% if entity.budget > 0 %} - {{ entity.budget|money(currency) }} - {% else %} - - - {% endif %} - | -
- {% if entity.budget > 0 %}
+{% if is_granted('time', entity) %}
+ {% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
+ {% import "macros/progressbar.html.twig" as progress %}
+ {% import "macros/widgets.html.twig" as widgets %}
+ {% block box_title %}
+ {{ 'label.timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %}
+ {% endblock %}
+ {% block box_body_class %}no-padding{% endblock %}
+ {% block box_attributes %}id="time_budget_box"{% endblock %}
+ {% block box_body %}
+ {% set durationTrans = entity.isMonthlyBudget() ? 'stats.durationMonth' : 'stats.durationTotal' %}
+
+
+
-
-
- {% if entity.budget > 0 %}
- {{ progress.progressbar(entity.budget, stats.rateBillable, rateTrans|trans, stats.rateBillable|money(currency) ~ ' / ' ~ entity.budget|money(currency)) }}
- {% endif %}
- {{ progress.progressbar(stats.rate, stats.rateBillable, 'label.billable'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ stats.rate|money(currency), true) }}
- {{ progress.progressbar(stats.rateBillable, stats.internalRate, 'label.rate_internal'|trans, stats.internalRate|money(currency) ~ ' / ' ~ stats.rateBillable|money(currency)) }}
+ {% endif %}
+ | |||||||||||||||||||||
| {{ 'label.budget'|trans }} | ++ {% if entity.budget > 0 %} + {{ entity.budget|money(currency) }} + {% else %} + - + {% endif %} + | ++ {% if entity.budget > 0 %} + 100% + {% else %} + - + {% endif %} + | +
| {{ rateTrans|trans }} | +{{ stats.rate|money(currency) }} | +{{ totalPercentReached|number_format(2) }}% | +
| {{ 'label.billable'|trans }} | +{{ stats.rateBillable|money(currency) }} | +{{ percentReached|number_format(2) }}% | +
| {{ 'label.rate_internal'|trans }} | +{{ stats.internalRate|money(currency) }} | +{{ percentReached|number_format(2) }}% | +
| + {% if totalPercentReached < 100 %} + {{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.budget - stats.rateBillable)|money(currency)}) }} + {% else %} + {{ 'stats.percentUsed'|trans({'%percent%': totalPercentReached|number_format(2)}) }} + {% endif %} + | +||
|---|---|---|
| {{ 'label.timeBudget'|trans }} @@ -370,7 +371,7 @@ | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ 'label.budget'|trans }} diff --git a/templates/reporting/project_view.html.twig b/templates/reporting/project_view.html.twig index 3e69c139..60067297 100644 --- a/templates/reporting/project_view.html.twig +++ b/templates/reporting/project_view.html.twig @@ -3,22 +3,36 @@ {% block report_title %}{{ (title|default('report_project_view'))|trans({}, 'reporting') }}{% endblock %} +{% set showMoneyBudget = is_granted('budget_money', 'project') %} +{% set showTimeBudget = is_granted('budget_time', 'project') %} +{% set showBudgets = showMoneyBudget or showTimeBudget %} + {% set availableColumns = { 'name': {'class': 'alwaysVisible'}, - 'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans}, - 'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans}, +} %} +{% if showMoneyBudget %} + {% set availableColumns = availableColumns|merge({ + 'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans}, + 'invoiced': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_invoiced'|trans, 'columnClass': 'w-min'}, + }) %} +{% endif %} +{% if showTimeBudget %} + {% set availableColumns = availableColumns|merge({ + 'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans}, + 'exported': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_exported'|trans, 'columnClass': 'w-min'}, + }) %} +{% endif %} +{% set availableColumns = availableColumns|merge({ 'lastRecord': {'class': 'text-center hw-min', 'title': 'label.last_record'|trans, 'columnClass': 'w-min'}, 'today': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationToday'|trans}, 'week': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationWeek'|trans}, 'month': {'class': 'hidden hidden-md hidden-sm hidden-xs text-center hw-min', 'title': 'stats.durationMonth'|trans}, 'durationTotal': {'class': 'text-center hw-min', 'title': 'stats.durationTotal'|trans, 'columnClass': 'w-min'}, - 'exported': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_exported'|trans, 'columnClass': 'w-min'}, - 'invoiced': {'class': 'hidden-sm hidden-xs hidden text-center hw-min', 'title': 'label.not_invoiced'|trans, 'columnClass': 'w-min'}, 'projectStart': {'class': 'hidden-md hidden-sm hidden-xs hidden text-center w-min', 'title': 'label.project_start'|trans}, 'projectEnd': {'class': 'hidden-md hidden-sm hidden-xs hidden text-center w-min', 'title': 'label.project_end'|trans}, 'comment': {'class': 'hidden-md hidden-sm hidden-xs hidden', 'title': 'label.comment'|trans}, 'actions': {'class': 'actions alwaysVisible'}, -} %} +}) %} {% set tableName = tableName|default('project_view_reporting') %} {% set skipColumns = skipColumns is defined ? skipColumns : {} %} {% set columns = {} %} @@ -93,73 +107,71 @@ {% set project = entry.project %} {% set budgetStats = entry.getBudgetStatisticModel() %} {% set currency = project.customer.currency %} - {% if is_granted('budget', project) %} - | |||||||||||||
| {{ widgets.label_project(project) }} | - {% elseif name == 'lastRecord' %} -- {% if entry.lastRecord is not null %} - {{ entry.lastRecord|date_short }} - {% else %} - – - {% endif %} - | - {% elseif name == 'today' %} -{{ entry.durationDay|duration }} | - {% elseif name == 'week' %} -{{ entry.durationWeek|duration }} | - {% elseif name == 'month' %} -{{ entry.durationMonth|duration }} | - {% elseif name == 'durationTotal' %} -{{ entry.durationTotal|duration }} | - {% elseif name == 'timeBudget' %} -- {% if budgetStats.hasTimeBudget() %} - {{ progress.progressbar_timebudget(budgetStats) }} - {% endif %} - | - {% elseif name == 'budget' %} -- {% if project.hasBudget() %} - {{ progress.progressbar_budget(budgetStats, project.customer.currency) }} - {% endif %} - | - {% elseif name == 'exported' %} -- {% if is_granted('create_export') %} - - {{ entry.notExportedDuration|duration }} - - {% else %} - {{ entry.notExportedDuration|duration }} - {% endif %} - | - {% elseif name == 'invoiced' %} -- {% if is_granted('view_invoice') %} - - {{ entry.notBilledRate|money(currency) }} - - {% else %} - {{ entry.notBilledRate|money(currency) }} - {% endif %} - | - {% elseif name == 'projectStart' %} -{% if project.start is not null %}{{ project.start|date_short }}{% endif %} | - {% elseif name == 'projectEnd' %} -{% if project.end is not null %}{{ project.end|date_short }}{% endif %} | - {% elseif name == 'comment' %} -{{ project.comment }} | - {% elseif name == 'actions' %} -- {{ projectActions.project(project, 'custom') }} - | +
| {{ widgets.label_project(project) }} | + {% elseif name == 'lastRecord' %} ++ {% if entry.lastRecord is not null %} + {{ entry.lastRecord|date_short }} + {% else %} + – {% endif %} - {% endfor %} - | {{ entry.durationDay|duration }} | + {% elseif name == 'week' %} +{{ entry.durationWeek|duration }} | + {% elseif name == 'month' %} +{{ entry.durationMonth|duration }} | + {% elseif name == 'durationTotal' %} +{{ entry.durationTotal|duration }} | + {% elseif name == 'timeBudget' %} ++ {% if budgetStats.hasTimeBudget() and is_granted('time', project) %} + {{ progress.progressbar_timebudget(budgetStats) }} + {% endif %} + | + {% elseif name == 'budget' %} ++ {% if project.hasBudget() and is_granted('budget', project) %} + {{ progress.progressbar_budget(budgetStats, project.customer.currency) }} + {% endif %} + | + {% elseif name == 'exported' %} ++ {% if is_granted('create_export') %} + + {{ entry.notExportedDuration|duration }} + + {% else %} + {{ entry.notExportedDuration|duration }} + {% endif %} + | + {% elseif name == 'invoiced' %} ++ {% if is_granted('view_invoice') %} + + {{ entry.notBilledRate|money(currency) }} + + {% else %} + {{ entry.notBilledRate|money(currency) }} + {% endif %} + | + {% elseif name == 'projectStart' %} +{% if project.start is not null %}{{ project.start|date_short }}{% endif %} | + {% elseif name == 'projectEnd' %} +{% if project.end is not null %}{{ project.end|date_short }}{% endif %} | + {% elseif name == 'comment' %} +{{ project.comment }} | + {% elseif name == 'actions' %} ++ {{ projectActions.project(project, 'custom') }} + | + {% endif %} + {% endfor %} + {% endfor %} {% endfor %} {{ tables.data_table_footer(entries) }} diff --git a/templates/widget/widget-userteamprojects.html.twig b/templates/widget/widget-userteamprojects.html.twig index 0d00868b..e1b9afe2 100644 --- a/templates/widget/widget-userteamprojects.html.twig +++ b/templates/widget/widget-userteamprojects.html.twig @@ -25,10 +25,10 @@ {{ widgets.label_customer(project.customer) }}- {% if row.hasTimeBudget() %} - {{ progress.progressbar_timebudget(row) }} - {% elseif row.hasBudget() %} + {% if row.hasBudget() and is_granted('budget', row.entity) %} {{ progress.progressbar_budget(row, row.entity.customer.currency) }} + {% elseif row.hasTimeBudget() and is_granted('time', row.entity) %} + {{ progress.progressbar_timebudget(row) }} {% endif %} | diff --git a/tests/Controller/PermissionControllerTest.php b/tests/Controller/PermissionControllerTest.php index 30546f31..7826a605 100644 --- a/tests/Controller/PermissionControllerTest.php +++ b/tests/Controller/PermissionControllerTest.php @@ -35,7 +35,7 @@ class PermissionControllerTest extends ControllerBaseTest $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $this->assertAccessIsGranted($client, '/admin/permissions'); $this->assertHasDataTable($client); - $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 123); + $this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 132); $this->assertPageActions($client, [ //'back' => $this->createUrl('/admin/user/'), 'create modal-ajax-form' => $this->createUrl('/admin/permissions/roles/create'), diff --git a/tests/Form/ActivityEditFormTest.php b/tests/Form/ActivityEditFormTest.php index 11ac0181..abe3ce99 100644 --- a/tests/Form/ActivityEditFormTest.php +++ b/tests/Form/ActivityEditFormTest.php @@ -35,16 +35,43 @@ class ActivityEditFormTest extends TypeTestCase self::assertTrue($form->has('color')); self::assertTrue($form->has('metaFields')); self::assertTrue($form->has('visible')); + self::assertFalse($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertFalse($form->has('budgetType')); } - public function testWithGlobalNewActivityAndOptions() + public function testWithGlobalNewActivityAndOptionsBudget() { $model = new Activity(); $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ - 'include_budget' => true + 'include_budget' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithGlobalNewActivityAndOptionsTimeBudget() + { + $model = new Activity(); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_time' => true, + ]); + self::assertFalse($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithGlobalNewActivityAndOptionsAllBudget() + { + $model = new Activity(); + $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ + 'include_budget' => true, + 'include_time' => true, ]); self::assertTrue($form->has('budget')); self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); } public function testWithGlobalExistingActivityAndOptions() @@ -52,11 +79,11 @@ class ActivityEditFormTest extends TypeTestCase $model = $this->createMock(Activity::class); $model->expects($this->once())->method('getId')->willReturn(1); $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ - 'include_budget' => true + 'include_budget' => true, ]); self::assertFalse($form->has('project')); self::assertTrue($form->has('budget')); - self::assertTrue($form->has('timeBudget')); + self::assertFalse($form->has('timeBudget')); } public function testWithNonGlobalExistingActivityAndOptions() @@ -69,7 +96,8 @@ class ActivityEditFormTest extends TypeTestCase $model->expects($this->any())->method('getId')->willReturn(1); $model->expects($this->any())->method('getProject')->willReturn($project); $form = $this->factory->createBuilder(ActivityEditForm::class, $model, [ - 'include_budget' => true + 'include_budget' => true, + 'include_time' => true, ]); self::assertTrue($form->has('name')); self::assertTrue($form->has('comment')); diff --git a/tests/Form/CustomerEditFormTest.php b/tests/Form/CustomerEditFormTest.php new file mode 100644 index 00000000..77ad9828 --- /dev/null +++ b/tests/Form/CustomerEditFormTest.php @@ -0,0 +1,73 @@ +factory->createBuilder(CustomerEditForm::class, $model); + + $attr = $form->getFormConfig()->getOption('attr'); + self::assertArrayHasKey('data-form-event', $attr); + self::assertEquals('kimai.customerUpdate', $attr['data-form-event']); + + self::assertTrue($form->has('name')); + self::assertTrue($form->has('comment')); + self::assertTrue($form->has('color')); + self::assertTrue($form->has('metaFields')); + self::assertTrue($form->has('visible')); + self::assertFalse($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertFalse($form->has('budgetType')); + } + + public function testWithBudget() + { + $model = new Customer(); + $form = $this->factory->createBuilder(CustomerEditForm::class, $model, [ + 'include_budget' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithTimeBudget() + { + $model = new Customer(); + $form = $this->factory->createBuilder(CustomerEditForm::class, $model, [ + 'include_time' => true, + ]); + self::assertFalse($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithBudgetAndTimeBudget() + { + $model = new Customer(); + $form = $this->factory->createBuilder(CustomerEditForm::class, $model, [ + 'include_budget' => true, + 'include_time' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } +} diff --git a/tests/Form/ProjectEditFormTest.php b/tests/Form/ProjectEditFormTest.php new file mode 100644 index 00000000..8a2f1865 --- /dev/null +++ b/tests/Form/ProjectEditFormTest.php @@ -0,0 +1,74 @@ +factory->createBuilder(ProjectEditForm::class, $model); + + $attr = $form->getFormConfig()->getOption('attr'); + self::assertArrayHasKey('data-form-event', $attr); + self::assertEquals('kimai.projectUpdate', $attr['data-form-event']); + + self::assertTrue($form->has('name')); + self::assertTrue($form->has('comment')); + self::assertTrue($form->has('customer')); + self::assertTrue($form->has('color')); + self::assertTrue($form->has('metaFields')); + self::assertTrue($form->has('visible')); + self::assertFalse($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertFalse($form->has('budgetType')); + } + + public function testWithBudget() + { + $model = new Project(); + $form = $this->factory->createBuilder(ProjectEditForm::class, $model, [ + 'include_budget' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertFalse($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithTimeBudget() + { + $model = new Project(); + $form = $this->factory->createBuilder(ProjectEditForm::class, $model, [ + 'include_time' => true, + ]); + self::assertFalse($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } + + public function testWithBudgetAndTimeBudget() + { + $model = new Project(); + $form = $this->factory->createBuilder(ProjectEditForm::class, $model, [ + 'include_budget' => true, + 'include_time' => true, + ]); + self::assertTrue($form->has('budget')); + self::assertTrue($form->has('timeBudget')); + self::assertTrue($form->has('budgetType')); + } +} diff --git a/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php b/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php index 25bedff6..2d50250b 100644 --- a/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php +++ b/tests/Validator/Constraints/TimesheetBudgetUsedValidatorTest.php @@ -31,6 +31,7 @@ use App\Timesheet\RateServiceInterface; use App\Validator\Constraints\TimesheetBudgetUsed; use App\Validator\Constraints\TimesheetBudgetUsedValidator; use DateTime; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; @@ -88,7 +89,9 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase $rateService = new RateService([], $timesheetRepository); } - return new TimesheetBudgetUsedValidator($configuration, $customerRepository, $projectRepository, $activityRepository, $timesheetRepository, $rateService); + $auth = $this->createMock(AuthorizationCheckerInterface::class); + + return new TimesheetBudgetUsedValidator($configuration, $customerRepository, $projectRepository, $activityRepository, $timesheetRepository, $rateService, $auth); } public function testConstraintIsInvalid() @@ -466,7 +469,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase if (null === $used && null === $budget && null === $free && $path === null) { $this->assertNoViolation(); } else { - $this->buildViolation('The budget is completely used.') + $this->buildViolation('Sorry, the budget is used up.') ->atPath('property.path.' . $path) ->setParameters([ '%used%' => $used, diff --git a/tests/Voter/AbstractVoterTest.php b/tests/Voter/AbstractVoterTest.php index 854f0165..55c64151 100644 --- a/tests/Voter/AbstractVoterTest.php +++ b/tests/Voter/AbstractVoterTest.php @@ -41,6 +41,7 @@ abstract class AbstractVoterTest extends TestCase $user = new User(); $user->setRoles($roles); + $user->setUsername($id); $reflection = new \ReflectionClass($user); $property = $reflection->getProperty('id'); @@ -58,12 +59,12 @@ abstract class AbstractVoterTest extends TestCase protected function getRolePermissionManager(array $permissions = [], bool $overwrite = false) { if (!$overwrite) { - $activities = ['view_activity', 'edit_activity', 'budget_activity', 'delete_activity', 'create_activity']; - $activitiesTeam = ['view_activity', 'create_activity', 'edit_teamlead_activity', 'budget_teamlead_activity']; - $projects = ['view_project', 'create_project', 'edit_project', 'budget_project', 'delete_project', 'permissions_project', 'comments_project', 'details_project']; - $projectsTeam = ['view_teamlead_project', 'edit_teamlead_project', 'budget_teamlead_project', 'permissions_teamlead_project', 'comments_teamlead_project', 'details_teamlead_project']; - $customers = ['view_customer', 'create_customer', 'edit_customer', 'budget_customer', 'delete_customer', 'permissions_customer', 'comments_customer', 'details_customer']; - $customersTeam = ['view_teamlead_customer', 'edit_teamlead_customer', 'budget_teamlead_customer', 'comments_teamlead_customer', 'details_teamlead_customer']; + $activities = ['view_activity', 'edit_activity', 'budget_activity', 'time_activity', 'delete_activity', 'create_activity']; + $activitiesTeam = ['view_activity', 'create_activity', 'edit_teamlead_activity', 'budget_teamlead_activity', 'time_teamlead_activity']; + $projects = ['view_project', 'create_project', 'edit_project', 'budget_project', 'time_project', 'delete_project', 'permissions_project', 'comments_project', 'details_project']; + $projectsTeam = ['view_teamlead_project', 'edit_teamlead_project', 'budget_teamlead_project', 'time_teamlead_project', 'permissions_teamlead_project', 'comments_teamlead_project', 'details_teamlead_project']; + $customers = ['view_customer', 'create_customer', 'edit_customer', 'budget_customer', 'time_customer', 'delete_customer', 'permissions_customer', 'comments_customer', 'details_customer']; + $customersTeam = ['view_teamlead_customer', 'edit_teamlead_customer', 'budget_teamlead_customer', 'time_teamlead_customer', 'comments_teamlead_customer', 'details_teamlead_customer']; $invoice = ['view_invoice', 'create_invoice']; $invoiceTemplate = ['manage_invoice_template']; $timesheet = ['view_own_timesheet', 'start_own_timesheet', 'stop_own_timesheet', 'create_own_timesheet', 'edit_own_timesheet', 'export_own_timesheet', 'delete_own_timesheet']; diff --git a/tests/Voter/EntityMultiRoleVoterTest.php b/tests/Voter/EntityMultiRoleVoterTest.php new file mode 100644 index 00000000..b29237d6 --- /dev/null +++ b/tests/Voter/EntityMultiRoleVoterTest.php @@ -0,0 +1,86 @@ +getRoles()); + $sut = $this->getVoter(EntityMultiRoleVoter::class); + + $this->assertEquals($result, $sut->vote($token, $subject, [$attribute]), 'Failed on permission "' . $attribute . '" for User ' . $user->getUsername()); + } + + public function getTestData() + { + $user0 = $this->getUser(0, null); + $user1 = $this->getUser(1, User::ROLE_USER); + $user2 = $this->getUser(2, User::ROLE_TEAMLEAD); + $user3 = $this->getUser(3, User::ROLE_ADMIN); + $user4 = $this->getUser(4, User::ROLE_SUPER_ADMIN); + + $result = VoterInterface::ACCESS_GRANTED; + $allPermissions = ['budget_money', 'budget_time', 'budget_any', 'details']; + $allSubjects = ['project', 'customer', new Project(), new Customer()]; + + foreach ($allPermissions as $permission) { + foreach ($allSubjects as $subject) { + yield [$user3, $subject, $permission, $result]; + yield [$user4, $subject, $permission, $result]; + } + } + + $result = VoterInterface::ACCESS_GRANTED; + $allPermissions = ['budget_money', 'budget_time', 'budget_any']; + $allSubjects = ['activity', new Activity()]; + + foreach ($allPermissions as $permission) { + foreach ($allSubjects as $subject) { + yield [$user3, $subject, $permission, $result]; + yield [$user4, $subject, $permission, $result]; + } + } + + $result = VoterInterface::ACCESS_DENIED; + yield [$user4, 'activity', 'details', $result]; // there is no details permission for activity + + $result = VoterInterface::ACCESS_ABSTAIN; + yield [$user0, 'team', 'view', $result]; + yield [$user0, 'team', 'edit', $result]; + yield [$user0, 'team', 'delete', $result]; + yield [$user1, 'team', 'view', $result]; + yield [$user1, 'team', 'edit', $result]; + yield [$user1, 'team', 'delete', $result]; + yield [$user2, 'team', 'view', $result]; + yield [$user2, 'team', 'edit', $result]; + yield [$user2, 'team', 'delete', $result]; + yield [$user3, 'team', 'view', $result]; + yield [$user3, 'team', 'edit', $result]; + yield [$user3, 'team', 'delete', $result]; + yield [$user4, 'team', 'view', $result]; + yield [$user4, 'team', 'edit', $result]; + yield [$user4, 'team', 'delete', $result]; + } +} diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 9079d10a..95b3cacb 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -66,6 +66,10 @@