allow to restrict usage of global activities for projects (#3437)

This commit is contained in:
Kevin Papst
2022-07-23 01:17:24 +02:00
committed by GitHub
parent 8d695d03df
commit ed7f89cfe1
12 changed files with 140 additions and 11 deletions

View File

@@ -252,6 +252,15 @@ class Project implements EntityWithMetaFields, EntityWithBudget
* @ORM\Column(name="invoice_text", type="text", nullable=true) * @ORM\Column(name="invoice_text", type="text", nullable=true)
*/ */
private $invoiceText; private $invoiceText;
/**
* Whether this project allows booking of global activities
*
* @var bool
*
* @ORM\Column(name="global_activities", type="boolean", nullable=false, options={"default": true})
* @Assert\NotNull()
*/
private $globalActivities = true;
public function __construct() public function __construct()
{ {
@@ -419,6 +428,16 @@ class Project implements EntityWithMetaFields, EntityWithBudget
return $this; return $this;
} }
public function isGlobalActivities(): bool
{
return $this->globalActivities;
}
public function setGlobalActivities(bool $globalActivities): void
{
$this->globalActivities = $globalActivities;
}
/** /**
* @return Collection|MetaTableTypeInterface[] * @return Collection|MetaTableTypeInterface[]
*/ */

View File

@@ -13,6 +13,7 @@ use App\Entity\Customer;
use App\Entity\Project; use App\Entity\Project;
use App\Form\Type\CustomerType; use App\Form\Type\CustomerType;
use App\Form\Type\DateTimePickerType; use App\Form\Type\DateTimePickerType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -95,7 +96,11 @@ class ProjectEditForm extends AbstractType
'placeholder' => (null === $id && null === $customer) ? '' : false, 'placeholder' => (null === $id && null === $customer) ? '' : false,
'customers' => $customer, 'customers' => $customer,
'query_builder_for_user' => true, 'query_builder_for_user' => true,
]); ])
->add('globalActivities', YesNoType::class, [
'label' => 'label.globalActivities',
])
;
$this->addCommonFields($builder, $options); $this->addCommonFields($builder, $options);
} }

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* 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 DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* @version 1.22.0
*/
final class Version20220722125847 extends AbstractMigration
{
public function getDescription(): string
{
return '(De-)Activate global activities for Projects';
}
public function up(Schema $schema): void
{
$projects = $schema->getTable('kimai2_projects');
$projects->addColumn('global_activities', 'boolean', ['notnull' => true, 'default' => true]);
}
public function down(Schema $schema): void
{
$projects = $schema->getTable('kimai2_projects');
$projects->dropColumn('global_activities');
}
}

View File

@@ -286,12 +286,28 @@ class ActivityRepository extends EntityRepository
if ($query->isGlobalsOnly()) { if ($query->isGlobalsOnly()) {
$mainQuery->add($qb->expr()->isNull('a.project')); $mainQuery->add($qb->expr()->isNull('a.project'));
} elseif ($query->hasProjects()) { } elseif ($query->hasProjects()) {
$mainQuery->add( $orX = $qb->expr()->orX(
$qb->expr()->orX( $qb->expr()->in('a.project', ':project')
$qb->expr()->isNull('a.project'),
$qb->expr()->in('a.project', ':project')
)
); );
$includeGlobals = true;
// projects have a setting to disallow global activities, and we check for it only
// if we query for exactly one project (usually used in dropdown queries)
if (\count($query->getProjects()) === 1) {
$project = $query->getProjects()[0];
if (!$project instanceof Project) {
$project = $this->getEntityManager()->getRepository(Project::class)->find($project);
}
if ($project instanceof Project) {
$includeGlobals = $project->isGlobalActivities();
}
}
if ($includeGlobals) {
$orX->add($qb->expr()->isNull('a.project'));
}
$mainQuery->add($orX);
$qb->setParameter('project', $query->getProjects()); $qb->setParameter('project', $query->getProjects());
} }
@@ -381,7 +397,21 @@ class ActivityRepository extends EntityRepository
); );
if (!$query->isExcludeGlobals()) { if (!$query->isExcludeGlobals()) {
$orX->add($qb->expr()->isNull('a.project')); $includeGlobals = true;
// projects have a setting to disallow global activities, and we check for it only
// if we query for exactly one project (usually used in dropdown queries)
if (\count($query->getProjects()) === 1) {
$project = $query->getProjects()[0];
if (!$project instanceof Project) {
$project = $this->getEntityManager()->getRepository(Project::class)->find($project);
}
if ($project instanceof Project) {
$includeGlobals = $project->isGlobalActivities();
}
}
if ($includeGlobals) {
$orX->add($qb->expr()->isNull('a.project'));
}
} }
$where->add($orX); $where->add($orX);

View File

@@ -27,6 +27,7 @@ class TimesheetBasic extends TimesheetConstraint
public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89'; public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89';
public const PROJECT_NOT_STARTED = 'kimai-timesheet-91'; public const PROJECT_NOT_STARTED = 'kimai-timesheet-91';
public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92'; public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92';
public const PROJECT_DISALLOWS_GLOBAL_ACTIVITY = 'kimai-timesheet-93';
protected static $errorNames = [ protected static $errorNames = [
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.', self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
@@ -39,6 +40,7 @@ class TimesheetBasic extends TimesheetConstraint
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.', self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
self::PROJECT_NOT_STARTED => 'The project has not started at that time.', self::PROJECT_NOT_STARTED => 'The project has not started at that time.',
self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.', self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.',
self::PROJECT_DISALLOWS_GLOBAL_ACTIVITY => 'Global activities are forbidden for the selected project.',
]; ];
public $message = 'This timesheet has invalid settings.'; public $message = 'This timesheet has invalid settings.';

View File

@@ -124,6 +124,14 @@ final class TimesheetBasicValidator extends ConstraintValidator
->addViolation(); ->addViolation();
} }
if (!$project->isGlobalActivities() && $activity->isGlobal()) {
$context->buildViolation('Global activities are forbidden for the selected project.')
->atPath('activity')
->setTranslationDomain('validators')
->setCode(TimesheetBasic::PROJECT_DISALLOWS_GLOBAL_ACTIVITY)
->addViolation();
}
$pathStart = 'begin'; $pathStart = 'begin';
$pathEnd = 'end'; $pathEnd = 'end';

View File

@@ -116,6 +116,14 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if not project.globalActivities %}
<tr>
<th>{{ 'label.globalActivities'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(project.globalActivities) }}
</td>
</tr>
{% endif %}
{% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %} {% for metaField in project.visibleMetaFields|sort((a, b) => a.order <=> b.order) %}
<tr> <tr>
<th>{{ metaField.label|trans }}</th> <th>{{ metaField.label|trans }}</th>

View File

@@ -60,12 +60,15 @@
{{ form_row(form.invoiceText) }} {{ form_row(form.invoiceText) }}
{% endif %} {% endif %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-4">
{{ form_row(form.visible) }} {{ form_row(form.visible) }}
</div> </div>
<div class="col-md-6"> <div class="col-md-4 text-center">
{{ form_row(form.billable) }} {{ form_row(form.billable) }}
</div> </div>
<div class="col-md-4 text-right">
{{ form_row(form.globalActivities) }}
</div>
</div> </div>
{% if form.metaFields is defined and form.metaFields is not empty %} {% if form.metaFields is defined and form.metaFields is not empty %}
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %} {% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}

View File

@@ -38,6 +38,7 @@ class ProjectTest extends AbstractEntityTest
self::assertNull($sut->getInvoiceText()); self::assertNull($sut->getInvoiceText());
self::assertTrue($sut->isVisible()); self::assertTrue($sut->isVisible());
self::assertTrue($sut->isBillable()); self::assertTrue($sut->isBillable());
self::assertTrue($sut->isGlobalActivities());
self::assertNull($sut->getColor()); self::assertNull($sut->getColor());
self::assertFalse($sut->hasColor()); self::assertFalse($sut->hasColor());
self::assertInstanceOf(Collection::class, $sut->getMetaFields()); self::assertInstanceOf(Collection::class, $sut->getMetaFields());
@@ -106,6 +107,9 @@ class ProjectTest extends AbstractEntityTest
self::assertFalse($sut->isVisible()); self::assertFalse($sut->isVisible());
$sut->setVisible(true); $sut->setVisible(true);
self::assertTrue($sut->isVisible()); self::assertTrue($sut->isVisible());
$sut->setGlobalActivities(false);
self::assertFalse($sut->isGlobalActivities());
} }
public function testMetaFields() public function testMetaFields()

View File

@@ -1201,6 +1201,10 @@
<source>label.globalsOnly</source> <source>label.globalsOnly</source>
<target>Nur globale</target> <target>Nur globale</target>
</trans-unit> </trans-unit>
<trans-unit id="G9hBaDJ" resname="label.globalActivities">
<source>label.globalActivities</source>
<target>Globale Tätigkeiten erlauben</target>
</trans-unit>
<trans-unit id="2Ek7sXu" resname="label.batch_meta_fields"> <trans-unit id="2Ek7sXu" resname="label.batch_meta_fields">
<source>label.batch_meta_fields</source> <source>label.batch_meta_fields</source>
<target>Zusätzliche Felder</target> <target>Zusätzliche Felder</target>
@@ -1221,7 +1225,7 @@
<source>label.includeBudgetType_month</source> <source>label.includeBudgetType_month</source>
<target>Einträge mit „Monats-Budget“ anzeigen</target> <target>Einträge mit „Monats-Budget“ anzeigen</target>
</trans-unit> </trans-unit>
<trans-unit id="Wq.h4adsfsdfnD" resname="label.budgetIndependent"> <trans-unit id="8mTf0eZ" resname="label.budgetIndependent">
<source>label.budgetIndependent</source> <source>label.budgetIndependent</source>
<target>Unabhängig vom „Budget-Typ“ anzeigen</target> <target>Unabhängig vom „Budget-Typ“ anzeigen</target>
</trans-unit> </trans-unit>

View File

@@ -1201,6 +1201,10 @@
<source>label.globalsOnly</source> <source>label.globalsOnly</source>
<target>Only global</target> <target>Only global</target>
</trans-unit> </trans-unit>
<trans-unit id="G9hBaDJ" resname="label.globalActivities">
<source>label.globalActivities</source>
<target>Allow global activities</target>
</trans-unit>
<trans-unit id="2Ek7sXu" resname="label.batch_meta_fields"> <trans-unit id="2Ek7sXu" resname="label.batch_meta_fields">
<source>label.batch_meta_fields</source> <source>label.batch_meta_fields</source>
<target>Additional fields</target> <target>Additional fields</target>
@@ -1221,7 +1225,7 @@
<source>label.includeBudgetType_month</source> <source>label.includeBudgetType_month</source>
<target>Show entries with "monthly" budget</target> <target>Show entries with "monthly" budget</target>
</trans-unit> </trans-unit>
<trans-unit id="Wq.h4adsfsdfnD" resname="label.budgetIndependent"> <trans-unit id="8mTf0eZ" resname="label.budgetIndependent">
<source>label.budgetIndependent</source> <source>label.budgetIndependent</source>
<target>Show regardless of "budget-type"</target> <target>Show regardless of "budget-type"</target>
</trans-unit> </trans-unit>

View File

@@ -86,6 +86,10 @@
<source>This timesheet is already exported.</source> <source>This timesheet is already exported.</source>
<target>Dieser Eintrag wurde bereits exportiert.</target> <target>Dieser Eintrag wurde bereits exportiert.</target>
</trans-unit> </trans-unit>
<trans-unit id="mjlHwer0la" resname="Global activities are forbidden for the selected project.">
<source>Global activities are forbidden for the selected project.</source>
<target>Globale Aktivitäten sind in dem gewählten Projekt nicht erlaubt.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>