added annotation based exporter (#1831)
* added user exporter * added project exporter * added customer exporter * added activity exporter
This commit is contained in:
@@ -16,6 +16,9 @@ use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ActivityMetaDefinitionEvent;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Form\ActivityEditForm;
|
||||
use App\Form\ActivityRateForm;
|
||||
use App\Form\Toolbar\ActivityToolbarForm;
|
||||
@@ -256,6 +259,34 @@ final class ActivityController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="activity_export", methods={"GET"})
|
||||
*/
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new ActivityQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
$entries = $this->repository->getActivitiesForQuery($query);
|
||||
|
||||
$spreadsheet = $exporter->export(
|
||||
Activity::class,
|
||||
$entries,
|
||||
new ActivityMetaDisplayEvent($query, ActivityMetaDisplayEvent::EXPORT)
|
||||
);
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-activities');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param Request $request
|
||||
|
||||
@@ -14,10 +14,12 @@ use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\CustomerRate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Form\CustomerCommentForm;
|
||||
use App\Form\CustomerEditForm;
|
||||
use App\Form\CustomerRateForm;
|
||||
@@ -407,6 +409,34 @@ final class CustomerController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="customer_export", methods={"GET"})
|
||||
*/
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
$entries = $this->repository->getCustomersForQuery($query);
|
||||
|
||||
$spreadsheet = $exporter->export(
|
||||
Customer::class,
|
||||
$entries,
|
||||
new CustomerMetaDisplayEvent($query, CustomerMetaDisplayEvent::EXPORT)
|
||||
);
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-customers');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param Request $request
|
||||
|
||||
@@ -19,6 +19,9 @@ use App\Entity\Rate;
|
||||
use App\Entity\Team;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Form\ProjectCommentForm;
|
||||
use App\Form\ProjectEditForm;
|
||||
use App\Form\ProjectRateForm;
|
||||
@@ -418,6 +421,34 @@ final class ProjectController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="project_export", methods={"GET"})
|
||||
*/
|
||||
public function exportAction(Request $request, EntityWithMetaFieldsExporter $exporter)
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
$entries = $this->repository->getProjectsForQuery($query);
|
||||
|
||||
$spreadsheet = $exporter->export(
|
||||
Project::class,
|
||||
$entries,
|
||||
new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT)
|
||||
);
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-projects');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Controller;
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\UserExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use App\Form\Toolbar\UserToolbarForm;
|
||||
use App\Form\UserCreateType;
|
||||
use App\Repository\Query\UserQuery;
|
||||
@@ -65,7 +68,6 @@ final class UserController extends AbstractController
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="admin_user", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated", methods={"GET"})
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
public function indexAction($page, Request $request): Response
|
||||
{
|
||||
@@ -191,6 +193,34 @@ final class UserController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/export", name="user_export", methods={"GET"})
|
||||
* @Security("is_granted('view_user')")
|
||||
*/
|
||||
public function exportAction(Request $request, UserExporter $exporter)
|
||||
{
|
||||
$query = new UserQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
if (!$form->isValid()) {
|
||||
$query->resetByFormError($form->getErrors());
|
||||
}
|
||||
|
||||
$entries = $this->getRepository()->getUsersForQuery($query);
|
||||
|
||||
$spreadsheet = $exporter->export(
|
||||
$entries,
|
||||
new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT)
|
||||
);
|
||||
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-users');
|
||||
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
protected function getToolbarForm(UserQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(UserToolbarForm::class, $query, [
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -44,6 +45,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @Serializer\Groups({"Default"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "project", "budget", "timeBudget", "color", "visible", "comment"})
|
||||
* @Exporter\Expose("project", label="label.project", exp="object.getProject() === null ? null : object.getProject().getName()")
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
class Activity implements EntityWithMetaFields
|
||||
{
|
||||
@@ -55,6 +60,8 @@ class Activity implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
@@ -75,6 +82,8 @@ class Activity implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.name")
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=150, allowEmptyString=false)
|
||||
@@ -88,6 +97,8 @@ class Activity implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
@@ -99,6 +110,8 @@ class Activity implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.visible", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -115,6 +128,8 @@ class Activity implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.budget")
|
||||
*
|
||||
* @ORM\Column(name="budget", type="float", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -127,6 +142,8 @@ class Activity implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
*
|
||||
* @ORM\Column(name="time_budget", type="integer", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -23,6 +24,8 @@ trait ColorTrait
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.color")
|
||||
*
|
||||
* @ORM\Column(name="color", type="string", length=7, nullable=true)
|
||||
* @Assert\Length(min=4, max=7)
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -25,6 +26,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "company", "number", "vatId", "address", "contact","email", "phone", "mobile", "fax", "homepage", "country", "currency", "timezone", "budget", "timeBudget", "color", "visible", "teams", "comment"})
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
class Customer implements EntityWithMetaFields
|
||||
{
|
||||
@@ -36,6 +40,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
@@ -47,6 +53,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.name")
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=150, allowEmptyString=false)
|
||||
@@ -58,6 +66,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.number")
|
||||
*
|
||||
* @ORM\Column(name="number", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
@@ -68,6 +78,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
@@ -77,6 +89,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.visible", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -87,6 +101,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.company")
|
||||
*
|
||||
* @ORM\Column(name="company", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
@@ -97,6 +113,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.vat_id")
|
||||
*
|
||||
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
@@ -107,6 +125,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.contact")
|
||||
*
|
||||
* @ORM\Column(name="contact", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
@@ -117,6 +137,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.address")
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
@@ -126,6 +148,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.country")
|
||||
*
|
||||
* @ORM\Column(name="country", type="string", length=2, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=2)
|
||||
@@ -137,6 +161,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.currency")
|
||||
*
|
||||
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=3)
|
||||
@@ -148,6 +174,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.phone")
|
||||
*
|
||||
* @ORM\Column(name="phone", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
@@ -158,6 +186,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.fax")
|
||||
*
|
||||
* @ORM\Column(name="fax", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
@@ -168,6 +198,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.mobile")
|
||||
*
|
||||
* @ORM\Column(name="mobile", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
@@ -182,6 +214,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.email")
|
||||
*
|
||||
* @ORM\Column(name="email", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=254)
|
||||
*/
|
||||
@@ -192,6 +226,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.homepage")
|
||||
*
|
||||
* @ORM\Column(name="homepage", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
@@ -206,6 +242,8 @@ class Customer implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.timezone")
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=64)
|
||||
@@ -216,25 +254,29 @@ class Customer implements EntityWithMetaFields
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* The total monetary budget, will be zero if unconfigured.
|
||||
* The total monetary budget, will be zero if not configured.
|
||||
*
|
||||
* @var float
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.budget")
|
||||
*
|
||||
* @ORM\Column(name="budget", type="float", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $budget = 0.00;
|
||||
/**
|
||||
* The time budget in seconds, will be be zero if unconfigured.
|
||||
* The time budget in seconds, will be be zero if not configured.
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
*
|
||||
* @ORM\Column(name="time_budget", type="integer", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -46,6 +47,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @Serializer\Groups({"Project", "Team", "Not_Expanded"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "customer", "orderNumber", "orderDate", "start", "end", "budget", "timeBudget", "color", "visible", "teams", "comment"})
|
||||
* @Exporter\Expose("customer", label="label.customer", exp="object.getCustomer() === null ? null : object.getCustomer().getName()")
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
class Project implements EntityWithMetaFields
|
||||
{
|
||||
@@ -57,6 +62,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
@@ -84,6 +91,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.name")
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=2, max=150, allowEmptyString=false)
|
||||
@@ -97,6 +106,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.orderNumber")
|
||||
*
|
||||
* @ORM\Column(name="order_number", type="text", length=20, nullable=true)
|
||||
* @Assert\Length(max=20)
|
||||
*/
|
||||
@@ -108,6 +119,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
*
|
||||
* @Exporter\Expose(label="label.orderDate", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="order_date", type="datetime", nullable=true)
|
||||
*/
|
||||
private $orderDate;
|
||||
@@ -118,6 +131,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Groups({"Project"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
*
|
||||
* @Exporter\Expose(label="label.project_start", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="start", type="datetime", nullable=true)
|
||||
*/
|
||||
private $start;
|
||||
@@ -128,6 +143,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Groups({"Project"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
*
|
||||
* @Exporter\Expose(label="label.project_end", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="end", type="datetime", nullable=true)
|
||||
*/
|
||||
private $end;
|
||||
@@ -149,6 +166,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
@@ -158,6 +177,8 @@ class Project implements EntityWithMetaFields
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.visible", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
@@ -167,25 +188,29 @@ class Project implements EntityWithMetaFields
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* The total monetary budget, will be zero if unconfigured.
|
||||
* The total monetary budget, will be zero if not configured.
|
||||
*
|
||||
* @var float
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.budget")
|
||||
*
|
||||
* @ORM\Column(name="budget", type="float", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $budget = 0.00;
|
||||
/**
|
||||
* The time budget in seconds, will be be zero if unconfigured.
|
||||
* The time budget in seconds, will be be zero if not configured.
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
*
|
||||
* @ Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
*
|
||||
* @ORM\Column(name="time_budget", type="integer", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Utils\StringHelper;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -49,6 +50,16 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "username", "alias", "title", "email", "last_login", "language", "timezone", "active", "registeredAt", "roles", "teams"})
|
||||
* @Exporter\Expose("email", label="label.email", exp="object.getEmail()")
|
||||
* @Exporter\Expose("username", label="label.username", exp="object.getUsername()")
|
||||
* @Exporter\Expose("timezone", label="label.timezone", exp="object.getTimezone()")
|
||||
* @Exporter\Expose("language", label="label.language", exp="object.getLanguage()")
|
||||
* @Exporter\Expose("last_login", label="label.lastLogin", exp="object.getLastLogin()", type="datetime")
|
||||
* @Exporter\Expose("roles", label="label.roles", exp="object.getRoles()", type="array")
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
* @Exporter\Expose("active", label="label.active", exp="object.isEnabled()", type="boolean")
|
||||
*/
|
||||
class User extends BaseUser implements UserInterface
|
||||
{
|
||||
@@ -73,6 +84,8 @@ class User extends BaseUser implements UserInterface
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
@@ -86,6 +99,8 @@ class User extends BaseUser implements UserInterface
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.alias")
|
||||
*
|
||||
* @ORM\Column(name="alias", type="string", length=60, nullable=true)
|
||||
* @Assert\Length(max=60)
|
||||
*/
|
||||
@@ -95,6 +110,8 @@ class User extends BaseUser implements UserInterface
|
||||
*
|
||||
* @var \DateTime
|
||||
*
|
||||
* @Exporter\Expose(label="profile.registration_date", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="registration_date", type="datetime", nullable=true)
|
||||
*/
|
||||
private $registeredAt;
|
||||
@@ -106,6 +123,8 @@ class User extends BaseUser implements UserInterface
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.title")
|
||||
*
|
||||
* @ORM\Column(name="title", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
@@ -179,7 +198,7 @@ class User extends BaseUser implements UserInterface
|
||||
* This flag will be initialized in UserEnvironmentSubscriber.
|
||||
*
|
||||
* @var bool|null
|
||||
* @internal has no database mapping. as the value is calculated from a permission
|
||||
* @internal has no database mapping as the value is calculated from a permission
|
||||
*/
|
||||
private $isAllowedToSeeAllData = null;
|
||||
|
||||
|
||||
76
src/Event/AbstractMetaDisplayEvent.php
Normal file
76
src/Event/AbstractMetaDisplayEvent.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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\Event;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
abstract class AbstractMetaDisplayEvent extends Event implements MetaDisplayEventInterface
|
||||
{
|
||||
/**
|
||||
* @var BaseQuery
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $location;
|
||||
/**
|
||||
* @var MetaTableTypeInterface[]
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(BaseQuery $query, string $location)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* To filter where your meta-field will be displayed, use the query settings.
|
||||
*
|
||||
* @return BaseQuery
|
||||
*/
|
||||
public function getQuery(): BaseQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, check the current location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation(): string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new meta field that should be included.
|
||||
*
|
||||
* @param MetaTableTypeInterface $meta
|
||||
*/
|
||||
public function addField(MetaTableTypeInterface $meta)
|
||||
{
|
||||
$this->fields[] = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all meta-fields to be included.
|
||||
*
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
}
|
||||
@@ -9,68 +9,20 @@
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for a activity query.
|
||||
*
|
||||
* @method ActivityQuery getQuery()
|
||||
*/
|
||||
final class ActivityMetaDisplayEvent extends Event implements MetaDisplayEventInterface
|
||||
final class ActivityMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
public const EXPORT = 'export';
|
||||
public const ACTIVITY = 'activity';
|
||||
|
||||
/**
|
||||
* @var ActivityQuery
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $location;
|
||||
/**
|
||||
* @var MetaTableTypeInterface[]
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(ActivityQuery $query, string $location)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, use the query settings.
|
||||
*
|
||||
* @return ActivityQuery
|
||||
*/
|
||||
public function getQuery(): BaseQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, check the current location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation(): string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function addField(MetaTableTypeInterface $meta)
|
||||
{
|
||||
$this->fields[] = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
parent::__construct($query, $location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,68 +9,20 @@
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for a customer query.
|
||||
*
|
||||
* @method CustomerQuery getQuery()
|
||||
*/
|
||||
final class CustomerMetaDisplayEvent extends Event implements MetaDisplayEventInterface
|
||||
final class CustomerMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
public const EXPORT = 'export';
|
||||
public const CUSTOMER = 'customer';
|
||||
|
||||
/**
|
||||
* @var CustomerQuery
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $location;
|
||||
/**
|
||||
* @var MetaTableTypeInterface[]
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(CustomerQuery $query, string $location)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, use the query settings.
|
||||
*
|
||||
* @return CustomerQuery
|
||||
*/
|
||||
public function getQuery(): BaseQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, check the current location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation(): string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function addField(MetaTableTypeInterface $meta)
|
||||
{
|
||||
$this->fields[] = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
parent::__construct($query, $location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,68 +9,20 @@
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for a project query.
|
||||
*
|
||||
* @method ProjectQuery getQuery()
|
||||
*/
|
||||
final class ProjectMetaDisplayEvent extends Event implements MetaDisplayEventInterface
|
||||
final class ProjectMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
public const EXPORT = 'export';
|
||||
public const PROJECT = 'project';
|
||||
|
||||
/**
|
||||
* @var ProjectQuery
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $location;
|
||||
/**
|
||||
* @var MetaTableTypeInterface[]
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(ProjectQuery $query, string $location)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, use the query settings.
|
||||
*
|
||||
* @return ProjectQuery
|
||||
*/
|
||||
public function getQuery(): BaseQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, check the current location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation(): string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function addField(MetaTableTypeInterface $meta)
|
||||
{
|
||||
$this->fields[] = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
parent::__construct($query, $location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,14 @@
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Repository\Query\BaseQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Dynamically find possible meta fields for a timesheet query.
|
||||
*
|
||||
* @method TimesheetQuery getQuery()
|
||||
*/
|
||||
final class TimesheetMetaDisplayEvent extends Event implements MetaDisplayEventInterface
|
||||
final class TimesheetMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||
{
|
||||
public const EXPORT = 'export';
|
||||
public const TIMESHEET = 'timesheet';
|
||||
@@ -25,55 +24,8 @@ final class TimesheetMetaDisplayEvent extends Event implements MetaDisplayEventI
|
||||
public const TIMESHEET_EXPORT = 'timesheet-export';
|
||||
public const TEAM_TIMESHEET_EXPORT = 'team-timesheet-export';
|
||||
|
||||
/**
|
||||
* @var TimesheetQuery
|
||||
*/
|
||||
private $query;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $location;
|
||||
/**
|
||||
* @var MetaTableTypeInterface[]
|
||||
*/
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(TimesheetQuery $query, string $location)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->location = $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, use the query settings.
|
||||
*
|
||||
* @return TimesheetQuery
|
||||
*/
|
||||
public function getQuery(): BaseQuery
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, check the current location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation(): string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function addField(MetaTableTypeInterface $meta)
|
||||
{
|
||||
$this->fields[] = $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
parent::__construct($query, $location);
|
||||
}
|
||||
}
|
||||
|
||||
57
src/Export/Annotation/Expose.php
Normal file
57
src/Export/Annotation/Expose.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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 App\Export\Annotation;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Enum;
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
|
||||
/**
|
||||
* Annotation class for @Expose().
|
||||
*
|
||||
* @Annotation
|
||||
* @Target({"CLASS", "PROPERTY", "METHOD"})
|
||||
*/
|
||||
final class Expose
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Required
|
||||
*/
|
||||
public $label;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
/**
|
||||
* @Enum({"string", "datetime", "date", "time", "integer", "float", "duration", "boolean", "array"})
|
||||
*/
|
||||
public $type = 'string';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $exp = null;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
if (isset($data['value'])) {
|
||||
$this->name = $data['value'];
|
||||
unset($data['value']);
|
||||
}
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (!property_exists(self::class, $key)) {
|
||||
throw new \InvalidArgumentException(sprintf('Unknown property "%s" on annotation "%s".', $key, self::class));
|
||||
}
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Export/Annotation/Order.php
Normal file
32
src/Export/Annotation/Order.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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 App\Export\Annotation;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS"})
|
||||
*/
|
||||
final class Order
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public $order = [];
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
if (isset($data['value'])) {
|
||||
$this->order = $data['value'];
|
||||
unset($data['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Export/Spreadsheet/AnnotatedObjectExporter.php
Normal file
32
src/Export/Spreadsheet/AnnotatedObjectExporter.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Export\Spreadsheet;
|
||||
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class AnnotatedObjectExporter
|
||||
{
|
||||
private $spreadsheetExporter;
|
||||
private $annotationExtractor;
|
||||
|
||||
public function __construct(SpreadsheetExporter $spreadsheetExporter, AnnotationExtractor $annotationExtractor)
|
||||
{
|
||||
$this->spreadsheetExporter = $spreadsheetExporter;
|
||||
$this->annotationExtractor = $annotationExtractor;
|
||||
}
|
||||
|
||||
public function export(string $class, array $entries): Spreadsheet
|
||||
{
|
||||
$columns = $this->annotationExtractor->extract($class);
|
||||
|
||||
return $this->spreadsheetExporter->export($columns, $entries);
|
||||
}
|
||||
}
|
||||
30
src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php
Normal file
30
src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class ArrayFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_array($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only array is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, implode(';', $value));
|
||||
}
|
||||
}
|
||||
30
src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php
Normal file
30
src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class BooleanFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_bool($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only boolean is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
interface CellFormatterInterface
|
||||
{
|
||||
/**
|
||||
* @param Worksheet $sheet
|
||||
* @param int $column
|
||||
* @param int $row
|
||||
* @param mixed $value
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void;
|
||||
}
|
||||
33
src/Export/Spreadsheet/CellFormatter/DateFormatter.php
Normal file
33
src/Export/Spreadsheet/CellFormatter/DateFormatter.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DateFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||
}
|
||||
}
|
||||
34
src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php
Normal file
34
src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DateTimeFormatter implements CellFormatterInterface
|
||||
{
|
||||
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
|
||||
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
}
|
||||
31
src/Export/Spreadsheet/CellFormatter/DurationFormatter.php
Normal file
31
src/Export/Spreadsheet/CellFormatter/DurationFormatter.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DurationFormatter implements CellFormatterInterface
|
||||
{
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$value = 0;
|
||||
}
|
||||
|
||||
if (!\is_int($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only int is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
}
|
||||
34
src/Export/Spreadsheet/CellFormatter/TimeFormatter.php
Normal file
34
src/Export/Spreadsheet/CellFormatter/TimeFormatter.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class TimeFormatter implements CellFormatterInterface
|
||||
{
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
}
|
||||
39
src/Export/Spreadsheet/ColumnDefinition.php
Normal file
39
src/Export/Spreadsheet/ColumnDefinition.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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\Export\Spreadsheet;
|
||||
|
||||
final class ColumnDefinition
|
||||
{
|
||||
private $label;
|
||||
private $type;
|
||||
private $accessor;
|
||||
|
||||
public function __construct(string $label, string $type, callable $accessor)
|
||||
{
|
||||
$this->label = $label;
|
||||
$this->type = $type;
|
||||
$this->accessor = $accessor;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getAccessor(): callable
|
||||
{
|
||||
return $this->accessor;
|
||||
}
|
||||
}
|
||||
36
src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php
Normal file
36
src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Export\Spreadsheet;
|
||||
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\MetaFieldExtractor;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class EntityWithMetaFieldsExporter
|
||||
{
|
||||
private $exporter;
|
||||
private $annotationExtractor;
|
||||
private $metaFieldExtractor;
|
||||
|
||||
public function __construct(SpreadsheetExporter $exporter, AnnotationExtractor $annotationExtractor, MetaFieldExtractor $metaFieldExtractor)
|
||||
{
|
||||
$this->exporter = $exporter;
|
||||
$this->annotationExtractor = $annotationExtractor;
|
||||
$this->metaFieldExtractor = $metaFieldExtractor;
|
||||
}
|
||||
|
||||
public function export(string $class, array $entries, MetaDisplayEventInterface $event): Spreadsheet
|
||||
{
|
||||
$columns = array_merge($this->annotationExtractor->extract($class), $this->metaFieldExtractor->extract($event));
|
||||
|
||||
return $this->exporter->export($columns, $entries);
|
||||
}
|
||||
}
|
||||
150
src/Export/Spreadsheet/Extractor/AnnotationExtractor.php
Normal file
150
src/Export/Spreadsheet/Extractor/AnnotationExtractor.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?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\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Export\Annotation\Expose;
|
||||
use App\Export\Annotation\Order;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AnnotationExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @var ExpressionLanguage
|
||||
*/
|
||||
private $expressionLanguage;
|
||||
/**
|
||||
* @var Reader
|
||||
*/
|
||||
private $annotationReader;
|
||||
|
||||
public function __construct(Reader $annotationReader)
|
||||
{
|
||||
$this->annotationReader = $annotationReader;
|
||||
$this->expressionLanguage = new ExpressionLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
{
|
||||
if (!\is_string($value)) {
|
||||
throw new ExtractorException('AnnotationExtractor needs a class name (string) for work');
|
||||
}
|
||||
|
||||
try {
|
||||
$reflectionClass = new \ReflectionClass($value);
|
||||
} catch (\ReflectionException $ex) {
|
||||
throw new ExtractorException($ex->getMessage());
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
|
||||
if (null !== ($definitions = $this->annotationReader->getClassAnnotations($reflectionClass))) {
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Order) {
|
||||
foreach ($definition->order as $columnName) {
|
||||
$columns[$columnName] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Expose) {
|
||||
if (null === $definition->name) {
|
||||
throw new ExtractorException(sprintf('@Expose needs a name attribute on class level hierarchy, check %s::class', $value));
|
||||
}
|
||||
if (null === $definition->exp) {
|
||||
throw new ExtractorException(sprintf('@Expose needs an expression attribute on class level hierarchy, check %s::class', $value));
|
||||
}
|
||||
|
||||
$parsed = $this->expressionLanguage->parse($definition->exp, ['object']);
|
||||
|
||||
$columns[$definition->name] = new ColumnDefinition(
|
||||
$definition->label,
|
||||
$definition->type,
|
||||
function ($obj) use ($parsed) {
|
||||
return $parsed->getNodes()->evaluate([], ['object' => $obj]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getProperties() as $property) {
|
||||
if (null !== ($definitions = $this->annotationReader->getPropertyAnnotations($property))) {
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Expose) {
|
||||
if (null !== $definition->exp) {
|
||||
throw new ExtractorException(sprintf('@Expose only supports the expression attribute on class level hierarchy, check %s::$%s', $value, $property->getName()));
|
||||
}
|
||||
|
||||
$name = empty($definition->name) ? $property->getName() : $definition->name;
|
||||
|
||||
$columns[$name] = new ColumnDefinition(
|
||||
$definition->label,
|
||||
$definition->type,
|
||||
function ($obj) use ($property) {
|
||||
if (!$property->isPublic()) {
|
||||
$property->setAccessible(true);
|
||||
}
|
||||
|
||||
return $property->getValue($obj);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getMethods() as $method) {
|
||||
if (null !== ($definitions = $this->annotationReader->getMethodAnnotations($method))) {
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Expose) {
|
||||
if (null !== $definition->exp) {
|
||||
throw new ExtractorException(sprintf('@Expose only supports the expression attribute on class level hierarchy, check %s::%s()', $value, $method->getName()));
|
||||
}
|
||||
$name = empty($definition->name) ? $method->getName() : $definition->name;
|
||||
|
||||
if (\count($method->getParameters()) > 0) {
|
||||
throw new ExtractorException(sprintf('@Expose does not support method %s::%s(...), it has required parameters.', $value, $method->getName()));
|
||||
}
|
||||
|
||||
$columns[$name] = new ColumnDefinition(
|
||||
$definition->label,
|
||||
$definition->type,
|
||||
function ($obj) use ($method) {
|
||||
if (!$method->isPublic()) {
|
||||
$method->setAccessible(true);
|
||||
}
|
||||
|
||||
return $method->invoke($obj);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($columns as $name => $definition) {
|
||||
if (null === $definition) {
|
||||
unset($columns[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($columns);
|
||||
}
|
||||
}
|
||||
14
src/Export/Spreadsheet/Extractor/ExtractorException.php
Normal file
14
src/Export/Spreadsheet/Extractor/ExtractorException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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\Export\Spreadsheet\Extractor;
|
||||
|
||||
class ExtractorException extends \Exception
|
||||
{
|
||||
}
|
||||
25
src/Export/Spreadsheet/Extractor/ExtractorInterface.php
Normal file
25
src/Export/Spreadsheet/Extractor/ExtractorInterface.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
|
||||
/**
|
||||
* Extract ColumnDefinition objects from various sources.
|
||||
*/
|
||||
interface ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array;
|
||||
}
|
||||
68
src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
Normal file
68
src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Entity\EntityWithMetaFields;
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MetaFieldExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetaDisplayEventInterface $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
{
|
||||
if (!($value instanceof MetaDisplayEventInterface)) {
|
||||
throw new ExtractorException('MetaFieldExtractor needs a MetaDisplayEventInterface instance for work');
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
|
||||
$this->eventDispatcher->dispatch($value);
|
||||
|
||||
foreach ($value->getFields() as $field) {
|
||||
if (!$field->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columns[] = new ColumnDefinition(
|
||||
$field->getLabel(),
|
||||
'string',
|
||||
function (EntityWithMetaFields $entityWithMetaFields) use ($field) {
|
||||
$meta = $entityWithMetaFields->getMetaField($field->getName());
|
||||
if (null === $meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $meta->getValue();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
68
src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
Normal file
68
src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class UserPreferenceExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserPreferenceDisplayEvent $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
{
|
||||
if (!($value instanceof UserPreferenceDisplayEvent)) {
|
||||
throw new ExtractorException('UserPreferenceExtractor needs a UserPreferenceDisplayEvent instance for work');
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
|
||||
$this->eventDispatcher->dispatch($value);
|
||||
|
||||
foreach ($value->getPreferences() as $field) {
|
||||
if (!$field->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columns[] = new ColumnDefinition(
|
||||
$field->getLabel(),
|
||||
'string',
|
||||
function (User $user) use ($field) {
|
||||
$meta = $user->getPreference($field->getName());
|
||||
if (null === $meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $meta->getValue();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
100
src/Export/Spreadsheet/SpreadsheetExporter.php
Normal file
100
src/Export/Spreadsheet/SpreadsheetExporter.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?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\Export\Spreadsheet;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\ArrayFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\BooleanFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SpreadsheetExporter
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var CellFormatterInterface[]
|
||||
*/
|
||||
private $formatter = [];
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
|
||||
$this->registerCellFormatter('datetime', new DateTimeFormatter());
|
||||
$this->registerCellFormatter('date', new DateFormatter());
|
||||
$this->registerCellFormatter('time', new TimeFormatter());
|
||||
$this->registerCellFormatter('duration', new DurationFormatter());
|
||||
$this->registerCellFormatter('boolean', new BooleanFormatter());
|
||||
$this->registerCellFormatter('array', new ArrayFormatter());
|
||||
}
|
||||
|
||||
public function registerCellFormatter(string $type, CellFormatterInterface $formatter)
|
||||
{
|
||||
$this->formatter[$type] = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ColumnDefinition[] $columns
|
||||
* @param array $entries
|
||||
* @return Spreadsheet
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*/
|
||||
public function export(array $columns, array $entries): Spreadsheet
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// Set default row height to automatic, so we can specify wrap text columns later on
|
||||
// without bloating the output file as we would need to store stylesheet info for every cell.
|
||||
// LibreOffice is still not considering this flag, @see https://github.com/PHPOffice/PHPExcel/issues/588
|
||||
// with no solution implemented so nothing we can do about it there.
|
||||
$sheet->getDefaultRowDimension()->setRowHeight(-1);
|
||||
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
foreach ($columns as $settings) {
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($settings->getLabel()));
|
||||
}
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$entryHeaderColumn = 1;
|
||||
|
||||
foreach ($columns as $settings) {
|
||||
$value = \call_user_func($settings->getAccessor(), $entry);
|
||||
|
||||
if (!\array_key_exists($settings->getType(), $this->formatter)) {
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $value);
|
||||
} else {
|
||||
$formatter = $this->formatter[$settings->getType()];
|
||||
$formatter->setFormattedValue($sheet, $entryHeaderColumn, $entryHeaderRow, $value);
|
||||
}
|
||||
|
||||
$entryHeaderColumn++;
|
||||
}
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
}
|
||||
47
src/Export/Spreadsheet/UserExporter.php
Normal file
47
src/Export/Spreadsheet/UserExporter.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Export\Spreadsheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\UserPreferenceExtractor;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class UserExporter
|
||||
{
|
||||
private $exporter;
|
||||
private $annotationExtractor;
|
||||
private $userPreferenceExtractor;
|
||||
|
||||
public function __construct(SpreadsheetExporter $exporter, AnnotationExtractor $annotationExtractor, UserPreferenceExtractor $userPreferenceExtractor)
|
||||
{
|
||||
$this->exporter = $exporter;
|
||||
$this->annotationExtractor = $annotationExtractor;
|
||||
$this->userPreferenceExtractor = $userPreferenceExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User[] $entries
|
||||
* @param UserPreferenceDisplayEvent $event
|
||||
* @return Spreadsheet
|
||||
* @throws Extractor\ExtractorException
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*/
|
||||
public function export(array $entries, UserPreferenceDisplayEvent $event): Spreadsheet
|
||||
{
|
||||
$columns = array_merge(
|
||||
$this->annotationExtractor->extract(User::class),
|
||||
$this->userPreferenceExtractor->extract($event)
|
||||
);
|
||||
|
||||
return $this->exporter->export($columns, $entries);
|
||||
}
|
||||
}
|
||||
71
src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php
Normal file
71
src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?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\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
|
||||
class BinaryFileResponseWriter implements WriterInterface
|
||||
{
|
||||
/**
|
||||
* @var WriterInterface
|
||||
*/
|
||||
private $writer;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $prefix;
|
||||
|
||||
/**
|
||||
* @param WriterInterface $writer
|
||||
* @param string $prefix is only urlencoded but not validated and can break the response if you pass in invalid character
|
||||
*/
|
||||
public function __construct(WriterInterface $writer, string $prefix)
|
||||
{
|
||||
$this->writer = $writer;
|
||||
$this->prefix = urlencode($prefix);
|
||||
}
|
||||
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return $this->writer->getFileExtension();
|
||||
}
|
||||
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->writer->getContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Spreadsheet $spreadsheet, array $options = []): \SplFileInfo
|
||||
{
|
||||
return $this->writer->save($spreadsheet, $options);
|
||||
}
|
||||
|
||||
public function getFileResponse(Spreadsheet $spreadsheet, array $options = []): BinaryFileResponse
|
||||
{
|
||||
$file = $this->save($spreadsheet, $options);
|
||||
|
||||
$filename = $this->prefix . '_' . (new \DateTime())->format('YmdHim') . '.' . $this->writer->getFileExtension();
|
||||
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
$response->headers->set('Content-Type', $this->getContentType());
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
$response->deleteFileAfterSend(true);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
28
src/Export/Spreadsheet/Writer/WriterInterface.php
Normal file
28
src/Export/Spreadsheet/Writer/WriterInterface.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
interface WriterInterface
|
||||
{
|
||||
public function getFileExtension(): string;
|
||||
|
||||
public function getContentType(): string;
|
||||
|
||||
/**
|
||||
* Save the given spreadsheet
|
||||
*
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @param array $options
|
||||
* @return \SplFileInfo
|
||||
*/
|
||||
public function save(Spreadsheet $spreadsheet, array $options = []): \SplFileInfo;
|
||||
}
|
||||
90
src/Export/Spreadsheet/Writer/XlsxWriter.php
Normal file
90
src/Export/Spreadsheet/Writer/XlsxWriter.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
|
||||
class XlsxWriter implements WriterInterface
|
||||
{
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
* - freeze (string, default: null) Coordinate of a column to freeze, like: D2
|
||||
* - autofilter (bool, default true) Enable auto filter for header row
|
||||
*
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @param array $options
|
||||
* @return \SplFileInfo
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function save(Spreadsheet $spreadsheet, array $options = []): \SplFileInfo
|
||||
{
|
||||
$options = array_merge(['autofilter' => true, 'freeze' => null], $options);
|
||||
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
// Store expensive calculations for later
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
$highestColumn = $sheet->getHighestColumn();
|
||||
|
||||
// Enable auto filter for header row
|
||||
if (false !== $options['autofilter']) {
|
||||
$sheet->setAutoFilter('A1:' . $highestColumn . '1');
|
||||
}
|
||||
|
||||
// Freeze first row and date & time columns for easier navigation
|
||||
if (!empty($options['freeze'])) {
|
||||
$sheet->freezePane($options['freeze']);
|
||||
}
|
||||
|
||||
/** @var string $column */
|
||||
foreach (range('A', $highestColumn) as $column) {
|
||||
// We default to a reasonable auto-width decided by the client,
|
||||
// sadly ->getDefaultColumnDimension() is not supported so it needs
|
||||
// to be specific about what column should be auto sized.
|
||||
$col = $sheet->getColumnDimension($column);
|
||||
|
||||
// If no other width is specified (which defaults to -1)
|
||||
if ((int) $col->getWidth() === -1) {
|
||||
$col->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Text inside cells should be top left
|
||||
$sheet
|
||||
->getStyle('A2:' . $highestColumn . $highestRow)
|
||||
->getAlignment()
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
return new \SplFileInfo($filename);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% set actions = {'search': {'class': 'search-toggle visible-xs-inline'}, 'visibility': '#modal_activity_admin'} %}
|
||||
|
||||
{% set actions = actions|merge({'download': {'url': path('activity_export'), 'class': 'toolbar-action'}}) %}
|
||||
|
||||
{% if is_granted('create_activity') %}
|
||||
{% set actions = actions|merge({'create': path('admin_activity_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% set actions = {'search': {'class': 'search-toggle visible-xs-inline'}, 'visibility': '#modal_customer_admin'} %}
|
||||
|
||||
{% set actions = actions|merge({'download': {'url': path('customer_export'), 'class': 'toolbar-action'}}) %}
|
||||
|
||||
{% if is_granted('create_customer') %}
|
||||
{% set actions = actions|merge({'create': path('admin_customer_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
{% set actions = {'search': {'class': 'search-toggle visible-xs-inline'}, 'visibility': '#modal_project_admin'} %}
|
||||
|
||||
{% set actions = actions|merge({'download': {'url': path('project_export'), 'class': 'toolbar-action'}}) %}
|
||||
|
||||
{% if is_granted('create_project') %}
|
||||
{% set actions = actions|merge({'create': path('admin_project_create')}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
{% set actions = actions|merge({'back': path('admin_user')}) %}
|
||||
{% endif %}
|
||||
|
||||
{% set actions = actions|merge({'download': {'url': path('user_export'), 'class': 'toolbar-action'}}) %}
|
||||
|
||||
{% if is_granted('role_permissions') %}
|
||||
{% set actions = actions|merge({'permissions': path('admin_user_permissions')}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
<th>{{ 'profile.first_entry'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ stats.firstEntry|date_short }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ 'profile.registration_date'|trans }}</th>
|
||||
<td class="text-nowrap pull-right">{{ user.registeredAt|date_short }}</td>
|
||||
</tr>
|
||||
{% if is_granted('hourly-rate', user) %}
|
||||
<tr>
|
||||
<th>{{ 'label.hourlyRate'|trans }}</th>
|
||||
|
||||
@@ -73,6 +73,48 @@ class ActivityControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_activity_admin', 5);
|
||||
}
|
||||
|
||||
public function testExportIsSecureForRole()
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/activity/export');
|
||||
}
|
||||
|
||||
public function testExportAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/export');
|
||||
$this->assertExcelExportResponse($client, 'kimai-activities_');
|
||||
}
|
||||
|
||||
public function testExportActionWithSearchTermQuery()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$fixture = new ActivityFixtures();
|
||||
$fixture->setAmount(5);
|
||||
$fixture->setCallback(function (Activity $activity) {
|
||||
$activity->setVisible(true);
|
||||
$activity->setComment('I am a foobar with tralalalala some more content');
|
||||
$activity->setMetaField((new ActivityMeta())->setName('location')->setValue('homeoffice'));
|
||||
$activity->setMetaField((new ActivityMeta())->setName('feature')->setValue('timetracking'));
|
||||
});
|
||||
$this->importFixture($fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/admin/activity/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form.header-search')->form();
|
||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/activity/export'));
|
||||
$client->submit($form, [
|
||||
'searchTerm' => 'feature:timetracking foo',
|
||||
'visibility' => 1,
|
||||
'pageSize' => 50,
|
||||
'customers' => [1],
|
||||
'projects' => [1],
|
||||
'page' => 1,
|
||||
]);
|
||||
|
||||
$this->assertExcelExportResponse($client, 'kimai-activities_');
|
||||
}
|
||||
|
||||
public function testDetailsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\DataFixtures\UserFixtures;
|
||||
use App\Entity\User;
|
||||
use App\Tests\KernelTestTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\HttpKernelBrowser;
|
||||
|
||||
@@ -341,4 +342,15 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
self::assertTrue($client->getResponse()->headers->has('Location'), 'Could not find "Location" header');
|
||||
self::assertStringEndsWith($url, $client->getResponse()->headers->get('Location'), 'Redirect URL does not match');
|
||||
}
|
||||
|
||||
protected function assertExcelExportResponse(HttpKernelBrowser $client, string $prefix)
|
||||
{
|
||||
/** @var BinaryFileResponse $response */
|
||||
$response = $client->getResponse();
|
||||
self::assertInstanceOf(BinaryFileResponse::class, $response);
|
||||
|
||||
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||
self::assertStringContainsString('attachment; filename=' . $prefix, $response->headers->get('Content-Disposition'));
|
||||
self::assertStringContainsString('.xlsx', $response->headers->get('Content-Disposition'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,37 @@ class CustomerControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_customer_admin', 5);
|
||||
}
|
||||
|
||||
public function testExportIsSecureForRole()
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/customer/export');
|
||||
}
|
||||
|
||||
public function testExportAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, '/admin/customer/export');
|
||||
$this->assertExcelExportResponse($client, 'kimai-customers_');
|
||||
}
|
||||
|
||||
public function testExportActionWithSearchTermQuery()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
|
||||
$this->request($client, '/admin/customer/');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form.header-search')->form();
|
||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/customer/export'));
|
||||
$client->submit($form, [
|
||||
'searchTerm' => 'feature:timetracking foo',
|
||||
'visibility' => 1,
|
||||
'pageSize' => 50,
|
||||
'page' => 1,
|
||||
]);
|
||||
|
||||
$this->assertExcelExportResponse($client, 'kimai-customers_');
|
||||
}
|
||||
|
||||
public function testDetailsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
@@ -80,6 +80,47 @@ class ProjectControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_project_admin', 5);
|
||||
}
|
||||
|
||||
public function testExportIsSecureForRole()
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/project/export');
|
||||
}
|
||||
|
||||
public function testExportAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->assertAccessIsGranted($client, '/admin/project/export');
|
||||
$this->assertExcelExportResponse($client, 'kimai-projects_');
|
||||
}
|
||||
|
||||
public function testExportActionWithSearchTermQuery()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$fixture = new ProjectFixtures();
|
||||
$fixture->setAmount(5);
|
||||
$fixture->setCallback(function (Project $project) {
|
||||
$project->setVisible(true);
|
||||
$project->setComment('I am a foobar with tralalalala some more content');
|
||||
$project->setMetaField((new ProjectMeta())->setName('location')->setValue('homeoffice'));
|
||||
$project->setMetaField((new ProjectMeta())->setName('feature')->setValue('timetracking'));
|
||||
});
|
||||
$this->importFixture($fixture);
|
||||
|
||||
$this->assertAccessIsGranted($client, '/admin/project/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form.header-search')->form();
|
||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/project/export'));
|
||||
$client->submit($form, [
|
||||
'searchTerm' => 'feature:timetracking foo',
|
||||
'visibility' => 1,
|
||||
'customers' => [1],
|
||||
'pageSize' => 50,
|
||||
'page' => 1,
|
||||
]);
|
||||
|
||||
$this->assertExcelExportResponse($client, 'kimai-projects_');
|
||||
}
|
||||
|
||||
public function testDetailsAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
@@ -37,6 +37,7 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$this->assertPageActions($client, [
|
||||
'search search-toggle visible-xs-inline' => '#',
|
||||
'visibility' => '#',
|
||||
'download toolbar-action' => $this->createUrl('/admin/user/export'),
|
||||
'permissions' => $this->createUrl('/admin/permissions'),
|
||||
'create' => $this->createUrl('/admin/user/create'),
|
||||
'help' => 'https://www.kimai.org/documentation/users.html'
|
||||
@@ -64,6 +65,38 @@ class UserControllerTest extends ControllerBaseTest
|
||||
$this->assertDataTableRowCount($client, 'datatable_user_admin', 1);
|
||||
}
|
||||
|
||||
public function testExportIsSecureForRole()
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/user/export');
|
||||
}
|
||||
|
||||
public function testExportAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/user/export');
|
||||
$this->assertExcelExportResponse($client, 'kimai-users_');
|
||||
}
|
||||
|
||||
public function testExportActionWithSearchTermQuery()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$this->request($client, '/admin/user/');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$form = $client->getCrawler()->filter('form.header-search')->form();
|
||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/admin/user/export'));
|
||||
$client->submit($form, [
|
||||
'searchTerm' => 'hourly_rate:35 tony',
|
||||
'role' => 'ROLE_TEAMLEAD',
|
||||
'visibility' => 1,
|
||||
'pageSize' => 50,
|
||||
'page' => 1,
|
||||
]);
|
||||
|
||||
$this->assertExcelExportResponse($client, 'kimai-users_');
|
||||
}
|
||||
|
||||
public function testCreateAction()
|
||||
{
|
||||
$username = '亚历山德拉';
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Tests\Entity;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityMeta;
|
||||
use App\Entity\Project;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -90,4 +93,36 @@ class ActivityTest extends TestCase
|
||||
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||
}
|
||||
|
||||
public function testExportAnnotations()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$columns = $sut->extract(Activity::class);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
|
||||
$expected = [
|
||||
['label.id', 'integer'],
|
||||
['label.name', 'string'],
|
||||
['label.project', 'string'],
|
||||
['label.color', 'string'],
|
||||
['label.visible', 'boolean'],
|
||||
['label.comment', 'string'],
|
||||
];
|
||||
|
||||
self::assertCount(\count($expected), $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
|
||||
foreach ($expected as $item) {
|
||||
$column = $columns[$i++];
|
||||
self::assertEquals($item[0], $column->getLabel());
|
||||
self::assertEquals($item[1], $column->getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Tests\Entity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerMeta;
|
||||
use App\Entity\Team;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -145,4 +148,48 @@ class CustomerTest extends TestCase
|
||||
self::assertCount(0, $sut->getTeams());
|
||||
self::assertCount(0, $team->getCustomers());
|
||||
}
|
||||
|
||||
public function testExportAnnotations()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$columns = $sut->extract(Customer::class);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
|
||||
$expected = [
|
||||
['label.id', 'integer'],
|
||||
['label.name', 'string'],
|
||||
['label.company', 'string'],
|
||||
['label.number', 'string'],
|
||||
['label.vat_id', 'string'],
|
||||
['label.address', 'string'],
|
||||
['label.contact', 'string'],
|
||||
['label.email', 'string'],
|
||||
['label.phone', 'string'],
|
||||
['label.mobile', 'string'],
|
||||
['label.fax', 'string'],
|
||||
['label.homepage', 'string'],
|
||||
['label.country', 'string'],
|
||||
['label.currency', 'string'],
|
||||
['label.timezone', 'string'],
|
||||
['label.color', 'string'],
|
||||
['label.visible', 'boolean'],
|
||||
['label.comment', 'string'],
|
||||
];
|
||||
|
||||
self::assertCount(\count($expected), $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
|
||||
foreach ($expected as $item) {
|
||||
$column = $columns[$i++];
|
||||
self::assertEquals($item[0], $column->getLabel());
|
||||
self::assertEquals($item[1], $column->getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectMeta;
|
||||
use App\Entity\Team;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -133,4 +136,40 @@ class ProjectTest extends TestCase
|
||||
self::assertCount(0, $sut->getTeams());
|
||||
self::assertCount(0, $team->getProjects());
|
||||
}
|
||||
|
||||
public function testExportAnnotations()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$columns = $sut->extract(Project::class);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
|
||||
$expected = [
|
||||
['label.id', 'integer'],
|
||||
['label.name', 'string'],
|
||||
['label.customer', 'string'],
|
||||
['label.orderNumber', 'string'],
|
||||
['label.orderDate', 'datetime'],
|
||||
['label.project_start', 'datetime'],
|
||||
['label.project_end', 'datetime'],
|
||||
['label.color', 'string'],
|
||||
['label.visible', 'boolean'],
|
||||
['label.comment', 'string'],
|
||||
];
|
||||
|
||||
self::assertCount(\count($expected), $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
|
||||
foreach ($expected as $item) {
|
||||
$column = $columns[$i++];
|
||||
self::assertEquals($item[0], $column->getLabel());
|
||||
self::assertEquals($item[1], $column->getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Tests\Entity;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -251,4 +254,41 @@ class UserTest extends TestCase
|
||||
self::assertTrue($sut->canSeeAllData());
|
||||
self::assertFalse($sut->initCanSeeAllData(true));
|
||||
}
|
||||
|
||||
public function testExportAnnotations()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$columns = $sut->extract(User::class);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
|
||||
$expected = [
|
||||
['label.id', 'integer'],
|
||||
['label.username', 'string'],
|
||||
['label.alias', 'string'],
|
||||
['label.title', 'string'],
|
||||
['label.email', 'string'],
|
||||
['label.lastLogin', 'datetime'],
|
||||
['label.language', 'string'],
|
||||
['label.timezone', 'string'],
|
||||
['label.active', 'boolean'],
|
||||
['profile.registration_date', 'datetime'],
|
||||
['label.roles', 'array'],
|
||||
];
|
||||
|
||||
self::assertCount(\count($expected), $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
|
||||
foreach ($expected as $item) {
|
||||
$column = $columns[$i++];
|
||||
self::assertEquals($item[0], $column->getLabel());
|
||||
self::assertEquals($item[1], $column->getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Repository\Query\ActivityQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractMetaDisplayEvent
|
||||
* @covers \App\Event\ActivityMetaDisplayEvent
|
||||
*/
|
||||
class ActivityMetaDisplayEventTest extends TestCase
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Repository\Query\CustomerQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractMetaDisplayEvent
|
||||
* @covers \App\Event\CustomerMetaDisplayEvent
|
||||
*/
|
||||
class CustomerMetaDisplayEventTest extends TestCase
|
||||
|
||||
40
tests/Event/ProjectMetaDisplayEventTest.php
Normal file
40
tests/Event/ProjectMetaDisplayEventTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\ProjectMeta;
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractMetaDisplayEvent
|
||||
* @covers \App\Event\ProjectMetaDisplayEvent
|
||||
*/
|
||||
class ProjectMetaDisplayEventTest extends TestCase
|
||||
{
|
||||
public function testGetterAndSetter()
|
||||
{
|
||||
$query = new ProjectQuery();
|
||||
$sut = new ProjectMetaDisplayEvent($query, ProjectMetaDisplayEvent::EXPORT);
|
||||
|
||||
self::assertInstanceOf(MetaDisplayEventInterface::class, $sut);
|
||||
self::assertSame($sut->getQuery(), $query);
|
||||
self::assertIsArray($sut->getFields());
|
||||
self::assertEmpty($sut->getFields());
|
||||
self::assertEquals('export', $sut->getLocation());
|
||||
|
||||
$sut->addField(new ProjectMeta());
|
||||
$sut->addField(new ProjectMeta());
|
||||
|
||||
self::assertCount(2, $sut->getFields());
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use App\Repository\Query\TimesheetQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractMetaDisplayEvent
|
||||
* @covers \App\Event\TimesheetMetaDisplayEvent
|
||||
*/
|
||||
class TimesheetMetaDisplayEventTest extends TestCase
|
||||
|
||||
56
tests/Export/Spreadsheet/AnnotatedObjectExporterTest.php
Normal file
56
tests/Export/Spreadsheet/AnnotatedObjectExporterTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\SpreadsheetExporter;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\AnnotatedObjectExporter
|
||||
*/
|
||||
class AnnotatedObjectExporterTest extends TestCase
|
||||
{
|
||||
public function testExport()
|
||||
{
|
||||
$spreadsheetExporter = new SpreadsheetExporter($this->createMock(TranslatorInterface::class));
|
||||
$annotationExtractor = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('test project');
|
||||
$project->setCustomer((new Customer())->setName('A customer'));
|
||||
$project->setComment('Lorem Ipsum');
|
||||
$project->setOrderNumber('1234567890');
|
||||
$project->setBudget(123456.7890);
|
||||
$project->setTimeBudget(1234567890);
|
||||
$project->setColor('#ababab');
|
||||
$project->setVisible(false);
|
||||
|
||||
$sut = new AnnotatedObjectExporter($spreadsheetExporter, $annotationExtractor);
|
||||
$spreadsheet = $sut->export(Project::class, [$project]);
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(1, 2, false)->getValue());
|
||||
self::assertEquals('test project', $worksheet->getCellByColumnAndRow(2, 2, false)->getValue());
|
||||
self::assertEquals('A customer', $worksheet->getCellByColumnAndRow(3, 2, false)->getValue());
|
||||
self::assertEquals(1234567890, $worksheet->getCellByColumnAndRow(4, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(5, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(6, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(7, 2, false)->getValue());
|
||||
self::assertEquals('#ababab', $worksheet->getCellByColumnAndRow(8, 2, false)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(9, 2, false)->getValue());
|
||||
self::assertEquals('Lorem Ipsum', $worksheet->getCellByColumnAndRow(10, 2, false)->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class AbstractFormatterTest extends TestCase
|
||||
{
|
||||
abstract protected function getFormatter(): CellFormatterInterface;
|
||||
|
||||
abstract protected function getActualValue();
|
||||
|
||||
abstract protected function getExpectedValue();
|
||||
|
||||
protected function assertCellStyle(Style $style)
|
||||
{
|
||||
}
|
||||
|
||||
protected function assertCellValue(Cell $cell)
|
||||
{
|
||||
self::assertEquals($this->getExpectedValue(), $cell->getValue());
|
||||
}
|
||||
|
||||
public function testSetFormattedValue()
|
||||
{
|
||||
$sut = $this->getFormatter();
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut->setFormattedValue($worksheet, 1, 1, $this->getActualValue());
|
||||
$cell = $worksheet->getCellByColumnAndRow(1, 1, false);
|
||||
$this->assertCellValue($cell);
|
||||
$this->assertCellStyle($worksheet->getStyleByColumnAndRow(1, 1));
|
||||
}
|
||||
|
||||
public function testSetNull()
|
||||
{
|
||||
$sut = $this->getFormatter();
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut->setFormattedValue($worksheet, 1, 1, null);
|
||||
$cell = $worksheet->getCellByColumnAndRow(1, 1, false);
|
||||
$this->assertNullValue($cell);
|
||||
}
|
||||
|
||||
protected function assertNullValue(Cell $cell)
|
||||
{
|
||||
self::assertEquals('', $cell->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\ArrayFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\CellFormatter\ArrayFormatter
|
||||
*/
|
||||
class ArrayFormatterTest extends AbstractFormatterTest
|
||||
{
|
||||
protected function getFormatter(): CellFormatterInterface
|
||||
{
|
||||
return new ArrayFormatter();
|
||||
}
|
||||
|
||||
protected function getActualValue()
|
||||
{
|
||||
return ['test', 'foo', 'bar'];
|
||||
}
|
||||
|
||||
protected function getExpectedValue()
|
||||
{
|
||||
return 'test;foo;bar';
|
||||
}
|
||||
|
||||
public function testFormattedValueWithInvalidValue()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unsupported value given, only array is supported');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut = $this->getFormatter();
|
||||
$sut->setFormattedValue($worksheet, 1, 1, 'sdfsdf');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\BooleanFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\CellFormatter\BooleanFormatter
|
||||
*/
|
||||
class BooleanFormatterTest extends AbstractFormatterTest
|
||||
{
|
||||
protected function getFormatter(): CellFormatterInterface
|
||||
{
|
||||
return new BooleanFormatter();
|
||||
}
|
||||
|
||||
protected function getActualValue()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getExpectedValue()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function testFormattedValueWithInvalidValue()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unsupported value given, only boolean is supported');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut = $this->getFormatter();
|
||||
$sut->setFormattedValue($worksheet, 1, 1, 'sdfsdf');
|
||||
}
|
||||
}
|
||||
57
tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php
Normal file
57
tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\CellFormatter\DateFormatter
|
||||
*/
|
||||
class DateFormatterTest extends AbstractFormatterTest
|
||||
{
|
||||
private $date;
|
||||
|
||||
protected function getFormatter(): CellFormatterInterface
|
||||
{
|
||||
return new DateFormatter();
|
||||
}
|
||||
|
||||
protected function getActualValue()
|
||||
{
|
||||
return $this->date = new \DateTime();
|
||||
}
|
||||
|
||||
protected function getExpectedValue()
|
||||
{
|
||||
return Date::PHPToExcel($this->date);
|
||||
}
|
||||
|
||||
public function testFormattedValueWithInvalidValue()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unsupported value given, only DateTime is supported');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut = $this->getFormatter();
|
||||
$sut->setFormattedValue($worksheet, 1, 1, 'sdfsdf');
|
||||
}
|
||||
|
||||
protected function assertCellStyle(Style $style)
|
||||
{
|
||||
self::assertEquals(NumberFormat::FORMAT_DATE_YYYYMMDD2, $style->getNumberFormat()->getFormatCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\CellFormatter\DateTimeFormatter
|
||||
*/
|
||||
class DateTimeFormatterTest extends AbstractFormatterTest
|
||||
{
|
||||
private $date;
|
||||
|
||||
protected function getFormatter(): CellFormatterInterface
|
||||
{
|
||||
return new DateTimeFormatter();
|
||||
}
|
||||
|
||||
protected function getActualValue()
|
||||
{
|
||||
return $this->date = new \DateTime();
|
||||
}
|
||||
|
||||
protected function getExpectedValue()
|
||||
{
|
||||
return Date::PHPToExcel($this->date);
|
||||
}
|
||||
|
||||
public function testFormattedValueWithInvalidValue()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unsupported value given, only DateTime is supported');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut = $this->getFormatter();
|
||||
$sut->setFormattedValue($worksheet, 1, 1, 'sdfsdf');
|
||||
}
|
||||
|
||||
protected function assertCellStyle(Style $style)
|
||||
{
|
||||
self::assertEquals(DateTimeFormatter::DATETIME_FORMAT, $style->getNumberFormat()->getFormatCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\CellFormatter\DurationFormatter
|
||||
*/
|
||||
class DurationFormatterTest extends AbstractFormatterTest
|
||||
{
|
||||
protected function getFormatter(): CellFormatterInterface
|
||||
{
|
||||
return new DurationFormatter();
|
||||
}
|
||||
|
||||
protected function getActualValue()
|
||||
{
|
||||
return 3600;
|
||||
}
|
||||
|
||||
protected function getExpectedValue()
|
||||
{
|
||||
return '=3600/86400';
|
||||
}
|
||||
|
||||
public function testFormattedValueWithInvalidValue()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unsupported value given, only int is supported');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut = $this->getFormatter();
|
||||
$sut->setFormattedValue($worksheet, 1, 1, 'sdfsdf');
|
||||
}
|
||||
|
||||
protected function assertNullValue(Cell $cell)
|
||||
{
|
||||
self::assertEquals('=0/86400', $cell->getValue());
|
||||
}
|
||||
|
||||
protected function assertCellStyle(Style $style)
|
||||
{
|
||||
self::assertEquals(DurationFormatter::DURATION_FORMAT, $style->getNumberFormat()->getFormatCode());
|
||||
}
|
||||
}
|
||||
56
tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php
Normal file
56
tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\CellFormatter\TimeFormatter
|
||||
*/
|
||||
class TimeFormatterTest extends AbstractFormatterTest
|
||||
{
|
||||
private $date;
|
||||
|
||||
protected function getFormatter(): CellFormatterInterface
|
||||
{
|
||||
return new TimeFormatter();
|
||||
}
|
||||
|
||||
protected function getActualValue()
|
||||
{
|
||||
return $this->date = new \DateTime();
|
||||
}
|
||||
|
||||
protected function getExpectedValue()
|
||||
{
|
||||
return Date::PHPToExcel($this->date);
|
||||
}
|
||||
|
||||
public function testFormattedValueWithInvalidValue()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unsupported value given, only DateTime is supported');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sut = $this->getFormatter();
|
||||
$sut->setFormattedValue($worksheet, 1, 1, 'sdfsdf');
|
||||
}
|
||||
|
||||
protected function assertCellStyle(Style $style)
|
||||
{
|
||||
self::assertEquals(TimeFormatter::TIME_FORMAT, $style->getNumberFormat()->getFormatCode());
|
||||
}
|
||||
}
|
||||
30
tests/Export/Spreadsheet/ColumnDefinitionTest.php
Normal file
30
tests/Export/Spreadsheet/ColumnDefinitionTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\ColumnDefinition
|
||||
*/
|
||||
class ColumnDefinitionTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
{
|
||||
$sut = new \App\Export\Spreadsheet\ColumnDefinition('foo', 'bar', function () {
|
||||
return 'hello world';
|
||||
});
|
||||
|
||||
self::assertEquals('foo', $sut->getLabel());
|
||||
self::assertEquals('bar', $sut->getType());
|
||||
self::assertIsCallable($sut->getAccessor());
|
||||
self::assertEquals('hello world', \call_user_func($sut->getAccessor()));
|
||||
}
|
||||
}
|
||||
76
tests/Export/Spreadsheet/Entities/DemoFull.php
Normal file
76
tests/Export/Spreadsheet/Entities/DemoFull.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Entities;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
|
||||
/**
|
||||
* @Exporter\Order({"a-time", "publicProperty", "a-date", "something", "privateProperty"})
|
||||
* @Exporter\Expose("accessor", label="label.accessor", exp="object.accessorMethod()")
|
||||
* @Exporter\Expose("a-date", label="label.type-date", exp="object.getDateTime()", type="date")
|
||||
* @Exporter\Expose("a-time", label="label.type-time", exp="object.getDateTime()", type="time")
|
||||
*/
|
||||
class DemoFull
|
||||
{
|
||||
/**
|
||||
* @Exporter\Expose(label="label.Public-Property", type="string")
|
||||
*/
|
||||
public $publicProperty = 'public-property';
|
||||
/**
|
||||
* @Exporter\Expose("fake-name", label="label.Protected-Property", type="boolean")
|
||||
*/
|
||||
protected $protectedProperty = false;
|
||||
/**
|
||||
* @Exporter\Expose(label="label.Private-Property", type="integer")
|
||||
*/
|
||||
private $privateProperty = 123;
|
||||
|
||||
/**
|
||||
* @Exporter\Expose(label="label.Public-Method")
|
||||
*/
|
||||
public function publicMethod(): string
|
||||
{
|
||||
return 'public-method';
|
||||
}
|
||||
|
||||
/**
|
||||
* @Exporter\Expose(label="label.Protected-Method", type="datetime")
|
||||
*/
|
||||
protected function protectedMethod(): \DateTime
|
||||
{
|
||||
return new \DateTime();
|
||||
}
|
||||
|
||||
public function getDateTime(): \DateTime
|
||||
{
|
||||
return new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Exporter\Expose("renamedDuration", label="label.duration", type="duration")
|
||||
*/
|
||||
protected function duration(): int
|
||||
{
|
||||
return 12345;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Exporter\Expose(name="fake-method", label="label.Private-Method", type="boolean")
|
||||
*/
|
||||
private function privateMethod(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function accessorMethod(): string
|
||||
{
|
||||
return 'accessor-method';
|
||||
}
|
||||
}
|
||||
22
tests/Export/Spreadsheet/Entities/ExpressionOnMethod.php
Normal file
22
tests/Export/Spreadsheet/Entities/ExpressionOnMethod.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Entities;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
|
||||
class ExpressionOnMethod
|
||||
{
|
||||
/**
|
||||
* @Exporter\Expose("accessor", label="label.accessor", exp="object.foo")
|
||||
*/
|
||||
public function foo()
|
||||
{
|
||||
}
|
||||
}
|
||||
20
tests/Export/Spreadsheet/Entities/ExpressionOnProperty.php
Normal file
20
tests/Export/Spreadsheet/Entities/ExpressionOnProperty.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Entities;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
|
||||
class ExpressionOnProperty
|
||||
{
|
||||
/**
|
||||
* @Exporter\Expose("accessor", label="label.accessor", exp="object.foo")
|
||||
*/
|
||||
private $foo;
|
||||
}
|
||||
22
tests/Export/Spreadsheet/Entities/MethodRequiresParams.php
Normal file
22
tests/Export/Spreadsheet/Entities/MethodRequiresParams.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Entities;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
|
||||
class MethodRequiresParams
|
||||
{
|
||||
/**
|
||||
* @Exporter\Expose("accessor", label="label.accessor")
|
||||
*/
|
||||
public function foo(string $foo)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Entities;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
|
||||
/**
|
||||
* @Exporter\Expose("accessor", label="label.accessor")
|
||||
*/
|
||||
class MissingExpressionOnClass
|
||||
{
|
||||
}
|
||||
19
tests/Export/Spreadsheet/Entities/MissingNameOnClass.php
Normal file
19
tests/Export/Spreadsheet/Entities/MissingNameOnClass.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Entities;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
|
||||
/**
|
||||
* @Exporter\Expose(label="label.accessor", exp="foo")
|
||||
*/
|
||||
class MissingNameOnClass
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\EntityWithMetaFieldsExporter;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\MetaFieldExtractor;
|
||||
use App\Export\Spreadsheet\SpreadsheetExporter;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\EntityWithMetaFieldsExporter
|
||||
*/
|
||||
class EntityWithMetaFieldsExporterTest extends TestCase
|
||||
{
|
||||
public function testExport()
|
||||
{
|
||||
$spreadsheetExporter = new SpreadsheetExporter($this->createMock(TranslatorInterface::class));
|
||||
$annotationExtractor = new AnnotationExtractor(new AnnotationReader());
|
||||
$metaFieldExtractor = new MetaFieldExtractor($this->createMock(EventDispatcherInterface::class));
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('test project');
|
||||
$project->setCustomer((new Customer())->setName('A customer'));
|
||||
$project->setComment('Lorem Ipsum');
|
||||
$project->setOrderNumber('1234567890');
|
||||
$project->setBudget(123456.7890);
|
||||
$project->setTimeBudget(1234567890);
|
||||
$project->setColor('#ababab');
|
||||
$project->setVisible(false);
|
||||
|
||||
$sut = new EntityWithMetaFieldsExporter($spreadsheetExporter, $annotationExtractor, $metaFieldExtractor);
|
||||
$spreadsheet = $sut->export(Project::class, [$project], new ProjectMetaDisplayEvent(new ProjectQuery(), ProjectMetaDisplayEvent::EXPORT));
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(1, 2, false)->getValue());
|
||||
self::assertEquals('test project', $worksheet->getCellByColumnAndRow(2, 2, false)->getValue());
|
||||
self::assertEquals('A customer', $worksheet->getCellByColumnAndRow(3, 2, false)->getValue());
|
||||
self::assertEquals(1234567890, $worksheet->getCellByColumnAndRow(4, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(5, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(6, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(7, 2, false)->getValue());
|
||||
self::assertEquals('#ababab', $worksheet->getCellByColumnAndRow(8, 2, false)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(9, 2, false)->getValue());
|
||||
self::assertEquals('Lorem Ipsum', $worksheet->getCellByColumnAndRow(10, 2, false)->getValue());
|
||||
}
|
||||
}
|
||||
132
tests/Export/Spreadsheet/Extractor/AnnotationExtractorTest.php
Normal file
132
tests/Export/Spreadsheet/Extractor/AnnotationExtractorTest.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\ExtractorException;
|
||||
use App\Tests\Export\Spreadsheet\Entities\DemoFull;
|
||||
use App\Tests\Export\Spreadsheet\Entities\ExpressionOnMethod;
|
||||
use App\Tests\Export\Spreadsheet\Entities\ExpressionOnProperty;
|
||||
use App\Tests\Export\Spreadsheet\Entities\MethodRequiresParams;
|
||||
use App\Tests\Export\Spreadsheet\Entities\MissingExpressionOnClass;
|
||||
use App\Tests\Export\Spreadsheet\Entities\MissingNameOnClass;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\Extractor\AnnotationExtractor
|
||||
* @covers \App\Export\Annotation\Expose
|
||||
* @covers \App\Export\Annotation\Order
|
||||
* @covers \App\Export\Spreadsheet\Extractor\ExtractorException
|
||||
*/
|
||||
class AnnotationExtractorTest extends TestCase
|
||||
{
|
||||
public function testExtract()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$columns = $sut->extract(DemoFull::class);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
self::assertCount(10, $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$expected = [
|
||||
['label.type-time', 'time', new \DateTime()],
|
||||
['label.Public-Property', 'string', 'public-property'],
|
||||
['label.type-date', 'date', new \DateTime()],
|
||||
['label.Private-Property', 'integer', 123],
|
||||
['label.accessor', 'string', 'accessor-method'],
|
||||
['label.Protected-Property', 'boolean', false],
|
||||
['label.Public-Method', 'string', 'public-method'],
|
||||
['label.Protected-Method', 'datetime', new \DateTime()],
|
||||
['label.duration', 'duration', 12345],
|
||||
['label.Private-Method', 'boolean', true],
|
||||
];
|
||||
|
||||
$i = 0;
|
||||
$object = new DemoFull();
|
||||
|
||||
foreach ($expected as $item) {
|
||||
$column = $columns[$i++];
|
||||
self::assertEquals($item[0], $column->getLabel());
|
||||
self::assertEquals($item[1], $column->getType());
|
||||
$result = \call_user_func($column->getAccessor(), $object);
|
||||
self::assertEquals(get_debug_type($item[2]), get_debug_type($result));
|
||||
if (\in_array(get_debug_type($result), ['string', 'int', 'bool', 'float'])) {
|
||||
self::assertEquals($item[2], $result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testExceptionOnInvalidType()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('AnnotationExtractor needs a class name (string) for work');
|
||||
|
||||
$sut->extract(new \stdClass());
|
||||
}
|
||||
|
||||
public function testExceptionOnMissingExpression()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('@Expose needs an expression attribute on class level hierarchy, check App\Tests\Export\Spreadsheet\Entities\MissingExpressionOnClass::class');
|
||||
|
||||
$sut->extract(MissingExpressionOnClass::class);
|
||||
}
|
||||
|
||||
public function testExceptionOnMissingName()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('@Expose needs a name attribute on class level hierarchy, check App\Tests\Export\Spreadsheet\Entities\MissingNameOnClass::class');
|
||||
|
||||
$sut->extract(MissingNameOnClass::class);
|
||||
}
|
||||
|
||||
public function testExceptionExpressionOnProperty()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('@Expose only supports the expression attribute on class level hierarchy, check App\Tests\Export\Spreadsheet\Entities\ExpressionOnProperty::$foo');
|
||||
|
||||
$sut->extract(ExpressionOnProperty::class);
|
||||
}
|
||||
|
||||
public function testExceptionExpressionOnMethod()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('@Expose only supports the expression attribute on class level hierarchy, check App\Tests\Export\Spreadsheet\Entities\ExpressionOnMethod::foo()');
|
||||
|
||||
$sut->extract(ExpressionOnMethod::class);
|
||||
}
|
||||
|
||||
public function testExceptionExpressionOnMethodWithRequiredParameters()
|
||||
{
|
||||
$sut = new AnnotationExtractor(new AnnotationReader());
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('@Expose does not support method App\Tests\Export\Spreadsheet\Entities\MethodRequiresParams::foo(...), it has required parameters.');
|
||||
|
||||
$sut->extract(MethodRequiresParams::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectMeta;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\ExtractorException;
|
||||
use App\Export\Spreadsheet\Extractor\MetaFieldExtractor;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\Extractor\MetaFieldExtractor
|
||||
* @covers \App\Export\Spreadsheet\Extractor\ExtractorException
|
||||
*/
|
||||
class MetaFieldExtractorTest extends TestCase
|
||||
{
|
||||
public function testExtract()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects(self::once())->method('dispatch')->willReturnCallback(function (ProjectMetaDisplayEvent $event) {
|
||||
$event->addField((new ProjectMeta())->setName('foo')->setIsVisible(true));
|
||||
$event->addField((new ProjectMeta())->setName('no')->setIsVisible(false));
|
||||
$event->addField((new ProjectMeta())->setName('bar')->setIsVisible(true));
|
||||
});
|
||||
|
||||
$sut = new MetaFieldExtractor($dispatcher);
|
||||
|
||||
$event = new ProjectMetaDisplayEvent(new ProjectQuery(), 'somewhere');
|
||||
|
||||
$columns = $sut->extract($event);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
self::assertCount(2, $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$definition = $columns[1];
|
||||
self::assertEquals('bar', $definition->getLabel());
|
||||
self::assertEquals('string', $definition->getType());
|
||||
self::assertEquals('tralalalala', \call_user_func($definition->getAccessor(), (new Project())->setMetaField((new ProjectMeta())->setName('bar')->setValue('tralalalala'))));
|
||||
}
|
||||
|
||||
public function testCheckType()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$sut = new MetaFieldExtractor($dispatcher);
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('MetaFieldExtractor needs a MetaDisplayEventInterface instance for work');
|
||||
|
||||
$sut->extract(new \stdClass());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\Extractor\ExtractorException;
|
||||
use App\Export\Spreadsheet\Extractor\UserPreferenceExtractor;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\Extractor\UserPreferenceExtractor
|
||||
* @covers \App\Export\Spreadsheet\Extractor\ExtractorException
|
||||
*/
|
||||
class UserPreferenceExtractorTest extends TestCase
|
||||
{
|
||||
public function testExtract()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects(self::once())->method('dispatch')->willReturnCallback(function (UserPreferenceDisplayEvent $event) {
|
||||
$event->addPreference((new UserPreference())->setName('foo')->setEnabled(true));
|
||||
$event->addPreference((new UserPreference())->setName('no')->setEnabled(false));
|
||||
$event->addPreference((new UserPreference())->setName('bar')->setEnabled(true));
|
||||
});
|
||||
|
||||
$sut = new UserPreferenceExtractor($dispatcher);
|
||||
|
||||
$event = new UserPreferenceDisplayEvent('somewhere');
|
||||
|
||||
$columns = $sut->extract($event);
|
||||
|
||||
self::assertIsArray($columns);
|
||||
self::assertCount(2, $columns);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
self::assertInstanceOf(ColumnDefinition::class, $column);
|
||||
}
|
||||
|
||||
$definition = $columns[1];
|
||||
self::assertEquals('bar', $definition->getLabel());
|
||||
self::assertEquals('string', $definition->getType());
|
||||
self::assertEquals('tralalalala', \call_user_func($definition->getAccessor(), (new User())->addPreference((new UserPreference())->setName('bar')->setValue('tralalalala'))));
|
||||
}
|
||||
|
||||
public function testCheckType()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$sut = new UserPreferenceExtractor($dispatcher);
|
||||
|
||||
$this->expectException(ExtractorException::class);
|
||||
$this->expectExceptionMessage('UserPreferenceExtractor needs a UserPreferenceDisplayEvent instance for work');
|
||||
|
||||
$sut->extract(new \stdClass());
|
||||
}
|
||||
}
|
||||
69
tests/Export/Spreadsheet/SpreadsheetExporterTest.php
Normal file
69
tests/Export/Spreadsheet/SpreadsheetExporterTest.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use App\Export\Spreadsheet\SpreadsheetExporter;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\SpreadsheetExporter
|
||||
*/
|
||||
class SpreadsheetExporterTest extends TestCase
|
||||
{
|
||||
public function testExport()
|
||||
{
|
||||
$sut = new SpreadsheetExporter($this->createMock(TranslatorInterface::class));
|
||||
$sut->registerCellFormatter('foo', new class() implements CellFormatterInterface {
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '##' . $value . '##');
|
||||
}
|
||||
});
|
||||
$sut->registerCellFormatter('bar', new class() implements CellFormatterInterface {
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '~' . $value . '~');
|
||||
}
|
||||
});
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('test project');
|
||||
$project->setVisible(false);
|
||||
|
||||
$columns = [
|
||||
new ColumnDefinition('test1', 'foo', function (Project $project) {
|
||||
return $project->getName();
|
||||
}),
|
||||
new ColumnDefinition('test2', 'bar', function (Project $project) {
|
||||
return $project->getName();
|
||||
}),
|
||||
new ColumnDefinition('test3', 'boolean', function (Project $project) {
|
||||
return $project->isVisible();
|
||||
}),
|
||||
];
|
||||
|
||||
$entries = [
|
||||
$project
|
||||
];
|
||||
|
||||
$spreadsheet = $sut->export($columns, $entries);
|
||||
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertEquals('##test project##', $worksheet->getCellByColumnAndRow(1, 2, false)->getValue());
|
||||
self::assertEquals('~test project~', $worksheet->getCellByColumnAndRow(2, 2, false)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(3, 2, false)->getValue());
|
||||
}
|
||||
}
|
||||
60
tests/Export/Spreadsheet/UserExporterTest.php
Normal file
60
tests/Export/Spreadsheet/UserExporterTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\UserPreferenceExtractor;
|
||||
use App\Export\Spreadsheet\SpreadsheetExporter;
|
||||
use App\Export\Spreadsheet\UserExporter;
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\UserExporter
|
||||
*/
|
||||
class UserExporterTest extends TestCase
|
||||
{
|
||||
public function testExport()
|
||||
{
|
||||
$spreadsheetExporter = new SpreadsheetExporter($this->createMock(TranslatorInterface::class));
|
||||
$annotationExtractor = new AnnotationExtractor(new AnnotationReader());
|
||||
$userPreferenceExtractor = new UserPreferenceExtractor($this->createMock(EventDispatcherInterface::class));
|
||||
|
||||
$user = new User();
|
||||
$user->setUsername('test user');
|
||||
$user->setAvatar('Lorem Ipsum');
|
||||
$user->setTimezone('Europe/Berlin');
|
||||
$user->setAlias('Another name');
|
||||
$user->setTitle('Mr. Title');
|
||||
$user->setLanguage('de');
|
||||
$user->setEmail('test@example.com');
|
||||
$user->setEnabled(false);
|
||||
$user->addRole(User::ROLE_TEAMLEAD);
|
||||
|
||||
$sut = new UserExporter($spreadsheetExporter, $annotationExtractor, $userPreferenceExtractor);
|
||||
$spreadsheet = $sut->export([$user], new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT));
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
self::assertNull($worksheet->getCellByColumnAndRow(1, 2, false)->getValue());
|
||||
self::assertEquals('test user', $worksheet->getCellByColumnAndRow(2, 2, false)->getValue());
|
||||
self::assertEquals('Another name', $worksheet->getCellByColumnAndRow(3, 2, false)->getValue());
|
||||
self::assertEquals('Mr. Title', $worksheet->getCellByColumnAndRow(4, 2, false)->getValue());
|
||||
self::assertEquals('test@example.com', $worksheet->getCellByColumnAndRow(5, 2, false)->getValue());
|
||||
self::assertEquals('', $worksheet->getCellByColumnAndRow(6, 2, false)->getValue());
|
||||
self::assertEquals('de', $worksheet->getCellByColumnAndRow(7, 2, false)->getValue());
|
||||
self::assertEquals('Europe/Berlin', $worksheet->getCellByColumnAndRow(8, 2, false)->getValue());
|
||||
self::assertFalse($worksheet->getCellByColumnAndRow(9, 2, false)->getValue());
|
||||
self::assertEquals('ROLE_TEAMLEAD;ROLE_USER', $worksheet->getCellByColumnAndRow(11, 2, false)->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Writer;
|
||||
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\Writer\BinaryFileResponseWriter
|
||||
*/
|
||||
class BinaryFileResponseWriterTest extends TestCase
|
||||
{
|
||||
public function testSave()
|
||||
{
|
||||
$sut = new BinaryFileResponseWriter(new XlsxWriter(), 'foobar');
|
||||
|
||||
self::assertEquals('xlsx', $sut->getFileExtension());
|
||||
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $sut->getContentType());
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$file = $sut->save($spreadsheet);
|
||||
self::assertInstanceOf(\SplFileInfo::class, $file);
|
||||
self::assertTrue(file_exists($file->getRealPath()));
|
||||
}
|
||||
|
||||
public function testGetResponse()
|
||||
{
|
||||
$sut = new BinaryFileResponseWriter(new XlsxWriter(), 'foobar');
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$response = $sut->getFileResponse($spreadsheet);
|
||||
|
||||
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||
self::assertStringContainsString('attachment; filename=foobar', $response->headers->get('Content-Disposition'));
|
||||
|
||||
$file = $response->getFile();
|
||||
self::assertTrue(file_exists($file->getRealPath()));
|
||||
|
||||
ob_start();
|
||||
$response->sendContent();
|
||||
$content2 = ob_get_clean();
|
||||
self::assertNotEmpty($content2);
|
||||
|
||||
self::assertFalse(file_exists($file->getRealPath()));
|
||||
}
|
||||
}
|
||||
37
tests/Export/Spreadsheet/Writer/XlsxWriterTest.php
Normal file
37
tests/Export/Spreadsheet/Writer/XlsxWriterTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Export\Spreadsheet\Writer;
|
||||
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Spreadsheet\Writer\XlsxWriter
|
||||
*/
|
||||
class XlsxWriterTest extends TestCase
|
||||
{
|
||||
public function testWriter()
|
||||
{
|
||||
$sut = new XlsxWriter();
|
||||
|
||||
self::assertEquals('xlsx', $sut->getFileExtension());
|
||||
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $sut->getContentType());
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$file = $sut->save($spreadsheet);
|
||||
self::assertInstanceOf(\SplFileInfo::class, $file);
|
||||
self::assertTrue(file_exists($file->getRealPath()));
|
||||
|
||||
// TODO test autofilter
|
||||
// TODO test freeze pane
|
||||
}
|
||||
}
|
||||
@@ -484,6 +484,10 @@
|
||||
<source>profile.first_entry</source>
|
||||
<target>Arbeitet seit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="profile.registration_date">
|
||||
<source>profile.registration_date</source>
|
||||
<target>Registriert am</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="profile.settings">
|
||||
<source>profile.settings</source>
|
||||
<target>Profil</target>
|
||||
|
||||
@@ -484,6 +484,10 @@
|
||||
<source>profile.first_entry</source>
|
||||
<target>Working since</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="profile.registration_date">
|
||||
<source>profile.registration_date</source>
|
||||
<target>Registered at</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="profile.settings">
|
||||
<source>profile.settings</source>
|
||||
<target>Profile</target>
|
||||
|
||||
Reference in New Issue
Block a user