diff --git a/src/Controller/ActivityController.php b/src/Controller/ActivityController.php index 2cbc9ec6..24e7c65d 100644 --- a/src/Controller/ActivityController.php +++ b/src/Controller/ActivityController.php @@ -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 diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index 9dc09d9e..d53465e0 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -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 diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 0654f477..a40f5540 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -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 diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 7d1465d0..0fa85ef9 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -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, [ diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php index 030cfff7..0eed73b4 100644 --- a/src/Entity/Activity.php +++ b/src/Entity/Activity.php @@ -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() */ diff --git a/src/Entity/ColorTrait.php b/src/Entity/ColorTrait.php index df247420..2a0f7ecc 100644 --- a/src/Entity/ColorTrait.php +++ b/src/Entity/ColorTrait.php @@ -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) */ diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index fdbc6f62..1e98d868 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -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() */ diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 5613a165..2bd029f2 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -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() */ diff --git a/src/Entity/User.php b/src/Entity/User.php index 72fb2674..bbb5211e 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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; diff --git a/src/Event/AbstractMetaDisplayEvent.php b/src/Event/AbstractMetaDisplayEvent.php new file mode 100644 index 00000000..76dd5ce7 --- /dev/null +++ b/src/Event/AbstractMetaDisplayEvent.php @@ -0,0 +1,76 @@ +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; + } +} diff --git a/src/Event/ActivityMetaDisplayEvent.php b/src/Event/ActivityMetaDisplayEvent.php index 530487ca..864664f2 100644 --- a/src/Event/ActivityMetaDisplayEvent.php +++ b/src/Event/ActivityMetaDisplayEvent.php @@ -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); } } diff --git a/src/Event/CustomerMetaDisplayEvent.php b/src/Event/CustomerMetaDisplayEvent.php index 79df6c55..8b718a98 100644 --- a/src/Event/CustomerMetaDisplayEvent.php +++ b/src/Event/CustomerMetaDisplayEvent.php @@ -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); } } diff --git a/src/Event/ProjectMetaDisplayEvent.php b/src/Event/ProjectMetaDisplayEvent.php index 340476ff..40a57ae0 100644 --- a/src/Event/ProjectMetaDisplayEvent.php +++ b/src/Event/ProjectMetaDisplayEvent.php @@ -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); } } diff --git a/src/Event/TimesheetMetaDisplayEvent.php b/src/Event/TimesheetMetaDisplayEvent.php index 88dfc8b0..66b24de3 100644 --- a/src/Event/TimesheetMetaDisplayEvent.php +++ b/src/Event/TimesheetMetaDisplayEvent.php @@ -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); } } diff --git a/src/Export/Annotation/Expose.php b/src/Export/Annotation/Expose.php new file mode 100644 index 00000000..1a371809 --- /dev/null +++ b/src/Export/Annotation/Expose.php @@ -0,0 +1,57 @@ +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; + } + } +} diff --git a/src/Export/Annotation/Order.php b/src/Export/Annotation/Order.php new file mode 100644 index 00000000..ca40df24 --- /dev/null +++ b/src/Export/Annotation/Order.php @@ -0,0 +1,32 @@ + + */ + public $order = []; + + public function __construct(array $data) + { + if (isset($data['value'])) { + $this->order = $data['value']; + unset($data['value']); + } + } +} diff --git a/src/Export/Spreadsheet/AnnotatedObjectExporter.php b/src/Export/Spreadsheet/AnnotatedObjectExporter.php new file mode 100644 index 00000000..1410da1e --- /dev/null +++ b/src/Export/Spreadsheet/AnnotatedObjectExporter.php @@ -0,0 +1,32 @@ +spreadsheetExporter = $spreadsheetExporter; + $this->annotationExtractor = $annotationExtractor; + } + + public function export(string $class, array $entries): Spreadsheet + { + $columns = $this->annotationExtractor->extract($class); + + return $this->spreadsheetExporter->export($columns, $entries); + } +} diff --git a/src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php b/src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php new file mode 100644 index 00000000..814c9950 --- /dev/null +++ b/src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php @@ -0,0 +1,30 @@ +setCellValueByColumnAndRow($column, $row, ''); + + return; + } + + if (!\is_array($value)) { + throw new \InvalidArgumentException('Unsupported value given, only array is supported'); + } + + $sheet->setCellValueByColumnAndRow($column, $row, implode(';', $value)); + } +} diff --git a/src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php b/src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php new file mode 100644 index 00000000..2d47ec17 --- /dev/null +++ b/src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php @@ -0,0 +1,30 @@ +setCellValueByColumnAndRow($column, $row, ''); + + return; + } + + if (!\is_bool($value)) { + throw new \InvalidArgumentException('Unsupported value given, only boolean is supported'); + } + + $sheet->setCellValueByColumnAndRow($column, $row, $value); + } +} diff --git a/src/Export/Spreadsheet/CellFormatter/CellFormatterInterface.php b/src/Export/Spreadsheet/CellFormatter/CellFormatterInterface.php new file mode 100644 index 00000000..120d619f --- /dev/null +++ b/src/Export/Spreadsheet/CellFormatter/CellFormatterInterface.php @@ -0,0 +1,24 @@ +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); + } +} diff --git a/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php b/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php new file mode 100644 index 00000000..671e2af3 --- /dev/null +++ b/src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php @@ -0,0 +1,34 @@ +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); + } +} diff --git a/src/Export/Spreadsheet/CellFormatter/DurationFormatter.php b/src/Export/Spreadsheet/CellFormatter/DurationFormatter.php new file mode 100644 index 00000000..d8d77f83 --- /dev/null +++ b/src/Export/Spreadsheet/CellFormatter/DurationFormatter.php @@ -0,0 +1,31 @@ +setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $value)); + $sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT); + } +} diff --git a/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php b/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php new file mode 100644 index 00000000..24e33496 --- /dev/null +++ b/src/Export/Spreadsheet/CellFormatter/TimeFormatter.php @@ -0,0 +1,34 @@ +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); + } +} diff --git a/src/Export/Spreadsheet/ColumnDefinition.php b/src/Export/Spreadsheet/ColumnDefinition.php new file mode 100644 index 00000000..d221ae5b --- /dev/null +++ b/src/Export/Spreadsheet/ColumnDefinition.php @@ -0,0 +1,39 @@ +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; + } +} diff --git a/src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php b/src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php new file mode 100644 index 00000000..ba5a538c --- /dev/null +++ b/src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php @@ -0,0 +1,36 @@ +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); + } +} diff --git a/src/Export/Spreadsheet/Extractor/AnnotationExtractor.php b/src/Export/Spreadsheet/Extractor/AnnotationExtractor.php new file mode 100644 index 00000000..da475338 --- /dev/null +++ b/src/Export/Spreadsheet/Extractor/AnnotationExtractor.php @@ -0,0 +1,150 @@ +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); + } +} diff --git a/src/Export/Spreadsheet/Extractor/ExtractorException.php b/src/Export/Spreadsheet/Extractor/ExtractorException.php new file mode 100644 index 00000000..ef5a427b --- /dev/null +++ b/src/Export/Spreadsheet/Extractor/ExtractorException.php @@ -0,0 +1,14 @@ +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; + } +} diff --git a/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php b/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php new file mode 100644 index 00000000..8a01363b --- /dev/null +++ b/src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php @@ -0,0 +1,68 @@ +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; + } +} diff --git a/src/Export/Spreadsheet/SpreadsheetExporter.php b/src/Export/Spreadsheet/SpreadsheetExporter.php new file mode 100644 index 00000000..c4b8cec7 --- /dev/null +++ b/src/Export/Spreadsheet/SpreadsheetExporter.php @@ -0,0 +1,100 @@ +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; + } +} diff --git a/src/Export/Spreadsheet/UserExporter.php b/src/Export/Spreadsheet/UserExporter.php new file mode 100644 index 00000000..6503118c --- /dev/null +++ b/src/Export/Spreadsheet/UserExporter.php @@ -0,0 +1,47 @@ +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); + } +} diff --git a/src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php b/src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php new file mode 100644 index 00000000..0e5d4098 --- /dev/null +++ b/src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php @@ -0,0 +1,71 @@ +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; + } +} diff --git a/src/Export/Spreadsheet/Writer/WriterInterface.php b/src/Export/Spreadsheet/Writer/WriterInterface.php new file mode 100644 index 00000000..56afe4f4 --- /dev/null +++ b/src/Export/Spreadsheet/Writer/WriterInterface.php @@ -0,0 +1,28 @@ + 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); + } +} diff --git a/templates/activity/actions.html.twig b/templates/activity/actions.html.twig index 1061fe3d..fc917cf0 100644 --- a/templates/activity/actions.html.twig +++ b/templates/activity/actions.html.twig @@ -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 %} diff --git a/templates/customer/actions.html.twig b/templates/customer/actions.html.twig index dd3018f6..b6a4e9c3 100644 --- a/templates/customer/actions.html.twig +++ b/templates/customer/actions.html.twig @@ -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 %} diff --git a/templates/project/actions.html.twig b/templates/project/actions.html.twig index 7d552a2d..080f89e2 100644 --- a/templates/project/actions.html.twig +++ b/templates/project/actions.html.twig @@ -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 %} diff --git a/templates/user/actions.html.twig b/templates/user/actions.html.twig index dea6d13b..38e594a7 100644 --- a/templates/user/actions.html.twig +++ b/templates/user/actions.html.twig @@ -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 %} diff --git a/templates/user/layout.html.twig b/templates/user/layout.html.twig index 5862c7a2..fe7aaca0 100644 --- a/templates/user/layout.html.twig +++ b/templates/user/layout.html.twig @@ -68,6 +68,10 @@ {{ 'profile.first_entry'|trans }} {{ stats.firstEntry|date_short }} + + {{ 'profile.registration_date'|trans }} + {{ user.registeredAt|date_short }} + {% if is_granted('hourly-rate', user) %} {{ 'label.hourlyRate'|trans }} diff --git a/tests/Controller/ActivityControllerTest.php b/tests/Controller/ActivityControllerTest.php index 5cae2c65..04522cdc 100644 --- a/tests/Controller/ActivityControllerTest.php +++ b/tests/Controller/ActivityControllerTest.php @@ -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); diff --git a/tests/Controller/ControllerBaseTest.php b/tests/Controller/ControllerBaseTest.php index d8991e9b..3de9a0bf 100644 --- a/tests/Controller/ControllerBaseTest.php +++ b/tests/Controller/ControllerBaseTest.php @@ -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')); + } } diff --git a/tests/Controller/CustomerControllerTest.php b/tests/Controller/CustomerControllerTest.php index 270efde9..a51e06b0 100644 --- a/tests/Controller/CustomerControllerTest.php +++ b/tests/Controller/CustomerControllerTest.php @@ -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); diff --git a/tests/Controller/ProjectControllerTest.php b/tests/Controller/ProjectControllerTest.php index 52ce209c..54dcd992 100644 --- a/tests/Controller/ProjectControllerTest.php +++ b/tests/Controller/ProjectControllerTest.php @@ -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); diff --git a/tests/Controller/UserControllerTest.php b/tests/Controller/UserControllerTest.php index 960b4fd6..e9af6c66 100644 --- a/tests/Controller/UserControllerTest.php +++ b/tests/Controller/UserControllerTest.php @@ -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 = '亚历山德拉'; diff --git a/tests/Entity/ActivityTest.php b/tests/Entity/ActivityTest.php index 4a226d77..3a8b4cbb 100644 --- a/tests/Entity/ActivityTest.php +++ b/tests/Entity/ActivityTest.php @@ -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()); + } + } } diff --git a/tests/Entity/CustomerTest.php b/tests/Entity/CustomerTest.php index acf800a2..15fae5b7 100644 --- a/tests/Entity/CustomerTest.php +++ b/tests/Entity/CustomerTest.php @@ -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()); + } + } } diff --git a/tests/Entity/ProjectTest.php b/tests/Entity/ProjectTest.php index 30cb2fb4..bf26e761 100644 --- a/tests/Entity/ProjectTest.php +++ b/tests/Entity/ProjectTest.php @@ -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()); + } + } } diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php index 340c72e3..93834b9d 100644 --- a/tests/Entity/UserTest.php +++ b/tests/Entity/UserTest.php @@ -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()); + } + } } diff --git a/tests/Event/ActivityMetaDisplayEventTest.php b/tests/Event/ActivityMetaDisplayEventTest.php index e826e842..d9c277bb 100644 --- a/tests/Event/ActivityMetaDisplayEventTest.php +++ b/tests/Event/ActivityMetaDisplayEventTest.php @@ -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 diff --git a/tests/Event/CustomerMetaDisplayEventTest.php b/tests/Event/CustomerMetaDisplayEventTest.php index bd8df6fb..1362e3ff 100644 --- a/tests/Event/CustomerMetaDisplayEventTest.php +++ b/tests/Event/CustomerMetaDisplayEventTest.php @@ -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 diff --git a/tests/Event/ProjectMetaDisplayEventTest.php b/tests/Event/ProjectMetaDisplayEventTest.php new file mode 100644 index 00000000..2033126e --- /dev/null +++ b/tests/Event/ProjectMetaDisplayEventTest.php @@ -0,0 +1,40 @@ +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()); + } +} diff --git a/tests/Event/TimesheetMetaDisplayEventTest.php b/tests/Event/TimesheetMetaDisplayEventTest.php index 88f2e6c0..508dc7ed 100644 --- a/tests/Event/TimesheetMetaDisplayEventTest.php +++ b/tests/Event/TimesheetMetaDisplayEventTest.php @@ -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 diff --git a/tests/Export/Spreadsheet/AnnotatedObjectExporterTest.php b/tests/Export/Spreadsheet/AnnotatedObjectExporterTest.php new file mode 100644 index 00000000..74339630 --- /dev/null +++ b/tests/Export/Spreadsheet/AnnotatedObjectExporterTest.php @@ -0,0 +1,56 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php new file mode 100644 index 00000000..2fb9c662 --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/AbstractFormatterTest.php @@ -0,0 +1,64 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/ArrayFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/ArrayFormatterTest.php new file mode 100644 index 00000000..7e071930 --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/ArrayFormatterTest.php @@ -0,0 +1,47 @@ +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'); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/BooleanFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/BooleanFormatterTest.php new file mode 100644 index 00000000..c93e0411 --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/BooleanFormatterTest.php @@ -0,0 +1,47 @@ +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'); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php new file mode 100644 index 00000000..a87d6b2d --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/DateFormatterTest.php @@ -0,0 +1,57 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php new file mode 100644 index 00000000..079fa629 --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php @@ -0,0 +1,56 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/DurationFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/DurationFormatterTest.php new file mode 100644 index 00000000..a68669cb --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/DurationFormatterTest.php @@ -0,0 +1,59 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php b/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php new file mode 100644 index 00000000..d4582d2e --- /dev/null +++ b/tests/Export/Spreadsheet/CellFormatter/TimeFormatterTest.php @@ -0,0 +1,56 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/ColumnDefinitionTest.php b/tests/Export/Spreadsheet/ColumnDefinitionTest.php new file mode 100644 index 00000000..46c2b6ad --- /dev/null +++ b/tests/Export/Spreadsheet/ColumnDefinitionTest.php @@ -0,0 +1,30 @@ +getLabel()); + self::assertEquals('bar', $sut->getType()); + self::assertIsCallable($sut->getAccessor()); + self::assertEquals('hello world', \call_user_func($sut->getAccessor())); + } +} diff --git a/tests/Export/Spreadsheet/Entities/DemoFull.php b/tests/Export/Spreadsheet/Entities/DemoFull.php new file mode 100644 index 00000000..26e9342c --- /dev/null +++ b/tests/Export/Spreadsheet/Entities/DemoFull.php @@ -0,0 +1,76 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/Extractor/AnnotationExtractorTest.php b/tests/Export/Spreadsheet/Extractor/AnnotationExtractorTest.php new file mode 100644 index 00000000..0eb238d3 --- /dev/null +++ b/tests/Export/Spreadsheet/Extractor/AnnotationExtractorTest.php @@ -0,0 +1,132 @@ +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); + } +} diff --git a/tests/Export/Spreadsheet/Extractor/MetaFieldExtractorTest.php b/tests/Export/Spreadsheet/Extractor/MetaFieldExtractorTest.php new file mode 100644 index 00000000..bd40fdc3 --- /dev/null +++ b/tests/Export/Spreadsheet/Extractor/MetaFieldExtractorTest.php @@ -0,0 +1,66 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/Extractor/UserPreferenceExtractorTest.php b/tests/Export/Spreadsheet/Extractor/UserPreferenceExtractorTest.php new file mode 100644 index 00000000..2dccbe20 --- /dev/null +++ b/tests/Export/Spreadsheet/Extractor/UserPreferenceExtractorTest.php @@ -0,0 +1,65 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/SpreadsheetExporterTest.php b/tests/Export/Spreadsheet/SpreadsheetExporterTest.php new file mode 100644 index 00000000..fbf69d6b --- /dev/null +++ b/tests/Export/Spreadsheet/SpreadsheetExporterTest.php @@ -0,0 +1,69 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/UserExporterTest.php b/tests/Export/Spreadsheet/UserExporterTest.php new file mode 100644 index 00000000..96f64581 --- /dev/null +++ b/tests/Export/Spreadsheet/UserExporterTest.php @@ -0,0 +1,60 @@ +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()); + } +} diff --git a/tests/Export/Spreadsheet/Writer/BinaryFileResponseWriterTest.php b/tests/Export/Spreadsheet/Writer/BinaryFileResponseWriterTest.php new file mode 100644 index 00000000..5cc33a81 --- /dev/null +++ b/tests/Export/Spreadsheet/Writer/BinaryFileResponseWriterTest.php @@ -0,0 +1,57 @@ +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())); + } +} diff --git a/tests/Export/Spreadsheet/Writer/XlsxWriterTest.php b/tests/Export/Spreadsheet/Writer/XlsxWriterTest.php new file mode 100644 index 00000000..3a66fbf9 --- /dev/null +++ b/tests/Export/Spreadsheet/Writer/XlsxWriterTest.php @@ -0,0 +1,37 @@ +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 + } +} diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 3ac4f2bf..18bb061e 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -484,6 +484,10 @@ profile.first_entry Arbeitet seit + + profile.registration_date + Registriert am + profile.settings Profil diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 126f66b6..aae0e882 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -484,6 +484,10 @@ profile.first_entry Working since + + profile.registration_date + Registered at + profile.settings Profile