Skip to content

Commit f043eea

Browse files
authored
Merge pull request #43 from itk-dev/feature/export
CSV export
2 parents b8144e5 + f53b1a5 commit f043eea

10 files changed

Lines changed: 156 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- [#43](https://github.com/itk-dev/devops_itksites/pull/43)
11+
Added CSV export
1012
- [#42](https://github.com/itk-dev/devops_itksites/pull/42)
1113
Add and apply CS fixer rule to enforce strict types on all files.
1214
- [#44](https://github.com/itk-dev/devops_itksites/pull/44)

src/Controller/Admin/OIDCCrudController.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use App\Entity\OIDC;
88
use App\Repository\SiteRepository;
9+
use App\Service\Exporter;
10+
use App\Trait\ExportCrudControllerTrait;
911
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
1012
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
1113
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
@@ -19,8 +21,13 @@
1921

2022
class OIDCCrudController extends AbstractCrudController
2123
{
22-
public function __construct(private readonly SiteRepository $siteRepository)
24+
use ExportCrudControllerTrait;
25+
26+
public function __construct(
27+
Exporter $exporter,
28+
private readonly SiteRepository $siteRepository)
2329
{
30+
$this->setExporter($exporter);
2431
}
2532

2633
public static function getEntityFqcn(): string
@@ -36,7 +43,9 @@ public function configureCrud(Crud $crud): Crud
3643
public function configureActions(Actions $actions): Actions
3744
{
3845
return $actions
39-
->add(Crud::PAGE_INDEX, Action::DETAIL);
46+
->add(Crud::PAGE_INDEX, Action::DETAIL)
47+
->add(Crud::PAGE_INDEX, $this->createExportAction())
48+
;
4049
}
4150

4251
public function configureFields(string $pageName): iterable
@@ -56,8 +65,7 @@ public function configureFields(string $pageName): iterable
5665
yield UrlField::new('onePasswordUrl')
5766
->setLabel(new TranslatableMessage('1Password url'));
5867
yield UrlField::new('usageDocumentationUrl')->hideOnIndex()
59-
->setHelp(new TranslatableMessage('Tell where to find documentation on how OpenID Connect is used on the site and
60-
how to configure the use.'));
68+
->setHelp(new TranslatableMessage('Tell where to find documentation on how OpenID Connect is used on the site and how to configure the use.'));
6169
yield DateField::new('expirationTime')->setFormat('yyyy-MM-dd')->setLabel('Expiration Date');
6270

6371
yield TextareaField::new('notes');

src/Controller/Admin/ServerCrudController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use App\Form\Type\Admin\MariaDbVersionFilter;
1010
use App\Form\Type\Admin\ServerTypeFilter;
1111
use App\Form\Type\Admin\SystemFilter;
12+
use App\Service\Exporter;
13+
use App\Trait\ExportCrudControllerTrait;
1214
use App\Types\DatabaseVersionType;
1315
use App\Types\HostingProviderType;
1416
use App\Types\ServerTypeType;
@@ -28,9 +30,13 @@
2830

2931
class ServerCrudController extends AbstractCrudController
3032
{
33+
use ExportCrudControllerTrait;
34+
3135
public function __construct(
36+
Exporter $exporter,
3237
private readonly RequestStack $requestStack
3338
) {
39+
$this->setExporter($exporter);
3440
}
3541

3642
public static function getEntityFqcn(): string
@@ -53,6 +59,7 @@ public function configureActions(Actions $actions): Actions
5359
{
5460
return $actions
5561
->add(Crud::PAGE_INDEX, Action::DETAIL)
62+
->add(Crud::PAGE_INDEX, $this->createExportAction())
5663
;
5764
}
5865

src/Controller/Admin/SiteCrudController.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use App\Admin\Field\SiteTypeField;
1212
use App\Admin\Field\VersionField;
1313
use App\Entity\Site;
14+
use App\Service\Exporter;
15+
use App\Trait\ExportCrudControllerTrait;
1416
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
1517
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
1618
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
@@ -21,6 +23,13 @@
2123

2224
class SiteCrudController extends AbstractCrudController
2325
{
26+
use ExportCrudControllerTrait;
27+
28+
public function __construct(Exporter $exporter)
29+
{
30+
$this->setExporter($exporter);
31+
}
32+
2433
public static function getEntityFqcn(): string
2534
{
2635
return Site::class;
@@ -35,6 +44,7 @@ public function configureActions(Actions $actions): Actions
3544
{
3645
return $actions
3746
->add(Crud::PAGE_INDEX, Action::DETAIL)
47+
->add(Crud::PAGE_INDEX, $this->createExportAction())
3848
->remove(Crud::PAGE_INDEX, Action::NEW)
3949
->remove(Crud::PAGE_INDEX, Action::EDIT)
4050
->remove(Crud::PAGE_INDEX, Action::DELETE)
@@ -63,7 +73,6 @@ public function configureFilters(Filters $filters): Filters
6373
->add('primaryDomain')
6474
->add('configFilePath')
6575
->add('phpVersion')
66-
->add('server')
67-
;
76+
->add('server');
6877
}
6978
}

src/Entity/AbstractHandlerResult.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use App\Utils\RootDirNormalizer;
88
use Doctrine\ORM\Mapping as ORM;
9+
use Symfony\Component\Serializer\Annotation\Groups;
10+
use Symfony\Component\Serializer\Annotation\SerializedName;
911

1012
#[ORM\MappedSuperclass]
1113
class AbstractHandlerResult extends AbstractBaseEntity
@@ -15,6 +17,8 @@ class AbstractHandlerResult extends AbstractBaseEntity
1517

1618
#[ORM\ManyToOne(targetEntity: Server::class)]
1719
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
20+
#[Groups(['export'])]
21+
#[SerializedName('Server')]
1822
protected ?Server $server;
1923

2024
#[ORM\ManyToOne(targetEntity: DetectionResult::class, fetch: 'EAGER')]

src/Entity/OIDC.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,44 @@
77
use App\Repository\OIDCRepository;
88
use Doctrine\DBAL\Types\Types;
99
use Doctrine\ORM\Mapping as ORM;
10+
use Symfony\Component\Serializer\Annotation\Groups;
11+
use Symfony\Component\Serializer\Annotation\SerializedName;
1012
use Symfony\Component\Validator\Constraints as Assert;
1113

1214
#[ORM\Entity(repositoryClass: OIDCRepository::class)]
1315
class OIDC extends AbstractBaseEntity
1416
{
1517
#[ORM\Column(length: 255)]
1618
#[Assert\NotBlank]
19+
#[Groups(['export'])]
20+
#[SerializedName('Domain')]
1721
private ?string $domain = null;
1822

1923
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
24+
#[Groups(['export'])]
25+
#[SerializedName('Expiration time')]
2026
private ?\DateTimeInterface $expirationTime = null;
2127

2228
#[ORM\Column(length: 255)]
2329
#[Assert\NotBlank]
2430
#[Assert\Url]
31+
#[Groups(['export'])]
32+
#[SerializedName('1Password URL')]
2533
private ?string $onePasswordUrl = null;
2634

2735
#[ORM\Column(length: 255)]
2836
#[Assert\NotBlank]
2937
#[Assert\Url]
38+
#[Groups(['export'])]
39+
#[SerializedName('Usage documentation URL')]
3040
private ?string $usageDocumentationUrl = null;
3141

3242
#[ORM\Column(length: 10)]
3343
private ?string $type = null;
3444

3545
#[ORM\Column(type: Types::TEXT, nullable: true)]
46+
#[Groups(['export'])]
47+
#[SerializedName('Notes')]
3648
private ?string $notes = null;
3749

3850
public function getDomain(): ?string

src/Entity/Server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Doctrine\Common\Collections\Collection;
1010
use Doctrine\ORM\Mapping as ORM;
1111
use Symfony\Component\Security\Core\User\UserInterface;
12+
use Symfony\Component\Serializer\Annotation\Groups;
13+
use Symfony\Component\Serializer\Annotation\SerializedName;
1214
use Symfony\Component\Validator\Constraints as Assert;
1315

1416
#[ORM\Entity(repositoryClass: ServerRepository::class)]
@@ -26,6 +28,8 @@ class Server extends AbstractBaseEntity implements UserInterface
2628
private string $apiKey;
2729

2830
#[ORM\Column(type: 'string', length: 255, unique: true)]
31+
#[Groups(['export'])]
32+
#[SerializedName('Name')]
2933
private string $name = '';
3034

3135
#[ORM\Column(type: 'string', length: 255, nullable: true)]

src/Entity/Site.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Doctrine\Common\Collections\ArrayCollection;
1010
use Doctrine\Common\Collections\Collection;
1111
use Doctrine\ORM\Mapping as ORM;
12+
use Symfony\Component\Serializer\Annotation\Groups;
13+
use Symfony\Component\Serializer\Annotation\SerializedName;
1214
use Symfony\Component\Validator\Constraints as Assert;
1315

1416
#[ORM\Entity(repositoryClass: SiteRepository::class)]
@@ -23,6 +25,8 @@ class Site extends AbstractHandlerResult
2325
maxMessage: 'Your php version string cannot be longer than {{ limit }} characters',
2426
)]
2527
#[Assert\NotNull]
28+
#[Groups(['export'])]
29+
#[SerializedName('PHP version')]
2630
private string $phpVersion = '';
2731

2832
#[ORM\Column(type: 'string', length: 255)]
@@ -47,9 +51,13 @@ class Site extends AbstractHandlerResult
4751
private Installation $installation;
4852

4953
#[ORM\Column(type: 'string', length: 255, nullable: true)]
54+
#[Groups(['export'])]
55+
#[SerializedName('Primary domain')]
5056
private string $primaryDomain;
5157

5258
#[ORM\Column(type: 'string', length: 25)]
59+
#[Groups(['export'])]
60+
#[SerializedName('Type')]
5361
private string $type = '';
5462

5563
public function __construct()

src/Service/Exporter.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Service;
6+
7+
use Doctrine\ORM\AbstractQuery;
8+
use Symfony\Component\HttpFoundation\HeaderUtils;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\Serializer\Encoder\CsvEncoder;
11+
use Symfony\Component\Serializer\SerializerInterface;
12+
13+
class Exporter
14+
{
15+
public function __construct(
16+
private readonly SerializerInterface $serializer
17+
) {
18+
}
19+
20+
public function export(AbstractQuery $query, string $className, string $format = CsvEncoder::FORMAT): Response
21+
{
22+
$contentType = match ($format) {
23+
CsvEncoder::FORMAT => 'text/csv; charset=utf-8',
24+
default => throw new \InvalidArgumentException(sprintf('Invalid format: %s', $format))
25+
};
26+
27+
$entities = $query->execute();
28+
$content = $this->serializer->serialize($entities, $format, [
29+
'groups' => ['export'],
30+
]);
31+
32+
$filename = sprintf(
33+
'itksites-export-%s-%s.%s',
34+
preg_replace('@^.+\\\\([^\\\\]+)$@', '$1', $className),
35+
// Windows cannot handle colons in filenames so we use . to separate time parts.
36+
(new \DateTimeImmutable())->format('Y-m-d\TH.i.s'),
37+
$format
38+
);
39+
40+
return new Response($content, Response::HTTP_OK, [
41+
'content-type' => $contentType,
42+
'content-disposition' => HeaderUtils::makeDisposition(
43+
HeaderUtils::DISPOSITION_ATTACHMENT,
44+
$filename
45+
),
46+
]);
47+
}
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Trait;
6+
7+
use App\Service\Exporter;
8+
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
9+
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
10+
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
11+
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
12+
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
13+
use EasyCorp\Bundle\EasyAdminBundle\Factory\FilterFactory;
14+
use Symfony\Component\Translation\TranslatableMessage;
15+
16+
trait ExportCrudControllerTrait
17+
{
18+
private Exporter $exporter;
19+
20+
protected function setExporter(Exporter $exporter)
21+
{
22+
$this->exporter = $exporter;
23+
}
24+
25+
protected function createExportAction(string|TranslatableMessage $label = null): Action
26+
{
27+
return Action::new('export', $label ?? new TranslatableMessage('Export'))
28+
->createAsGlobalAction()
29+
->linkToCrudAction('export');
30+
}
31+
32+
public function export(AdminContext $context)
33+
{
34+
if (!isset($this->exporter)) {
35+
throw new \RuntimeException(sprintf('Exporter not set in %s', static::class));
36+
}
37+
38+
assert($this instanceof AbstractCrudController);
39+
// Lifted from self::index().
40+
$fields = FieldCollection::new($this->configureFields(Crud::PAGE_INDEX));
41+
$context->getCrud()->setFieldAssets($this->getFieldAssets($fields));
42+
$filters = $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $fields,
43+
$context->getEntity());
44+
$queryBuilder = $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), $fields, $filters);
45+
46+
return $this->exporter->export($queryBuilder->getQuery(), static::getEntityFqcn());
47+
}
48+
}

0 commit comments

Comments
 (0)