Merge branch 'kicad-api'

This commit is contained in:
Jan Böhmer 2023-12-03 01:17:39 +01:00
commit 264ed3aaab
44 changed files with 29694 additions and 1507 deletions

View file

@ -0,0 +1,94 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Controller} from "@hotwired/stimulus";
import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
/**
* This is the frontend controller for StaticFileAutocompleteType form element.
* Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete.
* The file is just a list of strings, one per line, which will be used as the autocomplete options.
* Lines starting with # will be ignored.
*/
export default class extends Controller {
_tomSelect;
connect() {
let settings = {
persistent: false,
create: true,
maxItems: 1,
maxOptions: 100,
createOnBlur: true,
selectOnTab: true,
valueField: 'text',
searchField: 'text',
orderField: 'text',
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING'
};
if (this.element.dataset.url) {
const url = this.element.dataset.url;
settings.load = (query, callback) => {
const self = this;
if (self.loading > 1) {
callback();
return;
}
fetch(url)
.then(response => response.text())
.then(text => {
// Convert the text file to array
let lines = text.split("\n");
//Remove all lines beginning with #
lines = lines.filter(x => !x.startsWith("#"));
//Convert the array to an object, where each line is in the text field
lines = lines.map(x => {
return {text: x};
});
//Unset the load function to prevent endless recursion
self._tomSelect.settings.load = null;
callback(lines);
}).catch(() => {
callback();
});
};
}
this._tomSelect = new TomSelect(this.element, settings);
}
disconnect() {
super.disconnect();
//Destroy the TomSelect instance
this._tomSelect.destroy();
}
}

View file

@ -71,3 +71,5 @@ security:
- { path: "^/\\w{2}/tree", role: PUBLIC_ACCESS } - { path: "^/\\w{2}/tree", role: PUBLIC_ACCESS }
# Restrict access to API to users, which has the API access permission # Restrict access to API to users, which has the API access permission
- { path: "^/api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' } - { path: "^/api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' }
# Restrict access to KICAD to users, which has API access permission
- { path: "^/kicad-api", allow_if: 'is_granted("@api.access_api") and is_authenticated()' }

View file

@ -141,6 +141,19 @@ services:
$saml_role_mapping: '%env(json:SAML_ROLE_MAPPING)%' $saml_role_mapping: '%env(json:SAML_ROLE_MAPPING)%'
$update_group_on_login: '%env(bool:SAML_UPDATE_GROUP_ON_LOGIN)%' $update_group_on_login: '%env(bool:SAML_UPDATE_GROUP_ON_LOGIN)%'
security.access_token_extractor.header.token:
class: Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor
arguments:
$tokenType: 'Token'
security.access_token_extractor.main:
class: Symfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor
arguments:
$accessTokenExtractors:
- '@security.access_token_extractor.header'
- '@security.access_token_extractor.header.token'
#################################################################################################################### ####################################################################################################################
# Cache # Cache
#################################################################################################################### ####################################################################################################################

View file

@ -0,0 +1,58 @@
---
layout: default
title: EDA / KiCad integration
parent: Usage
---
# EDA / KiCad integration
Part-DB can function as central database for [EDA](https://en.wikipedia.org/wiki/Electronic_design_automation) or ECAD software used to design electronic schematics and PCBs.
You can connect your EDA software and can view your available parts, with the data saved from Part-DB directly in your EDA software.
Part-DB allows to configure additional metadata for the EDA, to associate symbols and footprints for use inside the EDA software, so the part becomes
directly usable inside the EDA software.
This also allows to configure available and usable parts and their properties in a central place, which is especially useful in teams, where multiple persons design PCBs.
**Currently only KiCad is supported!**
## KiCad Setup
{: .important }
> Part-DB uses the HTTP library feature of KiCad, which is experimental and not part of the stable KiCad 7 releases. If you want to use this feature, you need to install a KiCad nightly build (7.99 version). This feature will most likely also be part of KiCad 8.
Part-DB should be accessible from the PCs with KiCAD. The URL should be stable (so no dynamically changing IP).
You require a user account in Part-DB, which has the permission to access Part-DB API and create API tokens. Every user can has its own account, or you setup a shared read-only account.
To connect KiCad with Part-DB do following steps:
1. Create an API token on the user settings page for the KiCAD application and copy/save it, when it is shown. Currently KiCAD can only read Part-DB database, so a token with read only scope is enough.
2. Create a file `partd.kicad_httplib` (or similar, only the extension is important) with the following content:
```
{
"meta": {
"version": 1.0
},
"name": "Part-DB library",
"description": "This KiCAD library fetches information externally from ",
"source": {
"type": "REST_API",
"api_version": "v1",
"root_url": "http://kicad-instance.invalid/en/kicad-api/",
"token": "THE_GENERATED_API_TOKEN"
}
}
```
3. Replace the `root_url` with the URL of your Part-DB instance plus `/en/kicad-api/`. You can find the right value for this in the Part-DB user settings page under "API endpoints" in the "API tokens" panel.
4. Replace the `token` field value with the token you have generated in step 1.
5. Open KiCad and add this created file as library in the KiCad symbol table under (Preferences --> Manage Symbol Libraries)
If you then place a new part, the library dialog opens and you should be able to see the categories and parts from Part-DB.
### How to associate footprints and symbols with parts
Part-DB dont save any concrete footprints or symbols for the part. Instead Part-DB just contains a reference string in the part metadata, which points to a symbol/footprint in KiCads local library.
You can define this on a per-part basis using the KiCad symbol and KiCad footprint field in the EDA tab of the part editor. Or you can define it at a category (symbol) or footprint level, to assign this value to all parts with this category and footprint.
For example to configure the values for an BC547 transistor you would put `Transistor_BJT:BC547` on the parts Kicad symbol to give it the right schematic symbol in EEschema and `Package_TO_SOT_THT:TO-92` to give it the right footprint in PcbNew.
If you type in a character, you will get an autocomplete list of all symbols and footprints available in the kicad standard library. You can also input your own value.

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
final class Version20231130180903 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Added EDA fields';
}
public function mySQLUp(Schema $schema): void
{
$this->addSql('ALTER TABLE categories ADD eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, ADD eda_info_invisible TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_bom TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_board TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_sim TINYINT(1) DEFAULT NULL, ADD eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE footprints ADD eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, ADD eda_info_value VARCHAR(255) DEFAULT NULL, ADD eda_info_invisible TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_bom TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_board TINYINT(1) DEFAULT NULL, ADD eda_info_exclude_from_sim TINYINT(1) DEFAULT NULL, ADD eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, ADD eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL');
}
public function mySQLDown(Schema $schema): void
{
$this->addSql('ALTER TABLE `categories` DROP eda_info_reference_prefix, DROP eda_info_invisible, DROP eda_info_exclude_from_bom, DROP eda_info_exclude_from_board, DROP eda_info_exclude_from_sim, DROP eda_info_kicad_symbol');
$this->addSql('ALTER TABLE `footprints` DROP eda_info_kicad_footprint');
$this->addSql('ALTER TABLE `parts` DROP eda_info_reference_prefix, DROP eda_info_value, DROP eda_info_invisible, DROP eda_info_exclude_from_bom, DROP eda_info_exclude_from_board, DROP eda_info_exclude_from_sim, DROP eda_info_kicad_symbol, DROP eda_info_kicad_footprint');
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql('ALTER TABLE categories ADD COLUMN eda_info_reference_prefix VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD COLUMN eda_info_invisible BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD COLUMN eda_info_exclude_from_bom BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD COLUMN eda_info_exclude_from_board BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD COLUMN eda_info_exclude_from_sim BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD COLUMN eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE footprints ADD COLUMN eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_reference_prefix VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_value VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_invisible BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_exclude_from_bom BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_exclude_from_board BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_exclude_from_sim BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD COLUMN eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL');
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql('CREATE TEMPORARY TABLE __temp__categories AS SELECT id, parent_id, id_preview_attachment, name, last_modified, datetime_added, comment, not_selectable, alternative_names, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment FROM "categories"');
$this->addSql('DROP TABLE "categories"');
$this->addSql('CREATE TABLE "categories" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, partname_hint CLOB NOT NULL, partname_regex CLOB NOT NULL, disable_footprints BOOLEAN NOT NULL, disable_manufacturers BOOLEAN NOT NULL, disable_autodatasheets BOOLEAN NOT NULL, disable_properties BOOLEAN NOT NULL, default_description CLOB NOT NULL, default_comment CLOB NOT NULL, CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "categories" (id, parent_id, id_preview_attachment, name, last_modified, datetime_added, comment, not_selectable, alternative_names, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment) SELECT id, parent_id, id_preview_attachment, name, last_modified, datetime_added, comment, not_selectable, alternative_names, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment FROM __temp__categories');
$this->addSql('DROP TABLE __temp__categories');
$this->addSql('CREATE INDEX IDX_3AF34668727ACA70 ON "categories" (parent_id)');
$this->addSql('CREATE INDEX IDX_3AF34668EA7100A1 ON "categories" (id_preview_attachment)');
$this->addSql('CREATE INDEX category_idx_name ON "categories" (name)');
$this->addSql('CREATE INDEX category_idx_parent_name ON "categories" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__footprints AS SELECT id, parent_id, id_preview_attachment, id_footprint_3d, name, last_modified, datetime_added, comment, not_selectable, alternative_names FROM "footprints"');
$this->addSql('DROP TABLE "footprints"');
$this->addSql('CREATE TABLE "footprints" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_footprint_3d INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_A34D68A2727ACA70 FOREIGN KEY (parent_id) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A2EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A232A38C34 FOREIGN KEY (id_footprint_3d) REFERENCES "attachments" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "footprints" (id, parent_id, id_preview_attachment, id_footprint_3d, name, last_modified, datetime_added, comment, not_selectable, alternative_names) SELECT id, parent_id, id_preview_attachment, id_footprint_3d, name, last_modified, datetime_added, comment, not_selectable, alternative_names FROM __temp__footprints');
$this->addSql('DROP TABLE __temp__footprints');
$this->addSql('CREATE INDEX IDX_A34D68A2727ACA70 ON "footprints" (parent_id)');
$this->addSql('CREATE INDEX IDX_A34D68A2EA7100A1 ON "footprints" (id_preview_attachment)');
$this->addSql('CREATE INDEX IDX_A34D68A232A38C34 ON "footprints" (id_footprint_3d)');
$this->addSql('CREATE INDEX footprint_idx_name ON "footprints" (name)');
$this->addSql('CREATE INDEX footprint_idx_parent_name ON "footprints" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM "parts"');
$this->addSql('DROP TABLE "parts"');
$this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(255) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "parts" (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated FROM __temp__parts');
$this->addSql('DROP TABLE __temp__parts');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)');
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)');
$this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)');
$this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)');
$this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)');
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)');
$this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)');
$this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)');
$this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)');
}
}

13272
public/kicad/footprints.txt Normal file

File diff suppressed because it is too large Load diff

12880
public/kicad/symbols.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Services\EDAIntegration\KiCADHelper;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/kicad-api/v1')]
class KiCadApiController extends AbstractController
{
public function __construct(
private readonly KiCADHelper $kiCADHelper,
)
{
}
#[Route('/', name: 'kicad_api_root')]
public function root(): Response
{
$this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS');
//The API documentation says this can be either blank or the URL to the endpoints
return $this->json([
'categories' => '',
'parts' => '',
]);
}
#[Route('/categories.json', name: 'kicad_api_categories')]
public function categories(): Response
{
$this->denyAccessUnlessGranted('@categories.read');
return $this->json($this->kiCADHelper->getCategories());
}
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
public function categoryParts(Category $category): Response
{
$this->denyAccessUnlessGranted('read', $category);
$this->denyAccessUnlessGranted('@parts.read');
return $this->json($this->kiCADHelper->getCategoryParts($category));
}
#[Route('/parts/{part}.json', name: 'kicad_api_part')]
public function partDetails(Part $part): Response
{
$this->denyAccessUnlessGranted('read', $part);
return $this->json($this->kiCADHelper->getKiCADPart($part));
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\DataFixtures;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Part;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class EDADataFixtures extends Fixture implements DependentFixtureInterface
{
public function getDependencies(): array
{
return [PartFixtures::class];
}
public function load(ObjectManager $manager): void
{
//Load elements from DB
$category1 = $manager->find(Category::class, 1);
$footprint1 = $manager->find(Footprint::class, 1);
$part1 = $manager->find(Part::class, 1);
//Put some data into category1 and foorprint1
$category1?->getEdaInfo()
->setExcludeFromBoard(true)
->setKicadSymbol('Category:1')
->setReferencePrefix('C')
;
$footprint1?->getEdaInfo()
->setKicadFootprint('Footprint:1')
;
//Put some data into part1 (which overrides the data from category1 and footprint1 on part1)
$part1?->getEdaInfo()
->setExcludeFromSim(false)
->setKicadSymbol('Part:1')
->setKicadFootprint('Part:1')
->setReferencePrefix('P')
;
//Flush the changes
$manager->flush();
}
}

View file

@ -0,0 +1,132 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Entity\EDA;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embeddable;
use Symfony\Component\Serializer\Annotation\Groups;
#[Embeddable]
class EDACategoryInfo
{
/**
* @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors.
*/
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
private ?string $reference_prefix = null;
/** @var bool|null If this is true, then this part is invisible for the EDA software */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
private ?bool $invisible = null;
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
private ?bool $exclude_from_bom = null;
/** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
private ?bool $exclude_from_board = null;
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
private ?bool $exclude_from_sim = true;
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'category:read', 'category:write'])]
private ?string $kicad_symbol = null;
public function getReferencePrefix(): ?string
{
return $this->reference_prefix;
}
public function setReferencePrefix(?string $reference_prefix): EDACategoryInfo
{
$this->reference_prefix = $reference_prefix;
return $this;
}
public function getInvisible(): ?bool
{
return $this->invisible;
}
public function setInvisible(?bool $invisible): EDACategoryInfo
{
$this->invisible = $invisible;
return $this;
}
public function getExcludeFromBom(): ?bool
{
return $this->exclude_from_bom;
}
public function setExcludeFromBom(?bool $exclude_from_bom): EDACategoryInfo
{
$this->exclude_from_bom = $exclude_from_bom;
return $this;
}
public function getExcludeFromBoard(): ?bool
{
return $this->exclude_from_board;
}
public function setExcludeFromBoard(?bool $exclude_from_board): EDACategoryInfo
{
$this->exclude_from_board = $exclude_from_board;
return $this;
}
public function getExcludeFromSim(): ?bool
{
return $this->exclude_from_sim;
}
public function setExcludeFromSim(?bool $exclude_from_sim): EDACategoryInfo
{
$this->exclude_from_sim = $exclude_from_sim;
return $this;
}
public function getKicadSymbol(): ?string
{
return $this->kicad_symbol;
}
public function setKicadSymbol(?string $kicad_symbol): EDACategoryInfo
{
$this->kicad_symbol = $kicad_symbol;
return $this;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Entity\EDA;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embeddable;
use Symfony\Component\Serializer\Annotation\Groups;
#[Embeddable]
class EDAFootprintInfo
{
/** @var string|null The KiCAD footprint, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'footprint:read', 'footprint:write'])]
private ?string $kicad_footprint = null;
public function getKicadFootprint(): ?string
{
return $this->kicad_footprint;
}
public function setKicadFootprint(?string $kicad_footprint): EDAFootprintInfo
{
$this->kicad_footprint = $kicad_footprint;
return $this;
}
}

View file

@ -0,0 +1,170 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Entity\EDA;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embeddable;
use Symfony\Component\Serializer\Annotation\Groups;
#[Embeddable]
class EDAPartInfo
{
/**
* @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors.
*/
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?string $reference_prefix = null;
/** @var string|null The value, which should be shown together with the part (e.g. 470 for a 470 Ohm resistor) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?string $value = null;
/** @var bool|null If this is true, then this part is invisible for the EDA software */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?bool $invisible = null;
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?bool $exclude_from_bom = null;
/** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?bool $exclude_from_board = null;
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?bool $exclude_from_sim = null;
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?string $kicad_symbol = null;
/** @var string|null The KiCAD footprint, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?string $kicad_footprint = null;
public function __construct()
{
}
public function getReferencePrefix(): ?string
{
return $this->reference_prefix;
}
public function setReferencePrefix(?string $reference_prefix): EDAPartInfo
{
$this->reference_prefix = $reference_prefix;
return $this;
}
public function getValue(): ?string
{
return $this->value;
}
public function setValue(?string $value): EDAPartInfo
{
$this->value = $value;
return $this;
}
public function getInvisible(): ?bool
{
return $this->invisible;
}
public function setInvisible(?bool $invisible): EDAPartInfo
{
$this->invisible = $invisible;
return $this;
}
public function getExcludeFromBom(): ?bool
{
return $this->exclude_from_bom;
}
public function setExcludeFromBom(?bool $exclude_from_bom): EDAPartInfo
{
$this->exclude_from_bom = $exclude_from_bom;
return $this;
}
public function getExcludeFromBoard(): ?bool
{
return $this->exclude_from_board;
}
public function setExcludeFromBoard(?bool $exclude_from_board): EDAPartInfo
{
$this->exclude_from_board = $exclude_from_board;
return $this;
}
public function getExcludeFromSim(): ?bool
{
return $this->exclude_from_sim;
}
public function setExcludeFromSim(?bool $exclude_from_sim): EDAPartInfo
{
$this->exclude_from_sim = $exclude_from_sim;
return $this;
}
public function getKicadSymbol(): ?string
{
return $this->kicad_symbol;
}
public function setKicadSymbol(?string $kicad_symbol): EDAPartInfo
{
$this->kicad_symbol = $kicad_symbol;
return $this;
}
public function getKicadFootprint(): ?string
{
return $this->kicad_footprint;
}
public function setKicadFootprint(?string $kicad_footprint): EDAPartInfo
{
$this->kicad_footprint = $kicad_footprint;
return $this;
}
}

View file

@ -38,6 +38,8 @@ use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter; use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
use App\Entity\EDA\EDACategoryInfo;
use App\Entity\EDA\EDAPartInfo;
use App\Repository\Parts\CategoryRepository; use App\Repository\Parts\CategoryRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@ -47,6 +49,7 @@ use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\CategoryParameter; use App\Entity\Parameters\CategoryParameter;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -185,6 +188,19 @@ class Category extends AbstractPartsContainingDBElement
#[Groups(['category:read'])] #[Groups(['category:read'])]
protected ?\DateTimeInterface $lastModified = null; protected ?\DateTimeInterface $lastModified = null;
#[Assert\Valid]
#[ORM\Embedded(class: EDACategoryInfo::class)]
#[Groups(['full', 'category:read', 'category:write'])]
protected EDACategoryInfo $eda_info;
public function __construct()
{
parent::__construct();
$this->children = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection();
$this->eda_info = new EDACategoryInfo();
}
public function getPartnameHint(): string public function getPartnameHint(): string
{ {
@ -278,14 +294,17 @@ class Category extends AbstractPartsContainingDBElement
public function setDefaultComment(string $default_comment): self public function setDefaultComment(string $default_comment): self
{ {
$this->default_comment = $default_comment; $this->default_comment = $default_comment;
return $this; return $this;
} }
public function __construct()
public function getEdaInfo(): EDACategoryInfo
{ {
parent::__construct(); return $this->eda_info;
$this->children = new ArrayCollection(); }
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection(); public function setEdaInfo(EDACategoryInfo $eda_info): Category
{
$this->eda_info = $eda_info;
return $this;
} }
} }

View file

@ -39,6 +39,9 @@ use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter; use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\EDA\EDACategoryInfo;
use App\Entity\EDA\EDAFootprintInfo;
use App\Entity\EDA\EDAPartInfo;
use App\Repository\Parts\FootprintRepository; use App\Repository\Parts\FootprintRepository;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@ -47,6 +50,7 @@ use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\FootprintParameter;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -137,6 +141,19 @@ class Footprint extends AbstractPartsContainingDBElement
#[Groups(['footprint:read'])] #[Groups(['footprint:read'])]
protected ?\DateTimeInterface $lastModified = null; protected ?\DateTimeInterface $lastModified = null;
#[Assert\Valid]
#[ORM\Embedded(class: EDAFootprintInfo::class)]
#[Groups(['full', 'footprint:read', 'footprint:write'])]
protected EDAFootprintInfo $eda_info;
public function __construct()
{
parent::__construct();
$this->children = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection();
$this->eda_info = new EDAFootprintInfo();
}
/**************************************** /****************************************
* Getters * Getters
@ -166,11 +183,15 @@ class Footprint extends AbstractPartsContainingDBElement
return $this; return $this;
} }
public function __construct()
public function getEdaInfo(): EDAFootprintInfo
{ {
parent::__construct(); return $this->eda_info;
$this->children = new ArrayCollection(); }
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection(); public function setEdaInfo(EDAFootprintInfo $eda_info): Footprint
{
$this->eda_info = $eda_info;
return $this;
} }
} }

View file

@ -41,7 +41,9 @@ use App\ApiPlatform\Filter\EntityFilter;
use App\ApiPlatform\Filter\LikeFilter; use App\ApiPlatform\Filter\LikeFilter;
use App\ApiPlatform\Filter\PartStoragelocationFilter; use App\ApiPlatform\Filter\PartStoragelocationFilter;
use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\EDA\EDAPartInfo;
use App\Entity\Parts\PartTraits\AssociationTrait; use App\Entity\Parts\PartTraits\AssociationTrait;
use App\Entity\Parts\PartTraits\EDATrait;
use App\Repository\PartRepository; use App\Repository\PartRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
@ -83,7 +85,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read', new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read',
'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read'], 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'],
'openapi_definition_name' => 'Read', 'openapi_definition_name' => 'Read',
], security: 'is_granted("read", object)'), ], security: 'is_granted("read", object)'),
new GetCollection(security: 'is_granted("@parts.read")'), new GetCollection(security: 'is_granted("@parts.read")'),
@ -92,7 +94,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Delete(security: 'is_granted("delete", object)'), new Delete(security: 'is_granted("delete", object)'),
], ],
normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'], normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['part:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'], denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write'], 'openapi_definition_name' => 'Write'],
)] )]
#[ApiFilter(PropertyFilter::class)] #[ApiFilter(PropertyFilter::class)]
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])] #[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
@ -115,6 +117,7 @@ class Part extends AttachmentContainingDBElement
use ParametersTrait; use ParametersTrait;
use ProjectTrait; use ProjectTrait;
use AssociationTrait; use AssociationTrait;
use EDATrait;
/** @var Collection<int, PartParameter> /** @var Collection<int, PartParameter>
*/ */
@ -173,6 +176,7 @@ class Part extends AttachmentContainingDBElement
//By default, the part has no provider //By default, the part has no provider
$this->providerReference = InfoProviderReference::noProvider(); $this->providerReference = InfoProviderReference::noProvider();
$this->eda_info = new EDAPartInfo();
} }
public function __clone() public function __clone()
@ -208,6 +212,7 @@ class Part extends AttachmentContainingDBElement
//Deep clone info provider //Deep clone info provider
$this->providerReference = clone $this->providerReference; $this->providerReference = clone $this->providerReference;
$this->eda_info = clone $this->eda_info;
} }
parent::__clone(); parent::__clone();
} }

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Entity\Parts\PartTraits;
use App\Entity\EDA\EDAPartInfo;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embedded;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints\Valid;
trait EDATrait
{
#[Valid]
#[Embedded(class: EDAPartInfo::class)]
#[Groups(['full', 'part:read', 'part:write'])]
protected EDAPartInfo $eda_info;
public function getEdaInfo(): EDAPartInfo
{
return $this->eda_info;
}
public function setEdaInfo(?EDAPartInfo $eda_info): self
{
if ($eda_info !== null) {
//Do a clone, to ensure that the property is updated in the database
$eda_info = clone $eda_info;
}
$this->eda_info = $eda_info;
return $this;
}
}

View file

@ -24,49 +24,52 @@ namespace App\EntityListeners;
use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\UserSystem\Group; use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Services\UserSystem\UserCacheKeyGenerator; use App\Services\Cache\ElementCacheTagGenerator;
use Doctrine\ORM\Event\LifecycleEventArgs; use App\Services\Cache\UserCacheKeyGenerator;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostRemoveEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use function get_class;
use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface;
class TreeCacheInvalidationListener class TreeCacheInvalidationListener
{ {
public function __construct(protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator) public function __construct(
protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator,
protected ElementCacheTagGenerator $tagGenerator
)
{ {
} }
#[ORM\PostUpdate] #[ORM\PostUpdate]
#[ORM\PostPersist] #[ORM\PostPersist]
#[ORM\PostRemove] #[ORM\PostRemove]
public function invalidate(AbstractDBElement $element, LifecycleEventArgs $event): void public function invalidate(AbstractDBElement $element, PostUpdateEventArgs|PostPersistEventArgs|PostRemoveEventArgs $event): void
{ {
//If an element was changed, then invalidate all cached trees with this element class //For all changes, we invalidate the cache for all elements of this class
if ($element instanceof AbstractStructuralDBElement || $element instanceof LabelProfile) { $tags = [$this->tagGenerator->getElementTypeCacheTag($element)];
$secure_class_name = str_replace('\\', '_', $element::class);
$this->cache->invalidateTags([$secure_class_name]);
//Trigger a sidebar reload for all users (see SidebarTreeUpdater service)
if(!$element instanceof LabelProfile) { //For changes on structural elements, we also invalidate the sidebar tree
$this->cache->invalidateTags(['sidebar_tree_update']); if ($element instanceof AbstractStructuralDBElement) {
} $tags[] = 'sidebar_tree_update';
} }
//If a user change, then invalidate all cached trees for him //For user changes, we invalidate the cache for this user
if ($element instanceof User) { if ($element instanceof User) {
$secure_class_name = str_replace('\\', '_', $element::class); $tags[] = $this->keyGenerator->generateKey($element);
$tag = $this->keyGenerator->generateKey($element);
$this->cache->invalidateTags([$tag, $secure_class_name]);
} }
/* If any group change, then invalidate all cached trees. Users Permissions can be inherited from groups, /* If any group change, then invalidate all cached trees. Users Permissions can be inherited from groups,
so a change in any group can cause big permisssion changes for users. So to be sure, invalidate all trees */ so a change in any group can cause big permisssion changes for users. So to be sure, invalidate all trees */
if ($element instanceof Group) { if ($element instanceof Group) {
$tag = 'groups'; $tags[] = 'groups';
$this->cache->invalidateTags([$tag]); }
}
//Invalidate the cache for the given tags
$this->cache->invalidateTags($tags);
} }
} }

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Form\AdminPages; namespace App\Form\AdminPages;
use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractNamedDBElement;
use App\Form\Part\EDA\EDACategoryInfoType;
use App\Form\Type\RichTextEditorType; use App\Form\Type\RichTextEditorType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -104,5 +105,11 @@ class CategoryAdminForm extends BaseEntityAdminForm
], ],
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]); ]);
//EDA info
$builder->add('eda_info', EDACategoryInfoType::class, [
'label' => false,
'required' => false,
]);
} }
} }

View file

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace App\Form\AdminPages; namespace App\Form\AdminPages;
use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\EDA\EDAFootprintInfo;
use App\Form\Part\EDA\EDAFootprintInfoType;
use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\MasterPictureAttachmentType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@ -37,5 +39,11 @@ class FootprintAdminForm extends BaseEntityAdminForm
'filter' => '3d_model', 'filter' => '3d_model',
'entity' => $entity, 'entity' => $entity,
]); ]);
//EDA info
$builder->add('eda_info', EDAFootprintInfoType::class, [
'label' => false,
'required' => false,
]);
} }
} }

View file

@ -0,0 +1,87 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Part\EDA;
use App\Entity\EDA\EDACategoryInfo;
use App\Entity\EDA\EDAFootprintInfo;
use App\Form\Type\TriStateCheckboxType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function Symfony\Component\Translation\t;
class EDACategoryInfoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('reference_prefix', TextType::class, [
'label' => 'eda_info.reference_prefix',
'attr' => [
'placeholder' => t('eda_info.reference_prefix.placeholder'),
]
]
)
->add('invisible', TriStateCheckboxType::class, [
'label' => 'eda_info.invisible',
])
->add('exclude_from_bom', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_bom',
'label_attr' => [
'class' => 'checkbox-inline'
]
])
->add('exclude_from_board', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_board',
'label_attr' => [
'class' => 'checkbox-inline'
]
])
->add('exclude_from_sim', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_sim',
'label_attr' => [
'class' => 'checkbox-inline'
]
])
->add('kicad_symbol', KicadFieldAutocompleteType::class, [
'label' => 'eda_info.kicad_symbol',
'type' => KicadFieldAutocompleteType::TYPE_SYMBOL,
'attr' => [
'placeholder' => t('eda_info.kicad_symbol.placeholder'),
]
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EDACategoryInfo::class,
]);
}
}

View file

@ -0,0 +1,56 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Part\EDA;
use App\Entity\EDA\EDAFootprintInfo;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function Symfony\Component\Translation\t;
class EDAFootprintInfoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('kicad_footprint', KicadFieldAutocompleteType::class, [
'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT,
'label' => 'eda_info.kicad_footprint',
'attr' => [
'placeholder' => t('eda_info.kicad_footprint.placeholder'),
]
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EDAFootprintInfo::class,
]);
}
}

View file

@ -0,0 +1,96 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Part\EDA;
use App\Entity\EDA\EDAPartInfo;
use App\Form\Type\TriStateCheckboxType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function Symfony\Component\Translation\t;
class EDAPartInfoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('reference_prefix', TextType::class, [
'label' => 'eda_info.reference_prefix',
'attr' => [
'placeholder' => t('eda_info.reference_prefix.placeholder'),
]
]
)
->add('value', TextType::class, [
'label' => 'eda_info.value',
'attr' => [
'placeholder' => t('eda_info.value.placeholder'),
]
])
->add('invisible', TriStateCheckboxType::class, [
'label' => 'eda_info.invisible',
])
->add('exclude_from_bom', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_bom',
'label_attr' => [
'class' => 'checkbox-inline'
]
])
->add('exclude_from_board', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_board',
'label_attr' => [
'class' => 'checkbox-inline'
]
])
->add('exclude_from_sim', TriStateCheckboxType::class, [
'label' => 'eda_info.exclude_from_sim',
'label_attr' => [
'class' => 'checkbox-inline'
]
])
->add('kicad_symbol', KicadFieldAutocompleteType::class, [
'label' => 'eda_info.kicad_symbol',
'type' => KicadFieldAutocompleteType::TYPE_SYMBOL,
'attr' => [
'placeholder' => t('eda_info.kicad_symbol.placeholder'),
]
])
->add('kicad_footprint', KicadFieldAutocompleteType::class, [
'label' => 'eda_info.kicad_footprint',
'type' => KicadFieldAutocompleteType::TYPE_FOOTPRINT,
'attr' => [
'placeholder' => t('eda_info.kicad_footprint.placeholder'),
]
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EDAPartInfo::class,
]);
}
}

View file

@ -0,0 +1,60 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Part\EDA;
use App\Form\Type\StaticFileAutocompleteType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* This is a specialized version of the StaticFileAutocompleteType, which loads the different types of Kicad lists.
*/
class KicadFieldAutocompleteType extends AbstractType
{
public const TYPE_FOOTPRINT = 'footprint';
public const TYPE_SYMBOL = 'symbol';
public const FOOTPRINT_PATH = '/kicad/footprints.txt';
public const SYMBOL_PATH = '/kicad/symbols.txt';
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired('type');
$resolver->setAllowedValues('type', [self::TYPE_SYMBOL, self::TYPE_FOOTPRINT]);
$resolver->setDefaults([
'file' => fn(Options $options) => match ($options['type']) {
self::TYPE_FOOTPRINT => self::FOOTPRINT_PATH,
self::TYPE_SYMBOL => self::SYMBOL_PATH,
default => throw new \InvalidArgumentException('Invalid type'),
}
]);
}
public function getParent(): string
{
return StaticFileAutocompleteType::class;
}
}

View file

@ -22,27 +22,28 @@ declare(strict_types=1);
namespace App\Form\Part; namespace App\Form\Part;
use App\Entity\Parts\ManufacturingStatus;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Attachments\PartAttachment; use App\Entity\Attachments\PartAttachment;
use App\Entity\EDA\EDAPartInfo;
use App\Entity\Parameters\PartParameter; use App\Entity\Parameters\PartParameter;
use App\Entity\Parts\Category; use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint; use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Orderdetail;
use App\Form\AttachmentFormType; use App\Form\AttachmentFormType;
use App\Form\ParameterType; use App\Form\ParameterType;
use App\Form\Part\EDA\EDAPartInfoType;
use App\Form\Type\MasterPictureAttachmentType; use App\Form\Type\MasterPictureAttachmentType;
use App\Form\Type\RichTextEditorType; use App\Form\Type\RichTextEditorType;
use App\Form\Type\SIUnitType; use App\Form\Type\SIUnitType;
use App\Form\Type\StructuralEntityType; use App\Form\Type\StructuralEntityType;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\LogSystem\EventCommentNeededHelper; use App\Services\LogSystem\EventCommentNeededHelper;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\ResetType; use Symfony\Component\Form\Extension\Core\Type\ResetType;
@ -52,7 +53,6 @@ use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class PartBaseType extends AbstractType class PartBaseType extends AbstractType
{ {
@ -255,6 +255,12 @@ class PartBaseType extends AbstractType
'by_reference' => false, 'by_reference' => false,
]); ]);
//EDA info
$builder->add('eda_info', EDAPartInfoType::class, [
'label' => false,
'required' => false,
]);
$builder->add('log_comment', TextType::class, [ $builder->add('log_comment', TextType::class, [
'label' => 'edit.log_comment', 'label' => 'edit.log_comment',
'mapped' => false, 'mapped' => false,

View file

@ -0,0 +1,63 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Implements a text type with autocomplete functionality based on a static file, containing a list of autocomplete
* suggestions.
* Other values are allowed, but the user can select from the list of suggestions.
* The file must be located in the public directory!
*/
class StaticFileAutocompleteType extends AbstractType
{
public function __construct(
private readonly Packages $assets
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired('file');
$resolver->setAllowedTypes('file', 'string');
}
public function getParent(): string
{
return TextType::class;
}
public function finishView(FormView $view, FormInterface $form, array $options): void
{
//Add the data-controller and data-url attributes to the form field
$view->vars['attr']['data-controller'] = 'elements--static-file-autocomplete';
$view->vars['attr']['data-url'] = $this->assets->getUrl($options['file']);
}
}

View file

@ -46,7 +46,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class ApiTokenAuthenticator implements AuthenticatorInterface class ApiTokenAuthenticator implements AuthenticatorInterface
{ {
public function __construct( public function __construct(
#[Autowire(service: 'security.access_token_extractor.header')] #[Autowire(service: 'security.access_token_extractor.main')]
private readonly AccessTokenExtractorInterface $accessTokenExtractor, private readonly AccessTokenExtractorInterface $accessTokenExtractor,
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\Cache;
use Doctrine\Persistence\Proxy;
/**
* The purpose of this class is to generate cache tags for elements.
* E.g. to easily invalidate all caches for a given element type.
*/
class ElementCacheTagGenerator
{
private array $cache = [];
public function __construct()
{
}
/**
* Returns a cache tag for the given element type, which can be used to invalidate all caches for this element type.
* @param string|object $element
* @return string
*/
public function getElementTypeCacheTag(string|object $element): string
{
//Ensure that the given element is a class name
if (is_object($element)) {
$element = get_class($element);
} else { //And that the class exists
if (!class_exists($element)) {
throw new \InvalidArgumentException("The given class '$element' does not exist!");
}
}
//Check if the tag is already cached
if (isset($this->cache[$element])) {
return $this->cache[$element];
}
//If the element is a proxy, then get the real class name of the underlying object
if (is_a($element, Proxy::class, true) || str_starts_with($element, 'Proxies\\')) {
$element = get_parent_class($element);
}
//Replace all backslashes with underscores to prevent problems with the cache and save the result
$this->cache[$element] = str_replace('\\', '_', $element);
return $this->cache[$element];
}
}

View file

@ -20,12 +20,12 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Services\UserSystem; namespace App\Services\Cache;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use Locale; use Locale;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
/** /**

View file

@ -0,0 +1,212 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\EDAIntegration;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class KiCADHelper
{
public function __construct(
private readonly NodesListBuilder $nodesListBuilder,
private readonly TagAwareCacheInterface $kicadCache,
private readonly EntityManagerInterface $em,
private readonly ElementCacheTagGenerator $tagGenerator,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly TranslatorInterface $translator,
) {
}
/**
* Returns an array of objects containing all categories in the database in the format required by KiCAD.
* The categories are flattened and sorted by their full path.
* Categories, which contain no parts, are filtered out.
* The result is cached for performance and invalidated on category changes.
* @return array
*/
public function getCategories(): array
{
return $this->kicadCache->get('kicad_categories', function (ItemInterface $item) {
//Invalidate the cache on category changes
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class);
$item->tag($secure_class_name);
$categories = $this->nodesListBuilder->typeToNodesList(Category::class);
$repo = $this->em->getRepository(Category::class);
$result = [];
foreach ($categories as $category) {
//Skip invisible categories
if ($category->getEdaInfo()->getInvisible() ?? false) {
continue;
}
/** @var $category Category */
//Ensure that the category contains parts
if ($repo->getPartsCount($category) < 1) {
continue;
}
//Format the category for KiCAD
$result[] = [
'id' => (string)$category->getId(),
'name' => $category->getFullPath('/'),
];
}
return $result;
});
}
/**
* Returns an array of objects containing all parts for the given category in the format required by KiCAD.
* The result is cached for performance and invalidated on category or part changes.
* @param Category $category
* @return array
*/
public function getCategoryParts(Category $category): array
{
return $this->kicadCache->get('kicad_category_parts_'.$category->getID(),
function (ItemInterface $item) use ($category) {
$item->tag([
$this->tagGenerator->getElementTypeCacheTag(Category::class),
$this->tagGenerator->getElementTypeCacheTag(Part::class)
]);
$category_repo = $this->em->getRepository(Category::class);
$parts = $category_repo->getParts($category);
$result = [];
foreach ($parts as $part) {
//If the part is invisible, then skip it
if ($part->getEdaInfo()->getInvisible() ?? $part->getCategory()?->getEdaInfo()->getInvisible() ?? false) {
continue;
}
$result[] = [
'id' => (string)$part->getId(),
'name' => $part->getName(),
'description' => $part->getDescription(),
];
}
return $result;
});
}
public function getKiCADPart(Part $part): array
{
$result = [
'id' => (string)$part->getId(),
'name' => $part->getName(),
"symbolIdStr" => $part->getEdaInfo()->getKicadSymbol() ?? $part->getCategory()?->getEdaInfo()->getKicadSymbol() ?? "",
"exclude_from_bom" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBom() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBom() ?? false),
"exclude_from_board" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBoard() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBoard() ?? false),
"exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? true),
"fields" => []
];
$result["fields"]["footprint"] = $this->createField($part->getEdaInfo()->getKicadFootprint() ?? $part->getFootprint()?->getEdaInfo()->getKicadFootprint() ?? "");
$result["fields"]["reference"] = $this->createField($part->getEdaInfo()->getReferencePrefix() ?? 'U', true);
$result["fields"]["value"] = $this->createField($part->getEdaInfo()->getValue() ?? $part->getName(), true);
$result["fields"]["keywords"] = $this->createField($part->getTags());
//Use the part info page as datasheet link. It must be an absolute URL.
$result["fields"]["datasheet"] = $this->createField(
$this->urlGenerator->generate(
'part_info',
['id' => $part->getId()],
UrlGeneratorInterface::ABSOLUTE_URL)
);
//Add basic fields
$result["fields"]["description"] = $this->createField($part->getDescription());
if ($part->getCategory()) {
$result["fields"]["Category"] = $this->createField($part->getCategory()->getFullPath('/'));
}
if ($part->getManufacturer()) {
$result["fields"]["Manufacturer"] = $this->createField($part->getManufacturer()->getName());
}
if ($part->getManufacturerProductNumber() !== "") {
$result['fields']["MPN"] = $this->createField($part->getManufacturerProductNumber());
}
if ($part->getManufacturingStatus()) {
$result["fields"]["Manufacturing Status"] = $this->createField(
//Always use the english translation
$this->translator->trans($part->getManufacturingStatus()->toTranslationKey(), locale: 'en')
);
}
if ($part->getFootprint()) {
$result["fields"]["Part-DB Footprint"] = $this->createField($part->getFootprint()->getName());
}
if ($part->getPartUnit()) {
$unit = $part->getPartUnit()->getName();
if ($part->getPartUnit()->getUnit() !== "") {
$unit .= ' ('.$part->getPartUnit()->getUnit().')';
}
$result["fields"]["Part-DB Unit"] = $this->createField($unit);
}
if ($part->getMass()) {
$result["fields"]["Mass"] = $this->createField($part->getMass() . ' g');
}
$result["fields"]["Part-DB ID"] = $this->createField($part->getId());
if (!empty($part->getIpn())) {
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
}
return $result;
}
/**
* Converts a boolean value to the format required by KiCAD.
* @param bool $value
* @return string
*/
private function boolToKicadBool(bool $value): string
{
return $value ? 'True' : 'False';
}
/**
* Creates a field array for KiCAD
* @param string|int|float $value
* @param bool $visible
* @return array
*/
private function createField(string|int|float $value, bool $visible = false): array
{
return [
'value' => (string)$value,
'visible' => $this->boolToKicadBool($visible),
];
}
}

View file

@ -44,15 +44,20 @@ namespace App\Services\LabelSystem;
use App\Entity\LabelSystem\LabelProfile; use App\Entity\LabelSystem\LabelProfile;
use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\LabelSystem\LabelSupportedElement;
use App\Repository\LabelProfileRepository; use App\Repository\LabelProfileRepository;
use App\Services\UserSystem\UserCacheKeyGenerator; use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface;
final class LabelProfileDropdownHelper final class LabelProfileDropdownHelper
{ {
public function __construct(private readonly TagAwareCacheInterface $cache, private readonly EntityManagerInterface $entityManager, private readonly UserCacheKeyGenerator $keyGenerator) public function __construct(
{ private readonly TagAwareCacheInterface $cache,
private readonly EntityManagerInterface $entityManager,
private readonly UserCacheKeyGenerator $keyGenerator,
private readonly ElementCacheTagGenerator $tagGenerator,
) {
} }
/** /**
@ -67,7 +72,7 @@ final class LabelProfileDropdownHelper
$type = LabelSupportedElement::from($type); $type = LabelSupportedElement::from($type);
} }
$secure_class_name = str_replace('\\', '_', LabelProfile::class); $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(LabelProfile::class);
$key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.'_'.$type->value; $key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.'_'.$type->value;
/** @var LabelProfileRepository $repo */ /** @var LabelProfileRepository $repo */

View file

@ -22,14 +22,13 @@ declare(strict_types=1);
namespace App\Services\Trees; namespace App\Services\Trees;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\AttachmentContainingDBElementRepository; use App\Repository\AttachmentContainingDBElementRepository;
use App\Repository\DBElementRepository; use App\Repository\DBElementRepository;
use App\Repository\StructuralDBElementRepository; use App\Repository\StructuralDBElementRepository;
use App\Services\UserSystem\UserCacheKeyGenerator; use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface;
@ -40,8 +39,12 @@ use Symfony\Contracts\Cache\TagAwareCacheInterface;
*/ */
class NodesListBuilder class NodesListBuilder
{ {
public function __construct(protected EntityManagerInterface $em, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator) public function __construct(
{ protected EntityManagerInterface $em,
protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator,
protected ElementCacheTagGenerator $tagGenerator,
) {
} }
/** /**
@ -86,7 +89,7 @@ class NodesListBuilder
{ {
$parent_id = $parent instanceof AbstractStructuralDBElement ? $parent->getID() : '0'; $parent_id = $parent instanceof AbstractStructuralDBElement ? $parent->getID() : '0';
// Backslashes are not allowed in cache keys // Backslashes are not allowed in cache keys
$secure_class_name = str_replace('\\', '_', $class_name); $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class_name);
$key = 'list_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.$parent_id; $key = 'list_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.$parent_id;
return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) { return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) {

View file

@ -22,9 +22,7 @@ declare(strict_types=1);
namespace App\Services\Trees; namespace App\Services\Trees;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\ProjectSystem\Project;
use App\Entity\LabelSystem\LabelProfile; use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parts\Category; use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint; use App\Entity\Parts\Footprint;
@ -34,10 +32,12 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\StorageLocation; use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier; use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use App\Entity\ProjectSystem\Project;
use App\Entity\UserSystem\Group; use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNode;
use App\Services\UserSystem\UserCacheKeyGenerator; use App\Services\Cache\UserCacheKeyGenerator;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface;

View file

@ -25,17 +25,18 @@ namespace App\Services\Trees;
use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category; use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint; use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\StorageLocation; use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier; use App\Entity\Parts\Supplier;
use App\Entity\ProjectSystem\Project;
use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNode;
use App\Helpers\Trees\TreeViewNodeIterator; use App\Helpers\Trees\TreeViewNodeIterator;
use App\Repository\StructuralDBElementRepository; use App\Repository\StructuralDBElementRepository;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use App\Services\EntityURLGenerator; use App\Services\EntityURLGenerator;
use App\Services\UserSystem\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException; use InvalidArgumentException;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
@ -51,10 +52,18 @@ use function count;
*/ */
class TreeViewGenerator class TreeViewGenerator
{ {
public function __construct(protected EntityURLGenerator $urlGenerator, protected EntityManagerInterface $em, protected TagAwareCacheInterface $cache, public function __construct(
protected UserCacheKeyGenerator $keyGenerator, protected TranslatorInterface $translator, private UrlGeneratorInterface $router, protected EntityURLGenerator $urlGenerator,
protected bool $rootNodeExpandedByDefault, protected bool $rootNodeEnabled) protected EntityManagerInterface $em,
{ protected TagAwareCacheInterface $cache,
protected ElementCacheTagGenerator $tagGenerator,
protected UserCacheKeyGenerator $keyGenerator,
protected TranslatorInterface $translator,
private UrlGeneratorInterface $router,
protected bool $rootNodeExpandedByDefault,
protected bool $rootNodeEnabled,
) {
} }
/** /**
@ -68,8 +77,12 @@ class TreeViewGenerator
* *
* @return TreeViewNode[] an array of TreeViewNode[] elements of the root elements * @return TreeViewNode[] an array of TreeViewNode[] elements of the root elements
*/ */
public function getTreeView(string $class, ?AbstractStructuralDBElement $parent = null, string $mode = 'list_parts', ?AbstractDBElement $selectedElement = null): array public function getTreeView(
{ string $class,
?AbstractStructuralDBElement $parent = null,
string $mode = 'list_parts',
?AbstractDBElement $selectedElement = null
): array {
$head = []; $head = [];
$href_type = $mode; $href_type = $mode;
@ -192,13 +205,12 @@ class TreeViewGenerator
return $repo->getGenericNodeTree($parent); return $repo->getGenericNodeTree($parent);
} }
$secure_class_name = str_replace('\\', '_', $class); $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class);
$key = 'treeview_'.$this->keyGenerator->generateKey().'_'.$secure_class_name; $key = 'treeview_'.$this->keyGenerator->generateKey().'_'.$secure_class_name;
return $this->cache->get($key, function (ItemInterface $item) use ($repo, $parent, $secure_class_name) { return $this->cache->get($key, function (ItemInterface $item) use ($repo, $parent, $secure_class_name) {
// Invalidate when groups, an element with the class or the user changes // Invalidate when groups, an element with the class or the user changes
$item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]); $item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]);
return $repo->getGenericNodeTree($parent); return $repo->getGenericNodeTree($parent);
}); });
} }

View file

@ -7,6 +7,7 @@
{% block additional_pills %} {% block additional_pills %}
<li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#home_options">{% trans %}admin.options{% endtrans %}</a></li> <li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#home_options">{% trans %}admin.options{% endtrans %}</a></li>
<li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#home_advanced">{% trans %}admin.advanced{% endtrans %}</a></li> <li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#home_advanced">{% trans %}admin.advanced{% endtrans %}</a></li>
<li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#eda">{% trans %}part.edit.tab.eda{% endtrans %}</a></li>
{% endblock %} {% endblock %}
{% block edit_title %} {% block edit_title %}
@ -34,4 +35,29 @@
{{ form_row(form.default_description) }} {{ form_row(form.default_description) }}
{{ form_row(form.default_comment) }} {{ form_row(form.default_comment) }}
</div> </div>
<div class="tab-pane" id="eda">
{{ form_row(form.eda_info.reference_prefix) }}
<div class="row">
<div class="col-sm-9 offset-sm-3">
{{ form_row(form.eda_info.invisible) }}
</div>
</div>
<div class="row mb-2">
<div class="col-sm-9 offset-sm-3">
{{ form_widget(form.eda_info.exclude_from_bom) }}
{{ form_widget(form.eda_info.exclude_from_board) }}
{{ form_widget(form.eda_info.exclude_from_sim) }}
</div>
</div>
<div class="row">
<div class="col-sm-9 offset-sm-3">
<h6>{% trans %}eda_info.kicad_section.title{% endtrans %}:</h6>
</div>
</div>
{{ form_row(form.eda_info.kicad_symbol) }}
</div>
{% endblock %} {% endblock %}

View file

@ -20,3 +20,18 @@
{% block additional_controls %} {% block additional_controls %}
{{ form_row(form.alternative_names) }} {{ form_row(form.alternative_names) }}
{% endblock %} {% endblock %}
{% block additional_pills %}
<li class="nav-item"><a data-bs-toggle="tab" class="nav-link link-anchor" href="#eda">{% trans %}part.edit.tab.eda{% endtrans %}</a></li>
{% endblock %}
{% block additional_panes %}
<div class="tab-pane" id="eda">
<div class="row">
<div class="col-sm-9 offset-sm-3">
<h6>{% trans %}eda_info.kicad_section.title{% endtrans %}:</h6>
</div>
</div>
{{ form_row(form.eda_info.kicad_footprint) }}
</div>
{% endblock %}

View file

@ -0,0 +1,24 @@
{{ form_row(form.eda_info.reference_prefix) }}
{{ form_row(form.eda_info.value) }}
<div class="row">
<div class="col-sm-9 offset-sm-3">
{{ form_row(form.eda_info.invisible) }}
</div>
</div>
<div class="row mb-2">
<div class="col-sm-9 offset-sm-3">
{{ form_widget(form.eda_info.exclude_from_bom) }}
{{ form_widget(form.eda_info.exclude_from_board) }}
{{ form_widget(form.eda_info.exclude_from_sim) }}
</div>
</div>
<div class="row">
<div class="col-sm-9 offset-sm-3">
<h6>{% trans %}eda_info.kicad_section.title{% endtrans %}:</h6>
</div>
</div>
{{ form_row(form.eda_info.kicad_symbol) }}
{{ form_row(form.eda_info.kicad_footprint) }}

View file

@ -64,6 +64,12 @@
{% trans %}part.edit.tab.associations{% endtrans %} {% trans %}part.edit.tab.associations{% endtrans %}
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" role="tab" href="#eda">
<i class="fas fa-compass-drafting fa-fw"></i>
{% trans %}part.edit.tab.eda{% endtrans %}
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" role="tab" href="#comment"> <a class="nav-link" data-bs-toggle="tab" role="tab" href="#comment">
<i class="fas fa-sticky-note fa-fw"></i> <i class="fas fa-sticky-note fa-fw"></i>
@ -99,6 +105,9 @@
<div class="tab-pane fade p-2" id="associations" role="tabpanel"> <div class="tab-pane fade p-2" id="associations" role="tabpanel">
{% include "parts/edit/_associated_parts.html.twig" %} {% include "parts/edit/_associated_parts.html.twig" %}
</div> </div>
<div class="tab-pane fade p-2" id="eda" role="tabpanel">
{% include "parts/edit/_eda.html.twig" %}
</div>
<div class="tab-pane fade p-2" id="comment" role="tabpanel"> <div class="tab-pane fade p-2" id="comment" role="tabpanel">
{{ form_widget(form.comment)}} {{ form_widget(form.comment)}}
</div> </div>

View file

@ -85,5 +85,25 @@
</td> </td>
</tr> </tr>
<tr>
<td>{% trans %}part.edit.tab.eda{% endtrans %}</td>
<td>
<b>{% trans %}eda_info.reference_prefix{% endtrans %}:</b> {{ part.edaInfo.referencePrefix ?? part.category.edaInfo.referencePrefix ?? "" }}
<br>
<b>{% trans %}eda_info.value{% endtrans %}:</b> {{ part.edaInfo.value }}
<br>
<b>{% trans %}eda_info.invisible{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.invisible ?? part.category.edaInfo.invisible ?? false) }}
<br>
<b>{% trans %}eda_info.exclude_from_bom{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.excludeFromBom ?? part.category.edaInfo.excludeFromBom ?? false) }}
<br>
<b>{% trans %}eda_info.exclude_from_board{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.excludeFromBoard ?? part.category.edaInfo.excludeFromBoard ?? false) }}
<br>
<b>{% trans %}eda_info.exclude_from_sim{% endtrans %}:</b> {{ helper.boolean_badge( part.edaInfo.excludeFromSim ?? part.category.edaInfo.excludeFromSim ?? false) }}
<br>
<b>{% trans %}eda_info.kicad_symbol{% endtrans %}:</b> {{ part.edaInfo.kicadSymbol ?? part.category.edaInfo.kicadSymbol ?? "" }}
<br>
<b>{% trans %}eda_info.kicad_footprint{% endtrans %}:</b> {{ part.edaInfo.kicadFootprint ?? part.footprint.edaInfo.kicadFootprint ?? "" }}
</td>
</tr>
</tbody> </tbody>
</table> </table>

View file

@ -74,5 +74,32 @@
<i class="fas fa-plus-square fa-fw"></i> {% trans %}api_token.create_new{% endtrans %} <i class="fas fa-plus-square fa-fw"></i> {% trans %}api_token.create_new{% endtrans %}
</a> </a>
<hr>
<h5>{% trans %}api.api_endpoints.title{% endtrans %}:</h5>
<div class="row">
<div class="col-sm-2">
<b>{% trans %}api.api_endpoints.partdb{% endtrans %}:</b>
</div>
<div class="col-sm-3">
<button class="btn btn-outline-dark d-print-none btn-sm" data-clipboard-text="{{ url('api_entrypoint') }}">
<i class="fas fa-copy"></i>
</button>
<span>{{ url('api_entrypoint') }}</span>
</div>
</div>
<div class="row">
<div class="col-sm-2">
<b>{% trans %}api.api_endpoints.kicad_root_url{% endtrans %}:</b>
</div>
<div class="col-sm-10">
<button class="btn btn-outline-dark d-print-none btn-sm" data-clipboard-text="{{ absolute_url('/en/kicad-api/') }}">
<i class="fas fa-copy"></i>
</button>
<span>{{ absolute_url('/en/kicad-api/') }}</span>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -96,6 +96,24 @@ class APITokenAuthenticationTest extends ApiTestCase
self::assertResponseIsSuccessful(); self::assertResponseIsSuccessful();
} }
public function testWithAuthorizationToken(): void
{
//For the KICAD API it should also work with Authorization: Token header instead of Bearer
self::ensureKernelShutdown();
$client = static::createClient([], ['headers' => ['authorization' => 'Token '.APITokenFixtures::TOKEN_ADMIN]]);;
//Read should be possible
$client->request('GET', '/api/parts');
self::assertResponseIsSuccessful();
//Trying to list all users
$client->request('GET', '/api/users');
self::assertResponseIsSuccessful();
$client->request('POST', '/api/footprints', ['json' => ['name' => 'post test']]);
self::assertResponseIsSuccessful();
}
protected function createClientWithCredentials(string $token): Client protected function createClientWithCredentials(string $token): Client
{ {
return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]);

View file

@ -0,0 +1,250 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Tests\Controller;
use App\DataFixtures\APITokenFixtures;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class KiCadApiControllerTest extends WebTestCase
{
private const BASE_URL = '/en/kicad-api/v1';
protected function createClientWithCredentials(string $token = APITokenFixtures::TOKEN_READONLY): KernelBrowser
{
return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]);
}
public function testRoot(): void
{
$client = $this->createClientWithCredentials();
$client->request('GET', self::BASE_URL.'/');
self::assertResponseIsSuccessful();
$content = $client->getResponse()->getContent();
self::assertJson($content);
//Check if the response contains the expected keys
$array = json_decode($content, true);
self::assertArrayHasKey('categories', $array);
self::assertArrayHasKey('parts', $array);
}
public function testCategories(): void
{
$client = $this->createClientWithCredentials();
$client->request('GET', self::BASE_URL.'/categories.json');
self::assertResponseIsSuccessful();
$content = $client->getResponse()->getContent();
self::assertJson($content);
$data = json_decode($content, true);
//There should be only one category, as the other ones contain no parts
self::assertCount(1, $data);
//Check if the response contains the expected keys
$category = $data[0];
self::assertArrayHasKey('name', $category);
self::assertArrayHasKey('id', $category);
}
public function testCategoryParts(): void
{
$client = $this->createClientWithCredentials();
$client->request('GET', self::BASE_URL.'/parts/category/1.json');
self::assertResponseIsSuccessful();
$content = $client->getResponse()->getContent();
self::assertJson($content);
$data = json_decode($content, true);
//There should be 3 parts in the category
self::assertCount(3, $data);
//Check if the response contains the expected keys
$part = $data[0];
self::assertArrayHasKey('name', $part);
self::assertArrayHasKey('id', $part);
self::assertArrayHasKey('description', $part);
}
public function testCategoryPartsForEmptyCategory(): void
{
$client = $this->createClientWithCredentials();
$client->request('GET', self::BASE_URL.'/parts/category/2.json');
//Response should still be successful, but the result should be empty
self::assertResponseIsSuccessful();
$content = $client->getResponse()->getContent();
self::assertJson($content);
self::assertEmpty(json_decode($content, true));
}
public function testPartDetailsPart1(): void
{
$client = $this->createClientWithCredentials();
$client->request('GET', self::BASE_URL.'/parts/1.json');
//Response should still be successful, but the result should be empty
self::assertResponseIsSuccessful();
$content = $client->getResponse()->getContent();
self::assertJson($content);
$data = json_decode($content, true);
$expected = array(
'id' => '1',
'name' => 'Part 1',
'symbolIdStr' => 'Part:1',
'exclude_from_bom' => 'False',
'exclude_from_board' => 'True',
'exclude_from_sim' => 'False',
'fields' =>
array(
'footprint' =>
array(
'value' => 'Part:1',
'visible' => 'False',
),
'reference' =>
array(
'value' => 'P',
'visible' => 'True',
),
'value' =>
array(
'value' => 'Part 1',
'visible' => 'True',
),
'keywords' =>
array(
'value' => '',
'visible' => 'False',
),
'datasheet' =>
array(
'value' => 'http://localhost/en/part/1/info',
'visible' => 'False',
),
'description' =>
array(
'value' => '',
'visible' => 'False',
),
'Category' =>
array(
'value' => 'Node 1',
'visible' => 'False',
),
'Manufacturing Status' =>
array(
'value' => '',
'visible' => 'False',
),
'Part-DB ID' =>
array(
'value' => '1',
'visible' => 'False',
),
),
);
self::assertEquals($expected, $data);
}
public function testPartDetailsPart2(): void
{
$client = $this->createClientWithCredentials();
$client->request('GET', self::BASE_URL.'/parts/1.json');
//Response should still be successful, but the result should be empty
self::assertResponseIsSuccessful();
$content = $client->getResponse()->getContent();
self::assertJson($content);
$data = json_decode($content, true);
//For part 2 things info should be taken from the category and footprint
$expected = array (
'id' => '1',
'name' => 'Part 1',
'symbolIdStr' => 'Part:1',
'exclude_from_bom' => 'False',
'exclude_from_board' => 'True',
'exclude_from_sim' => 'False',
'fields' =>
array (
'footprint' =>
array (
'value' => 'Part:1',
'visible' => 'False',
),
'reference' =>
array (
'value' => 'P',
'visible' => 'True',
),
'value' =>
array (
'value' => 'Part 1',
'visible' => 'True',
),
'keywords' =>
array (
'value' => '',
'visible' => 'False',
),
'datasheet' =>
array (
'value' => 'http://localhost/en/part/1/info',
'visible' => 'False',
),
'description' =>
array (
'value' => '',
'visible' => 'False',
),
'Category' =>
array (
'value' => 'Node 1',
'visible' => 'False',
),
'Manufacturing Status' =>
array (
'value' => '',
'visible' => 'False',
),
'Part-DB ID' =>
array (
'value' => '1',
'visible' => 'False',
),
),
);
self::assertEquals($expected, $data);
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,13 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en"> <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
<file id="security.en"> <file id="security.en">
<unit id="aazoCks" name="user.login_error.user_disabled"> <unit id="aazoCks" name="user.login_error.user_disabled">
<segment state="translated"> <segment>
<source>user.login_error.user_disabled</source> <source>user.login_error.user_disabled</source>
<target>Your account is disabled! Contact an administrator if you think this is wrong.</target> <target>Your account is disabled! Contact an administrator if you think this is wrong.</target>
</segment> </segment>
</unit> </unit>
<unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml"> <unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml">
<segment state="translated"> <segment>
<source>saml.error.cannot_login_local_user_per_saml</source> <source>saml.error.cannot_login_local_user_per_saml</source>
<target>You cannot login as local user via SSO! Use your local user password instead.</target> <target>You cannot login as local user via SSO! Use your local user password instead.</target>
</segment> </segment>

View file

@ -37,7 +37,7 @@
<note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note> <note priority="1">Part-DB1\src\Entity\UserSystem\Group.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note> <note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>part.master_attachment.must_be_picture</source> <source>part.master_attachment.must_be_picture</source>
<target>The preview attachment must be a valid picture!</target> <target>The preview attachment must be a valid picture!</target>
</segment> </segment>
@ -82,7 +82,7 @@
<note priority="1">src\Entity\StructuralDBElement.php:0</note> <note priority="1">src\Entity\StructuralDBElement.php:0</note>
<note priority="1">src\Entity\Supplier.php:0</note> <note priority="1">src\Entity\Supplier.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>structural.entity.unique_name</source> <source>structural.entity.unique_name</source>
<target>An element with this name already exists on this level!</target> <target>An element with this name already exists on this level!</target>
</segment> </segment>
@ -102,7 +102,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>parameters.validator.min_lesser_typical</source> <source>parameters.validator.min_lesser_typical</source>
<target>Value must be lesser or equal the the typical value ({{ compared_value }}).</target> <target>Value must be lesser or equal the the typical value ({{ compared_value }}).</target>
</segment> </segment>
@ -122,7 +122,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>parameters.validator.min_lesser_max</source> <source>parameters.validator.min_lesser_max</source>
<target>Value must be lesser than the maximum value ({{ compared_value }}).</target> <target>Value must be lesser than the maximum value ({{ compared_value }}).</target>
</segment> </segment>
@ -142,7 +142,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\StorelocationParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\SupplierParameter.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>parameters.validator.max_greater_typical</source> <source>parameters.validator.max_greater_typical</source>
<target>Value must be greater or equal than the typical value ({{ compared_value }}).</target> <target>Value must be greater or equal than the typical value ({{ compared_value }}).</target>
</segment> </segment>
@ -152,7 +152,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note> <note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>validator.user.username_already_used</source> <source>validator.user.username_already_used</source>
<target>A user with this name is already exisiting</target> <target>A user with this name is already exisiting</target>
</segment> </segment>
@ -162,7 +162,7 @@
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note> <note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note> <note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>user.invalid_username</source> <source>user.invalid_username</source>
<target>The username must contain only letters, numbers, underscores, dots, pluses or minuses!</target> <target>The username must contain only letters, numbers, underscores, dots, pluses or minuses!</target>
</segment> </segment>
@ -171,7 +171,7 @@
<notes> <notes>
<note category="state" priority="1">obsolete</note> <note category="state" priority="1">obsolete</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>validator.noneofitschild.self</source> <source>validator.noneofitschild.self</source>
<target>An element can not be its own parent!</target> <target>An element can not be its own parent!</target>
</segment> </segment>
@ -180,163 +180,163 @@
<notes> <notes>
<note category="state" priority="1">obsolete</note> <note category="state" priority="1">obsolete</note>
</notes> </notes>
<segment state="translated"> <segment>
<source>validator.noneofitschild.children</source> <source>validator.noneofitschild.children</source>
<target>You can not assign children element as parent (This would cause loops)!</target> <target>You can not assign children element as parent (This would cause loops)!</target>
</segment> </segment>
</unit> </unit>
<unit id="ayNr6QK" name="validator.select_valid_category"> <unit id="ayNr6QK" name="validator.select_valid_category">
<segment state="translated"> <segment>
<source>validator.select_valid_category</source> <source>validator.select_valid_category</source>
<target>Please select a valid category!</target> <target>Please select a valid category!</target>
</segment> </segment>
</unit> </unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing"> <unit id="6vIlN5q" name="validator.part_lot.only_existing">
<segment state="translated"> <segment>
<source>validator.part_lot.only_existing</source> <source>validator.part_lot.only_existing</source>
<target>Can not add new parts to this location as it is marked as "Only Existing"</target> <target>Can not add new parts to this location as it is marked as "Only Existing"</target>
</segment> </segment>
</unit> </unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase"> <unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<segment state="translated"> <segment>
<source>validator.part_lot.location_full.no_increase</source> <source>validator.part_lot.location_full.no_increase</source>
<target>Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}).</target> <target>Location is full. Amount can not be increased (new value must be smaller than {{ old_amount }}).</target>
</segment> </segment>
</unit> </unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full"> <unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<segment state="translated"> <segment>
<source>validator.part_lot.location_full</source> <source>validator.part_lot.location_full</source>
<target>Location is full. Can not add new parts to it.</target> <target>Location is full. Can not add new parts to it.</target>
</segment> </segment>
</unit> </unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part"> <unit id="BNQk2e7" name="validator.part_lot.single_part">
<segment state="translated"> <segment>
<source>validator.part_lot.single_part</source> <source>validator.part_lot.single_part</source>
<target>This location can only contain a single part and it is already full!</target> <target>This location can only contain a single part and it is already full!</target>
</segment> </segment>
</unit> </unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null"> <unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<segment state="translated"> <segment>
<source>validator.attachment.must_not_be_null</source> <source>validator.attachment.must_not_be_null</source>
<target>You must select an attachment type!</target> <target>You must select an attachment type!</target>
</segment> </segment>
</unit> </unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null"> <unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated"> <segment>
<source>validator.orderdetail.supplier_must_not_be_null</source> <source>validator.orderdetail.supplier_must_not_be_null</source>
<target>You must select an supplier!</target> <target>You must select an supplier!</target>
</segment> </segment>
</unit> </unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit"> <unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated"> <segment>
<source>validator.measurement_unit.use_si_prefix_needs_unit</source> <source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>To enable SI prefixes, you have to set a unit symbol!</target> <target>To enable SI prefixes, you have to set a unit symbol!</target>
</segment> </segment>
</unit> </unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique"> <unit id="DuzIOCr" name="part.ipn.must_be_unique">
<segment state="translated"> <segment>
<source>part.ipn.must_be_unique</source> <source>part.ipn.must_be_unique</source>
<target>The internal part number must be unique. {{ value }} is already in use!</target> <target>The internal part number must be unique. {{ value }} is already in use!</target>
</segment> </segment>
</unit> </unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed"> <unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated"> <segment>
<source>validator.project.bom_entry.name_or_part_needed</source> <source>validator.project.bom_entry.name_or_part_needed</source>
<target>You have to choose a part for a part BOM entry or set a name for a non-part BOM entry.</target> <target>You have to choose a part for a part BOM entry or set a name for a non-part BOM entry.</target>
</segment> </segment>
</unit> </unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom"> <unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<segment state="translated"> <segment>
<source>project.bom_entry.name_already_in_bom</source> <source>project.bom_entry.name_already_in_bom</source>
<target>There is already an BOM entry with this name!</target> <target>There is already an BOM entry with this name!</target>
</segment> </segment>
</unit> </unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom"> <unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<segment state="translated"> <segment>
<source>project.bom_entry.part_already_in_bom</source> <source>project.bom_entry.part_already_in_bom</source>
<target>This part already exists in the BOM!</target> <target>This part already exists in the BOM!</target>
</segment> </segment>
</unit> </unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch"> <unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated"> <segment>
<source>project.bom_entry.mountnames_quantity_mismatch</source> <source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>The number of mountnames has to match the BOMs quantity!</target> <target>The number of mountnames has to match the BOMs quantity!</target>
</segment> </segment>
</unit> </unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part"> <unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated"> <segment>
<source>project.bom_entry.can_not_add_own_builds_part</source> <source>project.bom_entry.can_not_add_own_builds_part</source>
<target>You can not add a project's own builds part to the BOM.</target> <target>You can not add a project's own builds part to the BOM.</target>
</segment> </segment>
</unit> </unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts"> <unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated"> <segment>
<source>project.bom_has_to_include_all_subelement_parts</source> <source>project.bom_has_to_include_all_subelement_parts</source>
<target>The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing!</target> <target>The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing!</target>
</segment> </segment>
</unit> </unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts"> <unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated"> <segment>
<source>project.bom_entry.price_not_allowed_on_parts</source> <source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Prices are not allowed on BOM entries associated with a part. Define the price on the part instead.</target> <target>Prices are not allowed on BOM entries associated with a part. Define the price on the part instead.</target>
</segment> </segment>
</unit> </unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed"> <unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated"> <segment>
<source>validator.project_build.lot_bigger_than_needed</source> <source>validator.project_build.lot_bigger_than_needed</source>
<target>You have selected more quantity to withdraw than needed! Remove unnecessary quantity.</target> <target>You have selected more quantity to withdraw than needed! Remove unnecessary quantity.</target>
</segment> </segment>
</unit> </unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed"> <unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated"> <segment>
<source>validator.project_build.lot_smaller_than_needed</source> <source>validator.project_build.lot_smaller_than_needed</source>
<target>You have selected less quantity to withdraw than needed for the build! Add additional quantity.</target> <target>You have selected less quantity to withdraw than needed for the build! Add additional quantity.</target>
</segment> </segment>
</unit> </unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex"> <unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<segment state="translated"> <segment>
<source>part.name.must_match_category_regex</source> <source>part.name.must_match_category_regex</source>
<target>The part name does not match the regular expression stated by the category: %regex%</target> <target>The part name does not match the regular expression stated by the category: %regex%</target>
</segment> </segment>
</unit> </unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank"> <unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<segment state="translated"> <segment>
<source>validator.attachment.name_not_blank</source> <source>validator.attachment.name_not_blank</source>
<target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target> <target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target>
</segment> </segment>
</unit> </unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner"> <unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated"> <segment>
<source>validator.part_lot.owner_must_match_storage_location_owner</source> <source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>The owner of this lot must match the owner of the selected storage location (%owner_name%)!</target> <target>The owner of this lot must match the owner of the selected storage location (%owner_name%)!</target>
</segment> </segment>
</unit> </unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous"> <unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated"> <segment>
<source>validator.part_lot.owner_must_not_be_anonymous</source> <source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>A lot owner must not be the anonymous user!</target> <target>A lot owner must not be the anonymous user!</target>
</segment> </segment>
</unit> </unit>
<unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other"> <unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated"> <segment>
<source>validator.part_association.must_set_an_value_if_type_is_other</source> <source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>If you set the type to "other", then you have to set a descriptive value for it!</target> <target>If you set the type to "other", then you have to set a descriptive value for it!</target>
</segment> </segment>
</unit> </unit>
<unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself"> <unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated"> <segment>
<source>validator.part_association.part_cannot_be_associated_with_itself</source> <source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>A part can not be associated with itself!</target> <target>A part can not be associated with itself!</target>
</segment> </segment>
</unit> </unit>
<unit id="csc1PNn" name="validator.part_association.already_exists"> <unit id="csc1PNn" name="validator.part_association.already_exists">
<segment state="translated"> <segment>
<source>validator.part_association.already_exists</source> <source>validator.part_association.already_exists</source>
<target>The association with this part already exists!</target> <target>The association with this part already exists!</target>
</segment> </segment>
</unit> </unit>
<unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique"> <unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated"> <segment>
<source>validator.part_lot.vendor_barcode_must_be_unique</source> <source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>This vendor barcode value was already used in another lot. The barcode must be unique!</target> <target>This vendor barcode value was already used in another lot. The barcode must be unique!</target>
</segment> </segment>