added new permission to separate time and money budget (#3352)

This commit is contained in:
Kevin Papst
2022-06-11 12:23:02 +02:00
committed by GitHub
parent 3f827b3104
commit b46fdcefad
47 changed files with 853 additions and 346 deletions

View File

@@ -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']

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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),
]);
}
}

View File

@@ -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),
]);
}
}

View File

@@ -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,
]);
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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']);
}
}

View File

@@ -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'
],

View File

@@ -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'
],

View File

@@ -26,22 +26,30 @@ trait EntityFormTrait
{
$this->addColor($builder);
if ($options['include_budget']) {
$builder
->add('budget', MoneyType::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'],
])
->add('timeBudget', DurationType::class, [
]);
}
if ($showTime) {
$builder->add('timeBudget', DurationType::class, [
'empty_data' => 0,
'label' => 'label.timeBudget',
'icon' => 'clock',
'required' => false,
])
->add('budgetType', BudgetType::class)
;
]);
}
if ($showBudget) {
$builder->add('budgetType', BudgetType::class);
}
$builder->add('metaFields', MetaFieldsCollectionType::class);

View File

@@ -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' => [

View File

@@ -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'));
}

View File

@@ -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.';
}

View File

@@ -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([

View File

@@ -28,6 +28,7 @@ final class ActivityVoter extends Voter
'view',
'edit',
'budget',
'time',
'delete',
'permissions',
];

View File

@@ -29,6 +29,7 @@ final class CustomerVoter extends Voter
'create',
'edit',
'budget',
'time',
'delete',
'permissions',
'comments',

View File

@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Voter;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\User;
use App\Security\RolePermissionManager;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class EntityMultiRoleVoter extends Voter
{
/**
* support rules based on the given activity
*/
private const ALLOWED_ATTRIBUTES = [
'budget_money',
'budget_time',
'budget_any',
'details',
];
private const ALLOWED_SUBJECTS = [
'customer',
'project',
'activity',
];
private $permissionManager;
public function __construct(RolePermissionManager $permissionManager)
{
$this->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;
}
}

View File

@@ -28,6 +28,7 @@ final class ProjectVoter extends Voter
'view',
'edit',
'budget',
'time',
'delete',
'permissions',
'comments',

View File

@@ -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

View File

@@ -28,14 +28,18 @@
{% if form.project is defined %}
{{ form_row(form.project) }}
{% endif %}
{% if form.budget is defined %}
{% if form.budgetType is defined %}
<div class="row">
{% if form.budget is defined %}
<div class="col-md-4">
{{ form_row(form.budget) }}
</div>
{% endif %}
{% if form.timeBudget is defined %}
<div class="col-md-4">
{{ form_row(form.timeBudget) }}
</div>
{% endif %}
<div class="col-md-4">
{{ form_row(form.budgetType) }}
</div>

View File

@@ -14,9 +14,17 @@
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% if is_granted('budget_money', 'activity') %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_time', 'activity') %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'},
@@ -63,14 +71,14 @@
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if entry.hasBudget() %}
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money((entry.project is null ? defaultCurrency : entry.project.customer.currency)) }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if entry.hasTimeBudget() %}
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;

View File

@@ -67,14 +67,18 @@
{{ form_row(form.fax) }}
</div>
</div>
{% if form.budget is defined %}
{% if form.budgetType is defined %}
<div class="row">
{% if form.budget is defined %}
<div class="col-md-4">
{{ form_row(form.budget) }}
</div>
{% endif %}
{% if form.timeBudget is defined %}
<div class="col-md-4">
{{ form_row(form.timeBudget) }}
</div>
{% endif %}
<div class="col-md-4">
{{ form_row(form.budgetType) }}
</div>

View File

@@ -6,7 +6,7 @@
id="project_list_box" data-href="{{ path('customer_projects', {'id': customer.id}) }}" data-reload="kimai.projectUpdate kimai.projectDelete"
{% endblock %}
{% block box_tools %}
{% if is_granted('view_reporting') and is_granted('budget_project') %}
{% if is_granted('view_reporting') and is_granted('budget_any', 'project') %}
<a class="btn btn-default btn-sm btn-pager" href="{{ path('report_project_view', {'customer': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'report_project_view'|trans({}, 'reporting') }}"><i class="{{ 'reporting'|icon }}"></i></a>
{% endif %}
{% if customer.visible and is_granted('create_project') %}

View File

@@ -25,9 +25,17 @@
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% if is_granted('budget_money', 'customer') %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_time', 'customer') %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min'},
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min'},
'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'},
@@ -74,14 +82,14 @@
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if entry.hasBudget() %}
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money(entry.currency) }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if entry.hasTimeBudget() %}
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;

View File

@@ -1,3 +1,4 @@
{% 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 %}
@@ -70,6 +71,8 @@
</div>
{% endblock %}
{% endembed %}
{% endif %}
{% if is_granted('budget', entity) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
@@ -152,3 +155,4 @@
</div>
{% endblock %}
{% endembed %}
{% endif %}

View File

@@ -90,8 +90,7 @@
</tr>
{% endif %}
{% endif %}
{% if is_granted('budget', project) %}
{% if project.hasBudget() %}
{% if project.hasBudget() and is_granted('budget', project) %}
<tr>
<th>
{{ 'label.budget'|trans }}
@@ -104,7 +103,7 @@
</td>
</tr>
{% endif %}
{% if project.hasTimeBudget() %}
{% if project.hasTimeBudget() and is_granted('time', project) %}
<tr>
<th>
{{ 'label.timeBudget'|trans }}
@@ -117,7 +116,6 @@
</td>
</tr>
{% endif %}
{% endif %}
{% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %}
<tr>
<th>{{ metaField.label|trans }}</th>

View File

@@ -39,14 +39,18 @@
{{ form_row(form.end) }}
</div>
</div>
{% if form.budget is defined %}
{% if form.budgetType is defined %}
<div class="row">
{% if form.budget is defined %}
<div class="col-md-4">
{{ form_row(form.budget) }}
</div>
{% endif %}
{% if form.timeBudget is defined %}
<div class="col-md-4">
{{ form_row(form.timeBudget) }}
</div>
{% endif %}
<div class="col-md-4">
{{ form_row(form.budgetType) }}
</div>

View File

@@ -18,9 +18,17 @@
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% if is_granted('budget_money', 'project') %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_time', 'project') %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'},
@@ -60,14 +68,14 @@
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if entry.hasBudget() %}
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money(entry.customer.currency) }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if entry.hasTimeBudget() %}
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;

View File

@@ -3,15 +3,28 @@
{% block report_title %}{{ 'report_project_daterange'|trans({}, 'reporting') }}{% endblock %}
{% set showMoneyBudget = is_granted('budget_money', 'project') %}
{% set showTimeBudget = is_granted('budget_time', 'project') %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
} %}
{% if showTimeBudget %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% if showMoneyBudget %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'duration': {'class': 'text-center hw-min', 'title': 'stats.durationMonth'|trans, 'columnClass': 'w-min'},
'rate': {'class': 'text-center hw-min', 'title': 'stats.amountMonth'|trans, 'columnClass': 'w-min'},
'billable': {'class': 'text-center hw-min', 'columnClass': 'w-min'},
'actions': {'class': 'actions alwaysVisible'},
} %}
}) %}
{% set tableName = 'project_daterange_reporting' %}
{% block main_before %}
@@ -72,7 +85,6 @@
{% for entry in mapping.projects|sort((a, b) => a.entity.name <=> b.entity.name) %}
{% set project = entry.entity %}
{% set currency = project.customer.currency %}
{% if is_granted('budget', project) %}
<tr {{ widgets.project_row_attr(project, queryEnd) }}>
{% for name, column_config in columns %}
<td class="{{ tables.data_table_column_class(tableName, columns, name) }}">
@@ -85,16 +97,19 @@
{% elseif name == 'billable' %}
{{ widgets.percent(entry.statistic.rate, entry.statistic.rateBillable) }}
{% elseif name == 'timeBudget' %}
{% if is_granted('time', project) %}
{{ progress.progressbar_timebudget(entry) }}
{% endif %}
{% elseif name == 'budget' %}
{% if is_granted('budget', project) %}
{{ progress.progressbar_budget(entry, currency) }}
{% endif %}
{% elseif name == 'actions' %}
{{ projectActions.project(project, 'custom') }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endif %}
{% endfor %}
{% endfor %}
{{ tables.data_table_footer(entries) }}

View File

@@ -6,7 +6,8 @@
{% set tableName = tableName|default('project_details_reporting') %}
{% set tableId = 'project-details-form' %}
{% set view_budget = project_details is not null and is_granted('budget', project_details.project) %}
{% set showMoneyBudget = project_details is not null and is_granted('budget', project_details.project) %}
{% set showTimeBudget = project_details is not null and is_granted('time', project_details.project) %}
{% set view_revenue = project_details is not null and is_granted('view_rate_other_timesheet') %}
{% set see_users = is_granted('view_other_timesheet') or is_granted('view_other_reporting') %}
@@ -79,7 +80,7 @@
{% endif %}
{% if hasData %}
{{ _self.project_details(project, project_view, project_details, view_budget, view_revenue, see_users) }}
{{ _self.project_details(project, project_view, project_details, showMoneyBudget, showTimeBudget, view_revenue, see_users) }}
{% set currency = project.customer.currency %}
{%- for yearStat in project_details.years|reverse %}
@@ -245,7 +246,7 @@
project_view = ProjectViewModel
project_details = ProjectDetailsModel
#}
{% macro project_details(project, project_view, project_details, view_budget, view_revenue, see_users) %}
{% macro project_details(project, project_view, project_details, showMoneyBudget, showTimeBudget, view_revenue, see_users) %}
{% set activities = project_details.activities %}
{% set years = project_details.years %}
{% import "macros/progressbar.html.twig" as progress %}
@@ -352,12 +353,12 @@
</table>
</div>
</div>
{% if view_budget and (project.timeBudget > 0 or project.budget > 0) %}
{% if (showMoneyBudget and project.budget > 0) or (showTimeBudget and project.timeBudget > 0) %}
{% set budgetStats = project_details.budgetStatisticModel %}
<div class="row">
<div class="col-xs-12">
<table class="table table-hover dataTable">
{% if project.timeBudget > 0 %}
{% if showTimeBudget and project.timeBudget > 0 %}
<tr>
<th class="w-min">
{{ 'label.timeBudget'|trans }}
@@ -370,7 +371,7 @@
</td>
</tr>
{% endif %}
{% if project.budget > 0 %}
{% if showMoneyBudget and project.budget > 0 %}
<tr>
<th class="w-min">
{{ 'label.budget'|trans }}

View File

@@ -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},
} %}
{% 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,7 +107,6 @@
{% set project = entry.project %}
{% set budgetStats = entry.getBudgetStatisticModel() %}
{% set currency = project.customer.currency %}
{% if is_granted('budget', project) %}
<tr {{ widgets.project_row_attr(project, now) }}>
{% for name, column_config in columns %}
{% if name == 'name' %}
@@ -116,13 +129,13 @@
<td class="{{ tables.data_table_column_class(tableName, columns, 'durationTotal') }} w-min">{{ entry.durationTotal|duration }}</td>
{% elseif name == 'timeBudget' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if budgetStats.hasTimeBudget() %}
{% if budgetStats.hasTimeBudget() and is_granted('time', project) %}
{{ progress.progressbar_timebudget(budgetStats) }}
{% endif %}
</td>
{% elseif name == 'budget' %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if project.hasBudget() %}
{% if project.hasBudget() and is_granted('budget', project) %}
{{ progress.progressbar_budget(budgetStats, project.customer.currency) }}
{% endif %}
</td>
@@ -159,7 +172,6 @@
{% endif %}
{% endfor %}
</tr>
{% endif %}
{% endfor %}
{% endfor %}
{{ tables.data_table_footer(entries) }}

View File

@@ -25,10 +25,10 @@
<small>{{ widgets.label_customer(project.customer) }}</small>
</td>
<td style="width:50%">
{% 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 %}
</td>
</tr>

View File

@@ -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'),

View File

@@ -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'));

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form;
use App\Entity\Customer;
use App\Form\CustomerEditForm;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @covers \App\Form\CustomerEditForm
*/
class CustomerEditFormTest extends TypeTestCase
{
public function testWithNewProject()
{
$model = new Customer();
$form = $this->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'));
}
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Form;
use App\Entity\Project;
use App\Form\ProjectEditForm;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @covers \App\Form\ProjectEditForm
*/
class ProjectEditFormTest extends TypeTestCase
{
public function testWithNewProject()
{
$model = new Project();
$form = $this->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'));
}
}

View File

@@ -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,

View File

@@ -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'];

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Voter;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\User;
use App\Voter\EntityMultiRoleVoter;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
* @covers \App\Voter\EntityMultiRoleVoter
*/
class EntityMultiRoleVoterTest extends AbstractVoterTest
{
/**
* @dataProvider getTestData
*/
public function testVote(User $user, $subject, $attribute, $result)
{
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->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];
}
}

View File

@@ -66,6 +66,10 @@
<source>The budget is completely used.</source>
<target>Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%.</target>
</trans-unit>
<trans-unit id="dcPei9G" resname="Sorry, the budget is used up.">
<source>Sorry, the budget is used up.</source>
<target>Entschuldigung, das Budget ist aufgebraucht.</target>
</trans-unit>
<trans-unit id="6rO8GZ1" resname="Maximum duration of {{ value }} hours exceeded.">
<source>Maximum duration of {{ value }} hours exceeded.</source>
<target>Erlaubt sind max. {{ value }} Stunden.</target>

View File

@@ -62,6 +62,10 @@
<source>The budget is completely used.</source>
<target>Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%.</target>
</trans-unit>
<trans-unit id="dcPei9G" resname="Sorry, the budget is used up.">
<source>Sorry, the budget is used up.</source>
<target>Entschuldigung, das Budget ist aufgebraucht.</target>
</trans-unit>
<trans-unit id="6rO8GZ1" resname="Maximum duration of {{ value }} hours exceeded.">
<source>Maximum duration of {{ value }} hours exceeded.</source>
<target>Erlaubt sind max. {{ value }} Stunden.</target>

View File

@@ -66,6 +66,10 @@
<source>The budget is completely used.</source>
<target>The budget is used up. Of the available %budget%, %used% has been booked so far, %free% can still be used.</target>
</trans-unit>
<trans-unit id="dcPei9G" resname="Sorry, the budget is used up.">
<source>Sorry, the budget is used up.</source>
<target>Sorry, the budget is used up.</target>
</trans-unit>
<trans-unit id="6rO8GZ1" resname="Maximum duration of {{ value }} hours exceeded.">
<source>Maximum duration of {{ value }} hours exceeded.</source>
<target>Maximum {{ value }} hours allowed.</target>