diff --git a/migrations/Version20230715225205.php b/migrations/Version20230715225205.php new file mode 100644 index 00000000..03cdc930 --- /dev/null +++ b/migrations/Version20230715225205.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE oauth_tokens (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, UNIQUE INDEX oauth_tokens_unique_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE parts ADD provider_reference_provider_key VARCHAR(255) DEFAULT NULL, ADD provider_reference_provider_id VARCHAR(255) DEFAULT NULL, ADD provider_reference_provider_url VARCHAR(255) DEFAULT NULL, ADD provider_reference_last_updated DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE oauth_tokens'); + $this->addSql('ALTER TABLE `parts` DROP provider_reference_provider_key, DROP provider_reference_provider_id, DROP provider_reference_provider_url, DROP provider_reference_last_updated'); + } +} diff --git a/src/Entity/OAuthToken.php b/src/Entity/OAuthToken.php new file mode 100644 index 00000000..9fbacb46 --- /dev/null +++ b/src/Entity/OAuthToken.php @@ -0,0 +1,67 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\AbstractNamedDBElement; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; + +/** + * This entity represents a OAuth token pair (access and refresh token), for an application + */ +#[ORM\Entity()] +#[ORM\Table(name: 'oauth_tokens')] +#[ORM\UniqueConstraint(name: 'oauth_tokens_unique_name', columns: ['name'])] +#[ORM\Index(columns: ['name'], name: 'oauth_tokens_name_idx')] +class OAuthToken extends AbstractNamedDBElement +{ + /** @var string|null The short-term usable OAuth2 token */ + #[ORM\Column(type: 'string', nullable: true)] + private ?string $token = null; + + /** @var \DateTimeInterface The date when the token expires */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeInterface $expires_at = null; + + /** @var string The refresh token for the OAuth2 auth */ + #[ORM\Column(type: 'string')] + private string $refresh_token = ''; + + public function __construct(string $name, string $refresh_token, string $token = null, \DateTimeInterface $expires_at = null) + { + parent::__construct(); + + //If token is given, you also have to give the expires_at date + if ($token !== null && $expires_at === null) { + throw new \InvalidArgumentException('If you give a token, you also have to give the expires_at date'); + } + + $this->name = $name; + $this->refresh_token = $refresh_token; + $this->expires_at = $expires_at; + $this->token = $token; + } + +} \ No newline at end of file diff --git a/src/Entity/Parts/InfoProviderReference.php b/src/Entity/Parts/InfoProviderReference.php new file mode 100644 index 00000000..26e23d34 --- /dev/null +++ b/src/Entity/Parts/InfoProviderReference.php @@ -0,0 +1,155 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Entity\Parts; + +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Embeddable; + +/** + * This class represents a reference to a info provider inside a part. + */ +#[Embeddable] +class InfoProviderReference +{ + + /** @var string|null The key referencing the provider used to get this part, or null if it was not provided by a data provider */ + #[Column(type: 'string', nullable: true)] + private ?string $provider_key = null; + + /** @var string|null The id of this part inside the provider system or null if the part was not provided by a data provider */ + #[Column(type: 'string', nullable: true)] + private ?string $provider_id = null; + + /** + * @var string|null The url of this part inside the provider system or null if this info is not existing + */ + #[Column(type: 'string', nullable: true)] + private ?string $provider_url = null; + + #[Column(type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])] + private ?\DateTimeInterface $last_updated = null; + + /** + * Constructing is forbidden from outside. + */ + private function __construct() + { + + } + + /** + * Returns the key usable to identify the provider, which provided this part. Returns null, if the part was not created by a provider. + * @return string|null + */ + public function getProviderKey(): ?string + { + return $this->provider_key; + } + + /** + * Returns the id of this part inside the provider system or null if the part was not provided by a data provider. + * @return string|null + */ + public function getProviderId(): ?string + { + return $this->provider_id; + } + + /** + * Returns the url of this part inside the provider system or null if this info is not existing. + * @return string|null + */ + public function getProviderUrl(): ?string + { + return $this->provider_url; + } + + /** + * Gets the time, when the part was last time updated by the provider. + * @return \DateTimeInterface|null + */ + public function getLastUpdated(): ?\DateTimeInterface + { + return $this->last_updated; + } + + /** + * Returns true, if this part was created based on infos from a provider. + * Or false, if this part was created by a user manually. + * @return bool + */ + public function isProviderCreated(): bool + { + return $this->provider_key !== null; + } + + /** + * Creates a new instance, without any provider information. + * Use this for parts, which are created by a user manually. + * @return InfoProviderReference + */ + public static function noProvider(): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = null; + $ref->provider_id = null; + $ref->provider_url = null; + $ref->last_updated = null; + return $ref; + } + + /** + * Creates a reference to an info provider based on the given parameters. + * @param string $provider_key + * @param string $provider_id + * @param string|null $provider_url + * @return self + */ + public static function providerReference(string $provider_key, string $provider_id, ?string $provider_url = null): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = $provider_key; + $ref->provider_id = $provider_id; + $ref->provider_url = $provider_url; + $ref->last_updated = new \DateTimeImmutable(); + return $ref; + } + + /** + * Creates a reference to an info provider based on the given Part DTO + * @param SearchResultDTO $dto + * @return self + */ + public static function fromPartDTO(SearchResultDTO $dto): self + { + $ref = new InfoProviderReference(); + $ref->provider_key = $dto->provider_key; + $ref->provider_id = $dto->provider_id; + $ref->provider_url = $dto->provider_url; + $ref->last_updated = new \DateTimeImmutable(); + return $ref; + } +} \ No newline at end of file diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 9279ec11..2826e5fe 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -114,6 +114,9 @@ class Part extends AttachmentContainingDBElement $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); $this->project_bom_entries = new ArrayCollection(); + + //By default, the part has no provider + $this->providerReference = InfoProviderReference::noProvider(); } public function __clone() @@ -139,6 +142,9 @@ class Part extends AttachmentContainingDBElement foreach ($parameters as $parameter) { $this->addParameter(clone $parameter); } + + //Deep clone info provider + $this->providerReference = clone $this->providerReference; } parent::__clone(); } diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 633bf9d0..51bce445 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Entity\Parts\PartTraits; +use App\Entity\Parts\InfoProviderReference; use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; @@ -63,6 +64,12 @@ trait AdvancedPropertyTrait #[ORM\Column(type: Types::STRING, length: 100, nullable: true, unique: true)] protected ?string $ipn = null; + /** + * @var InfoProviderReference The reference to the info provider, that provided the information about this part + */ + #[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')] + protected InfoProviderReference $providerReference; + /** * Checks if this part is marked, for that it needs further review. */ @@ -150,5 +157,27 @@ trait AdvancedPropertyTrait return $this; } + /** + * Returns the reference to the info provider, that provided the information about this part. + * @return InfoProviderReference + */ + public function getProviderReference(): InfoProviderReference + { + return $this->providerReference; + } + + /** + * Sets the reference to the info provider, that provided the information about this part. + * @param InfoProviderReference $providerReference + * @return AdvancedPropertyTrait + */ + public function setProviderReference(InfoProviderReference $providerReference): self + { + $this->providerReference = $providerReference; + return $this; + } + + + } diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php index 5518c880..0c7a639d 100644 --- a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -29,6 +29,7 @@ use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\PartParameter; use App\Entity\Parts\Footprint; +use App\Entity\Parts\InfoProviderReference; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\Part; @@ -157,6 +158,9 @@ final class DTOtoEntityConverter $entity->setManufacturerProductNumber($dto->mpn ?? ''); $entity->setManufacturingStatus($dto->manufacturing_status ?? ManufacturingStatus::NOT_SET); + //Set the provider reference on the part + $entity->setProviderReference(InfoProviderReference::fromPartDTO($dto)); + //Add parameters foreach ($dto->parameters ?? [] as $parameter) { $entity->addParameter($this->convertParameter($parameter)); diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php new file mode 100644 index 00000000..7cb04db4 --- /dev/null +++ b/src/Twig/InfoProviderExtension.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Twig; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +class InfoProviderExtension extends AbstractExtension +{ + public function __construct( + private readonly ProviderRegistry $providerRegistry + ) {} + + public function getFunctions(): array + { + return [ + new TwigFunction('info_provider', $this->getInfoProvider(...)), + new TwigFunction('info_provider_label', $this->getInfoProviderName(...)) + ]; + } + + /** + * Gets the info provider with the given key. Returns null, if the provider does not exist. + * @param string $key + * @return InfoProviderInterface|null + */ + private function getInfoProvider(string $key): ?InfoProviderInterface + { + try { + return $this->providerRegistry->getProviderByKey($key); + } catch (\InvalidArgumentException $exception) { + return null; + } + } + + /** + * Gets the label of the info provider with the given key. Returns null, if the provider does not exist. + * @param string $key + * @return string|null + */ + private function getInfoProviderName(string $key): ?string + { + try { + return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name']; + } catch (\InvalidArgumentException $exception) { + return null; + } + } +} \ No newline at end of file diff --git a/templates/parts/info/_extended_infos.html.twig b/templates/parts/info/_extended_infos.html.twig index e0bb01d7..b80a1a9a 100644 --- a/templates/parts/info/_extended_infos.html.twig +++ b/templates/parts/info/_extended_infos.html.twig @@ -62,5 +62,28 @@ {% endif %} + + + {% trans %}part.info_provider_reference{% endtrans %} + + {% if part.providerReference.providerCreated %} + {% if part.providerReference.providerUrl %} + + {% endif %} + {{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }}: {{ part.providerReference.providerId }} + ({{ part.providerReference.lastUpdated | format_datetime() }}) + {% if part.providerReference.providerUrl %} + + {% endif %} + + {# Show last updated date #} + + {% else %} + {{ helper.boolean_badge(part.providerReference.providerCreated) }} + {% endif %} + + + + \ No newline at end of file diff --git a/templates/parts/info/_sidebar.html.twig b/templates/parts/info/_sidebar.html.twig index 5fb8caef..28eada04 100644 --- a/templates/parts/info/_sidebar.html.twig +++ b/templates/parts/info/_sidebar.html.twig @@ -67,4 +67,16 @@ {{ helper.string_to_tags(part.tags) }} +{% endif %} + +{# Info provider badge #} +{% if part.providerReference.providerCreated %} +
+
+ + + {{ info_provider_label(part.providerReference.providerKey)|default(part.providerReference.providerKey) }} + +
+
{% endif %} \ No newline at end of file diff --git a/tests/Entity/Parts/InfoProviderReferenceTest.php b/tests/Entity/Parts/InfoProviderReferenceTest.php new file mode 100644 index 00000000..365eb68c --- /dev/null +++ b/tests/Entity/Parts/InfoProviderReferenceTest.php @@ -0,0 +1,68 @@ +. + */ + +namespace App\Tests\Entity\Parts; + +use App\Entity\Parts\InfoProviderReference; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use PHPUnit\Framework\TestCase; + +class InfoProviderReferenceTest extends TestCase +{ + public function testNoProvider(): void + { + $provider = InfoProviderReference::noProvider(); + + //The no provider instance should return false for the providerCreated method + $this->assertFalse($provider->isProviderCreated()); + //And null for all other methods + $this->assertNull($provider->getProviderKey()); + $this->assertNull($provider->getProviderId()); + $this->assertNull($provider->getProviderUrl()); + $this->assertNull($provider->getLastUpdated()); + } + + public function testProviderReference(): void + { + $provider = InfoProviderReference::providerReference('test', 'id', 'url'); + + //The provider reference instance should return true for the providerCreated method + $this->assertTrue($provider->isProviderCreated()); + //And the correct values for all other methods + $this->assertEquals('test', $provider->getProviderKey()); + $this->assertEquals('id', $provider->getProviderId()); + $this->assertEquals('url', $provider->getProviderUrl()); + $this->assertNotNull($provider->getLastUpdated()); + } + + public function testFromPartDTO(): void + { + $dto = new PartDetailDTO(provider_key: 'test', provider_id: 'id', name: 'name', description: 'description', provider_url: 'url'); + $reference = InfoProviderReference::fromPartDTO($dto); + + //The provider reference instance should return true for the providerCreated method + $this->assertTrue($reference->isProviderCreated()); + //And the correct values for all other methods + $this->assertEquals('test', $reference->getProviderKey()); + $this->assertEquals('id', $reference->getProviderId()); + $this->assertEquals('url', $reference->getProviderUrl()); + $this->assertNotNull($reference->getLastUpdated()); + } +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index be059355..049480e5 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -11483,5 +11483,17 @@ Please note, that you can not impersonate a disabled user. If you try you will g Prices + + + part.info_provider_reference.badge + The information provider used to create this part. + + + + + part.info_provider_reference + Created by Information provider + +