Merge pull request 'feature/views' (#8) from feature/views into main

Reviewed-on: #8
feature/comunication-ogagent
Manuel Aranda Rosales 2024-08-08 08:31:57 +02:00
commit 2e856947f2
39 changed files with 1167 additions and 34 deletions

View File

@ -30,6 +30,7 @@
"symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*",
"symfony/serializer": "6.4.*",
"symfony/translation": "6.4.*",
"symfony/twig-bundle": "6.4.*",
"symfony/validator": "6.4.*",
"symfony/yaml": "6.4.*"

97
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e953b0066fb773aaf6b3835086280c86",
"content-hash": "bf1165324e27bddd1a412f25e438fc4c",
"packages": [
{
"name": "api-platform/core",
@ -6061,6 +6061,101 @@
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/translation",
"version": "v6.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "a002933b13989fc4bd0b58e04bf7eec5210e438a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/a002933b13989fc4bd0b58e04bf7eec5210e438a",
"reference": "a002933b13989fc4bd0b58e04bf7eec5210e438a",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5|^3.0"
},
"conflict": {
"symfony/config": "<5.4",
"symfony/console": "<5.4",
"symfony/dependency-injection": "<5.4",
"symfony/http-client-contracts": "<2.5",
"symfony/http-kernel": "<5.4",
"symfony/service-contracts": "<2.5",
"symfony/twig-bundle": "<5.4",
"symfony/yaml": "<5.4"
},
"provide": {
"symfony/translation-implementation": "2.3|3.0"
},
"require-dev": {
"nikic/php-parser": "^4.18|^5.0",
"psr/log": "^1|^2|^3",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/finder": "^5.4|^6.0|^7.0",
"symfony/http-client-contracts": "^2.5|^3.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0",
"symfony/intl": "^5.4|^6.0|^7.0",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/routing": "^5.4|^6.0|^7.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\Translation\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v6.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/translation-contracts",
"version": "v3.5.0",

View File

@ -0,0 +1,30 @@
resources:
App\Entity\View:
processor: App\State\Processor\ViewProcessor
input: App\Dto\Input\ViewInput
output: App\Dto\Output\ViewOutput
normalizationContext:
groups: ['default', 'view:read']
denormalizationContext:
groups: ['view:write']
operations:
ApiPlatform\Metadata\GetCollection:
provider: App\State\Provider\ViewProvider
filters:
- 'api_platform.filter.view.order'
- 'api_platform.filter.view.search'
ApiPlatform\Metadata\Get:
provider: App\State\Provider\ViewProvider
ApiPlatform\Metadata\Put:
provider: App\State\Provider\ViewProvider
ApiPlatform\Metadata\Patch:
provider: App\State\Provider\ViewProvider
ApiPlatform\Metadata\Post: ~
ApiPlatform\Metadata\Delete: ~
properties:
App\Entity\View:
id:
identifier: false
uuid:
identifier: true

View File

@ -1,6 +1,6 @@
api_platform:
title: 'OgCore API'
description: 'API Documentation for OgCore'
title: 'OgCore Api'
description: 'Api Documentation for OgCore'
version: 1.0.0
path_segment_name_generator: 'api_platform.path_segment_name_generator.dash'
formats:

View File

@ -11,7 +11,7 @@ security:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
stateless: true
stateless: false
provider: app_user_provider
entry_point: jwt
json_login:

View File

@ -0,0 +1,8 @@
framework:
default_locale: es
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
- es
providers:

View File

@ -21,6 +21,12 @@ services:
- { name: api_platform.doctrine.orm.query_extension.collection }
- { name: api_platform.doctrine.orm.query_extension.item }
App\EventListener\LocaleSubscriber:
arguments:
$defaultLocale: '%kernel.default_locale%'
tags:
- { name: kernel.event_subscriber }
App\OpenApi\OpenApiFactory:
decorates: 'api_platform.openapi.factory'
arguments: [ '@App\OpenApi\OpenApiFactory.inner' ]
@ -90,3 +96,8 @@ services:
bind:
$collectionProvider: '@api_platform.doctrine.orm.state.collection_provider'
$itemProvider: '@api_platform.doctrine.orm.state.item_provider'
App\State\Provider\ViewProvider:
bind:
$collectionProvider: '@api_platform.doctrine.orm.state.collection_provider'
$itemProvider: '@api_platform.doctrine.orm.state.item_provider'

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240717094811 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE view (id INT AUTO_INCREMENT NOT NULL, uuid CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', migration_id VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, created_by VARCHAR(255) DEFAULT NULL, updated_by VARCHAR(255) DEFAULT NULL, favourite TINYINT(1) NOT NULL, filters JSON DEFAULT NULL COMMENT \'(DC2Type:json)\', name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_FEFDAB8ED17F50A6 (uuid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE view');
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240718064611 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client ADD position JSON DEFAULT NULL COMMENT \'(DC2Type:json)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client DROP position');
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240801130155 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE view ADD user_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE view ADD CONSTRAINT FK_FEFDAB8EA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)');
$this->addSql('CREATE INDEX IDX_FEFDAB8EA76ED395 ON view (user_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE view DROP FOREIGN KEY FK_FEFDAB8EA76ED395');
$this->addSql('DROP INDEX IDX_FEFDAB8EA76ED395 ON view');
$this->addSql('ALTER TABLE view DROP user_id');
}
}

View File

@ -62,6 +62,7 @@ class MigrateClientsCommand extends Command
$clientEntity->setNetdriver($client['netdriver']);
$clientEntity->setMac($client['mac']);
$clientEntity->setIp($client['ip']);
$clientEntity->setPosition(['x' => 0, 'y' => 0]);
}
$migrationId = $client['ordenadores.grupoid'] === 0 ? $client['ordenadores.idaula'] : $client['ordenadores.grupoid'];

View File

@ -14,7 +14,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'opengnsys:migrate-hardware-profile', description: 'Migrate hardware and hardware profile data')]
#[AsCommand(name: 'opengnsys:migration:hardware-profile', description: 'Migrate hardware and hardware profile data')]
class MigrateHardwareAndHardwareProfileCommand extends Command
{
public function __construct(

View File

@ -0,0 +1,81 @@
<?php
namespace App\Command\Migration;
use App\Entity\Client;
use App\Entity\HardwareProfile;
use App\Entity\OperativeSystem;
use App\Entity\OrganizationalUnit;
use App\Entity\Partition;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'opengnsys:migration:partition', description: 'Migrate os data')]
class MigratePartitionClientCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ManagerRegistry $doctrine
)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
ini_set('memory_limit', '512M');
/** @var EntityManagerInterface $oldDatabaseEntityManager */
$oldDatabaseEntityManager = $this->doctrine->getManager('og_1');
$clientRepository = $this->entityManager->getRepository(Client::class);
$operativeSystemRepository = $this->entityManager->getRepository(OperativeSystem::class);
/** Obtener las particiones de los clientes de la base de datos antigua **/
$rsmPartitions = new ResultSetMapping();
$rsmPartitions->addScalarResult('idordenador', 'idordenador');
$rsmPartitions->addScalarResult('idnombreso', 'idnombreso');
$rsmPartitions->addScalarResult('numdisk', 'numdisk');
$rsmPartitions->addScalarResult('numpar', 'numpar');
$rsmPartitions->addScalarResult('codpar', 'codpar');
$rsmPartitions->addScalarResult('tamano', 'tamano');
$rsmPartitions->addScalarResult('uso', 'uso');
$partitionQuery = $oldDatabaseEntityManager->createNativeQuery('SELECT idordenador, idnombreso, numdisk, numpar, codpar, tamano, uso FROM ordenadores_particiones', $rsmPartitions);
$partitions = $partitionQuery->getResult();
/** Particiones **/
$output->writeln("PARTICIONES TOTAL: ". count($partitions));
foreach ($partitions as $partition){
$clientEntity = $clientRepository->findOneBy(['migrationId' => $partition['idordenador']]);
if(!$clientEntity){
$output->writeln("No se ha encontrado el cliente con id: ". $partition['idordenador']);
continue;
}
$operativeSystemEntity = $operativeSystemRepository->findOneBy(['migrationId' => $partition['idnombreso']]);
if(!$operativeSystemEntity){
$output->writeln("No se ha encontrado el sistema operativo con id: ". $partition['idnombreso']);
continue;
}
$partitionEntity = new Partition();
$partitionEntity->setDiskNumber($partition['numdisk']);
$partitionEntity->setPartitionNumber($partition['numpar']);
$partitionEntity->setPartitionCode($partition['codpar']);
$partitionEntity->setSize($partition['tamano']);
$partitionEntity->setMemoryUsage($partition['uso']);
$partitionEntity->setClient($clientEntity);
$partitionEntity->setOperativeSystem($operativeSystemEntity);
$this->entityManager->persist($partitionEntity);
$this->entityManager->flush();
}
return 1;
}
}

View File

@ -13,7 +13,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'opengnsys:migrate-software-profile', description: 'Migrate software and software profile data')]
#[AsCommand(name: 'opengnsys:migration:software-profile', description: 'Migrate software and software profile data')]
class MigrateSoftwareAndSoftwareProfileCommand extends Command
{
public function __construct(

View File

@ -0,0 +1,37 @@
<?php
namespace App\Controller\Api;
use App\Entity\Client;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SearchController extends AbstractController
{
#[Route('/search', methods: ['GET'])]
public function index(Request $request, EntityManagerInterface $entityManager): Response
{
$filters = $request->query->get('filters');
if ($filters) {
$filters = json_decode($filters, true);
}
$repository = $entityManager->getRepository(Client::class);
$queryBuilder = $repository->createQueryBuilder('e');
if (!empty($filters)) {
foreach ($filters as $field => $value) {
$queryBuilder->andWhere("e.$field = :$field")
->setParameter($field, $value);
}
}
$results = $queryBuilder->getQuery()->getResult();
return new JsonResponse($results);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Doctrine;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\User;
use App\Entity\View;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
final readonly class UserViewExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(
private Security $security,
)
{
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if (View::class !== $resourceClass ) {
return;
}
/** @var User $user */
$user = $this->security->getUser();
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
$queryBuilder->setParameter('current_user', $user->getId());
}
}

View File

@ -12,46 +12,75 @@ use Symfony\Component\Validator\Constraints as Assert;
final class ClientInput
{
#[Assert\NotBlank]
#[Assert\NotBlank(message: 'validators.client.name.not_blank')]
#[Groups(['client:write'])]
#[ApiProperty(description: 'The name of the client', example: "Client 1")]
#[ApiProperty(
description: 'El nombre del cliente',
example: 'Cliente 1',
)]
public ?string $name = null;
#[Groups(['client:write'])]
#[ApiProperty(description: 'The serial number of the client', example: "123456")]
#[ApiProperty(
description: 'La descripción del cliente',
example: 'Cliente descripcion 1',
)]
public ?string $serialNumber = null;
#[Groups(['client:write'])]
#[ApiProperty(description: 'The network interface of the client', example: "eth0")]
#[ApiProperty(
description: 'La interfaz de red del cliente',
example: 'eth0'
)]
public ?string $netiface = null;
#[Groups(['client:write'])]
#[ApiProperty(description: 'The network driver of the client', example: "e1000e")]
#[ApiProperty(
description: 'El driver de red del cliente',
example: 'e1000e'
)]
public ?string $netDriver = null;
#[Groups(['client:write'])]
#[ApiProperty(description: 'The MAC address of the client', example: "00:11:22:33:44:55")]
#[ApiProperty(
description: 'La dirección MAC del cliente',
example: '00:00:00:00:00:00'
)]
public ?string $mac = null;
#[Groups(['client:write'])]
#[Assert\Ip]
#[ApiProperty(description: 'The IP address of the client', example: "127.0.0.1")]
#[Assert\Ip(message: 'validators.ip_address.invalid')]
#[ApiProperty(
description: 'La dirección IP del cliente',
example: '192.168.1.1'
)]
public ?string $ip = null;
#[Groups(['client:write'])]
public ?string $status = null;
#[Assert\NotNull]
#[Assert\NotNull(message: 'validators.organizational_unit.not_null')]
#[Groups(['client:write', 'client:patch'])]
#[ApiProperty(description: 'The organizational unit of the client')]
#[ApiProperty(
description: 'La unidad organizativa del cliente'
)]
public ?OrganizationalUnitOutput $organizationalUnit = null;
#[Groups(['client:write'])]
#[ApiProperty(
description: 'El menú del cliente'
)]
public ?MenuOutput $menu = null;
#[Groups(['client:write'])]
#[ApiProperty(
description: 'El perfil de hardware del cliente'
)]
public ?HardwareProfileOutput $hardwareProfile = null;
#[Groups(['client:write'])]
#[ApiProperty(
description: 'descriptions.client.validation'
)]
public ?array $position = ['x' => 0, 'y' => 0];
public function __construct(?Client $client = null)
{
if (!$client) {
@ -65,7 +94,7 @@ final class ClientInput
$this->netDriver = $client->getNetDriver();
$this->mac = $client->getMac();
$this->ip = $client->getIp();
$this->status = $client->getStatus();
$this->position = $client->getPosition();
if ($client->getMenu()) {
$this->menu = new MenuOutput($client->getMenu());
@ -89,10 +118,10 @@ final class ClientInput
$client->setNetDriver($this->netDriver);
$client->setMac($this->mac);
$client->setIp($this->ip);
$client->setStatus($this->status);
$client->setMenu($this->menu?->getEntity());
$client->setHardwareProfile($this->hardwareProfile?->getEntity());
$client->setPosition($this->position);
return $client;
}
}
}

View File

@ -10,7 +10,7 @@ use Symfony\Component\Validator\Constraints as Assert;
final class HardwareInput
{
#[Assert\NotBlank]
#[Assert\NotBlank(message: 'validators.hardware.name.not_blank')]
#[Groups(['hardware:write'])]
#[ApiProperty(description: 'The name of the hardware', example: "Hardware 1")]
public ?string $name = null;
@ -19,7 +19,7 @@ final class HardwareInput
#[ApiProperty(description: 'The description of the hardware', example: "Hardware 1 description")]
public ?string $description = null;
#[Assert\NotNull]
#[Assert\NotNull(message: 'validators.hardware.type.not_null')]
#[Groups(['hardware:write'])]
#[ApiProperty(description: 'The type of the hardware', example: "Server")]
public ?HardwareTypeOutput $type = null;

View File

@ -8,7 +8,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class HardwareTypeInput
{
#[Assert\NotBlank]
#[Assert\NotBlank(message: 'validators.hardware_type.name.not_blank')]
#[Groups(['hardware-type:write'])]
public ?string $name = null;

View File

@ -11,7 +11,7 @@ use Symfony\Component\Validator\Constraints as Assert;
final class ImageInput
{
#[Assert\NotBlank]
#[Assert\NotBlank(message: 'validators.image.name.not_blank')]
#[Groups(['image:write'])]
#[ApiProperty(description: 'The name of the image', example: "Image 1")]
public ?string $name = null;

View File

@ -18,11 +18,11 @@ class NetworkSettingsInput
#[Groups(['organizational-unit:write'])]
public ?string $proxy = null;
#[Assert\Ip()]
#[Assert\Ip(message: 'validators.network_settings.ip_address.invalid')]
#[Groups(['organizational-unit:write'])]
public ?string $dns = null;
#[Assert\Ip()]
#[Assert\Ip(message: 'validators.network_settings.ip_address.invalid')]
#[Groups(['organizational-unit:write'])]
public ?string $netmask = null;
@ -36,15 +36,15 @@ class NetworkSettingsInput
#[Groups(['organizational-unit:write'])]
public ?string $p2pMode = null;
#[Assert\GreaterThan(0)]
#[Assert\GreaterThan(0, message: 'validators.network_settings.p2p_time.invalid')]
#[Groups(['organizational-unit:write'])]
public ?int $p2pTime = null;
#[Assert\Ip()]
#[Assert\Ip(message: 'validators.network_settings.ip_address.invalid')]
#[Groups(['organizational-unit:write'])]
public ?string $mcastIp = null;
#[Assert\GreaterThan(0)]
#[Assert\GreaterThan(0, message: 'validators.network_settings.mcast_speed.invalid')]
#[Groups(['organizational-write:write'])]
public ?int $mcastSpeed = null;

View File

@ -8,7 +8,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class OperativeSystemInput
{
#[Assert\NotBlank]
#[Assert\NotBlank(message: 'validators.operative_system.name.not_blank')]
#[Groups(['operative-system:write'])]
public ?string $name = null;

View File

@ -16,7 +16,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[OrganizationalUnitParent]
class OrganizationalUnitInput
{
#[Assert\NotBlank]
#[Assert\NotBlank(message: 'validators.organizational_unit.name.not_blank')]
#[Groups(['organizational-unit:write'])]
public ?string $name = null;

View File

@ -0,0 +1,49 @@
<?php
namespace App\Dto\Input;
use ApiPlatform\Metadata\ApiProperty;
use App\Entity\View;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
final class ViewInput
{
#[Assert\NotBlank(message: 'validators.view.name.not_blank')]
#[Groups(['view:write'])]
#[ApiProperty(description: 'The name of the view', example: "View 1")]
public ?string $name = null;
#[Groups(['view:write'])]
#[ApiProperty(description: 'The favourite status of the view', example: true)]
public ?bool $favourite = null;
#[Groups(['view:write'])]
#[ApiProperty(description: 'The filters of the view', example: ["filter1" => "value1"])]
public ?array $filters = null;
public function __construct(?View $view = null)
{
if (!$view) {
return;
}
$this->name = $view->getName();
$this->favourite = $view->isFavourite();
$this->filters= $view->getFilters();
}
public function createOrUpdateEntity(?View $view = null): View
{
if (!$view) {
$view = new View();
}
$view->setName($this->name);
$view->setFavourite($this->favourite);
$view->setFilters($this->filters);
return $view;
}
}

View File

@ -48,6 +48,9 @@ final class ClientOutput extends AbstractOutput
#[ApiProperty(readableLink: true )]
public ?HardwareProfileOutput $hardwareProfile = null;
#[Groups(['client:read'])]
public ?array $position = ['x' => 0, 'y' => 0];
#[Groups(['client:read'])]
public \DateTime $createdAt;
@ -74,8 +77,8 @@ final class ClientOutput extends AbstractOutput
)->toArray();
$this->menu = $client->getMenu() ? new MenuOutput($client->getMenu()) : null;
$this->position = $client->getPosition();
$this->hardwareProfile = $client->getHardwareProfile() ? new HardwareProfileOutput($client->getHardwareProfile()) : null;
$this->createdAt = $client->getCreatedAt();
$this->createdBy = $client->getCreatedBy();
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Dto\Output;
use ApiPlatform\Metadata\Get;
use App\Entity\View;
use Symfony\Component\Serializer\Annotation\Groups;
#[Get(shortName: 'View')]
final class ViewOutput extends AbstractOutput
{
#[Groups(['view:read'])]
public string $name;
#[Groups(['view:read'])]
public bool $favorite;
#[Groups(['view:read'])]
public ?array $filters = null;
public function __construct(View $view)
{
parent::__construct($view);
$this->name = $view->getName();
$this->favorite = $view->isFavourite();
$this->filters = $view->getFilters();
}
}

View File

@ -54,6 +54,9 @@ class Client extends AbstractEntity
#[ORM\Column(nullable: true)]
private ?bool $validation = null;
#[ORM\Column(nullable: true)]
private ?array $position = ['x' => 0, 'y' => 0];
public function __construct()
{
parent::__construct();
@ -209,4 +212,16 @@ class Client extends AbstractEntity
return $this;
}
public function getPosition(): ?array
{
return $this->position;
}
public function setPosition(?array $position): static
{
$this->position = $position;
return $this;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Entity;
use App\Repository\ViewRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ViewRepository::class)]
class View extends \App\Entity\AbstractEntity
{
use NameableTrait;
#[ORM\Column]
private ?bool $favourite = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $filters = [];
#[ORM\ManyToOne]
private ?User $user = null;
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function isFavourite(): ?bool
{
return $this->favourite;
}
public function setFavourite(bool $favourite): static
{
$this->favourite = $favourite;
return $this;
}
public function getFilters(): ?array
{
return $this->filters;
}
public function setFilters(?array $filters): static
{
$this->filters = $filters;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
// src/EventListener/LocaleSubscriber.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\RequestStack;
class LocaleSubscriber implements EventSubscriberInterface
{
private string $defaultLocale;
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack, string $defaultLocale = 'es')
{
$this->defaultLocale = $defaultLocale;
$this->requestStack = $requestStack;
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$locale = $request->getSession()->get('_locale', $this->defaultLocale);
if ($request->attributes->get('_locale')) {
$locale = $request->attributes->get('_locale');
}
$localeFromHeader = $request->getPreferredLanguage(['en', 'es']);
if ($localeFromHeader) {
$locale = $localeFromHeader;
}
$request->setLocale($locale);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Factory;
use App\Entity\View;
use App\Repository\ViewRepository;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator;
/**
* @extends PersistentProxyObjectFactory<View>
*/
final class ViewFactory extends ModelFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
parent::__construct();
}
public static function class(): string
{
return View::class;
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function getDefaults(): array
{
return [
'createdAt' => self::faker()->dateTime(),
'favourite' => self::faker()->boolean(),
'name' => self::faker()->text(255),
'updatedAt' => self::faker()->dateTime()
];
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): self
{
return $this
// ->afterInstantiate(function(View $view): void {})
;
}
protected static function getClass(): string
{
return View::class;
}
}

View File

@ -19,6 +19,8 @@ final readonly class OpenApiFactory implements OpenApiFactoryInterface
$openApi = $this->decorated->__invoke($context);
$this->addRefreshToken($openApi);
$this->addSearchEndpoint($openApi);
return $openApi;
}
@ -75,4 +77,53 @@ final readonly class OpenApiFactory implements OpenApiFactoryInterface
)
));
}
private function addSearchEndpoint(OpenApi $openApi): void
{
$openApi
->getPaths()
->addPath('/search', (new Model\PathItem())->withGet(
(new Model\Operation('getSearch'))
->withTags(['Search'])
->withResponses([
Response::HTTP_OK => [
'description' => 'Search results',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'results' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'example' => 1,
],
'name' => [
'type' => 'string',
'example' => 'Item name',
],
],
],
],
],
],
],
],
],
])
->withSummary('Search for items')
->withParameters([
new Model\Parameter(
'query',
'query',
'Search query parameter',
true,
false,
),
])
));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Repository;
use App\Entity\View;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<View>
*/
class ViewRepository extends AbstractRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, View::class);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\State\Processor;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Validator\ValidatorInterface;
use App\Dto\Input\ViewInput;
use App\Dto\Output\ViewOutput;
use App\Repository\ViewRepository;
use Symfony\Component\Security\Core\Security;
readonly class ViewProcessor implements ProcessorInterface
{
public function __construct(
private ViewRepository $viewRepository,
private ValidatorInterface $validator,
private Security $security
)
{
}
/**
* @throws \Exception
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ViewOutput|null
{
switch ($operation){
case $operation instanceof Post:
case $operation instanceof Put:
case $operation instanceof Patch:
return $this->processCreateOrUpdate($data, $operation, $uriVariables, $context);
case $operation instanceof Delete:
return $this->processDelete($data, $operation, $uriVariables, $context);
}
}
/**
* @throws \Exception
*/
private function processCreateOrUpdate($data, Operation $operation, array $uriVariables = [], array $context = []): ViewOutput
{
if (!($data instanceof ViewInput)) {
throw new \Exception(sprintf('data is not instance of %s', ViewInput::class));
}
$entity = null;
if (isset($uriVariables['uuid'])) {
$entity = $this->viewRepository->findOneByUuid($uriVariables['uuid']);
}
$view = $data->createOrUpdateEntity($entity);
$view->setUser($this->security->getUser());
$this->validator->validate($view);
$this->viewRepository->save($view);
return new ViewOutput($view);
}
private function processDelete($data, Operation $operation, array $uriVariables = [], array $context = []): null
{
$user = $this->viewRepository->findOneByUuid($uriVariables['uuid']);
$this->viewRepository->delete($user);
return null;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\State\Provider;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Put;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use App\Dto\Input\ViewInput;
use App\Dto\Output\ViewOutput;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
readonly class ViewProvider implements ProviderInterface
{
public function __construct(
private ProviderInterface $collectionProvider,
private ProviderInterface $itemProvider
)
{
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
switch ($operation){
case $operation instanceof GetCollection:
return $this->provideCollection($operation, $uriVariables, $context);
case $operation instanceof Patch:
case $operation instanceof Put:
return $this->provideInput($operation, $uriVariables, $context);
case $operation instanceof Get:
return $this->provideItem($operation, $uriVariables, $context);
}
}
private function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object
{
$paginator = $this->collectionProvider->provide($operation, $uriVariables, $context);
$items = new \ArrayObject();
foreach ($paginator->getIterator() as $item){
$items[] = new ViewOutput($item);
}
return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems());
}
public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$item = $this->itemProvider->provide($operation, $uriVariables, $context);
if (!$item) {
throw new NotFoundHttpException('View not found');
}
return new ViewOutput($item);
}
public function provideInput(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if (isset($uriVariables['uuid'])) {
$item = $this->itemProvider->provide($operation, $uriVariables, $context);
return $item !== null ? new ViewInput($item) : null;
}
return new ViewInput();
}
}

View File

@ -219,6 +219,19 @@
"config/routes/security.yaml"
]
},
"symfony/translation": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
},
"files": [
"config/packages/translation.yaml",
"translations/.gitignore"
]
},
"symfony/twig-bundle": {
"version": "6.4",
"recipe": {

View File

@ -0,0 +1,128 @@
<?php
namespace Functional;
use App\Entity\HardwareProfile;
use App\Entity\OrganizationalUnit;
use App\Entity\View;
use App\Factory\HardwareProfileFactory;
use App\Factory\OrganizationalUnitFactory;
use App\Factory\UserFactory;
use App\Factory\ViewFactory;
use App\Model\OrganizationalUnitTypes;
use App\Model\UserGroupPermissions;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class ViewTest extends AbstractTest
{
CONST string USER_ADMIN = 'ogadmin';
CONST string VIEW_CREATE = 'test-view-create';
CONST string VIEW_UPDATE = 'test-view-update';
CONST string VIEW_DELETE = 'test-view-delete';
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function testGetCollectionViews(): void
{
$user = UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
ViewFactory::createMany(10, ['user' => $user]);
$this->createClientWithCredentials()->request('GET', '/views');
$this->assertResponseStatusCodeSame(Response::HTTP_OK);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/contexts/View',
'@id' => '/views',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 10,
]);
}
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function testCreateView(): void
{
$user = UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
ViewFactory::createOne(['name' => self::VIEW_CREATE, 'favourite' => true, 'user' => $user]);
$viewIri = $this->findIriBy(View::class, ['name' => self::VIEW_CREATE]);
$this->createClientWithCredentials()->request('POST', '/views',['json' => [
'name' => self::VIEW_CREATE,
'favourite' => true
]]);
$this->assertResponseStatusCodeSame(201);
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/contexts/ViewOutput',
'@type' => 'View',
'name' => self::VIEW_CREATE
]);
}
/**
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function testUpdateView(): void
{
$user = UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
ViewFactory::createOne(['name' => self::VIEW_CREATE, 'favourite' => true, 'user' => $user]);
$viewIri = $this->findIriBy(View::class, ['name' => self::VIEW_CREATE]);
$this->createClientWithCredentials()->request('PUT', $viewIri, ['json' => [
'name' => self::VIEW_UPDATE,
'favourite' => false
]]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@id' => $viewIri,
'name' => self::VIEW_UPDATE,
'favorite' => false
]);
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws DecodingExceptionInterface
* @throws ClientExceptionInterface
*/
public function testDeleteView(): void
{
$user = UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
ViewFactory::createOne(['name' => self::VIEW_DELETE, 'favourite' => true, 'user' => $user]);
$viewIri = $this->findIriBy(View::class, ['name' => self::VIEW_DELETE]);
$this->createClientWithCredentials()->request('DELETE', $viewIri);
$this->assertResponseStatusCodeSame(204);
$this->assertNull(
static::getContainer()->get('doctrine')->getRepository(View::class)->findOneBy(['name' => self::VIEW_DELETE])
);
}
}

View File

View File

@ -0,0 +1,39 @@
# messages.en.yaml
validators:
client:
ip_address:
invalid: 'The IP address "{{ value }}" is not valid.'
name:
not_blank: 'The name should not be blank.'
organizational_unit:
not_null: 'The organizational unit should not be null.'
view:
name:
not_blank: 'The name should not be blank.'
hardware:
name:
not_blank: 'The name should not be blank.'
type:
not_null: 'The type should not be null.'
hardware_type:
name:
not_blank: 'The name should not be blank.'
image:
name:
not_blank: 'The name should not be blank.'
network_settings:
ip_address:
invalid: 'The IP address "{{ value }}" is not valid.'
p2p_time:
invalid: 'The P2P time "{{ value }}" is not valid.'
mcast_speed:
invalid: 'The MCAST speed "{{ value }}" is not valid.'
operative_system:
name:
not_blank: 'The name should not be blank.'

View File

@ -0,0 +1,39 @@
# messages.es.yaml
validators:
client:
ip_address:
invalid: 'La dirección IP "{{ value }}" no es válida.'
name:
not_blank: 'El nombre no debería estar vacío.'
organizational_unit:
not_null: 'La unidad organizativa no debería estar vacía.'
view:
name:
not_blank: 'El nombre no debería estar vacío.'
hardware:
name:
not_blank: 'El nombre no debería estar vacío.'
type:
not_null: 'El tipo no debería estar vacío.'
hardware_type:
name:
not_blank: 'El nombre no debería estar vacío.'
image:
name:
not_blank: 'El nombre no debería estar vacío.'
network_settings:
ip_address:
invalid: 'La dirección IP "{{ value }}" no es válida.'
p2p_time:
invalid: 'El tiempo P2P "{{ value }}" no es válido.'
mcast_speed:
invalid: 'La velocidad MCAST "{{ value }}" no es válida.'
operative_system:
name:
not_blank: 'El nombre no debería estar vacío.'