diff --git a/composer.json b/composer.json index 4a2dab4..580ef6b 100644 --- a/composer.json +++ b/composer.json @@ -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.*" diff --git a/composer.lock b/composer.lock index f49d68c..e756ea9 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/api_platform/View.yaml b/config/api_platform/View.yaml new file mode 100644 index 0000000..d6206ee --- /dev/null +++ b/config/api_platform/View.yaml @@ -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 \ No newline at end of file diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 8871ca7..352abc2 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -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: diff --git a/config/packages/security.yaml b/config/packages/security.yaml index cf97c8d..5e2315b 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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: diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml new file mode 100644 index 0000000..bf1213d --- /dev/null +++ b/config/packages/translation.yaml @@ -0,0 +1,8 @@ +framework: + default_locale: es + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - en + - es + providers: diff --git a/config/services.yaml b/config/services.yaml index a26d706..beee276 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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' diff --git a/migrations/Version20240717094811.php b/migrations/Version20240717094811.php new file mode 100644 index 0000000..c1eb1f2 --- /dev/null +++ b/migrations/Version20240717094811.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/migrations/Version20240718064611.php b/migrations/Version20240718064611.php new file mode 100644 index 0000000..fedb106 --- /dev/null +++ b/migrations/Version20240718064611.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/migrations/Version20240801130155.php b/migrations/Version20240801130155.php new file mode 100644 index 0000000..5c389bf --- /dev/null +++ b/migrations/Version20240801130155.php @@ -0,0 +1,35 @@ +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'); + } +} diff --git a/src/Command/Migration/MigrateClientsCommand.php b/src/Command/Migration/MigrateClientsCommand.php index 1dc517c..b5117a4 100644 --- a/src/Command/Migration/MigrateClientsCommand.php +++ b/src/Command/Migration/MigrateClientsCommand.php @@ -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']; diff --git a/src/Command/Migration/MigrateHardwareAndHardwareProfileCommand.php b/src/Command/Migration/MigrateHardwareAndHardwareProfileCommand.php index 0fdfd04..e71b417 100644 --- a/src/Command/Migration/MigrateHardwareAndHardwareProfileCommand.php +++ b/src/Command/Migration/MigrateHardwareAndHardwareProfileCommand.php @@ -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( diff --git a/src/Command/Migration/MigratePartitionClientCommand.php b/src/Command/Migration/MigratePartitionClientCommand.php new file mode 100644 index 0000000..32dd3ca --- /dev/null +++ b/src/Command/Migration/MigratePartitionClientCommand.php @@ -0,0 +1,81 @@ +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; + } +} \ No newline at end of file diff --git a/src/Command/Migration/MigrateSoftwareAndSoftwareProfileCommand.php b/src/Command/Migration/MigrateSoftwareAndSoftwareProfileCommand.php index 6daf869..667de64 100644 --- a/src/Command/Migration/MigrateSoftwareAndSoftwareProfileCommand.php +++ b/src/Command/Migration/MigrateSoftwareAndSoftwareProfileCommand.php @@ -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( diff --git a/src/Controller/Api/SearchController.php b/src/Controller/Api/SearchController.php new file mode 100644 index 0000000..45a5d44 --- /dev/null +++ b/src/Controller/Api/SearchController.php @@ -0,0 +1,37 @@ +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); + } +} diff --git a/src/Doctrine/UserViewExtension.php b/src/Doctrine/UserViewExtension.php new file mode 100644 index 0000000..f5f8938 --- /dev/null +++ b/src/Doctrine/UserViewExtension.php @@ -0,0 +1,46 @@ +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()); + } +} \ No newline at end of file diff --git a/src/Dto/Input/ClientInput.php b/src/Dto/Input/ClientInput.php index 2b124dd..ec4a2c2 100644 --- a/src/Dto/Input/ClientInput.php +++ b/src/Dto/Input/ClientInput.php @@ -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; } -} \ No newline at end of file +} diff --git a/src/Dto/Input/HardwareInput.php b/src/Dto/Input/HardwareInput.php index 7674310..4e763bd 100644 --- a/src/Dto/Input/HardwareInput.php +++ b/src/Dto/Input/HardwareInput.php @@ -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; diff --git a/src/Dto/Input/HardwareTypeInput.php b/src/Dto/Input/HardwareTypeInput.php index 9eb0ae1..7078492 100644 --- a/src/Dto/Input/HardwareTypeInput.php +++ b/src/Dto/Input/HardwareTypeInput.php @@ -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; diff --git a/src/Dto/Input/ImageInput.php b/src/Dto/Input/ImageInput.php index d6623ab..10c46a5 100644 --- a/src/Dto/Input/ImageInput.php +++ b/src/Dto/Input/ImageInput.php @@ -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; diff --git a/src/Dto/Input/NetworkSettingsInput.php b/src/Dto/Input/NetworkSettingsInput.php index 750f0de..e32c732 100644 --- a/src/Dto/Input/NetworkSettingsInput.php +++ b/src/Dto/Input/NetworkSettingsInput.php @@ -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; diff --git a/src/Dto/Input/OperativeSystemInput.php b/src/Dto/Input/OperativeSystemInput.php index a38d809..8ebac93 100644 --- a/src/Dto/Input/OperativeSystemInput.php +++ b/src/Dto/Input/OperativeSystemInput.php @@ -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; diff --git a/src/Dto/Input/OrganizationalUnitInput.php b/src/Dto/Input/OrganizationalUnitInput.php index 7ce2c2b..1b83437 100644 --- a/src/Dto/Input/OrganizationalUnitInput.php +++ b/src/Dto/Input/OrganizationalUnitInput.php @@ -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; diff --git a/src/Dto/Input/ViewInput.php b/src/Dto/Input/ViewInput.php new file mode 100644 index 0000000..c3932db --- /dev/null +++ b/src/Dto/Input/ViewInput.php @@ -0,0 +1,49 @@ + "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; + } + +} \ No newline at end of file diff --git a/src/Dto/Output/ClientOutput.php b/src/Dto/Output/ClientOutput.php index a7b49e3..97c0b5a 100644 --- a/src/Dto/Output/ClientOutput.php +++ b/src/Dto/Output/ClientOutput.php @@ -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(); } diff --git a/src/Dto/Output/ViewOutput.php b/src/Dto/Output/ViewOutput.php new file mode 100644 index 0000000..e2f17bb --- /dev/null +++ b/src/Dto/Output/ViewOutput.php @@ -0,0 +1,29 @@ +name = $view->getName(); + $this->favorite = $view->isFavourite(); + $this->filters = $view->getFilters(); + } +} \ No newline at end of file diff --git a/src/Entity/Client.php b/src/Entity/Client.php index 867fa3c..638ee48 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -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; + } } diff --git a/src/Entity/View.php b/src/Entity/View.php new file mode 100644 index 0000000..db4608f --- /dev/null +++ b/src/Entity/View.php @@ -0,0 +1,65 @@ +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; + } +} diff --git a/src/EventListener/LocaleSubscriber.php b/src/EventListener/LocaleSubscriber.php new file mode 100644 index 0000000..4269f8a --- /dev/null +++ b/src/EventListener/LocaleSubscriber.php @@ -0,0 +1,45 @@ +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]], + ]; + } +} diff --git a/src/Factory/ViewFactory.php b/src/Factory/ViewFactory.php new file mode 100644 index 0000000..09417c8 --- /dev/null +++ b/src/Factory/ViewFactory.php @@ -0,0 +1,61 @@ + + */ +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; + } +} diff --git a/src/OpenApi/OpenApiFactory.php b/src/OpenApi/OpenApiFactory.php index d7a732e..9e0ae6f 100644 --- a/src/OpenApi/OpenApiFactory.php +++ b/src/OpenApi/OpenApiFactory.php @@ -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, + ), + ]) + )); + } } \ No newline at end of file diff --git a/src/Repository/ViewRepository.php b/src/Repository/ViewRepository.php new file mode 100644 index 0000000..573429a --- /dev/null +++ b/src/Repository/ViewRepository.php @@ -0,0 +1,18 @@ + + */ +class ViewRepository extends AbstractRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, View::class); + } +} diff --git a/src/State/Processor/ViewProcessor.php b/src/State/Processor/ViewProcessor.php new file mode 100644 index 0000000..3e03780 --- /dev/null +++ b/src/State/Processor/ViewProcessor.php @@ -0,0 +1,71 @@ +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; + } +} diff --git a/src/State/Provider/ViewProvider.php b/src/State/Provider/ViewProvider.php new file mode 100644 index 0000000..683ab06 --- /dev/null +++ b/src/State/Provider/ViewProvider.php @@ -0,0 +1,71 @@ +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(); + } +} diff --git a/symfony.lock b/symfony.lock index d9f1015..caba1f7 100644 --- a/symfony.lock +++ b/symfony.lock @@ -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": { diff --git a/tests/Functional/ViewTest.php b/tests/Functional/ViewTest.php new file mode 100644 index 0000000..5a0b016 --- /dev/null +++ b/tests/Functional/ViewTest.php @@ -0,0 +1,128 @@ + 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]) + ); + } +} \ No newline at end of file diff --git a/translations/messages.es.yaml b/translations/messages.es.yaml new file mode 100644 index 0000000..e69de29 diff --git a/translations/validators.en.yaml b/translations/validators.en.yaml new file mode 100644 index 0000000..e5b6e69 --- /dev/null +++ b/translations/validators.en.yaml @@ -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.' \ No newline at end of file diff --git a/translations/validators.es.yaml b/translations/validators.es.yaml new file mode 100644 index 0000000..9bf6d15 --- /dev/null +++ b/translations/validators.es.yaml @@ -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.' \ No newline at end of file