added global activities (#259)
This commit is contained in:
@@ -68,7 +68,7 @@ chmod -R g+rw var/
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite.
|
||||
It's up to you which database server you want to use, Kimai v2 supports MySQL/MariaDB and SQLite, but SQLite is [not recommended](var/docs/faq.md) for production usage.
|
||||
Configure the database connection string in your the `.env` file:
|
||||
```
|
||||
# adjust all settings in .env to your needs
|
||||
|
||||
165
assets/css/app.scss
Normal file
165
assets/css/app.scss
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
|
||||
.error-page {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* ================================ CONTENT ================================ */
|
||||
|
||||
.content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-header {
|
||||
h1 {
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================ NAVBAR ================================ */
|
||||
|
||||
.navbar-nav > li > a.ddt-large {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
li.messages-menu ul.menu li .pull-left i {
|
||||
color: #dd4b39;
|
||||
}
|
||||
|
||||
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.navbar-nav .start_record {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
.ticktac:hover i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
li.open .ticktac i.running{
|
||||
color: #4ff131;
|
||||
}
|
||||
|
||||
.ticktac i.stopped{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ticktac:hover i.stopped{
|
||||
color: #4ff131;
|
||||
}
|
||||
.user-panel>.info {
|
||||
left: 40px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* ================================ TOOLBAR ================================ */
|
||||
|
||||
/* Page based action buttons in the upper right corner of the content area */
|
||||
.content-header > .breadcrumb {
|
||||
position: absolute;
|
||||
float: right;
|
||||
background: transparent;
|
||||
}
|
||||
/* Right sidebar with additional tabs for personal settings and "About Kimai" section */
|
||||
.control-sidebar {
|
||||
select {
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
div.image {
|
||||
img.img-circle {
|
||||
max-width: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The filter form is available on most pages above the datatable */
|
||||
@media (min-width: 768px) {
|
||||
.toolbar {
|
||||
form.navbar-form {
|
||||
font-size: $font-size-base;
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ================================ SIDEBAR ================================ */
|
||||
|
||||
/* Used for the Sidebar UserPanel (which is deactivated right now) */
|
||||
.user-panel>.info {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
/* ================================ FOOTER ================================ */
|
||||
|
||||
footer.main-footer {
|
||||
padding: 5px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/* ================================ PRINT ================================ */
|
||||
|
||||
@media print{
|
||||
.sf-toolbar, .control-sidebar {display: none !important;}
|
||||
}
|
||||
|
||||
/* ================================ INVOICES ================================ */
|
||||
|
||||
.invoice {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.invoice {
|
||||
margin: 10px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-small{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
div.invoice-address {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
table.invoice-records {}
|
||||
table.invoice-meta th {
|
||||
padding-right: 40px;
|
||||
}
|
||||
table.invoice-sum th {
|
||||
width: 70%;
|
||||
}
|
||||
table.invoice-sum th, table.invoice-sum td {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -84,6 +84,28 @@ $(function() {
|
||||
$(this).trigger("change");
|
||||
});
|
||||
|
||||
$('select[data-related-select]').change(function() {
|
||||
var apiUrl = $(this).attr('data-api-url').replace('-s-', $(this).val());
|
||||
var targetSelect = $(this).attr('data-related-select');
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl,
|
||||
headers: {
|
||||
'X-AUTH-SESSION': true,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
$('#' + targetSelect).find('option').remove().end().find('optgroup').remove().end();
|
||||
$.each(data, function(i, obj) {
|
||||
$('#' + targetSelect).append('<option value="' + obj.id + '">' + obj.name + '</option>');
|
||||
});
|
||||
$('#' + targetSelect).trigger('change')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
|
||||
@@ -18,14 +18,17 @@ $(document).ready(function () {
|
||||
case 'customer':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#project').parent().remove();
|
||||
//$('.toolbar form select#project').parent().parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#project').val('');
|
||||
}
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
//$('.toolbar form select#activity').parent().parent().remove();
|
||||
break;
|
||||
case 'project':
|
||||
if ($(this).val() === '') {
|
||||
$('.toolbar form select#activity').parent().remove();
|
||||
//$('.toolbar form select#activity').parent().parent().remove();
|
||||
} else {
|
||||
$('.toolbar form select#activity').val('');
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ fos_rest:
|
||||
# formats:
|
||||
# name: ~
|
||||
# unauthorized_challenge: null
|
||||
# param_fetcher_listener:
|
||||
# enabled: false
|
||||
param_fetcher_listener:
|
||||
enabled: true
|
||||
# force: false
|
||||
# service: null
|
||||
# cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
cache_dir: '%kernel.cache_dir%/fos_rest'
|
||||
# allowed_methods_listener:
|
||||
# enabled: false
|
||||
# service: null
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"build/app.js": "/build/app.js?3c09b71f2907a08b4e32",
|
||||
"build/app.js": "/build/app.js?07d671a96df83ac52da6",
|
||||
"build/app.css": "/build/app.css?6501689dff217d14b179e3049295343e",
|
||||
"build/fonts/fa-regular-400.woff": "/build/fonts/fa-regular-400.woff?d9e29124",
|
||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace App\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation as API;
|
||||
@@ -55,12 +58,31 @@ class ActivityController extends Controller
|
||||
* description="Returns the collection of all existing activities",
|
||||
* @SWG\Schema(ref=@API\Model(type=Activity::class)),
|
||||
* )
|
||||
* @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, only global activities will be returned.")
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities")
|
||||
* @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Pass true to fetch only global activities")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new ActivityQuery();
|
||||
$query->setOrderGlobalsFirst(true)
|
||||
->setResultType(ActivityQuery::RESULT_TYPE_OBJECTS);
|
||||
|
||||
if (null !== ($globals = $paramFetcher->get('globals'))) {
|
||||
$query->setGlobalsOnly(true);
|
||||
}
|
||||
|
||||
if (null !== ($project = $paramFetcher->get('project'))) {
|
||||
$query->setProject($project);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace App\API;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
@@ -55,12 +58,26 @@ class ProjectController extends Controller
|
||||
* description="Returns the collection of all existing projects",
|
||||
* @SWG\Schema(ref=@Model(type=Project::class)),
|
||||
* )
|
||||
* @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter projects")
|
||||
* @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects")
|
||||
*
|
||||
* @param ParamFetcherInterface $paramFetcher
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction()
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$data = $this->repository->findAll();
|
||||
$query = new ProjectQuery();
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_OBJECTS);
|
||||
|
||||
if (null !== ($customer = $paramFetcher->get('customer'))) {
|
||||
$query->setCustomer($customer);
|
||||
}
|
||||
|
||||
if (null !== ($visible = $paramFetcher->get('visible'))) {
|
||||
$query->setVisibility($visible);
|
||||
}
|
||||
|
||||
$data = $this->repository->findByQuery($query);
|
||||
$view = new View($data, 200);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
|
||||
@@ -63,8 +63,8 @@ class TimesheetEntity
|
||||
$this->start = $entry->getBegin();
|
||||
$this->title = $entry->getActivity()->getName();
|
||||
$this->description = $entry->getDescription();
|
||||
$this->customer = $entry->getActivity()->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getActivity()->getProject()->getName();
|
||||
$this->customer = $entry->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getProject()->getName();
|
||||
$this->activity = $entry->getActivity()->getName();
|
||||
|
||||
if (null === $entry->getEnd()) {
|
||||
|
||||
@@ -37,12 +37,11 @@ class ActivityController extends AbstractController
|
||||
public function recentActivitiesAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
// TODO make days configurable
|
||||
$activeEntries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
|
||||
$entries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
|
||||
|
||||
return $this->render(
|
||||
'navbar/recent-activities.html.twig',
|
||||
['activities' => $activeEntries]
|
||||
['entries' => $entries]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
@@ -19,6 +18,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* Controller used to manage timesheets.
|
||||
@@ -143,20 +143,36 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* The route to start a running entry.
|
||||
* The route to re-start a timesheet entry.
|
||||
*
|
||||
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
|
||||
* @Security("is_granted('start', activity)")
|
||||
* @Security("is_granted('start', timesheet)")
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function startAction(Activity $activity)
|
||||
public function startAction(ValidatorInterface $validator, Timesheet $timesheet)
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
try {
|
||||
$this->getRepository()->startRecording($user, $activity);
|
||||
$this->flashSuccess('timesheet.start.success');
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setBegin(new \DateTime())
|
||||
->setUser($user)
|
||||
->setActivity($timesheet->getActivity())
|
||||
->setProject($timesheet->getProject())
|
||||
;
|
||||
|
||||
$errors = $validator->validate($entry);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $errors[0]->getPropertyPath() . ' = ' . $errors[0]->getMessage()]);
|
||||
} else {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
$this->flashSuccess('timesheet.start.success');
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class CustomerFixtures extends Fixture
|
||||
public const MAX_CUSTOMERS = 15;
|
||||
public const MIN_BUDGET = 0;
|
||||
public const MAX_BUDGET = 100000;
|
||||
public const MIN_GLOBAL_ACTIVITIES = 5;
|
||||
public const MAX_GLOBAL_ACTIVITIES = 50;
|
||||
public const MIN_PROJECTS_PER_CUSTOMER = 10;
|
||||
public const MAX_PROJECTS_PER_CUSTOMER = 50;
|
||||
public const MIN_ACTIVITIES_PER_PROJECT = 0;
|
||||
@@ -66,6 +68,16 @@ class CustomerFixtures extends Fixture
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
$amountGlobalActivities = rand(self::MIN_GLOBAL_ACTIVITIES, self::MAX_GLOBAL_ACTIVITIES);
|
||||
for ($c = 1; $c <= $amountGlobalActivities; $c++) {
|
||||
$visibleActivity = 0 != $a % 3;
|
||||
$activity = $this->createActivity($faker, null, $visibleActivity);
|
||||
$manager->persist($activity);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,11 +125,11 @@ class CustomerFixtures extends Fixture
|
||||
|
||||
/**
|
||||
* @param Generator $faker
|
||||
* @param Project $project
|
||||
* @param Project|null $project
|
||||
* @param bool $visible
|
||||
* @return Activity
|
||||
*/
|
||||
private function createActivity(Generator $faker, Project $project, $visible)
|
||||
private function createActivity(Generator $faker, ?Project $project, $visible)
|
||||
{
|
||||
$entry = new Activity();
|
||||
$entry
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
@@ -56,6 +57,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$activities = $this->getAllActivities($manager);
|
||||
$projects = $this->getAllProjects($manager);
|
||||
|
||||
$faker = Factory::create();
|
||||
|
||||
@@ -80,6 +82,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
$description
|
||||
);
|
||||
|
||||
@@ -98,6 +101,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
null,
|
||||
false
|
||||
);
|
||||
@@ -126,6 +130,22 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Project[]
|
||||
*/
|
||||
protected function getAllProjects(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Activity[]
|
||||
@@ -133,7 +153,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
protected function getAllActivities(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var User[] $entries */
|
||||
/* @var Activity[] $entries */
|
||||
$entries = $manager->getRepository(Activity::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
@@ -142,7 +162,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return $all;
|
||||
}
|
||||
|
||||
private function createTimesheetEntry(User $user, Activity $activity, $description, $setEndDate = true)
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, $description, $setEndDate = true)
|
||||
{
|
||||
$start = new \DateTime();
|
||||
$start = $start->modify('- ' . (rand(1, self::TIMERANGE_DAYS)) . ' days');
|
||||
@@ -151,6 +171,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setActivity($activity)
|
||||
->setProject($activity->getProject() ?? $project)
|
||||
->setDescription($description)
|
||||
->setUser($user)
|
||||
->setBegin($start);
|
||||
|
||||
@@ -34,7 +34,6 @@ class Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="activities")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class Project
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="projects")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
@@ -101,6 +101,13 @@ class Project
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Timesheet", mappedBy="project")
|
||||
*/
|
||||
private $timesheets;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@@ -118,8 +125,8 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $customer
|
||||
* @return $this
|
||||
* @param Customer $customer
|
||||
* @return Project
|
||||
*/
|
||||
public function setCustomer($customer)
|
||||
{
|
||||
@@ -129,8 +136,6 @@ class Project
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
* @return Project
|
||||
*/
|
||||
@@ -206,6 +211,25 @@ class Project
|
||||
return $this->budget;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @return Project
|
||||
*/
|
||||
public function setTimesheets($timesheets)
|
||||
{
|
||||
$this->timesheets = $timesheets;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getTimesheets()
|
||||
{
|
||||
return $this->timesheets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity[] $activities
|
||||
* @return Project
|
||||
|
||||
@@ -64,7 +64,7 @@ class Timesheet
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
@@ -73,11 +73,20 @@ class Timesheet
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="timesheets")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="timesheets")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
@@ -232,6 +241,25 @@ class Timesheet
|
||||
return $this->activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
*/
|
||||
public function getProject()
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setProject(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description
|
||||
*
|
||||
@@ -317,6 +345,8 @@ class Timesheet
|
||||
}
|
||||
|
||||
/**
|
||||
* These validations are used in places, where we don't use a form yet (like the API).
|
||||
*
|
||||
* @param ExecutionContextInterface $context
|
||||
* @param mixed $payload
|
||||
*
|
||||
@@ -324,6 +354,27 @@ class Timesheet
|
||||
*/
|
||||
public function validate(ExecutionContextInterface $context, $payload)
|
||||
{
|
||||
if (null === $this->getActivity()) {
|
||||
$context->buildViolation('A timesheet must have an activity.')
|
||||
->atPath('activity')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->getProject()) {
|
||||
$context->buildViolation('A timesheet must have a project.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $this->getActivity() && null !== $this->getProject() && null !== $this->getActivity()->getProject() && $this->getActivity()->getProject() !== $this->getProject()) {
|
||||
$context->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
|
||||
->atPath('project')
|
||||
->setTranslationDomain('validators')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) {
|
||||
$context->buildViolation('End date must not be earlier then start date.')
|
||||
->atPath('end')
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -35,8 +37,11 @@ class ActivityEditForm extends AbstractType
|
||||
$entry = $options['data'];
|
||||
|
||||
$project = null;
|
||||
if ($entry->getId() !== null) {
|
||||
$customer = null;
|
||||
|
||||
if (null !== $entry->getProject()) {
|
||||
$project = $entry->getProject();
|
||||
$customer = $project->getCustomer();
|
||||
}
|
||||
|
||||
$builder
|
||||
@@ -49,9 +54,21 @@ class ActivityEditForm extends AbstractType
|
||||
'label' => 'label.comment',
|
||||
'required' => false,
|
||||
])
|
||||
// entity type: project
|
||||
->add('customer', CustomerType::class, [
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_project',
|
||||
'data-api-url' => ['get_projects', ['customer' => '-s-']],
|
||||
],
|
||||
])
|
||||
->add('project', ProjectType::class, [
|
||||
'label' => 'label.project',
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType($project);
|
||||
},
|
||||
|
||||
@@ -10,15 +10,21 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\Type\ActivityGroupedWithCustomerNameType;
|
||||
use App\Form\Type\ActivityType;
|
||||
use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DurationType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -34,9 +40,12 @@ class TimesheetEditForm extends AbstractType
|
||||
/** @var Timesheet $entry */
|
||||
$entry = $options['data'];
|
||||
|
||||
$activity = null;
|
||||
if ($entry->getId() !== null) {
|
||||
$activity = $entry->getActivity();
|
||||
$activity = $entry->getActivity();
|
||||
$project = $entry->getProject();
|
||||
$customer = null === $entry->getProject() ? null : $entry->getProject()->getCustomer();
|
||||
|
||||
if (null === $project && null !== $activity) {
|
||||
$project = $activity->getProject();
|
||||
}
|
||||
|
||||
if (null === $entry->getEnd() || !$options['duration_only']) {
|
||||
@@ -65,7 +74,32 @@ class TimesheetEditForm extends AbstractType
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('activity', ActivityGroupedWithCustomerNameType::class, [
|
||||
->add('customer', CustomerType::class, [
|
||||
'label' => 'label.customer',
|
||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType($customer);
|
||||
},
|
||||
'data' => $customer ? $customer : '',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_project',
|
||||
'data-api-url' => ['get_projects', ['customer' => '-s-']],
|
||||
],
|
||||
])
|
||||
->add('project', ProjectType::class, [
|
||||
'required' => true,
|
||||
'placeholder' => '',
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType($project);
|
||||
},
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_activity',
|
||||
'data-api-url' => ['get_activities', ['project' => '-s-']],
|
||||
],
|
||||
])
|
||||
->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($activity) {
|
||||
return $repo->builderForEntityType($activity);
|
||||
@@ -84,6 +118,38 @@ class TimesheetEditForm extends AbstractType
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
/*
|
||||
$builder->get('customer')->addEventListener(
|
||||
FormEvents::POST_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$customer = $event->getForm()->getData();
|
||||
$event->getForm()->getParent()->add('project', ProjectType::class, [
|
||||
'required' => true,
|
||||
'placeholder' => '',
|
||||
'label' => 'label.project',
|
||||
'query_builder' => function (ProjectRepository $repo) use ($customer) {
|
||||
return $repo->builderForEntityType(null, $customer);
|
||||
},
|
||||
'attr' => [
|
||||
'data-related-select' => $this->getBlockPrefix() . '_activity',
|
||||
'data-api-url' => ['get_activities', ['project' => '-s-']],
|
||||
],
|
||||
]);
|
||||
}
|
||||
);
|
||||
*/
|
||||
$builder->get('project')->addEventListener(
|
||||
FormEvents::POST_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$project = $event->getForm()->getData();
|
||||
$event->getForm()->getParent()->add('activity', ActivityType::class, [
|
||||
'label' => 'label.activity',
|
||||
'query_builder' => function (ActivityRepository $repo) use ($project) {
|
||||
return $repo->builderForEntityType(null, $project);
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
if ($options['include_user']) {
|
||||
$builder->add('user', UserType::class);
|
||||
|
||||
@@ -150,7 +150,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'required' => false,
|
||||
'query_builder' => function (ProjectRepository $repo) use ($data) {
|
||||
$qb = $repo->builderForEntityType();
|
||||
$qb->where('p.customer = :customer')->setParameter('customer', $data['customer']);
|
||||
$qb->andWhere('p.customer = :customer')->setParameter('customer', $data['customer']);
|
||||
|
||||
return $qb;
|
||||
},
|
||||
@@ -176,8 +176,7 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'group_by' => null,
|
||||
'required' => false,
|
||||
'query_builder' => function (ActivityRepository $repo) use ($data) {
|
||||
$qb = $repo->builderForEntityType();
|
||||
$qb->where('a.project = :project')->setParameter('project', $data['project']);
|
||||
$qb = $repo->builderForEntityType(null, $data['project']);
|
||||
|
||||
return $qb;
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
|
||||
],
|
||||
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use App\Entity\Activity;
|
||||
|
||||
/**
|
||||
* Custom form field type to select an activity which are grouped by their Projects, preceeded by their customer names.
|
||||
*/
|
||||
class ActivityGroupedWithCustomerNameType extends ActivityType
|
||||
{
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param $key
|
||||
* @param $index
|
||||
* @return string
|
||||
*/
|
||||
public function groupBy(Activity $activity, $key, $index)
|
||||
{
|
||||
return $activity->getProject()->getCustomer()->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return string
|
||||
*/
|
||||
public function choiceLabel(Activity $activity)
|
||||
{
|
||||
return $activity->getProject()->getName() . ': ' . $activity->getName();
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,11 @@ class ActivityType extends AbstractType
|
||||
*/
|
||||
public function groupBy(Activity $activity, $key, $index)
|
||||
{
|
||||
return '[' . $activity->getProject()->getId() . '] ' . $activity->getProject()->getName();
|
||||
if (null === $activity->getProject()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $activity->getProject()->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,8 +51,17 @@ class ActivityType extends AbstractType
|
||||
'choice_label' => [$this, 'choiceLabel'],
|
||||
'group_by' => [$this, 'groupBy'],
|
||||
'query_builder' => function (ActivityRepository $repo) {
|
||||
return $repo->builderForEntityType(null);
|
||||
return $repo->builderForEntityType();
|
||||
},
|
||||
'choice_attr' => function (Activity $activity, $key, $value) {
|
||||
$attributes = [];
|
||||
if (null !== $activity->getProject()) {
|
||||
$attributes['data-project'] = $activity->getProject()->getId();
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
},
|
||||
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class CustomerType extends AbstractType
|
||||
'query_builder' => function (CustomerRepository $repo) {
|
||||
return $repo->builderForEntityType(null);
|
||||
},
|
||||
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class PageSizeType extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.pageSize',
|
||||
'choices' => [10 => 10, 25 => 25, 50 => 50, 75 => 75, 100 => 100],
|
||||
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class ProjectType extends AbstractType
|
||||
'query_builder' => function (ProjectRepository $repo) {
|
||||
return $repo->builderForEntityType(null);
|
||||
},
|
||||
//'attr' => ['class' => 'selectpicker', 'data-size' => 10, 'data-live-search' => true, 'data-width' => '100%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class VisibilityType extends AbstractType
|
||||
'yes' => VisibilityQuery::SHOW_VISIBLE,
|
||||
'no' => VisibilityQuery::SHOW_HIDDEN,
|
||||
],
|
||||
//'attr' => ['class' => 'selectpicker', 'data-live-search' => false, 'data-width' => '100%']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
127
src/Migrations/Version20181031220003.php
Normal file
127
src/Migrations/Version20181031220003.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Migration that adds the project column to timesheets table, to allow global activities.
|
||||
* It also fixes foreign-key columns on timesheet and projects table, which are not allowed.
|
||||
*/
|
||||
final class Version20181031220003 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$platform = $this->getPlatform();
|
||||
|
||||
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||
}
|
||||
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
$projects = $this->getTableName('projects');
|
||||
$activities = $this->getTableName('activities');
|
||||
$users = $this->getTableName('users');
|
||||
$customers = $this->getTableName('customers');
|
||||
|
||||
if ($platform === 'sqlite') {
|
||||
// project table
|
||||
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $projects . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects);
|
||||
$this->addSql('DROP TABLE ' . $projects);
|
||||
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, order_number CLOB DEFAULT NULL COLLATE BINARY, comment CLOB DEFAULT NULL COLLATE BINARY, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_407F12069395C3F3 FOREIGN KEY (customer_id) REFERENCES ' . $customers . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__' . $projects);
|
||||
$this->addSql('DROP TABLE __temp__' . $projects);
|
||||
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
|
||||
// timesheet table
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
|
||||
$this->addSql('DROP TABLE ' . $timesheet);
|
||||
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, project_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
|
||||
$this->addSql('DROP TABLE __temp__' . $timesheet);
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
} else {
|
||||
// project table
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT NOT NULL');
|
||||
// timesheet table
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD project_id INT DEFAULT NULL AFTER activity_id');
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
}
|
||||
|
||||
// update timesheet table and insert project_id from activity table
|
||||
$this->addSql('UPDATE ' . $timesheet . ' SET project_id = (SELECT project_id FROM ' . $activities . ' WHERE id = activity_id)');
|
||||
|
||||
// now update the timesheet table and disallow null values for all required columns (that was a bug before)
|
||||
if ($platform === 'sqlite') {
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
|
||||
$this->addSql('DROP TABLE ' . $timesheet);
|
||||
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $users . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activities . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B1166D1F9C FOREIGN KEY (project_id) REFERENCES ' . $projects . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
|
||||
$this->addSql('DROP TABLE __temp__' . $timesheet);
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||
} else {
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' CHANGE project_id project_id INT NOT NULL, CHANGE user user INT NOT NULL, CHANGE activity_id activity_id INT NOT NULL');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$platform = $this->getPlatform();
|
||||
|
||||
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||
}
|
||||
|
||||
$timesheet = $this->getTableName('timesheet');
|
||||
$projects = $this->getTableName('projects');
|
||||
|
||||
if ($platform === 'sqlite') {
|
||||
// project table
|
||||
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $projects . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM ' . $projects);
|
||||
$this->addSql('DROP TABLE ' . $projects);
|
||||
$this->addSql('CREATE TABLE ' . $projects . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, customer_id INTEGER DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $projects . ' (id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate) SELECT id, customer_id, name, order_number, comment, visible, budget, fixed_rate, hourly_rate FROM __temp__' . $projects);
|
||||
$this->addSql('DROP TABLE __temp__' . $projects);
|
||||
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $projects . ' (customer_id)');
|
||||
// timesheet table
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
|
||||
$this->addSql('DROP TABLE ' . $timesheet);
|
||||
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
|
||||
$this->addSql('DROP TABLE __temp__' . $timesheet);
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||
} else {
|
||||
// project table
|
||||
$this->addSql('ALTER TABLE ' . $projects . ' CHANGE customer_id customer_id INT DEFAULT NULL');
|
||||
// timesheet table
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B1166D1F9C');
|
||||
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet);
|
||||
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP project_id, CHANGE user user INT DEFAULT NULL, CHANGE activity_id activity_id INT DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\ActivityStatistic;
|
||||
@@ -35,7 +36,7 @@ class ActivityRepository extends AbstractRepository
|
||||
/**
|
||||
* @param User|null $user
|
||||
* @param \DateTime|null $startFrom
|
||||
* @return mixed
|
||||
* @return Timesheet[]
|
||||
*/
|
||||
public function getRecentActivities(User $user = null, \DateTime $startFrom = null)
|
||||
{
|
||||
@@ -45,7 +46,7 @@ class ActivityRepository extends AbstractRepository
|
||||
->distinct()
|
||||
->from(Timesheet::class, 't')
|
||||
->join('t.activity', 'a')
|
||||
->join('a.project', 'p')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere('a.visible = 1')
|
||||
@@ -66,15 +67,7 @@ class ActivityRepository extends AbstractRepository
|
||||
->setParameter('begin', $startFrom);
|
||||
}
|
||||
|
||||
$results = $qb->getQuery()->getResult();
|
||||
|
||||
$activities = [];
|
||||
/* @var Timesheet $entry */
|
||||
foreach ($results as $entry) {
|
||||
$activities[] = $entry->getActivity();
|
||||
}
|
||||
|
||||
return $activities;
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,14 +111,21 @@ class ActivityRepository extends AbstractRepository
|
||||
/**
|
||||
* Returns a query builder that is used for ActivityType and your own 'query_builder' option.
|
||||
*
|
||||
* @param Activity|null $entity
|
||||
* @param Activity|string|null $activity
|
||||
* @param Project|string|null $project
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
*/
|
||||
public function builderForEntityType(Activity $entity = null)
|
||||
public function builderForEntityType($activity = null, $project = null)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setHiddenEntity($entity);
|
||||
$query->setHiddenEntity($activity);
|
||||
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
$query->setProject($project);
|
||||
$query->setOrderGlobalsFirst(true);
|
||||
|
||||
if (null === $activity && $project === null) {
|
||||
$query->setGlobalsOnly(true);
|
||||
}
|
||||
|
||||
return $this->findByQuery($query);
|
||||
}
|
||||
@@ -140,34 +140,68 @@ class ActivityRepository extends AbstractRepository
|
||||
|
||||
$qb->select('a', 'p', 'c')
|
||||
->from(Activity::class, 'a')
|
||||
->join('a.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->leftJoin('a.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->orderBy('a.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
$where = $qb->expr()->andX();
|
||||
|
||||
if (ActivityQuery::SHOW_VISIBLE == $query->getVisibility()) {
|
||||
$where->add('a.visible = :visible');
|
||||
if (!$query->isExclusiveVisibility()) {
|
||||
$qb->andWhere('c.visible = 1');
|
||||
$qb->andWhere('p.visible = 1');
|
||||
}
|
||||
$qb->andWhere('a.visible = 1');
|
||||
|
||||
/** @var Activity $entity */
|
||||
$entity = $query->getHiddenEntity();
|
||||
if (null !== $entity) {
|
||||
$qb->orWhere('a.id = :activity')->setParameter('activity', $entity);
|
||||
$where->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('c.visible', ':visible'),
|
||||
$qb->expr()->isNull('c.visible')
|
||||
)
|
||||
);
|
||||
$where->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('p.visible', ':visible'),
|
||||
$qb->expr()->isNull('p.visible')
|
||||
)
|
||||
);
|
||||
}
|
||||
$qb->setParameter('visible', 1);
|
||||
} elseif (ActivityQuery::SHOW_HIDDEN == $query->getVisibility()) {
|
||||
$qb->andWhere('a.visible = 0');
|
||||
$where->add('a.visible = :visible');
|
||||
$qb->setParameter('visible', 0);
|
||||
}
|
||||
|
||||
if (null !== $query->getProject()) {
|
||||
$qb->andWhere('a.project = :project')
|
||||
->setParameter('project', $query->getProject());
|
||||
if ($query->isGlobalsOnly()) {
|
||||
$where->add($qb->expr()->isNull('a.project'));
|
||||
} elseif (null !== $query->getProject()) {
|
||||
$where->add(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('a.project', ':project'),
|
||||
$qb->expr()->isNull('a.project')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('project', $query->getProject());
|
||||
} elseif (null !== $query->getCustomer()) {
|
||||
$qb->andWhere('p.customer = :customer')
|
||||
->setParameter('customer', $query->getCustomer());
|
||||
$where->add('p.customer = :customer');
|
||||
$qb->setParameter('customer', $query->getCustomer());
|
||||
}
|
||||
|
||||
$or = $qb->expr()->orX();
|
||||
|
||||
// this must always be the last part before the or
|
||||
$or->add($where);
|
||||
|
||||
// this must always be the last part of the query
|
||||
/** @var Activity $entity */
|
||||
$entity = $query->getHiddenEntity();
|
||||
if (null !== $entity) {
|
||||
$or->add($qb->expr()->eq('a.id', ':activity'));
|
||||
$qb->setParameter('activity', $entity);
|
||||
}
|
||||
|
||||
if ($query->isOrderGlobalsFirst()) {
|
||||
$qb->orderBy('a.project', 'ASC');
|
||||
}
|
||||
|
||||
$qb->andWhere($or);
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class CustomerRepository extends AbstractRepository
|
||||
->join(Project::class, 'p')
|
||||
->join(Customer::class, 'c')
|
||||
->andWhere('t.activity = a.id')
|
||||
->andWhere('a.project = p.id')
|
||||
->andWhere('t.project = p.id')
|
||||
->andWhere('p.customer = c.id')
|
||||
->andWhere('c.id = :customer')
|
||||
;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Model\ProjectStatistic;
|
||||
@@ -41,7 +42,7 @@ class ProjectRepository extends AbstractRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves statistics for one activity.
|
||||
* Retrieves statistics for one project.
|
||||
*
|
||||
* @param Project $project
|
||||
* @return ProjectStatistic
|
||||
@@ -55,7 +56,7 @@ class ProjectRepository extends AbstractRepository
|
||||
->addSelect('COUNT(DISTINCT(a.id)) as activityAmount')
|
||||
->from(Activity::class, 'a')
|
||||
->join(Timesheet::class, 't')
|
||||
->where('a.project = :project')
|
||||
->where('t.project = :project')
|
||||
->andWhere('t.activity = a.id')
|
||||
;
|
||||
|
||||
@@ -79,12 +80,14 @@ class ProjectRepository extends AbstractRepository
|
||||
* Returns a query builder that is used for ProjectType and your own 'query_builder' option.
|
||||
*
|
||||
* @param Project|null $entity
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
* @param Customer|null $customer
|
||||
* @return array|QueryBuilder|Pagerfanta
|
||||
*/
|
||||
public function builderForEntityType(Project $entity = null)
|
||||
public function builderForEntityType(Project $entity = null, Customer $customer = null)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setHiddenEntity($entity);
|
||||
$query->setCustomer($customer);
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
|
||||
return $this->findByQuery($query);
|
||||
|
||||
@@ -17,12 +17,58 @@ use App\Entity\Project;
|
||||
class ActivityQuery extends ProjectQuery
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
* @var Project|int
|
||||
*/
|
||||
protected $project;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $orderGlobalsFirst = false;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $globalsOnly = false;
|
||||
|
||||
/**
|
||||
* @return Project
|
||||
* @return bool
|
||||
*/
|
||||
public function isOrderGlobalsFirst(): bool
|
||||
{
|
||||
return $this->orderGlobalsFirst;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $orderGlobalsFirst
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function setOrderGlobalsFirst(bool $orderGlobalsFirst)
|
||||
{
|
||||
$this->orderGlobalsFirst = $orderGlobalsFirst;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isGlobalsOnly(): bool
|
||||
{
|
||||
return $this->globalsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $globalsOnly
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function setGlobalsOnly(bool $globalsOnly)
|
||||
{
|
||||
$this->globalsOnly = $globalsOnly;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Project|int
|
||||
*/
|
||||
public function getProject()
|
||||
{
|
||||
@@ -30,10 +76,10 @@ class ActivityQuery extends ProjectQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param Project|int $project
|
||||
* @return $this
|
||||
*/
|
||||
public function setProject(Project $project = null)
|
||||
public function setProject($project = null)
|
||||
{
|
||||
$this->project = $project;
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class BaseQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $resultType
|
||||
* @param $resultType
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
@@ -166,7 +166,7 @@ class BaseQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $hiddenEntity
|
||||
* @param object|string $hiddenEntity
|
||||
* @return BaseQuery
|
||||
*/
|
||||
public function setHiddenEntity($hiddenEntity)
|
||||
|
||||
@@ -17,12 +17,12 @@ use App\Entity\Customer;
|
||||
class ProjectQuery extends VisibilityQuery
|
||||
{
|
||||
/**
|
||||
* @var Customer
|
||||
* @var Customer|int
|
||||
*/
|
||||
protected $customer;
|
||||
|
||||
/**
|
||||
* @return Customer
|
||||
* @return Customer|int
|
||||
*/
|
||||
public function getCustomer()
|
||||
{
|
||||
@@ -30,10 +30,10 @@ class ProjectQuery extends VisibilityQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Customer|int $customer
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomer(Customer $customer = null)
|
||||
public function setCustomer($customer = null)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Model\Statistic\Month;
|
||||
@@ -54,28 +53,6 @@ class TimesheetRepository extends AbstractRepository
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Activity $activity
|
||||
* @return Timesheet
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function startRecording(User $user, Activity $activity)
|
||||
{
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setBegin(new DateTime())
|
||||
->setUser($user)
|
||||
->setActivity($activity);
|
||||
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param DateTime|null $begin
|
||||
@@ -223,17 +200,17 @@ class TimesheetRepository extends AbstractRepository
|
||||
$qb->andWhere($qb->expr()->isNotNull('t.end'));
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
$qb
|
||||
->orderBy('year', 'DESC')
|
||||
->addOrderBy('month', 'ASC')
|
||||
->groupBy('year')
|
||||
->addGroupBy('month');
|
||||
|
||||
if (null !== $user) {
|
||||
$qb->andWhere('t.user = :user')
|
||||
->setParameter('user', $user);
|
||||
}
|
||||
|
||||
$years = [];
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
@@ -267,7 +244,7 @@ class TimesheetRepository extends AbstractRepository
|
||||
$qb->select('t', 'a', 'p', 'c')
|
||||
->from(Timesheet::class, 't')
|
||||
->join('t.activity', 'a')
|
||||
->join('a.project', 'p')
|
||||
->join('t.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->where($qb->expr()->gt('t.begin', '0'))
|
||||
->andWhere($qb->expr()->isNull('t.end'))
|
||||
@@ -293,10 +270,10 @@ class TimesheetRepository extends AbstractRepository
|
||||
|
||||
$qb->select('t', 'a', 'p', 'c', 'u')
|
||||
->from(Timesheet::class, 't')
|
||||
->join('t.activity', 'a')
|
||||
->join('t.user', 'u')
|
||||
->join('a.project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
->leftJoin('t.activity', 'a')
|
||||
->leftJoin('t.user', 'u')
|
||||
->leftJoin('t.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
if (null !== $query->getUser()) {
|
||||
@@ -323,7 +300,7 @@ class TimesheetRepository extends AbstractRepository
|
||||
$qb->andWhere('t.activity = :activity')
|
||||
->setParameter('activity', $query->getActivity());
|
||||
} elseif (null !== $query->getProject()) {
|
||||
$qb->andWhere('a.project = :project')
|
||||
$qb->andWhere('t.project = :project')
|
||||
->setParameter('project', $query->getProject());
|
||||
} elseif (null !== $query->getCustomer()) {
|
||||
$qb->andWhere('p.customer = :customer')
|
||||
|
||||
@@ -74,7 +74,7 @@ class RateCalculator implements CalculatorInterface
|
||||
return $activity->getHourlyRate();
|
||||
}
|
||||
|
||||
$project = $activity->getProject();
|
||||
$project = $record->getProject();
|
||||
if (null !== $project) {
|
||||
if (null !== $project->getHourlyRate()) {
|
||||
return $project->getHourlyRate();
|
||||
@@ -104,7 +104,7 @@ class RateCalculator implements CalculatorInterface
|
||||
return $activity->getFixedRate();
|
||||
}
|
||||
|
||||
$project = $activity->getProject();
|
||||
$project = $record->getProject();
|
||||
if (null !== $project) {
|
||||
if (null !== $project->getFixedRate()) {
|
||||
return $project->getFixedRate();
|
||||
|
||||
@@ -44,10 +44,6 @@ class TimesheetVoter extends AbstractVoter
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subject instanceof Activity && self::START == $attribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
@@ -71,38 +67,18 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
switch ($attribute) {
|
||||
case self::STOP:
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canStop($subject, $user, $token);
|
||||
|
||||
case self::START:
|
||||
if (!$subject instanceof Activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canStart($subject, $user, $token);
|
||||
|
||||
case self::VIEW:
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canView($subject, $user, $token);
|
||||
|
||||
case self::EDIT:
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canEdit($subject, $user, $token);
|
||||
|
||||
case self::DELETE:
|
||||
if (!$subject instanceof Timesheet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->canDelete($subject, $user, $token);
|
||||
}
|
||||
|
||||
@@ -122,23 +98,21 @@ class TimesheetVoter extends AbstractVoter
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canStart(Activity $activity, User $user, TokenInterface $token)
|
||||
protected function canStart(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
{
|
||||
// we could check the amount of active entries
|
||||
// if a teamlead starts an entry for another user, check that this user is part of his team
|
||||
|
||||
if (!$activity->getVisible()) {
|
||||
if (!$timesheet->getActivity()->getVisible() || !$timesheet->getProject()->getVisible()) {
|
||||
return false;
|
||||
}
|
||||
if (!$activity->getProject()->getVisible()) {
|
||||
return false;
|
||||
}
|
||||
if (!$activity->getProject()->getCustomer()->getVisible()) {
|
||||
|
||||
if (!$timesheet->getProject()->getCustomer()->getVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,18 @@
|
||||
<tr>
|
||||
<td>{{ entry.name }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||
<a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
||||
{% if entry.project and entry.project.customer %}
|
||||
<a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
|
||||
{% else %}
|
||||
{#{ widgets.label('x', 'success') }#}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||
<a href="{{ path('admin_project_edit', {'id' : entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
|
||||
{% if entry.project %}
|
||||
<a href="{{ path('admin_project_edit', {'id' : entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
|
||||
{% else %}
|
||||
{#{ widgets.label('x', 'success') }#}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
|
||||
{% set params = {
|
||||
'%activity%': '<strong>' ~ activity.name ~ '</strong>',
|
||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
||||
'%project%': '<strong>-</strong>',
|
||||
'%customer%': '<strong>-</strong>',
|
||||
'%records%': '<strong>' ~ stats.recordAmount ~ '</strong>',
|
||||
'%duration%': '<strong>' ~ stats.recordDuration|duration ~ '</strong>'
|
||||
} %}
|
||||
|
||||
{% if activity.project is not null %}
|
||||
{% set params = params|merge({
|
||||
'%project%': '<strong>' ~ activity.project.name ~ '</strong>',
|
||||
'%customer%': '<strong>' ~ activity.project.customer.name ~ '</strong>',
|
||||
}) %}
|
||||
{% endif %}
|
||||
{{ include('default/_form_delete.html.twig', {
|
||||
'message': "admin_activity.delete_confirm"|trans(params)|raw,
|
||||
'form': form,
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.activity.project.customer.currency) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||
{% else %}
|
||||
{% if not duration_only %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ swagger_data.spec.info.title }}</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700">
|
||||
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('bundles/nelmioapidoc/style.css') }}">
|
||||
{# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #}
|
||||
<script id="swagger-data" type="application/json">{{ swagger_data|json_encode(65)|raw }}</script>
|
||||
</head>
|
||||
<body style="margin-top:0;">
|
||||
<div id="swagger-ui" class="api-platform"></div>
|
||||
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-bundle.js') }}"></script>
|
||||
<script src="{{ asset('bundles/nelmioapidoc/swagger-ui/swagger-ui-standalone-preset.js') }}"></script>
|
||||
<script src="{{ asset('bundles/nelmioapidoc/init-swagger-ui.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,3 +6,10 @@
|
||||
{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock form_label %}
|
||||
|
||||
{% block choice_widget_collapsed %}
|
||||
{% if 'data-api-url' in attr|keys %}
|
||||
{% set attr = attr|merge({'data-api-url': path(attr['data-api-url']|first, attr['data-api-url']|last)}) %}
|
||||
{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|desc2html }}
|
||||
{% else %}
|
||||
{{ entry.activity.name }} / {{ entry.activity.project.name }}
|
||||
{{ entry.activity.name }} / {{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center" contenteditable="true">
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
{% for entry in model.calculator.entries %}
|
||||
<tr>
|
||||
<td>{{ entry.begin|date_short }}</td>
|
||||
<td>{{ entry.activity.name }} / {{ entry.activity.project.name }}</td>
|
||||
<td>{{ entry.activity.name }} / {{ entry.project.name }}</td>
|
||||
<td contenteditable="true">{{ app.user.getPreferenceValue('hourly_rate')|money(model.calculator.currency) }}</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
<td>{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
{% if model.query.user is empty %}
|
||||
<td>{{ widgets.username(entry.user) }}</td>
|
||||
{% endif %}
|
||||
<td>{{ entry.activity.name }} / {{ entry.activity.project.name }}</td>
|
||||
<td>{{ entry.activity.name }} / {{ entry.project.name }}</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -56,11 +56,18 @@
|
||||
|
||||
{% macro label_activity(activity) %}
|
||||
{% import _self as macro %}
|
||||
{% if activity.visible and activity.project.visible and activity.project.customer.visible %}
|
||||
{{ macro.label(activity.name, 'primary', activity.project.customer.name ~ ': ' ~ activity.project.name) }}
|
||||
{% else %}
|
||||
{{ macro.label(activity.name, 'warning', activity.project.customer.name ~ ': ' ~ activity.project.name) }}
|
||||
{% set isVisible = activity.visible %}
|
||||
{% if isVisible and not activity.project is null %}
|
||||
{% set isVisible = activity.project.visible %}
|
||||
{% if isVisible and not activity.project.customer is null %}
|
||||
{% set isVisible = activity.project.customer.visible %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set label = '' %}
|
||||
{% if not activity.project is null %}
|
||||
{% set label = activity.project.customer.name ~ ': ' ~ activity.project.name %}
|
||||
{% endif %}
|
||||
{{ macro.label(activity.name, (isVisible ? 'primary' : 'warning'), label) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro label_project(project) %}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{{ entry.activity.name }}
|
||||
<small><i class="{{ 'timesheet'|icon }}"></i> {{ entry|duration }}</small>
|
||||
</h4>
|
||||
<p>{{ entry.activity.project.name }} ({{ entry.activity.project.customer.name }})</p>
|
||||
<p>{{ entry.project.name }} ({{ entry.project.customer.name }})</p>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{% if activities is not empty %}
|
||||
{% if entries is defined and entries is not empty %}
|
||||
<li class="dropdown notifications-menu">
|
||||
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown">
|
||||
<i class="{{ 'activity'|icon }} fa-2x"></i>
|
||||
<span class="label label-success">{{ activities|length }}</span>
|
||||
<span class="label label-success">{{ entries|length }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header">{{ 'recent.activities'|transchoice(activities|length) }}</li>
|
||||
<li class="header">{{ 'recent.activities'|transchoice(entries|length) }}</li>
|
||||
<li>
|
||||
<ul class="menu">
|
||||
{% for activity in activities %}
|
||||
{% for timesheet in entries %}
|
||||
<li>
|
||||
<a href="{{ path('timesheet_start', {'id' : activity.id}) }}">
|
||||
<i class="{{ 'start-small'|icon }} text-green"></i> {{ 'recent.activities.format'|trans({'%activity%': activity.name, '%project%': activity.project.name, '%customer%': activity.project.customer.name}) }}
|
||||
<a href="{{ path('timesheet_start', {'id' : timesheet.id}) }}">
|
||||
<i class="{{ 'start-small'|icon }} text-green"></i> {{ 'recent.activities.format'|trans({'%activity%': timesheet.activity.name, '%project%': timesheet.project.name, '%customer%': timesheet.project.customer.name}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
{% endif %}
|
||||
<span class="small">
|
||||
{{ 'label.activity'|trans }}: {{ entry.activity.name }} |
|
||||
{{ 'label.project'|trans }}: {{ entry.activity.project.name }} |
|
||||
{{ 'label.customer'|trans }}: {{ entry.activity.project.customer.name }}
|
||||
{{ 'label.project'|trans }}: {{ entry.project.name }} |
|
||||
{{ 'label.customer'|trans }}: {{ entry.project.customer.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ entry.duration|duration }}</td>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
||||
{% endif %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.activity.project.customer.currency) }}</td>
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||
{% else %}
|
||||
{% if not duration_only %}
|
||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
||||
|
||||
@@ -138,11 +138,12 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return Crawler
|
||||
*/
|
||||
protected function request(Client $client, string $url, $method = 'GET')
|
||||
protected function request(Client $client, string $url, $method = 'GET', array $parameters = [])
|
||||
{
|
||||
return $client->request($method, $this->createUrl($url), [], [], ['HTTP_CONTENT_TYPE' => 'application/json']);
|
||||
return $client->request($method, $this->createUrl($url), $parameters, [], ['HTTP_CONTENT_TYPE' => 'application/json']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
|
||||
namespace App\Tests\API;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\VisibilityQuery;
|
||||
use Symfony\Bundle\FrameworkBundle\Client;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\API\ActivityController
|
||||
@@ -22,16 +26,71 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->assertUrlIsSecured('/api/activities');
|
||||
}
|
||||
|
||||
public function testGetCollection()
|
||||
protected function loadActivityTestData(Client $client)
|
||||
{
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$project = $em->getRepository(Project::class)->find(1);
|
||||
|
||||
$project2 = new Project();
|
||||
$project2->setName('Activity Test');
|
||||
$em->persist($project2);
|
||||
|
||||
$activity = (new Activity())->setName('first one')->setComment('1')->setProject($project2);
|
||||
$em->persist($activity);
|
||||
|
||||
$activity = (new Activity())->setName('second one')->setComment('2');
|
||||
$em->persist($activity);
|
||||
|
||||
$activity = (new Activity())->setName('third one')->setComment('3')->setProject($project);
|
||||
$em->persist($activity);
|
||||
|
||||
$activity = (new Activity())->setName('fourth one')->setComment('4')->setProject($project2)->setVisible(false);
|
||||
$em->persist($activity);
|
||||
|
||||
$activity = (new Activity())->setName('fifth one')->setComment('5')->setProject($project2);
|
||||
$em->persist($activity);
|
||||
|
||||
$activity = (new Activity())->setName('sixth one')->setComment('6')->setVisible(false);
|
||||
$em->persist($activity);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getCollectionTestData
|
||||
*/
|
||||
public function testGetCollection($url, $parameters, $expected)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/api/activities');
|
||||
$this->loadActivityTestData($client);
|
||||
$this->assertAccessIsGranted($client, $url, 'GET', $parameters);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertInternalType('array', $result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(1, count($result));
|
||||
$this->assertStructure($result[0]);
|
||||
$this->assertEquals(count($expected), count($result));
|
||||
for ($i = 0; $i < count($result); $i++) {
|
||||
$activity = $result[$i];
|
||||
$hasProject = $expected[$i][0];
|
||||
$this->assertStructure($activity, $hasProject);
|
||||
if ($hasProject) {
|
||||
$this->assertEquals($expected[$i][0], $activity['project_id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCollectionTestData()
|
||||
{
|
||||
yield ['/api/activities', [], [[false], [false], [true, 2], [true, 1], [true, 2]]];
|
||||
yield ['/api/activities', ['globals' => 'true'], [[false], [false]]];
|
||||
yield ['/api/activities', ['globals' => 'true', 'visible' => VisibilityQuery::SHOW_BOTH], [[false], [false], [false]]];
|
||||
yield ['/api/activities', ['globals' => 'true', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false]]];
|
||||
yield ['/api/activities', ['globals' => 'true', 'visible' => VisibilityQuery::SHOW_VISIBLE], [[false], [false]]];
|
||||
yield ['/api/activities', ['project' => '1'], [[false], [false], [true, 1]]];
|
||||
yield ['/api/activities', ['project' => '2', 'visible' => VisibilityQuery::SHOW_VISIBLE], [[false], [false], [true, 2], [true, 2]]];
|
||||
yield ['/api/activities', ['project' => '2', 'visible' => VisibilityQuery::SHOW_BOTH], [[false], [false], [false], [true, 2], [true, 2], [true, 2]]];
|
||||
yield ['/api/activities', ['project' => '2', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false], [true, 2]]];
|
||||
}
|
||||
|
||||
public function testGetEntity()
|
||||
@@ -41,7 +100,7 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertInternalType('array', $result);
|
||||
$this->assertStructure($result);
|
||||
$this->assertStructure($result, false);
|
||||
}
|
||||
|
||||
public function testNotFound()
|
||||
@@ -49,15 +108,19 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/activities/2');
|
||||
}
|
||||
|
||||
protected function assertStructure(array $result)
|
||||
protected function assertStructure(array $result, $project = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'comment', 'visible', 'project_id'
|
||||
'id', 'name', 'comment', 'visible'
|
||||
];
|
||||
|
||||
if ($project) {
|
||||
$expectedKeys[] = 'project_id';
|
||||
}
|
||||
|
||||
$actual = array_keys($result);
|
||||
|
||||
$this->assertEquals(count($expectedKeys), count($actual), 'Activity entity has different amount of keys');
|
||||
$this->assertEquals(count($expectedKeys), count($actual), 'Activity entity has different amount of keys: ' . $result['id']);
|
||||
$this->assertEquals($expectedKeys, $actual, 'Activity structure does not match');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
|
||||
namespace App\Tests\API;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Query\VisibilityQuery;
|
||||
use Symfony\Bundle\FrameworkBundle\Client;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\API\ProjectController
|
||||
@@ -34,6 +38,72 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->assertStructure($result[0]);
|
||||
}
|
||||
|
||||
protected function loadProjectTestData(Client $client)
|
||||
{
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$customer = $em->getRepository(Customer::class)->find(1);
|
||||
|
||||
$customer2 = (new Customer())->setName('first one')->setVisible(false)->setCountry('de')->setTimezone('Europe/Berlin');
|
||||
$em->persist($customer2);
|
||||
|
||||
$customer3 = (new Customer())->setName('second one')->setCountry('at')->setTimezone('Europe/Vienna');
|
||||
$em->persist($customer3);
|
||||
|
||||
$project = (new Project())->setName('first')->setVisible(false)->setCustomer($customer2);
|
||||
$em->persist($project);
|
||||
|
||||
$project = (new Project())->setName('second')->setVisible(false)->setCustomer($customer);
|
||||
$em->persist($project);
|
||||
|
||||
$project = (new Project())->setName('third')->setVisible(true)->setCustomer($customer2);
|
||||
$em->persist($project);
|
||||
|
||||
$project = (new Project())->setName('fourth')->setVisible(true)->setCustomer($customer3);
|
||||
$em->persist($project);
|
||||
|
||||
$project = (new Project())->setName('fifth')->setVisible(true)->setCustomer($customer);
|
||||
$em->persist($project);
|
||||
|
||||
$project = (new Project())->setName('sixth')->setVisible(false)->setCustomer($customer3);
|
||||
$em->persist($project);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getCollectionTestData
|
||||
*/
|
||||
public function testGetCollectionWithParams($url, $parameters, $expected)
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->loadProjectTestData($client);
|
||||
$this->assertAccessIsGranted($client, $url, 'GET', $parameters);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertInternalType('array', $result);
|
||||
$this->assertEquals(count($expected), count($result), 'Found wrong amount of projects');
|
||||
|
||||
for ($i = 0; $i < count($expected); $i++) {
|
||||
$project = $result[$i];
|
||||
$compare = $expected[$i];
|
||||
$this->assertStructure($project, $compare[0]);
|
||||
$this->assertEquals($compare[1], $project['customer_id']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCollectionTestData()
|
||||
{
|
||||
yield ['/api/projects', [], [[true, 1], [false, 3], [false, 1]]];
|
||||
yield ['/api/projects', ['customer' => '1'], [[true, 1], [false, 1]]];
|
||||
yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_VISIBLE], [[true, 1], [false, 1]]];
|
||||
yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]];
|
||||
yield ['/api/projects', ['customer' => '1', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false, 1]]];
|
||||
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_VISIBLE], []];
|
||||
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_BOTH], [[false, 2], [false, 2]]];
|
||||
yield ['/api/projects', ['customer' => '2', 'visible' => VisibilityQuery::SHOW_HIDDEN], [[false, 2]]];
|
||||
}
|
||||
|
||||
public function testGetEntity()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
@@ -49,12 +119,18 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/projects/2');
|
||||
}
|
||||
|
||||
protected function assertStructure(array $result)
|
||||
protected function assertStructure(array $result, $complete = true)
|
||||
{
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'comment', 'visible', 'budget', 'order_number', 'customer_id'
|
||||
];
|
||||
|
||||
if (!$complete) {
|
||||
$expectedKeys = [
|
||||
'id', 'name', 'visible', 'budget', 'customer_id'
|
||||
];
|
||||
}
|
||||
|
||||
$actual = array_keys($result);
|
||||
|
||||
$this->assertEquals(count($expectedKeys), count($actual), 'Project entity has different amount of keys');
|
||||
|
||||
@@ -80,7 +80,9 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||
$client->submit($form, [
|
||||
'timesheet_edit_form' => [
|
||||
'description' => 'Testing is fun!'
|
||||
'description' => 'Testing is fun!',
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -81,11 +81,12 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
* @return \Symfony\Component\DomCrawler\Crawler
|
||||
*/
|
||||
protected function request(Client $client, string $url, $method = 'GET')
|
||||
protected function request(Client $client, string $url, $method = 'GET', array $parameters = [])
|
||||
{
|
||||
return $client->request($method, $this->createUrl($url));
|
||||
return $client->request($method, $this->createUrl($url), $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,13 +141,14 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param string $url
|
||||
* @param $url
|
||||
* @param string $method
|
||||
* @param array $parameters
|
||||
*/
|
||||
protected function assertAccessIsGranted(Client $client, $url)
|
||||
protected function assertAccessIsGranted(Client $client, $url, $method = 'GET', array $parameters = [])
|
||||
{
|
||||
$this->request($client, $url);
|
||||
$this->request($client, $url, $method, $parameters);
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
// TODO improve this test?
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,7 +111,9 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||
$client->submit($form, [
|
||||
'timesheet_edit_form' => [
|
||||
'description' => 'Testing is fun!'
|
||||
'description' => 'Testing is fun!',
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
]
|
||||
]);
|
||||
|
||||
@@ -157,7 +159,14 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testStartAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
|
||||
$fixture->setAmount(1);
|
||||
$this->importFixture($em, $fixture);
|
||||
|
||||
$this->request($client, '/timesheet/start/1');
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||
@@ -167,10 +176,11 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
/** @var Timesheet $timesheet */
|
||||
$timesheet = $em->getRepository(Timesheet::class)->find(1);
|
||||
$timesheet = $em->getRepository(Timesheet::class)->find(2);
|
||||
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
|
||||
$this->assertNull($timesheet->getEnd());
|
||||
$this->assertEquals(1, $timesheet->getActivity()->getId());
|
||||
$this->assertEquals(1, $timesheet->getProject()->getId());
|
||||
}
|
||||
|
||||
public function testStopAction()
|
||||
@@ -184,6 +194,8 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
'timesheet_edit_form' => [
|
||||
'description' => 'Testing is fun!',
|
||||
'fixedRate' => 100,
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
]
|
||||
]);
|
||||
|
||||
@@ -218,6 +230,8 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$client->submit($form, [
|
||||
'timesheet_edit_form' => [
|
||||
'hourlyRate' => 100,
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
]
|
||||
]);
|
||||
|
||||
@@ -250,6 +264,8 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$client->submit($form, [
|
||||
'timesheet_edit_form' => [
|
||||
'hourlyRate' => 100,
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Tests\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
@@ -108,6 +109,8 @@ class TimesheetFixtures extends Fixture
|
||||
$activities = $this->getAllActivities($manager);
|
||||
}
|
||||
|
||||
$projects = $this->getAllProjects($manager);
|
||||
|
||||
$faker = Factory::create();
|
||||
$user = $this->user;
|
||||
|
||||
@@ -118,9 +121,18 @@ class TimesheetFixtures extends Fixture
|
||||
} elseif ($i % 2 == 0) {
|
||||
$description = '';
|
||||
}
|
||||
|
||||
$activity = $activities[array_rand($activities)];
|
||||
$project = $activity->getProject();
|
||||
|
||||
if (null === $project) {
|
||||
$project = $projects[array_rand($projects)];
|
||||
}
|
||||
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$activity,
|
||||
$project,
|
||||
$description,
|
||||
$this->getDateTime($i)
|
||||
);
|
||||
@@ -129,11 +141,20 @@ class TimesheetFixtures extends Fixture
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->running; $i++) {
|
||||
$activity = $activities[array_rand($activities)];
|
||||
$project = $activity->getProject();
|
||||
|
||||
if (null === $project) {
|
||||
$project = $projects[array_rand($projects)];
|
||||
}
|
||||
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$activity,
|
||||
$project,
|
||||
$faker->text,
|
||||
$this->getDateTime($i)
|
||||
$this->getDateTime($i),
|
||||
false
|
||||
);
|
||||
$manager->persist($entry);
|
||||
}
|
||||
@@ -160,7 +181,7 @@ class TimesheetFixtures extends Fixture
|
||||
protected function getAllActivities(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var User[] $entries */
|
||||
/* @var Activity[] $entries */
|
||||
$entries = $manager->getRepository(Activity::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
@@ -169,15 +190,32 @@ class TimesheetFixtures extends Fixture
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Project[]
|
||||
*/
|
||||
protected function getAllProjects(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var Project[] $entries */
|
||||
$entries = $manager->getRepository(Project::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Activity $activity
|
||||
* @param $description
|
||||
* @param Project $project
|
||||
* @param string $description
|
||||
* @param \DateTime $start
|
||||
* @param bool $setEndDate
|
||||
* @return Timesheet
|
||||
*/
|
||||
private function createTimesheetEntry(User $user, Activity $activity, $description, \DateTime $start, $setEndDate = true)
|
||||
private function createTimesheetEntry(User $user, Activity $activity, Project $project, $description, \DateTime $start, $setEndDate = true)
|
||||
{
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ ' . (rand(1, 172800)) . ' seconds');
|
||||
@@ -188,6 +226,7 @@ class TimesheetFixtures extends Fixture
|
||||
$entry = new Timesheet();
|
||||
$entry
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setDescription($description)
|
||||
->setUser($user)
|
||||
->setRate(round(($duration / 3600) * $rate))
|
||||
|
||||
@@ -35,14 +35,14 @@ abstract class AbstractEntityTest extends KernelTestCase
|
||||
$expected = count($fieldNames);
|
||||
$actual = $violations->count();
|
||||
|
||||
$this->assertEquals($expected, $actual, sprintf('Expected %s violations, found %s.', $expected, $actual));
|
||||
|
||||
$violatedFields = [];
|
||||
/** @var ConstraintViolationInterface $validation */
|
||||
foreach ($violations as $validation) {
|
||||
$violatedFields[$validation->getPropertyPath()] = $validation->getPropertyPath();
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, count($violatedFields), sprintf('Expected %s violations, found %s in %s.', $expected, $actual, implode(', ', array_keys($violatedFields))));
|
||||
|
||||
foreach ($fieldNames as $id => $propertyPath) {
|
||||
$foundField = false;
|
||||
if (in_array($propertyPath, $violatedFields)) {
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Tests\Entity;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
/**
|
||||
* @covers \App\Entity\Project
|
||||
@@ -31,6 +32,7 @@ class ProjectTest extends AbstractEntityTest
|
||||
// activities
|
||||
$this->assertNull($sut->getFixedRate());
|
||||
$this->assertNull($sut->getHourlyRate());
|
||||
$this->assertNull($sut->getTimesheets());
|
||||
}
|
||||
|
||||
public function testSetterAndGetter()
|
||||
@@ -64,5 +66,9 @@ class ProjectTest extends AbstractEntityTest
|
||||
$this->assertEquals(13.47, $sut->getFixedRate());
|
||||
$this->assertInstanceOf(Project::class, $sut->setHourlyRate(99));
|
||||
$this->assertEquals(99, $sut->getHourlyRate());
|
||||
|
||||
$timesheets = [(new Timesheet())->setDescription('foo'), (new Timesheet())->setDescription('bar')];
|
||||
$this->assertInstanceOf(Project::class, $sut->setTimesheets($timesheets));
|
||||
$this->assertSame($timesheets, $sut->getTimesheets());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class TimesheetTest extends AbstractEntityTest
|
||||
$this->assertSame(0, $sut->getDuration());
|
||||
$this->assertNull($sut->getUser());
|
||||
$this->assertNull($sut->getActivity());
|
||||
$this->assertNull($sut->getProject());
|
||||
$this->assertNull($sut->getDescription());
|
||||
$this->assertSame(0.00, $sut->getRate());
|
||||
$this->assertNull($sut->getFixedRate());
|
||||
@@ -56,10 +57,52 @@ class TimesheetTest extends AbstractEntityTest
|
||||
$entity = new Timesheet();
|
||||
$entity->setUser(new User());
|
||||
$entity->setActivity($activity);
|
||||
$entity->setProject($project);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function testValidationNeedsActivity()
|
||||
{
|
||||
$entity = new Timesheet();
|
||||
$entity
|
||||
->setUser(new User())
|
||||
->setProject(new Project())
|
||||
->setBegin(new \DateTime())
|
||||
;
|
||||
|
||||
$this->assertHasViolationForField($entity, 'activity');
|
||||
}
|
||||
|
||||
public function testValidationNeedsProject()
|
||||
{
|
||||
$entity = new Timesheet();
|
||||
$entity
|
||||
->setUser(new User())
|
||||
->setActivity(new Activity())
|
||||
->setBegin(new \DateTime())
|
||||
;
|
||||
|
||||
$this->assertHasViolationForField($entity, 'project');
|
||||
}
|
||||
|
||||
public function testValidationProjectMismatch()
|
||||
{
|
||||
$project = (new Project())->setName('foo');
|
||||
$project2 = (new Project())->setName('bar');
|
||||
$activity = (new Activity())->setName('hello-world')->setProject($project);
|
||||
|
||||
$entity = new Timesheet();
|
||||
$entity
|
||||
->setUser(new User())
|
||||
->setActivity($activity)
|
||||
->setProject($project2)
|
||||
->setBegin(new \DateTime())
|
||||
;
|
||||
|
||||
$this->assertHasViolationForField($entity, 'project');
|
||||
}
|
||||
|
||||
public function testValidationEndNotEarlierThanBegin()
|
||||
{
|
||||
$entity = $this->getEntity();
|
||||
|
||||
@@ -72,7 +72,7 @@ abstract class AbstractCalculatorTest extends TestCase
|
||||
->setRate(293.27)
|
||||
->setUser(new User())
|
||||
->setActivity((new Activity())->setName('foo'))
|
||||
;
|
||||
->setProject((new Project())->setName('bar'));
|
||||
|
||||
$model = new InvoiceModel();
|
||||
$model->setCustomer($customer);
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Tests\Invoice\Calculator;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Invoice\Calculator\ShortInvoiceCalculator;
|
||||
@@ -35,8 +36,12 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
$template = new InvoiceTemplate();
|
||||
$template->setVat(19);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('sdfsdf');
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setName('activity description');
|
||||
$activity->setProject($project);
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet
|
||||
@@ -44,6 +49,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
->setRate(293.27)
|
||||
->setUser(new User())
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
@@ -54,6 +60,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
->setRate(84)
|
||||
->setUser(new User())
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
@@ -64,6 +71,7 @@ class ShortInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
->setRate(111.11)
|
||||
->setUser(new User())
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
|
||||
@@ -104,6 +104,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
->setRate(293.27)
|
||||
->setUser($user1)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
@@ -114,6 +115,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
->setRate(84.75)
|
||||
->setUser($user2)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
@@ -124,6 +126,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
->setRate(111.11)
|
||||
->setUser($user1)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
@@ -134,6 +137,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
->setRate(1947.99)
|
||||
->setUser($user2)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
@@ -144,6 +148,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
->setFixedRate(84)
|
||||
->setUser((new User())->setUsername('kevin'))
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
;
|
||||
|
||||
@@ -40,5 +40,12 @@ class ActivityQueryTest extends BaseQueryTest
|
||||
$sut->setProject($expected);
|
||||
|
||||
$this->assertEquals($expected, $sut->getProject());
|
||||
|
||||
// make sure int is allowed as well
|
||||
$sut->setProject(99);
|
||||
$this->assertEquals(99, $sut->getProject());
|
||||
|
||||
$sut->setCustomer(99);
|
||||
$this->assertEquals(99, $sut->getCustomer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,5 +32,9 @@ class ProjectQueryTest extends BaseQueryTest
|
||||
$sut->setCustomer($expected);
|
||||
|
||||
$this->assertEquals($expected, $sut->getCustomer());
|
||||
|
||||
// make sure int is allowed as well
|
||||
$sut->setCustomer(99);
|
||||
$this->assertEquals(99, $sut->getCustomer());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
$repository->stopRecording($entities[0]);
|
||||
}
|
||||
|
||||
public function testStartAndStop()
|
||||
public function testStopRecording()
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
@@ -82,16 +82,10 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
|
||||
|
||||
$fixtures = new TimesheetFixtures();
|
||||
$fixtures->setUser($user);
|
||||
$fixtures->setAmount(1);
|
||||
$fixtures->setAmountRunning(1);
|
||||
$this->importFixture($em, $fixtures);
|
||||
|
||||
$query = new TimesheetQuery();
|
||||
$query->setResultType(BaseQuery::RESULT_TYPE_OBJECTS);
|
||||
$entities = $repository->findByQuery($query);
|
||||
$activity = $entities[0]->getActivity();
|
||||
|
||||
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||
$timesheet = $repository->startRecording($user, $activity);
|
||||
$timesheet = $repository->find(1);
|
||||
$this->assertInstanceOf(Timesheet::class, $timesheet);
|
||||
$this->assertNull($timesheet->getEnd());
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ class RateCalculatorTest extends TestCase
|
||||
->setHourlyRate($timesheetHourly)
|
||||
->setFixedRate($timesheetFixed)
|
||||
->setActivity($activity)
|
||||
->setProject($project)
|
||||
->setDuration($duration)
|
||||
->setUser($this->getTestUser($userRate))
|
||||
;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Tests\Voter;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
@@ -26,9 +27,9 @@ class TimesheetVoterTest extends TestCase
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testVote(User $user, $allow, $subject, $attributes, $result)
|
||||
public function testVote($user, $roles, $allow, $subject, $attributes, $result)
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $user->getRoles());
|
||||
$token = new UsernamePasswordToken($user, 'foo', 'bar', $roles);
|
||||
|
||||
$accessManager = $this->getMockBuilder(AclDecisionManager::class)->disableOriginalConstructor()->getMock();
|
||||
$accessManager->method('isFullyAuthenticated')->willReturn($allow);
|
||||
@@ -43,13 +44,17 @@ class TimesheetVoterTest extends TestCase
|
||||
{
|
||||
$user0 = $this->getUser(0, User::ROLE_CUSTOMER);
|
||||
$user1 = $this->getUser(1, User::ROLE_USER);
|
||||
$user2 = $this->getUser(1, User::ROLE_TEAMLEAD);
|
||||
$user2 = $this->getUser(2, User::ROLE_TEAMLEAD);
|
||||
|
||||
return [
|
||||
[$user0, false, new Customer(), [TimesheetVoter::EDIT], VoterInterface::ACCESS_ABSTAIN],
|
||||
[$user1, false, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
|
||||
[$user1, false, $this->getTimesheet($user0), [TimesheetVoter::EDIT], VoterInterface::ACCESS_DENIED],
|
||||
[$user2, true, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
|
||||
[$user0, $user0->getRoles(), false, new Customer(), [TimesheetVoter::EDIT], VoterInterface::ACCESS_ABSTAIN],
|
||||
[$user1, $user1->getRoles(), false, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
|
||||
[$user1, $user1->getRoles(), false, $this->getTimesheet($user0), [TimesheetVoter::EDIT], VoterInterface::ACCESS_DENIED],
|
||||
[$user2, $user2->getRoles(), true, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_GRANTED],
|
||||
['foo', [], false, $this->getTimesheet($user1), [TimesheetVoter::EDIT], VoterInterface::ACCESS_DENIED],
|
||||
[$user2, $user2->getRoles(), true, new Activity(), [TimesheetVoter::EDIT], VoterInterface::ACCESS_ABSTAIN],
|
||||
[$user2, $user2->getRoles(), true, $this->getTimesheet($user2), [TimesheetVoter::VIEW], VoterInterface::ACCESS_GRANTED],
|
||||
[$user1, $user1->getRoles(), false, $this->getTimesheet($user2), [TimesheetVoter::VIEW], VoterInterface::ACCESS_DENIED],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -61,6 +66,11 @@ class TimesheetVoterTest extends TestCase
|
||||
return $timesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $role
|
||||
* @return User
|
||||
*/
|
||||
protected function getUser($id, $role)
|
||||
{
|
||||
$user = $this->getMockBuilder(User::class)->getMock();
|
||||
|
||||
Binary file not shown.
@@ -35,3 +35,10 @@ Further readings:
|
||||
|
||||
- [MariaDB - JSON support was added with 10.2.7](https://mariadb.com/kb/en/library/json-data-type/)
|
||||
- [Using JSON fields with Doctrine ORM on PostgreSQL & MySQL](https://symfony.fi/entry/using-json-fields-with-doctrine-orm-on-postgresql-mysql)
|
||||
|
||||
## Why is SQLite not recommended for production usage
|
||||
|
||||
SQLite is a great database engine for testing, but when it comes to production usage it fails due to several reasons:
|
||||
|
||||
- It does not support ALTER TABLE commands and makes update procedures very clunky and problematic/errorsome (we still try to support updates, but they are heavy on large databases)
|
||||
- It does not support FOREIGN KEY constraints out of the box, which can lead to critical bugs when deleting activities/projects/customers
|
||||
|
||||
Reference in New Issue
Block a user