Compare commits

..

127 Commits

Author SHA1 Message Date
Jan Böhmer
78bd858ebb Bumped version to 1.10.2 2024-01-06 15:57:59 +01:00
Jan Böhmer
19819454fa Dont split up links when extracting parameters from notes and description
This partly fixes issue #469
2024-01-06 15:14:07 +01:00
Jan Böhmer
26a4b57cfb Fixed tests related to PartNormalizer 2024-01-06 15:01:50 +01:00
Jan Böhmer
f3729ef9db Merge remote-tracking branch 'origin/l10n_master' 2024-01-06 01:07:20 +01:00
Jan Böhmer
ab09d319e9 Fixed wrong path for assets managed by webpack and loaded via twig asset() function.
This had also the effect that 2FA via webauthn were not working, as the request the invalid path resetted the webauthn request saved in session.
2024-01-06 01:06:56 +01:00
Jan Böhmer
df23ba07ba Fixed excpetion that no IRI could be generated if a new Part was created via POST operation via API
This was because the objectSerializer in PartNormalizer messed up the JSONLD IRI generation of the paramaters property. It tried to generate this IRI via the Part ressource class, which is not possible
2024-01-05 23:38:49 +01:00
Jan Böhmer
d20b668e87 Decorate error handler of API platform to show a better error message, if a user tries to cascade persist a new entity through an API operation 2024-01-05 23:10:46 +01:00
Jan Böhmer
f0646597fe Updated dependencies 2024-01-05 22:33:00 +01:00
Jan Böhmer
6d783fd581 New translations messages.en.xlf (Dutch) 2024-01-04 10:00:32 +01:00
Jan Böhmer
14fbf18733 New translations messages.en.xlf (Dutch) 2024-01-04 09:00:23 +01:00
Jan Böhmer
e35c7c496f New translations security.en.xlf (Dutch) 2024-01-03 16:00:42 +01:00
Jan Böhmer
a218b8fdd6 New translations validators.en.xlf (Dutch) 2024-01-03 16:00:41 +01:00
Jan Böhmer
1491672cf8 New translations messages.en.xlf (Dutch) 2024-01-03 16:00:40 +01:00
Jan Böhmer
f9894ffff7 New translations messages.en.xlf (Italian) 2023-12-30 19:00:21 +01:00
Jan Böhmer
7b565817d6 Disable update checking for tests 2023-12-24 15:50:42 +01:00
Jan Böhmer
a03b2ecf73 Use sqlite database for testing by default 2023-12-24 15:27:05 +01:00
Jan Böhmer
dd2f74e19e Merge branch 'master' of github.com:Part-DB/Part-DB-server 2023-12-24 15:21:04 +01:00
Jan Böhmer
c1dcaf926a Updated dependencies 2023-12-24 15:20:52 +01:00
dependabot[bot]
c116db9593 Bump actions/upload-artifact from 3 to 4 (#461)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-24 15:03:39 +01:00
Jan Böhmer
1b92b9f171 Bump to 1.10.1 release 2023-12-12 22:42:53 +01:00
Jan Böhmer
17e79207f0 Suppress static analysis issue 2023-12-12 22:42:34 +01:00
Jan Böhmer
4d187741e0 Added the right copyright header to the foundation emails CSS asset 2023-12-12 22:39:44 +01:00
Jan Böhmer
85c3031fcd Upgraded dependencies 2023-12-12 22:30:45 +01:00
Jan Böhmer
a3e012d754 Added an event listener for console commands which shows a warning if the console is called as root or as wrong user
The idea is to prevent permission issues, by accidential calling the console wrong.
2023-12-12 22:23:19 +01:00
Jan Böhmer
60f8e754c2 Use the DEFAULT_URI setting for SAML base url instead of auto determining it.
This should fix issue #436
2023-12-10 22:52:59 +01:00
Jan Böhmer
3e13a0d9d9 Disable update checking in tests to prevent false-negatives in github actions caused by github api blocking 2023-12-10 22:52:01 +01:00
Jan Böhmer
cd91dc8b5a Fixed wrong path for event log datatables, when accessing via a prefixed reverse proxy 2023-12-10 21:40:49 +01:00
Jan Böhmer
bcaf96ed59 Use a svg file as default user avatar instead of SVG. Also changed path generation logic
This should also fix the path issue described in issue #446
2023-12-10 21:25:40 +01:00
Jan Böhmer
e2437d4c33 Updated dependencies 2023-12-10 00:40:39 +01:00
Jan Böhmer
3798217abc Use PDO constant instead of magic number in SetSQLModeMiddleware 2023-12-10 00:37:58 +01:00
Jan Böhmer
57423436ce Added options to use MySQL connection via SSL 2023-12-10 00:36:29 +01:00
Jan Böhmer
e824f6376a Mention update capability of part info providers in docs 2023-12-07 16:27:25 +01:00
Jan Böhmer
1f4bedc9b0 New Crowdin updates (#451)
* New translations messages.en.xlf (English)

* New translations validators.en.xlf (English)

* New translations security.en.xlf (English)

* New translations messages.en.xlf (German)
2023-12-07 13:32:29 +01:00
Jan Böhmer
aa66285909 Bumped to version 1.10.0 2023-12-07 13:19:07 +01:00
Jan Böhmer
c6229568c5 Added an workaround to github CI issue. setup php action has php-psr ext enabled somehow, which causes trouble. 2023-12-07 00:42:44 +01:00
Jan Böhmer
6110f5be40 Added an workaround to github CI issue. setup php action has php-psr ext enabled somehow, which causes trouble. 2023-12-07 00:41:07 +01:00
Jan Böhmer
ea9cc6723f Show a meaningful flash warning if trying to add/withdraw an amount of 0 instead of throwing an exception
Fixes issue #448
2023-12-07 00:36:16 +01:00
Jan Böhmer
b5721dcfd0 Revert "Migrated deprecated doctrine event subsrcibers"
For some very very weird reasoning this cause issues with the ObjectNormalizer, which does not get an an serializer injected anymore.
When the EventLoggerSubscriber is a doctrine subscriber it seems that the serializer service is initialized (as its requested in constructor but not used) and later injected into the object normalizer.
When its an listener, this does not work anymore.
2023-12-07 00:17:27 +01:00
Jan Böhmer
d7383539ba Merge remote-tracking branch 'origin/l10n_master' 2023-12-06 14:00:29 +01:00
Jan Böhmer
11cdc282d2 Mention KiCad integration in README 2023-12-06 14:00:15 +01:00
Jan Böhmer
b23f59271b New translations messages.en.xlf (German) 2023-12-06 13:11:47 +01:00
Jan Böhmer
999fe48a31 Removed SnakeCasePropertyAccessor as this fix is now part of the symfony property-info component 2023-12-06 00:05:37 +01:00
Jan Böhmer
963079afbf Fixed static analysis issue 2023-12-06 00:00:32 +01:00
Jan Böhmer
a6d508205b Fixed SQLite regex test 2023-12-06 00:00:20 +01:00
Jan Böhmer
fdf52a59fe Fixed error in sqlite regex implementation in certain edge cases 2023-12-05 23:51:54 +01:00
Jan Böhmer
dd0f8ec97c Implement the SQLite extension for doctrine via a middleware instead of an deprecated event listener 2023-12-05 23:50:07 +01:00
Jan Böhmer
641b47b189 Updated omines/datatables-bundle 2023-12-05 23:36:26 +01:00
Jan Böhmer
e1120dbfa7 Upgraded own bundle dependencies to fix some deprecations 2023-12-05 23:30:59 +01:00
Jan Böhmer
f9d47e0865 Migrated deprecated doctrine event subscribers to doctrine event listeners 2023-12-05 22:08:07 +01:00
Jan Böhmer
d991643b0e Removed deprecated google 2FA option 2023-12-05 22:07:48 +01:00
Jan Böhmer
8b8079a6f1 Fixed deprecations 2023-12-05 21:55:20 +01:00
Jan Böhmer
5faeb5dd56 Fixed problem with loading Fixtures on MySQL in combination with savepoints
We must now load the fixtures using custom command partdb:fixtures:load
2023-12-05 21:33:29 +01:00
Jan Böhmer
5b3156ccf4 Merge branch 'symfony6.4-upgrade' 2023-12-04 00:04:57 +01:00
Jan Böhmer
dc355773c9 Updated tecnickcom/tc-lib-barcode 2023-12-04 00:01:47 +01:00
Jan Böhmer
1daf556206 Updated brick/math 2023-12-04 00:00:00 +01:00
Jan Böhmer
1911c62edd Upgraded DAMA doctrine test bundle 2023-12-03 23:58:19 +01:00
Jan Böhmer
2fe2740b62 Updated dompdf 2023-12-03 23:40:16 +01:00
Jan Böhmer
4d7d624033 Updated web-auth/webauthn-symfony-bundle recipe 2023-12-03 23:32:34 +01:00
Jan Böhmer
0abe3f0e61 Updated webpack-encore-bundle recipe 2023-12-03 23:31:34 +01:00
Jan Böhmer
77a6204798 Updated symfony/translation bundle 2023-12-03 23:28:26 +01:00
Jan Böhmer
64af418be2 Updated security-bundle recipe 2023-12-03 23:27:18 +01:00
Jan Böhmer
15411d6c81 Updated phpunit-bridge recipe 2023-12-03 23:24:33 +01:00
Jan Böhmer
fd645a0bce Updated symfony-framework-bundle recipe
This removes the annotation reader services, which are not needed anymore
2023-12-03 23:04:35 +01:00
Jan Böhmer
f888028823 Updated phpstan recipe 2023-12-03 22:46:20 +01:00
Jan Böhmer
abc554c7b8 Updated doctrine recipe 2023-12-03 22:44:55 +01:00
Jan Böhmer
07cc8a9534 Updated API platform recipe 2023-12-03 22:43:42 +01:00
Jan Böhmer
60ecbc7c32 Upgraded all symfony components to 6.4 2023-12-03 22:18:44 +01:00
Jan Böhmer
b7af538cbf Updated symbols list, to include symbols containing special chars 2023-12-03 22:12:25 +01:00
Jan Böhmer
49c8b8003b New translations messages.en.xlf (Italian) 2023-12-03 21:43:16 +01:00
Jan Böhmer
65d04d4afb New translations messages.en.xlf (English) 2023-12-03 20:43:38 +01:00
Jan Böhmer
a449e82a22 New translations messages.en.xlf (Italian) 2023-12-03 20:43:30 +01:00
Jan Böhmer
34fd611946 Merge branch 'kicad-api' 2023-12-03 20:30:58 +01:00
Jan Böhmer
62cbc168fb Updated documentation about new visibility changes. 2023-12-03 20:30:49 +01:00
Jan Böhmer
74d1904df1 Only show parts and their categories in KiCad if they have useful info defined on them 2023-12-03 20:22:47 +01:00
Jan Böhmer
7d69d6ba30 Changed logic of invisible to a (forced) visibility field 2023-12-03 15:29:17 +01:00
Jan Böhmer
bc37d11f13 Fixed static analysis issue 2023-12-03 15:11:06 +01:00
Jan Böhmer
1825080d9e Added documentation about the EDA_KICAD_CATEGORY_DEPTH env 2023-12-03 15:07:41 +01:00
Jan Böhmer
6926f6b233 Allow to show all parts of all categories in a single KiCad category by setting EDA_KICAD_CATEGORY_DEPTH to -1 2023-12-03 15:03:00 +01:00
Jan Böhmer
459ae163da Restrict the depth of the category tree shown inside KiCAD to improve performance
The depth can be controlled via the EDA_KICAD_CATEGORY_DEPTH env
2023-12-03 14:42:33 +01:00
Jan Böhmer
fc7b1e6d31 Merge branch 'master' into kicad-api 2023-12-03 14:15:44 +01:00
Jan Böhmer
3198e5d750 New translations messages.en.xlf (Italian) 2023-12-03 09:40:17 +01:00
Jan Böhmer
f31cac580a New translations messages.en.xlf (Italian) 2023-12-03 08:40:16 +01:00
Jan Böhmer
753a12765b New translations messages.en.xlf (English) 2023-12-03 01:31:09 +01:00
Jan Böhmer
cbffc485f3 Updated dependencies 2023-12-03 01:20:39 +01:00
Jan Böhmer
c15ddcdf9f We are in development of Part-DB 1.10.0 now 2023-12-03 01:18:07 +01:00
Jan Böhmer
264ed3aaab Merge branch 'kicad-api' 2023-12-03 01:17:39 +01:00
Jan Böhmer
61a5ebde6b Show the correct KICad API endpoint on the user settings page. 2023-12-03 01:16:16 +01:00
Jan Böhmer
f4b4f14a67 Added ability to modify the EDA data via the Part-DB API 2023-12-03 00:57:11 +01:00
Jan Böhmer
9994dbd9db Added tests to test the KICad API endpoints 2023-12-03 00:43:34 +01:00
Jan Böhmer
d976865e7a Fixed static analysis issues 2023-12-03 00:05:41 +01:00
Jan Böhmer
0445b87567 Added EDA column migration for SQLite 2023-12-03 00:00:46 +01:00
Jan Böhmer
64c86fa11d Show EDA metadata in extended info table of part info page 2023-12-02 23:55:42 +01:00
Jan Böhmer
548339911f Added info about autocomplete to documentation 2023-12-02 19:57:55 +01:00
Jan Böhmer
e914a32894 Updated KiCad library autocomplete lists 2023-12-02 19:54:55 +01:00
Jan Böhmer
f28e369c01 Added an autocomplete feature for Kicad symbols and footprints 2023-12-02 19:40:26 +01:00
Jan Böhmer
30b2c8b841 Added forms to change EDA infos of footprints and categories 2023-12-01 22:47:05 +01:00
Jan Böhmer
b5c7a789a2 Made EDA form for parts prettier 2023-12-01 22:36:14 +01:00
Jan Böhmer
168b4f6c15 Started to write documentation on KiCAD integration 2023-12-01 14:09:19 +01:00
Jan Böhmer
bf5ed030fe Use the EDAInfo data to send info to KiCAD 2023-11-30 19:34:50 +01:00
Jan Böhmer
b76b2740a7 Use Embeddables for EDACategoryInfo instead of a json column 2023-11-30 19:13:32 +01:00
Jan Böhmer
d5f002ac20 Added basic ability to store EDA Data in a Part
But that might change, as it is currently not ideal
2023-11-30 12:54:30 +01:00
Jan Böhmer
2ec1a10623 Add various info from the Part-DB database to the KICAD parts 2023-11-29 21:28:06 +01:00
Jan Böhmer
ee69f9e576 Cache the results for the parts of a category for KiCAD 2023-11-29 20:57:11 +01:00
Jan Böhmer
b7af08503c Refactored cache tags and invalidation 2023-11-29 20:49:16 +01:00
Jan Böhmer
08a1ce5f64 Moved some logic from KICAD controller into its own service 2023-11-29 20:17:17 +01:00
Jan Böhmer
22f8448c65 Added an very basic API implementation for KICAD 2023-11-28 14:24:22 +01:00
Jan Böhmer
6b0f0d31b9 Allow to authenticate using Authorization: Token header, which the KiCAD API uses 2023-11-28 14:24:22 +01:00
Jan Böhmer
feca20ef77 Added a hint about quotes and TRUSTED_PROXIES setting in docker-compose example 2023-11-28 13:44:17 +01:00
Jan Böhmer
9e04a3405f New translations messages.en.xlf (English) 2023-11-28 00:22:20 +01:00
Jan Böhmer
46adb6d8b8 Release v1.9.1 2023-11-27 23:26:27 +01:00
Jan Böhmer
66e184c6b1 Merge remote-tracking branch 'origin/l10n_master' 2023-11-27 23:25:52 +01:00
Jan Böhmer
5b812104af New translations messages.en.xlf (German) 2023-11-27 23:23:32 +01:00
Jan Böhmer
0346b339c4 Updated dependencies 2023-11-27 23:22:08 +01:00
Jan Böhmer
c6bff42cf7 New translations messages.en.xlf (English) 2023-11-27 23:22:04 +01:00
Jan Böhmer
03712fcf96 Show an error flash, if the info providers cannnot communicate with the servers instead of throwing an exception 2023-11-27 23:17:20 +01:00
Jan Böhmer
dbff543fa8 Remove an attachment as preview image of an element, if it is not an image anymore through a change 2023-11-27 22:59:02 +01:00
Jan Böhmer
08bd4d54e3 Fix exception if uploading a new file for an already existing attachment 2023-11-27 22:48:18 +01:00
Jan Böhmer
eb30fb6e83 Fixed thumbnail for SVG files where the original name had no svg extension 2023-11-27 18:27:36 +01:00
Jan Böhmer
05e9b63f89 Fixed exception, when downloading an attachment file, which does not have a usable extension 2023-11-27 18:13:55 +01:00
Jan Böhmer
da0845c11c Added Timestampable interface to entities that missed it, to fix timetravel in certain cases 2023-11-27 17:53:35 +01:00
Jan Böhmer
584062c29a Move alternative names field of attachmenttype admin page to right position
Formerly it was not inside the common tab but below all tabs
2023-11-27 17:40:39 +01:00
Jan Böhmer
752cfb3698 Try to automatically determine an attachment name from a given URL similar to the name of an uploaded file 2023-11-27 17:39:24 +01:00
Jan Böhmer
18db20e511 Added the option env option to configure that all new attachment files should be downloaded by default 2023-11-26 23:44:02 +01:00
Jan Böhmer
0f0adfcf36 Filter out duplicate file DTO returned by the info providers 2023-11-26 22:24:22 +01:00
Jan Böhmer
7e99746b1e New translations security.en.xlf (English) 2023-11-25 21:10:30 +01:00
Jan Böhmer
30afcc02b9 New translations validators.en.xlf (English) 2023-11-25 21:10:29 +01:00
Jan Böhmer
8ff2fef855 New translations messages.en.xlf (English) 2023-11-25 21:10:28 +01:00
120 changed files with 41179 additions and 4336 deletions

View File

@@ -27,8 +27,8 @@
# Pass the configuration from the docker env to the PHP environment (here you should list all .env options)
PassEnv APP_ENV APP_DEBUG APP_SECRET
PassEnv TRUSTED_PROXIES TRUSTED_HOSTS LOCK_DSN
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES
PassEnv DATABASE_URL ENFORCE_CHANGE_COMMENTS_FOR DATABASE_MYSQL_USE_SSL_CA DATABASE_MYSQL_SSL_VERIFY_CERT
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES ATTACHMENT_DOWNLOAD_BY_DEFAULT
PassEnv MAILER_DSN ALLOW_EMAIL_PW_RESET EMAIL_SENDER_EMAIL EMAIL_SENDER_NAME
PassEnv HISTORY_SAVE_CHANGED_FIELDS HISTORY_SAVE_CHANGED_DATA HISTORY_SAVE_REMOVED_DATA HISTORY_SAVE_NEW_DATA
PassEnv ERROR_PAGE_ADMIN_EMAIL ERROR_PAGE_SHOW_HELP
@@ -42,6 +42,7 @@
PassEnv PROVIDER_TME_KEY PROVIDER_TME_SECRET PROVIDER_TME_CURRENCY PROVIDER_TME_LANGUAGE PROVIDER_TME_COUNTRY PROVIDER_TME_GET_GROSS_PRICES
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
PassEnv EDA_KICAD_CATEGORY_DEPTH
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to

23
.env
View File

@@ -14,6 +14,15 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
# Uncomment this line (and comment the line above to use a MySQL database
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db?serverVersion=5.7
# Set this value to 1, if you want to use SSL to connect to the MySQL server. It will be tried to use the CA certificate
# otherwise a CA bundle shipped with PHP will be used.
# Leave it at 0, if you do not want to use SSL or if your server does not support it
DATABASE_MYSQL_USE_SSL_CA=0
# Set this value to 0, if you don't want to verify the CA certificate of the MySQL server
# Only do this, if you know what you are doing!
DATABASE_MYSQL_SSL_VERIFY_CERT=1
###################################################################################
# General settings
###################################################################################
@@ -29,13 +38,15 @@ INSTANCE_NAME="Part-DB"
# Allow users to download attachments to the server by providing an URL
# This could be a potential security issue, as the user can retrieve any file the server has access to (via internet)
ALLOW_ATTACHMENT_DOWNLOADS=0
# Set this to 1, if the "download external files" checkbox should be checked by default for new attachments
ATTACHMENT_DOWNLOAD_BY_DEFAULT=0
# Use gravatars for user avatars, when user has no own avatar defined
USE_GRAVATAR=0
# The maximum allowed size for attachment files in bytes (you can use M for megabytes and G for gigabytes)
# Please note that the php.ini setting upload_max_filesize also limits the maximum size of uploaded files
MAX_ATTACHMENT_FILE_SIZE="100M"
# The public reachable URL of this Part-DB installation. This is used for generating links to the website in emails and so on
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates
# This must end with a slash!
DEFAULT_URI="https://partdb.changeme.invalid/"
@@ -157,10 +168,20 @@ PROVIDER_MOUSER_SEARCH_LIMIT=50
# Used when searching for keywords in the language specified when you signed up for Search API.
PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE='true'
##################################################################################
# EDA integration related settings
##################################################################################
# This value determines the depth of the category tree, that is visible inside KiCad
# 0 means that only the top level categories are visible. Set to a value > 0 to show more levels.
# Set to -1, to show all parts of Part-DB inside a single category in KiCad
EDA_KICAD_CATEGORY_DEPTH=0
###################################################################################
# SAML Single sign on-settings
###################################################################################
# Set this to 1 to enable SAML single sign on
# Be also sure to set the correct values for DEFAULT_URI
SAML_ENABLED=0
# Set to 1, if your Part-DB installation is behind a reverse proxy and you want to use SAML

View File

@@ -5,5 +5,9 @@ SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
DATABASE_URL="sqlite:///%kernel.project_dir%/var/app_test.db"
# Doctrine automatically adds an _test suffix to database name in test env
DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
#DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db
# Disable update checks, as tests would fail, when github is not reachable
CHECK_FOR_UPDATES=0

View File

@@ -27,7 +27,7 @@ jobs:
php-version: '8.2'
coverage: none
ini-values: xdebug.max_nesting_level=1000
extensions: mbstring, intl, gd, xsl, gmp, bcmath
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
- name: Get Composer Cache Directory
id: composer-cache
@@ -77,13 +77,13 @@ jobs:
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
- name: Upload assets artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Only dependencies and built assets
path: /tmp/partdb_assets.zip
- name: Upload full artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Full Part-DB including dependencies and built assets
path: /tmp/partdb_with_assets.zip

View File

@@ -24,7 +24,7 @@ jobs:
php-version: '8.2'
coverage: none
ini-values: xdebug.max_nesting_level=1000
extensions: mbstring, intl, gd, xsl, gmp, bcmath
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
- name: Get Composer Cache Directory
id: composer-cache

View File

@@ -26,6 +26,7 @@ jobs:
SYMFONY_DEPRECATIONS_HELPER: disabled
PHP_VERSION: ${{ matrix.php-versions }}
DB_TYPE: ${{ matrix.db-type }}
CHECK_FOR_UPDATES: false # Disable update checks for tests
steps:
- name: Set Database env for MySQL
@@ -45,7 +46,7 @@ jobs:
php-version: ${{ matrix.php-versions }}
coverage: pcov
ini-values: xdebug.max_nesting_level=1000
extensions: mbstring, intl, gd, xsl, gmp, bcmath
extensions: mbstring, intl, gd, xsl, gmp, bcmath, :php-psr
- name: Start MySQL
run: sudo systemctl start mysql.service
@@ -107,9 +108,10 @@ jobs:
- name: Do migrations
run: php bin/console --env test doctrine:migrations:migrate -n
# Use our own custom fixtures loading command to circumvent some problems with reset the autoincrement values
- name: Load fixtures
run: php bin/console --env test doctrine:fixtures:load -n
run: php bin/console --env test partdb:fixtures:load -n
- name: Run PHPunit and generate coverage
run: ./bin/phpunit --coverage-clover=coverage.xml

View File

@@ -63,6 +63,8 @@ for the first time.
* Use cloud providers (like Octopart, Digikey, farnell or TME) to automatically get part information, datasheets and
prices for parts
* API to access Part-DB from other applications/scripts
* [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as central datasource for your
KiCad and see available parts from Part-DB directly inside KiCad.
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
or makerspaces, where many users have should have (controlled) access to the shared inventory.

View File

@@ -1 +1 @@
1.9.0
1.10.2

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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,13 @@ if (!ini_get('date.timezone')) {
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";

View File

@@ -12,7 +12,8 @@
"ext-mbstring": "*",
"api-platform/core": "^3.1",
"beberlei/doctrineextensions": "^1.2",
"brick/math": "^0.11.0",
"brick/math": "0.12.1 as 0.11.0",
"composer/ca-bundle": "^1.3",
"composer/package-versions-deprecated": "^1.11.99.5",
"doctrine/annotations": "1.14.3",
"doctrine/data-fixtures": "^1.6.6",
@@ -20,12 +21,12 @@
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.16",
"dompdf/dompdf": "dev-master#87bea32efe0b0db309e1d31537201f64d5508280 as v2.0.3",
"dompdf/dompdf": "dev-master#c9cf4be933e2406a51990bd4eb9e70612e790cc0 as v2.0.4",
"erusev/parsedown": "^1.7",
"florianv/swap": "^4.0",
"florianv/swap-bundle": "dev-master",
"gregwar/captcha-bundle": "^2.1.0",
"jbtronics/2fa-webauthn": "^v2.0.0",
"jbtronics/2fa-webauthn": "^v2.2.0",
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
"jfcherng/php-diff": "^6.14",
"knpuniversity/oauth2-client-bundle": "^2.15",
@@ -38,7 +39,7 @@
"nelmio/security-bundle": "^3.0",
"nyholm/psr7": "^1.1",
"ocramius/proxy-manager": "2.2.*",
"omines/datatables-bundle": "^0.7.2",
"omines/datatables-bundle": "^0.8.0",
"part-db/label-fonts": "^1.0",
"php-translation/symfony-bundle": "^0.14.0",
"phpdocumentor/reflection-docblock": "^5.2",
@@ -51,35 +52,36 @@
"shivas/versioning-bundle": "^4.0",
"spatie/db-dumper": "^3.3.1",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.3.*",
"symfony/console": "6.3.*",
"symfony/dotenv": "6.3.*",
"symfony/expression-language": "6.3.*",
"symfony/asset": "6.4.*",
"symfony/console": "6.4.*",
"symfony/dotenv": "6.4.*",
"symfony/expression-language": "6.4.*",
"symfony/flex": "^v2.3.1",
"symfony/form": "6.3.*",
"symfony/framework-bundle": "6.3.*",
"symfony/http-client": "6.3.*",
"symfony/http-kernel": "6.3.*",
"symfony/mailer": "6.3.*",
"symfony/form": "6.4.*",
"symfony/framework-bundle": "6.4.*",
"symfony/http-client": "6.4.*",
"symfony/http-kernel": "6.4.*",
"symfony/mailer": "6.4.*",
"symfony/monolog-bundle": "^3.1",
"symfony/process": "6.3.*",
"symfony/property-access": "6.3.*",
"symfony/property-info": "6.3.*",
"symfony/proxy-manager-bridge": "6.3.*",
"symfony/rate-limiter": "6.3.*",
"symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*",
"symfony/string": "6.3.*",
"symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*",
"symfony/polyfill-php82": "^1.28",
"symfony/process": "6.4.*",
"symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*",
"symfony/proxy-manager-bridge": "6.4.*",
"symfony/rate-limiter": "6.4.*",
"symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*",
"symfony/serializer": "6.4.*",
"symfony/string": "6.4.*",
"symfony/translation": "6.4.*",
"symfony/twig-bundle": "6.4.*",
"symfony/ux-translator": "^2.10",
"symfony/ux-turbo": "^2.0",
"symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*",
"symfony/validator": "6.4.*",
"symfony/web-link": "6.4.*",
"symfony/webpack-encore-bundle": "^v2.0.1",
"symfony/yaml": "6.3.*",
"tecnickcom/tc-lib-barcode": "^1.15",
"symfony/yaml": "6.4.*",
"tecnickcom/tc-lib-barcode": "^2.1.4",
"twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0",
"twig/html-extra": "^3.0",
@@ -90,7 +92,7 @@
"webmozart/assert": "^1.4"
},
"require-dev": {
"dama/doctrine-test-bundle": "^7.0",
"dama/doctrine-test-bundle": "^v8.0.0",
"doctrine/doctrine-fixtures-bundle": "^3.2",
"ekino/phpstan-banned-code": "^v1.0.0",
"phpstan/extension-installer": "^1.0",
@@ -102,13 +104,13 @@
"psalm/plugin-symfony": "^v5.0.1",
"rector/rector": "^0.18.0",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*",
"symfony/debug-bundle": "6.3.*",
"symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*",
"symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.13",
"symfony/phpunit-bridge": "6.3.*",
"symfony/stopwatch": "6.3.*",
"symfony/web-profiler-bundle": "6.3.*",
"symfony/phpunit-bridge": "6.4.*",
"symfony/stopwatch": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*",
"symplify/easy-coding-standard": "^12.0",
"vimeo/psalm": "^5.6.0"
},
@@ -161,7 +163,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.3.*"
"require": "6.4.*"
}
}
}

2552
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,18 @@
api_platform:
title: 'Part-DB API'
description: 'API of Part-DB'
version: '0.1.0'
# eager_loading:
# max_joins: 100
formats:
jsonld: ['application/ld+json']
json: ['application/json']
jsonapi: ['application/vnd.api+json']
keep_legacy_inflector: false
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
json: ['application/vnd.openapi+json']
swagger:
api_keys:
@@ -24,5 +28,9 @@ api_platform:
vary: ['Content-Type', 'Authorization', 'Origin']
extra_properties:
standard_put: true
rfc_7807_compliant_errors: true
pagination_client_items_per_page: true # Allow clients to override the default items per page
pagination_client_items_per_page: true # Allow clients to override the default items per page
keep_legacy_inflector: false
event_listeners_backward_compatibility_layer: false

View File

@@ -2,6 +2,9 @@ doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# Required for DAMA doctrine test bundle
use_savepoints: true
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
@@ -28,8 +31,8 @@ doctrine:
auto_mapping: true
mappings:
App:
is_bundle: false
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App

View File

@@ -2,9 +2,10 @@
framework:
secret: '%env(APP_SECRET)%'
csrf_protection: true
annotations: false
handle_all_throwables: true
# We set this header by ourself, so we can disable it here
# We set this header by ourselves, so we can disable it here
disallow_search_engine_index: false
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
@@ -26,7 +27,6 @@ framework:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
#esi: true
#fragments: true

View File

@@ -32,7 +32,7 @@ nbgrp_onelogin_saml:
privateKey: '%env(string:default:saml.sp.privateKey:string:SAMLP_SP_PRIVATE_KEY)%'
# Optional settings
#baseurl: 'http://myapp.com'
baseurl: '%partdb.default_uri%saml/'
strict: true
debug: false
security:

View File

@@ -6,7 +6,7 @@ scheb_two_factor:
server_name: '$$DOMAIN$$' # This field is replaced by the domain name of the server in DecoratedGoogleAuthenticator
issuer: '%partdb.title%' # Issuer name used in QR code
digits: 6 # Number of digits in authentication code
window: 1 # How many codes before/after the current one would be accepted as valid
leeway: 5 # Acceptable time drift in seconds
template: security/2fa_form.html.twig
backup_codes:

View File

@@ -71,3 +71,5 @@ security:
- { path: "^/\\w{2}/tree", role: PUBLIC_ACCESS }
# Restrict access to API to users, which has the API access permission
- { 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

@@ -35,6 +35,7 @@ parameters:
# Attachments and files
######################################################################################################################
partdb.attachments.allow_downloads: '%env(bool:ALLOW_ATTACHMENT_DOWNLOADS)%' # Allow users to download attachments to server. Warning: This can be dangerous, because via that feature attackers maybe can access ressources on your intranet!
partdb.attachments.download_by_default: '%env(bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT)%' # If this is set the 'download external files' checkbox is set by default for new attachments (only if allow_downloads is set to true)
partdb.attachments.dir.media: 'public/media/' # The folder where uploaded attachment files are saved (must be in public folder)
partdb.attachments.dir.secure: 'uploads/' # The folder where secured attachment files are saved (must not be in public/)
partdb.attachments.max_file_size: '%env(string:MAX_ATTACHMENT_FILE_SIZE)%' # The maximum size of an attachment file (in bytes, you can use M for megabytes and G for gigabytes)
@@ -141,3 +142,4 @@ parameters:
env(HISTORY_SAVE_REMOVED_DATA): 1
env(HISTORY_SAVE_NEW_DATA): 1
env(EDA_KICAD_CATEGORY_DEPTH): 0

View File

@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

View File

@@ -93,6 +93,7 @@ services:
arguments:
$allow_attachments_download: '%partdb.attachments.allow_downloads%'
$max_file_size: '%partdb.attachments.max_file_size%'
$download_by_default: '%partdb.attachments.download_by_default%'
App\Services\Attachments\AttachmentSubmitHandler:
arguments:
@@ -140,6 +141,19 @@ services:
$saml_role_mapping: '%env(json:SAML_ROLE_MAPPING)%'
$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
####################################################################################################################
@@ -302,6 +316,13 @@ services:
$global_locale: '%partdb.locale%'
$global_timezone: '%partdb.timezone%'
####################################################################################################################
# EDA system
####################################################################################################################
App\Services\EDA\KiCadHelper:
arguments:
$category_depth: '%env(int:EDA_KICAD_CATEGORY_DEPTH)%'
####################################################################################################################
# Symfony overrides
####################################################################################################################
@@ -349,6 +370,10 @@ services:
$partdb_banner: '%partdb.banner%'
$project_dir: '%kernel.project_dir%'
App\Doctrine\Middleware\MySQLSSLConnectionMiddlewareWrapper:
arguments:
$enabled: '%env(bool:DATABASE_MYSQL_USE_SSL_CA)%'
$verify: '%env(bool:DATABASE_MYSQL_SSL_VERIFY_CERT)%'
####################################################################################################################
# Monolog
@@ -376,4 +401,4 @@ when@test:
arguments:
- '@doctrine.fixtures.loader'
- '@doctrine'
- { default: '@App\Doctrine\Purger\ResetAutoIncrementPurgerFactory' }
- { default: '@App\Doctrine\Purger\DoNotUsePurgerFactory' }

View File

@@ -37,6 +37,9 @@ options listed, see `.env` file for full list of possible env variables.
(e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`). For sqlite use the following format to specify the
absolute path where it should be located `sqlite:///path/part/app.db`. You can use `%kernel.project_dir%` as
placeholder for the Part-DB root folder (e.g. `sqlite:///%kernel.project_dir%/var/app.db`)
* `DATABASE_MYSQL_USE_SSL_CA`: If this value is set to `1` or `true` and a MySQL connection is used, then the connection
is encrypted by SSL/TLS and the server certificate is verified against the system CA certificates or the CA certificate
bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept all certificates.
* `DEFAULT_LANG`: The default language to use server wide (when no language is explicitly specified by a user or via
language chooser). Must be something like `en`, `de`, `fr`, etc.
* `DEFAULT_TIMEZONE`: The default timezone to use globally, when a user has no timezone specified. Must be something
@@ -53,6 +56,9 @@ options listed, see `.env` file for full list of possible env variables.
download a file specified as a URL and create it as local file. Please note that this allows users access to all
resources publicly available to the server (so full access to other servers in the same local network), which could
be a security risk.
* `ATTACHMENT_DOWNLOAD_BY_DEFAULT`: When this is set to 1, the "download external file" checkbox is checked by default
when adding a new attachment. Otherwise, it is unchecked by default. Use this if you wanna download all attachments
locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1.
* `USE_GRAVATAR`: Set to `1` to use [gravatar.com](https://gravatar.com/) images for user avatars (as long as they have
not set their own picture). The users browsers have to download the pictures from a third-party (gravatar) server, so
this might be a privacy risk.
@@ -125,6 +131,14 @@ then `HISTORY_SAVE_CHANGED_FIELDS`, `HISTORY_SAVE_CHANGED_DATA` and `HISTORY_SAV
* `ERROR_PAGE_SHOW_HELP`: Set this 0, to disable the solution hints shown on an error page. These hints should not
contain sensitive information, but could confuse end-users.
### EDA related settings
* `EDA_KICAD_CATEGORY_DEPTH`: A number, which determines how many levels of Part-DB categories should be shown inside KiCad.
All parts in the selected category and all subcategories are shown in KiCad.
For performance reason this value should not be too high. The default is 0, which means that only the top level categories are shown in KiCad.
All parts in the selected category and all subcategories are shown in KiCad. Set this to a higher value, if you want to show more categories in KiCad.
When you set this value to -1, all parts are shown inside a single category in KiCad.
### SAML SSO settings
The following settings can be used to enable and configure Single-Sign on via SAML. This allows users to log in to

View File

@@ -49,6 +49,8 @@ It is installed on a web server and so can be accessed with any browser without
* Use cloud providers (like Octopart, Digikey, farnell or TME) to automatically get part information, datasheets and
prices for parts (see [here]({% link usage/information_provider_system.md %}))
* API to access Part-DB from other applications/scripts
* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as central datasource for your
KiCad and see available parts from Part-DB directly inside KiCad.
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
or makerspaces, where many users have should have (controlled) access to the shared inventory.

View File

@@ -47,8 +47,9 @@ services:
# You can configure Part-DB using environment variables
# Below you can find the most essential ones predefined
# However you can add add any other environment configuration you want here
# However you can add any other environment configuration you want here
# See .env file for all available options or https://docs.part-db.de/configuration.html
# !!! Do not use quotes around the values, as they will be interpreted as part of the value and this will lead to errors !!!
# The language to use serverwide as default (en, de, ru, etc.)
- DEFAULT_LANG=en
@@ -65,9 +66,12 @@ services:
# Use gravatars for user avatars, when user has no own avatar defined
- USE_GRAVATAR=0
# Override value if you want to show to show a given text on homepage.
# Override value if you want to show a given text on homepage.
# When this is empty the content of config/banner.md is used as banner
#- BANNER=This is a test banner<br>with a line break
# If you use a reverse proxy in front of Part-DB, you must configure the trusted proxies IP addresses here (see reverse proxy documentation for more information):
# - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
```
4. Customize the settings by changing the environment variables (or add new ones). See [Configuration]({% link

View File

@@ -0,0 +1,79 @@
---
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. Add some EDA metadata to parts, categories or footprints. Only parts with useable info will show up in KiCad. See below for more info.
3. 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"
}
}
```
4. 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.
5. Replace the `token` field value with the token you have generated in step 1.
6. 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.
### Parts and category visibility
Only parts and their categories, on which there is any kind of EDA metadata are defined show up in KiCad. So if you want to see parts in KiCad,
you need to define at least a symbol, footprint, reference prefix or value on a part, category or footprint.
You can use the "Force visibility" checkbox on a part or category to override this behavior and force parts to be visible or hidden in KiCad.
*Please note that KiCad caches the library categories. So if you change something, which would change the visibile categories in KiCad, you have to reload EEschema to see the changes.*
### Category depth in KiCad
For performance reasons, only the most top level categories of Part-DB are shown as categories in KiCad. All parts in the subcategories are shown in the top level category.
You can configure the depth of the categories shown in KiCad, via the `EDA_KICAD_CATEGORY_DEPTH` env option. The default value is 0, which meabs only the top level categories are shown.
To show more levels of categories, you can set this value to a higher number.
If you set this value to -1, all parts are shown inside a single category in KiCad, without any subcategories.
You can view the "real" category path of a part in the part details dialog in KiCad.

View File

@@ -37,6 +37,10 @@ filled in.
![image]({% link assets/usage/information_provider_system/animation.gif %})
If you want to update an existing part, go to the parts info page and click on the "Update from info provider" button in
the tools tab. You will be redirected to a search page, where you can search the info providers to automatically update this
part.
## Alternative names
Part-DB tries to automatically find existing elements from your database for the information it got from the providers

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)');
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Ebene1"><rect x="0.021" y="-0.023" width="19.977" height="20" style="fill:#95bbdf;"/></g><path d="M10,10c2.194,0 4,-1.806 4,-4c0,-2.194 -1.806,-4 -4,-4c-2.194,0 -4,1.806 -4,4c0,2.194 1.806,4 4,4Zm-1.428,1.5c-3.078,0 -5.572,2.494 -5.572,5.572c0,0.512 0.416,0.928 0.928,0.928l12.144,0c0.512,0 0.928,-0.416 0.928,-0.928c0,-3.078 -2.494,-5.572 -5.572,-5.572l-2.856,0Z" style="fill:#fff;fill-rule:nonzero;"/></svg>

After

Width:  |  Height:  |  Size: 856 B

13272
public/kicad/footprints.txt Normal file

File diff suppressed because it is too large Load Diff

19594
public/kicad/symbols.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2024 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\ApiPlatform;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Doctrine\ORM\ORMInvalidArgumentException;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use ApiPlatform\State\ApiResource\Error;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* This class adds a custom error if the user tries to create a new entity through a relation, and suggests to do reference it through an IRI instead.
* This class decorates the default error handler of API Platform.
*/
#[AsDecorator('api_platform.state.error_provider')]
final class ErrorHandler implements ProviderInterface
{
public function __construct(private readonly ProviderInterface $decorated, #[Autowire('%kernel.debug%')] private readonly bool $debug)
{
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$request = $context['request'];
$format = $request->getRequestFormat();
$exception = $request->attributes->get('exception');
//Check if the exception is a ORM InvalidArgument exception and complains about a not-persisted entity through relation
if ($exception instanceof ORMInvalidArgumentException && str_contains($exception->getMessage(), 'A new entity was found through the relationship')) {
//Extract the entity class and property name from the exception message
$matches = [];
preg_match('/A new entity was found through the relationship \'(?<property>.*)\'/i', $exception->getMessage(), $matches);
$property = $matches['property'] ?? "unknown";
//Create a new error response
$error = Error::createFromException($exception, 400);
//Return the error response
$detail = "You tried to create a new entity through the relation '$property', but this is not allowed. Please create the entity first and then reference it through an IRI!";
//If we are in debug mode, add the exception message to the error response
if ($this->debug) {
$detail .= " Original exception message: " . $exception->getMessage();
}
$error->setDetail($detail);
return $error;
}
return $this->decorated->provide($operation, $uriVariables, $context);
}
}

View File

@@ -0,0 +1,76 @@
<?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\Command;
use App\Doctrine\Purger\ResetAutoIncrementORMPurger;
use App\Doctrine\Purger\DoNotUsePurgerFactory;
use App\Doctrine\Purger\ResetAutoIncrementPurgerFactory;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* This command does basically the same as doctrine:fixtures:load, but it purges the database before loading the fixtures.
* It does so in another transaction, so we can modify the purger to reset the autoincrement, which would not be possible
* because the implicit commit otherwise.
*/
#[AsCommand(name: 'partdb:fixtures:load', description: 'Load test fixtures into the database and allows to reset the autoincrement before loading the fixtures.', hidden: true)]
class LoadFixturesCommand extends Command
{
public function __construct(private readonly EntityManagerInterface $entityManager)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = new SymfonyStyle($input, $output);
$ui->warning('This command is for development and testing purposes only. It will purge the database and load fixtures afterwards. Do not use in production!');
if (! $ui->confirm(sprintf('Careful, database "%s" will be purged. Do you want to continue?', $this->entityManager->getConnection()->getDatabase()), ! $input->isInteractive())) {
return 0;
}
$factory = new ResetAutoIncrementPurgerFactory();
$purger = $factory->createForEntityManager(null, $this->entityManager);
$purger->purge();
//Afterwards run the load fixtures command as normal, but with the --append option
$new_input = new ArrayInput([
'command' => 'doctrine:fixtures:load',
'--append' => true,
]);
$returnCode = $this->getApplication()?->doRun($new_input, $output);
return $returnCode ?? Command::FAILURE;
}
}

View File

@@ -33,14 +33,18 @@ use App\Services\InfoProviderSystem\ProviderRegistry;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\Parts\PartFormHelper;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\Translation\t;
#[Route('/tools/info_providers')]
class InfoProviderController extends AbstractController
{
@@ -64,7 +68,7 @@ class InfoProviderController extends AbstractController
#[Route('/search', name: 'info_providers_search')]
#[Route('/update/{target}', name: 'info_providers_update_part_search')]
public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target): Response
public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger): Response
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
@@ -83,7 +87,14 @@ class InfoProviderController extends AbstractController
$keyword = $form->get('keyword')->getData();
$providers = $form->get('providers')->getData();
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
try {
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
} catch (ClientException $e) {
$this->addFlash('error', t('info_providers.search.error.client_exception'));
$this->addFlash('error',$e->getMessage());
//Log the exception
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
}
}
return $this->render('info_providers/search/part_search.html.twig', [

View File

@@ -0,0 +1,84 @@
<?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\EDA\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
{
if ($category) {
$this->denyAccessUnlessGranted('read', $category);
} else {
$this->denyAccessUnlessGranted('@categories.read');
}
$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

@@ -414,6 +414,12 @@ class PartController extends AbstractController
throw new \LogicException("The timestamp must not be in the future!");
}
//Ensure that the amount is not null or negative
if ($amount <= 0) {
$this->addFlash('warning', 'part.withdraw.zero_amount');
goto err;
}
try {
switch ($action) {
case "withdraw":

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

@@ -79,10 +79,7 @@ class LocaleDateTimeColumn extends AbstractColumn
);
}
/**
* @return $this
*/
protected function configureOptions(OptionsResolver $resolver): self
protected function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);

View File

@@ -57,10 +57,7 @@ class LogEntryTargetColumn extends AbstractColumn
return $value;
}
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): self
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);
$resolver->setDefault('show_associated', true);

View File

@@ -79,10 +79,7 @@ class PartAttachmentsColumn extends AbstractColumn
return $tmp;
}
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): self
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);

View File

@@ -28,11 +28,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class RowClassColumn extends AbstractColumn
{
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): self
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);
@@ -56,7 +52,7 @@ class RowClassColumn extends AbstractColumn
/**
* @return mixed
*/
public function normalize($value)
public function normalize($value): mixed
{
return $value;
}

View File

@@ -32,10 +32,7 @@ class SIUnitNumberColumn extends AbstractColumn
{
}
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): self
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);

View File

@@ -30,10 +30,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class SelectColumn extends AbstractColumn
{
/**
* @return $this
*/
public function configureOptions(OptionsResolver $resolver): self
public function configureOptions(OptionsResolver $resolver): static
{
parent::configureOptions($resolver);

View File

@@ -67,8 +67,10 @@ class ErrorDataTable implements DataTableTypeInterface
//Build the array containing data
$data = [];
$n = 0;
foreach ($options['errors'] as $error) {
$data[] = ['error' => $error];
$data['error_' . $n] = ['error' => $error];
$n++;
}
$dataTable->createAdapter(ArrayAdapter::class, $data);

View File

@@ -0,0 +1,52 @@
<?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\Doctrine\Middleware;
use Composer\CaBundle\CaBundle;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
/**
* This middleware sets SSL options for MySQL connections
*/
class MySQLSSLConnectionMiddlewareDriver extends AbstractDriverMiddleware
{
public function __construct(Driver $wrappedDriver, private readonly bool $enabled, private readonly bool $verify = true)
{
parent::__construct($wrappedDriver);
}
public function connect(array $params): Connection
{
//Only set this on MySQL connections, as other databases don't support this parameter
if($this->enabled && $this->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
$params['driverOptions'][\PDO::MYSQL_ATTR_SSL_CA] = CaBundle::getSystemCaRootBundlePath();
$params['driverOptions'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->verify;
}
return parent::connect($params);
}
}

View File

@@ -0,0 +1,39 @@
<?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\Doctrine\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
class MySQLSSLConnectionMiddlewareWrapper implements Middleware
{
public function __construct(private readonly bool $enabled, private readonly bool $verify = true)
{
}
public function wrap(Driver $driver): Driver
{
return new MySQLSSLConnectionMiddlewareDriver($driver, $this->enabled, $this->verify);
}
}

View File

@@ -1,11 +1,8 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
* 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
@@ -20,29 +17,31 @@ declare(strict_types=1);
* 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/>.
*/
namespace App\Doctrine;
declare(strict_types=1);
namespace App\Doctrine\Middleware;
use App\Exceptions\InvalidRegexException;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\SqlitePlatform;
/**
* This subscriber is used to add the regexp operator to the SQLite platform.
* This middleware is used to add the regexp operator to the SQLite platform.
* As a PHP callback is called for every entry to compare it is most likely much slower than using regex on MySQL.
* But as regex is not often used, this should be fine for most use cases, also it is almost impossible to implement a better solution.
*/
#[AsDoctrineListener(Events::postConnect)]
class SQLiteRegexExtension
class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
{
public function postConnect(ConnectionEventArgs $eventArgs): void
public function connect(#[\SensitiveParameter] array $params): Connection
{
$connection = $eventArgs->getConnection();
//Do connect process first
$connection = parent::connect($params); // TODO: Change the autogenerated stub
//We only execute this on SQLite databases
if ($connection->getDatabasePlatform() instanceof SqlitePlatform) {
//Then add the functions if we are on SQLite
if ($this->getDatabasePlatform() instanceof SqlitePlatform) {
$native_connection = $connection->getNativeConnection();
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
@@ -52,6 +51,9 @@ class SQLiteRegexExtension
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
}
}
return $connection;
}
/**
@@ -60,8 +62,12 @@ class SQLiteRegexExtension
* @param string $value
* @return int
*/
final public static function regexp(string $pattern, string $value): int
final public static function regexp(string $pattern, ?string $value): int
{
if ($value === null) {
return 0;
}
try {
return (mb_ereg($pattern, $value)) ? 1 : 0;
@@ -107,4 +113,4 @@ class SQLiteRegexExtension
return $index + 1;
}
}
}

View File

@@ -0,0 +1,35 @@
<?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\Doctrine\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
class SQLiteRegexExtensionMiddlewareWrapper implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new SQLiteRegexExtensionMiddlewareDriver($driver);
}
}

View File

@@ -20,7 +20,7 @@ declare(strict_types=1);
* 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/>.
*/
namespace App\Doctrine\SetSQLMode;
namespace App\Doctrine\Middleware;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
@@ -37,7 +37,7 @@ class SetSQLModeMiddlewareDriver extends AbstractDriverMiddleware
//Only set this on MySQL connections, as other databases don't support this parameter
if($this->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
//1002 is \PDO::MYSQL_ATTR_INIT_COMMAND constant value
$params['driverOptions'][1002] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
$params['driverOptions'][\PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode, \'ONLY_FULL_GROUP_BY\', \'\'))';
}
return parent::connect($params);

View File

@@ -20,7 +20,7 @@ declare(strict_types=1);
* 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/>.
*/
namespace App\Doctrine\SetSQLMode;
namespace App\Doctrine\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
@@ -30,7 +30,6 @@ use Doctrine\DBAL\Driver\Middleware;
*/
class SetSQLModeMiddlewareWrapper implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new SetSQLModeMiddlewareDriver($driver);

View File

@@ -0,0 +1,53 @@
<?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\Doctrine\Purger;
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class DoNotUsePurgerFactory implements PurgerFactory
{
public function createForEntityManager(
?string $emName,
EntityManagerInterface $em,
array $excluded = [],
bool $purgeWithTruncate = false
): PurgerInterface {
return new class() implements ORMPurgerInterface {
public function purge(): void
{
throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!');
}
public function setEntityManager(EntityManagerInterface $em)
{
// TODO: Implement setEntityManager() method.
}
};
}
}

View File

@@ -190,7 +190,6 @@ class ResetAutoIncrementORMPurger implements PurgerInterface, ORMPurgerInterface
//Reseting autoincrement is only supported on MySQL platforms
if ($platform instanceof AbstractMySQLPlatform ) { //|| $platform instanceof SqlitePlatform) {
$connection->beginTransaction();
$connection->executeQuery($this->getResetAutoIncrementSQL($tbl, $platform));
}
}

View File

@@ -47,10 +47,10 @@ class TinyIntType extends Type
* @param T $value
*
* @return (T is null ? null : int)
*
*
* @template T
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
public function convertToPHPValue($value, AbstractPlatform $platform): ?int
{
return $value === null ? null : (int) $value;
}

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 Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
#[Groups(['full', 'category:read', 'category:write'])]
private ?bool $visibility = 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 getVisibility(): ?bool
{
return $this->visibility;
}
public function setVisibility(?bool $visibility): EDACategoryInfo
{
$this->visibility = $visibility;
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 Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
private ?bool $visibility = 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 getVisibility(): ?bool
{
return $this->visibility;
}
public function setVisibility(?bool $visibility): EDAPartInfo
{
$this->visibility = $visibility;
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 App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
use App\Entity\EDA\EDACategoryInfo;
use App\Entity\EDA\EDAPartInfo;
use App\Repository\Parts\CategoryRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\Common\Collections\ArrayCollection;
@@ -47,6 +49,7 @@ use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parameters\CategoryParameter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
@@ -185,6 +188,19 @@ class Category extends AbstractPartsContainingDBElement
#[Groups(['category:read'])]
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
{
@@ -278,14 +294,17 @@ class Category extends AbstractPartsContainingDBElement
public function setDefaultComment(string $default_comment): self
{
$this->default_comment = $default_comment;
return $this;
}
public function __construct()
public function getEdaInfo(): EDACategoryInfo
{
parent::__construct();
$this->children = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection();
return $this->eda_info;
}
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\Entity\Attachments\Attachment;
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\Entity\Base\AbstractStructuralDBElement;
use Doctrine\Common\Collections\ArrayCollection;
@@ -47,6 +50,7 @@ use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Parameters\FootprintParameter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
@@ -137,6 +141,19 @@ class Footprint extends AbstractPartsContainingDBElement
#[Groups(['footprint:read'])]
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
@@ -166,11 +183,15 @@ class Footprint extends AbstractPartsContainingDBElement
return $this;
}
public function __construct()
public function getEdaInfo(): EDAFootprintInfo
{
parent::__construct();
$this->children = new ArrayCollection();
$this->attachments = new ArrayCollection();
$this->parameters = new ArrayCollection();
return $this->eda_info;
}
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\PartStoragelocationFilter;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\EDA\EDAPartInfo;
use App\Entity\Parts\PartTraits\AssociationTrait;
use App\Entity\Parts\PartTraits\EDATrait;
use App\Repository\PartRepository;
use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\Attachment;
@@ -83,7 +85,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiResource(
operations: [
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',
], security: 'is_granted("read", object)'),
new GetCollection(security: 'is_granted("@parts.read")'),
@@ -92,7 +94,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Delete(security: 'is_granted("delete", object)'),
],
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(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
@@ -102,8 +104,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
#[DocumentedAPIProperty(schemaName: 'Part-Read', property: 'total_instock', type: 'number', nullable: false,
description: 'The total amount of this part in stock (sum of all part lots).')]
class Part extends AttachmentContainingDBElement
{
use AdvancedPropertyTrait;
@@ -115,6 +115,7 @@ class Part extends AttachmentContainingDBElement
use ParametersTrait;
use ProjectTrait;
use AssociationTrait;
use EDATrait;
/** @var Collection<int, PartParameter>
*/
@@ -173,6 +174,7 @@ class Part extends AttachmentContainingDBElement
//By default, the part has no provider
$this->providerReference = InfoProviderReference::noProvider();
$this->eda_info = new EDAPartInfo();
}
public function __clone()
@@ -208,6 +210,7 @@ class Part extends AttachmentContainingDBElement
//Deep clone info provider
$this->providerReference = clone $this->providerReference;
$this->eda_info = clone $this->eda_info;
}
parent::__clone();
}

View File

@@ -37,6 +37,7 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Contracts\TimeStampableInterface;
use App\Repository\DBElementRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
@@ -68,7 +69,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiFilter(LikeFilter::class, properties: ["other_type", "comment"])]
#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['comment', 'addedDate', 'lastModified'])]
class PartAssociation extends AbstractDBElement
class PartAssociation extends AbstractDBElement implements TimeStampableInterface
{
use TimestampTrait;

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

@@ -28,6 +28,7 @@ use App\Entity\Parts\PartLot;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Validator\Constraints as Assert;
/**
@@ -181,6 +182,8 @@ trait InstockTrait
*
* @return float The amount of parts given in partUnit
*/
#[Groups(['simple', 'extended', 'full', 'part:read'])]
#[SerializedName('total_instock')]
public function getAmountSum(): float
{
//TODO: Find a method to do this natively in SQL, the current method could be a bit slow

View File

@@ -35,6 +35,7 @@ use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Contracts\TimeStampableInterface;
use App\Validator\UniqueValidatableInterface;
use Doctrine\DBAL\Types\Types;
use App\Entity\Base\AbstractDBElement;
@@ -84,7 +85,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", 'mountnames'])]
#[ApiFilter(RangeFilter::class, properties: ['quantity'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified', 'quantity'])]
class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInterface
class ProjectBOMEntry extends AbstractDBElement implements UniqueValidatableInterface, TimeStampableInterface
{
use TimestampTrait;

View File

@@ -30,6 +30,7 @@ use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\TimestampTrait;
use App\Entity\Contracts\TimeStampableInterface;
use App\Repository\UserSystem\ApiTokenRepository;
use App\State\CurrentApiTokenProvider;
use App\State\PartDBInfoProvider;
@@ -54,7 +55,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
provider: CurrentApiTokenProvider::class,
)]
#[ApiFilter(PropertyFilter::class)]
class ApiToken
class ApiToken implements TimeStampableInterface
{
use TimestampTrait;

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\UserSystem;
use App\Entity\Contracts\TimeStampableInterface;
use Doctrine\DBAL\Types\Types;
use App\Entity\Base\TimestampTrait;
use Doctrine\ORM\Mapping as ORM;
@@ -31,7 +32,7 @@ use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'u2f_keys')]
#[ORM\UniqueConstraint(name: 'user_unique', columns: ['user_id', 'key_handle'])]
class U2FKey implements LegacyU2FKeyInterface
class U2FKey implements LegacyU2FKeyInterface, TimeStampableInterface
{
use TimestampTrait;

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Entity\UserSystem;
use App\Entity\Contracts\TimeStampableInterface;
use Doctrine\DBAL\Types\Types;
use App\Entity\Base\TimestampTrait;
use Doctrine\ORM\Mapping as ORM;
@@ -31,7 +32,7 @@ use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'webauthn_keys')]
class WebauthnKey extends BasePublicKeyCredentialSource
class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampableInterface
{
use TimestampTrait;

View File

@@ -24,49 +24,52 @@ namespace App\EntityListeners;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use App\Services\UserSystem\UserCacheKeyGenerator;
use Doctrine\ORM\Event\LifecycleEventArgs;
use App\Services\Cache\ElementCacheTagGenerator;
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 function get_class;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
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\PostPersist]
#[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
if ($element instanceof AbstractStructuralDBElement || $element instanceof LabelProfile) {
$secure_class_name = str_replace('\\', '_', $element::class);
$this->cache->invalidateTags([$secure_class_name]);
//For all changes, we invalidate the cache for all elements of this class
$tags = [$this->tagGenerator->getElementTypeCacheTag($element)];
//Trigger a sidebar reload for all users (see SidebarTreeUpdater service)
if(!$element instanceof LabelProfile) {
$this->cache->invalidateTags(['sidebar_tree_update']);
}
//For changes on structural elements, we also invalidate the sidebar tree
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) {
$secure_class_name = str_replace('\\', '_', $element::class);
$tag = $this->keyGenerator->generateKey($element);
$this->cache->invalidateTags([$tag, $secure_class_name]);
$tags[] = $this->keyGenerator->generateKey($element);
}
/* 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 */
if ($element instanceof Group) {
$tag = 'groups';
$this->cache->invalidateTags([$tag]);
$tags[] = 'groups';
}
//Invalidate the cache for the given tags
$this->cache->invalidateTags($tags);
}
}

View File

@@ -0,0 +1,151 @@
<?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\EventListener;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
/**
* This event listener is called before any console command is executed and should ensure that the webserver
* user is used for all operations (and show a warning if not). This ensures that all files are created with the
* correct permissions.
* If the console is in non-interactive mode, a warning is shown, but the command is still executed.
*/
#[AsEventListener(ConsoleEvents::COMMAND)]
class ConsoleEnsureWebserverUserListener
{
public function __construct(
#[Autowire('%kernel.project_dir%')]
private readonly string $project_root)
{
}
public function __invoke(ConsoleCommandEvent $event): void
{
$input = $event->getInput();
$io = new SymfonyStyle($event->getInput(), $event->getOutput());
//Check if we are (not) running as the webserver user
$webserver_user = $this->getWebserverUser();
$running_user = $this->getRunningUser();
//Check if we are trying to run as root
if ($this->isRunningAsRoot()) {
$io->warning('You are running this command as root. This is not recommended, as it can cause permission problems. Please run this command as the webserver user "'. ($webserver_user ?? '??') . '" instead.');
$io->info('You might have already caused permission problems by running this command as wrong user. If you encounter issues with Part-DB, delete the var/cache directory completely and let it be recreated by Part-DB.');
if ($input->isInteractive() && !$io->confirm('Do you want to continue?', false)) {
$event->disableCommand();
}
return;
}
if ($webserver_user !== null && $running_user !== null && $webserver_user !== $running_user) {
$io->warning('You are running this command as the user "' . $running_user . '". This is not recommended, as it can cause permission problems. Please run this command as the webserver user "' . $webserver_user . '" instead.');
$io->info('You might have already caused permission problems by running this command as wrong user. If you encounter issues with Part-DB, delete the var/cache directory completely and let it be recreated by Part-DB.');
if ($input->isInteractive() && !$io->confirm('Do you want to continue?', false)) {
$event->disableCommand();
}
return;
}
}
private function isRunningAsRoot(): bool
{
//If we are on windows, we can't run as root
if (PHP_OS_FAMILY === 'Windows') {
return false;
}
//Try to use the posix extension if available (Linux)
if (function_exists('posix_geteuid')) {
//Check if the current user is root
return posix_geteuid() === 0;
}
//Otherwise we can't determine the username
return false;
}
/**
* Determines the username of the user who started the current script if possible.
* Returns null if the username could not be determined.
* @return string|null
*/
private function getRunningUser(): ?string
{
//Try to use the posix extension if available (Linux)
if (function_exists('posix_geteuid') && function_exists('posix_getpwuid')) {
$id = posix_geteuid();
$user = posix_getpwuid($id);
//Try to get the username from the posix extension or return the id
return $user['name'] ?? ("ID: " . $id);
}
//Otherwise we can't determine the username
return $_SERVER['USERNAME'] ?? $_SERVER['USER'] ?? null;
}
private function getWebserverUser(): ?string
{
//Determine the webserver user, by checking who owns the uploads/ directory
$path_to_check = $this->project_root . '/uploads/';
//Determine the owner of this directory
if (!is_dir($path_to_check)) {
return null;
}
//If we are on windows we need some special logic
if (PHP_OS_FAMILY === 'Windows') {
//If we have the COM extension available, we can use it to determine the owner
if (extension_loaded('com_dotnet')) {
$su = new \COM("ADsSecurityUtility"); // Call interface
//@phpstan-ignore-next-line
$securityInfo = $su->GetSecurityDescriptor($path_to_check, 1, 1); // Call method
return $securityInfo->owner; // Get file owner
}
//Otherwise we can't determine the owner
return null;
}
//When we are on a POSIX system, we can use the fileowner function
$owner = fileowner($path_to_check);
if (function_exists('posix_getpwuid')) {
$user = posix_getpwuid($owner);
//Try to get the username from the posix extension or return the id
return $user['name'] ?? ("ID: " . $owner);
}
return null;
}
}

View File

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

View File

@@ -48,8 +48,16 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class AttachmentFormType extends AbstractType
{
public function __construct(protected AttachmentManager $attachment_helper, protected UrlGeneratorInterface $urlGenerator, protected Security $security, protected AttachmentSubmitHandler $submitHandler, protected TranslatorInterface $translator, protected bool $allow_attachments_download, protected string $max_file_size)
{
public function __construct(
protected AttachmentManager $attachment_helper,
protected UrlGeneratorInterface $urlGenerator,
protected Security $security,
protected AttachmentSubmitHandler $submitHandler,
protected TranslatorInterface $translator,
protected bool $allow_attachments_download,
protected bool $download_by_default,
protected string $max_file_size
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
@@ -85,7 +93,8 @@ class AttachmentFormType extends AbstractType
'required' => false,
'attr' => [
'data-controller' => 'elements--attachment-autocomplete',
'data-autocomplete' => $this->urlGenerator->generate('typeahead_builtInRessources', ['query' => '__QUERY__']),
'data-autocomplete' => $this->urlGenerator->generate('typeahead_builtInRessources',
['query' => '__QUERY__']),
//Disable browser autocomplete
'autocomplete' => 'off',
],
@@ -132,6 +141,12 @@ class AttachmentFormType extends AbstractType
}
if (!$file instanceof UploadedFile) {
//When no file was uploaded, but a URL was entered, try to determine the attachment name from the URL
if (empty($attachment->getName()) && !empty($attachment->getURL())) {
$name = basename(parse_url($attachment->getURL(), PHP_URL_PATH));
$attachment->setName($name);
}
return;
}
@@ -159,6 +174,30 @@ class AttachmentFormType extends AbstractType
}
}
);
//If the attachment should be downloaded by default (and is download allowed at all), register a listener,
// which sets the downloadURL checkbox to true for new attachments
if ($this->download_by_default && $this->allow_attachments_download) {
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void {
$form = $event->getForm();
$attachment = $form->getData();
if (!$attachment instanceof Attachment && $attachment !== null) {
return;
}
//If the attachment was not created yet, set the downloadURL checkbox to true
if ($attachment === null || $attachment->getId() === null) {
$checkbox = $form->get('downloadURL');
//Ensure that the checkbox is not disabled
if ($checkbox->isDisabled()) {
return;
}
//Set the checkbox
$checkbox->setData(true);
}
});
}
}
public function configureOptions(OptionsResolver $resolver): void

View File

@@ -0,0 +1,88 @@
<?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('visibility', TriStateCheckboxType::class, [
'help' => 'eda_info.visibility.help',
'label' => 'eda_info.visibility',
])
->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,97 @@
<?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('visibility', TriStateCheckboxType::class, [
'help' => 'eda_info.visibility.help',
'label' => 'eda_info.visibility',
])
->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;
use App\Entity\Parts\ManufacturingStatus;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Attachments\PartAttachment;
use App\Entity\EDA\EDAPartInfo;
use App\Entity\Parameters\PartParameter;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\PriceInformations\Orderdetail;
use App\Form\AttachmentFormType;
use App\Form\ParameterType;
use App\Form\Part\EDA\EDAPartInfoType;
use App\Form\Type\MasterPictureAttachmentType;
use App\Form\Type\RichTextEditorType;
use App\Form\Type\SIUnitType;
use App\Form\Type\StructuralEntityType;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\LogSystem\EventCommentNeededHelper;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType;
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\EnumType;
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\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class PartBaseType extends AbstractType
{
@@ -255,6 +255,12 @@ class PartBaseType extends AbstractType
'by_reference' => false,
]);
//EDA info
$builder->add('eda_info', EDAPartInfoType::class, [
'label' => false,
'required' => false,
]);
$builder->add('log_comment', TextType::class, [
'label' => 'edit.log_comment',
'mapped' => false,

View File

@@ -75,7 +75,8 @@ class StructuralEntityChoiceHelper
}
if ($choice instanceof HasMasterAttachmentInterface) {
$tmp['data-image'] = $choice->getMasterPictureAttachment() instanceof Attachment ?
$tmp['data-image'] = ($choice->getMasterPictureAttachment() instanceof Attachment
&& $choice->getMasterPictureAttachment()->isPicture()) ?
$this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(),
'thumbnail_xs')
: null

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

@@ -64,6 +64,11 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
return $this->getPartsCountRecursiveWithDepthN($element, self::RECURSION_LIMIT);
}
public function getPartsRecursive(AbstractPartsContainingDBElement $element): array
{
return $this->getPartsRecursiveWithDepthN($element, self::RECURSION_LIMIT);
}
/**
* The implementation of the recursive function to get the parts count.
* This function is used to limit the recursion depth (remaining_depth is decreased on each call).
@@ -91,6 +96,23 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
return $count;
}
protected function getPartsRecursiveWithDepthN(AbstractPartsContainingDBElement $element, int $remaining_depth): array
{
if ($remaining_depth <= 0) {
throw new \RuntimeException('Recursion limit reached!');
}
//Add direct parts
$parts = $this->getParts($element);
//Then iterate over all children and add their parts
foreach ($element->getChildren() as $child) {
$parts = array_merge($parts, $this->getPartsRecursiveWithDepthN($child, $remaining_depth - 1));
}
return $parts;
}
protected function getPartsByField(object $element, array $order_by, string $field_name): array
{
if (!$element instanceof AbstractPartsContainingDBElement) {

View File

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

View File

@@ -39,6 +39,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* @see \App\Tests\Serializer\PartNormalizerTest
* TODO: Properly rewrite this class to use the SerializerAware interface and dont use the ObjectNormalizer directly
*/
class PartNormalizer implements NormalizerInterface, DenormalizerInterface
{
@@ -65,7 +66,8 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface
public function supportsNormalization($data, string $format = null, array $context = []): bool
{
return $data instanceof Part;
//We only remove the type field for CSV export
return $format === 'csv' && $data instanceof Part ;
}
/**
@@ -86,8 +88,6 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface
unset($data['type']);
}
$data['total_instock'] = $object->getAmountSum();
return $data;
}

View File

@@ -64,7 +64,7 @@ class AttachmentSubmitHandler
'htpasswd', ''];
public function __construct(protected AttachmentPathResolver $pathResolver, protected bool $allow_attachments_downloads,
protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes,
protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes,
protected FileTypeFilterTools $filterTools, /**
* @var string The user configured maximum upload size. This is a string like "10M" or "1G" and will be converted to
*/
@@ -143,19 +143,35 @@ class AttachmentSubmitHandler
{
$base_path = $secure_upload ? $this->pathResolver->getSecurePath() : $this->pathResolver->getMediaPath();
//Ensure the given attachment class is known to mapping
if (!isset($this->folder_mapping[$attachment::class])) {
throw new InvalidArgumentException('The given attachment class is not known! The passed class was: '.$attachment::class);
}
//Ensure the attachment has an assigned element
if (!$attachment->getElement() instanceof AttachmentContainingDBElement) {
throw new InvalidArgumentException('The given attachment is not assigned to an element! An element is needed to generate a path!');
}
//Determine the folder prefix for the given attachment class:
$prefix = null;
//Check if we can use the class name dire
if (isset($this->folder_mapping[$attachment::class])) {
$prefix = $this->folder_mapping[$attachment::class];
} else {
//If not, check for instance of:
foreach ($this->folder_mapping as $class => $folder) {
if ($attachment instanceof $class) {
$prefix = $folder;
break;
}
}
}
//Ensure the given attachment class is known to mapping
if (!$prefix) {
throw new InvalidArgumentException('The given attachment class is not known! The passed class was: '.$attachment::class);
}
//Build path
return
$base_path.DIRECTORY_SEPARATOR //Base path
.$this->folder_mapping[$attachment::class].DIRECTORY_SEPARATOR.$attachment->getElement()->getID();
.$prefix.DIRECTORY_SEPARATOR.$attachment->getElement()->getID();
}
/**
@@ -188,13 +204,22 @@ class AttachmentSubmitHandler
//Rename blacklisted (unsecure) files to a better extension
$this->renameBlacklistedExtensions($attachment);
//Check if we should assign this attachment to master picture
//this is only possible if the attachment is new (not yet persisted to DB)
if ($options['become_preview_if_empty'] && null === $attachment->getID() && $attachment->isPicture()) {
$element = $attachment->getElement();
if ($element instanceof AttachmentContainingDBElement && !$element->getMasterPictureAttachment() instanceof Attachment) {
//Set / Unset the master picture attachment / preview image
$element = $attachment->getElement();
if ($element instanceof AttachmentContainingDBElement) {
//Make this attachment the master picture if needed and this was requested
if ($options['become_preview_if_empty']
&& $element->getMasterPictureAttachment() === null //Element must not have an preview image yet
&& null === $attachment->getID() //Attachment must be null
&& $attachment->isPicture() //Attachment must be a picture
) {
$element->setMasterPictureAttachment($attachment);
}
//If this attachment is the master picture, but is not a picture anymore, dont use it as master picture anymore
if ($element->getMasterPictureAttachment() === $attachment && !$attachment->isPicture()) {
$element->setMasterPictureAttachment(null);
}
}
return $attachment;
@@ -222,7 +247,7 @@ class AttachmentSubmitHandler
//Check if the extension is blacklisted and replace the file extension with txt if needed
if(in_array($ext, self::BLACKLISTED_EXTENSIONS, true)) {
$new_path = $this->generateAttachmentPath($attachment, $attachment->isSecure())
.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'txt');
.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'txt');
//Move file to new directory
$fs = new Filesystem();
@@ -357,7 +382,7 @@ class AttachmentSubmitHandler
//Check if we have an extension given
$pathinfo = pathinfo($filename);
if ($pathinfo['extension'] !== '') {
if (isset($pathinfo['extension']) && $pathinfo['extension'] !== '') {
$new_ext = $pathinfo['extension'];
} else { //Otherwise we have to guess the extension for the new file, based on its content
$new_ext = $this->mimeTypes->getExtensions($this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp';

View File

@@ -135,7 +135,10 @@ class AttachmentURLGenerator
}
//GD can not work with SVG, so serve it directly...
if ('svg' === $attachment->getExtension()) {
//We can not use getExtension here, because it uses the original filename and not the real extension
//Instead we use the logic, which is also used to determine if the attachment is a picture
$extension = pathinfo(parse_url($attachment->getPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
if ('svg' === $extension) {
return $this->assets->getUrl($asset_path);
}

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);
namespace App\Services\UserSystem;
namespace App\Services\Cache;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use App\Entity\UserSystem\User;
use Locale;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**

View File

@@ -0,0 +1,344 @@
<?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\EDA;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Part;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
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,
/** The maximum level of the shown categories. 0 Means only the top level categories are shown. -1 means only a single one containing */
private readonly int $category_depth,
) {
}
/**
* 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_' . $this->category_depth, function (ItemInterface $item) {
//Invalidate the cache on category changes
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class);
$item->tag($secure_class_name);
//If the category depth is smaller than 0, create only one dummy category
if ($this->category_depth < 0) {
return [
[
'id' => '0',
'name' => 'Part-DB',
]
];
}
//Otherwise just get the categories and filter them
$categories = $this->nodesListBuilder->typeToNodesList(Category::class);
$repo = $this->em->getRepository(Category::class);
$result = [];
foreach ($categories as $category) {
//Skip invisible categories
if ($category->getEdaInfo()->getVisibility() === false) {
continue;
}
//Skip categories with a depth greater than the configured one
if ($category->getLevel() > $this->category_depth) {
continue;
}
//Ensure that the category contains parts
//For the last level, we need to use a recursive query, otherwise we can use a simple query
/** @var Category $category */
$parts_count = $category->getLevel() >= $this->category_depth ? $repo->getPartsCountRecursive($category) : $repo->getPartsCount($category);
if ($parts_count < 1) {
continue;
}
//Check if the category should be visible
if (!$this->shouldCategoryBeVisible($category)) {
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|null $category
* @return array
*/
public function getCategoryParts(?Category $category): array
{
return $this->kicadCache->get('kicad_category_parts_'.($category?->getID() ?? 0) . '_' . $this->category_depth,
function (ItemInterface $item) use ($category) {
$item->tag([
$this->tagGenerator->getElementTypeCacheTag(Category::class),
$this->tagGenerator->getElementTypeCacheTag(Part::class),
//Visibility can change based on the footprint
$this->tagGenerator->getElementTypeCacheTag(Footprint::class)
]);
if ($this->category_depth >= 0) {
//Ensure that the category is set
if (!$category) {
throw new NotFoundHttpException('Category must be set, if category_depth is greater than 1!');
}
$category_repo = $this->em->getRepository(Category::class);
if ($category->getLevel() >= $this->category_depth) {
//Get all parts for the category and its children
$parts = $category_repo->getPartsRecursive($category);
} else {
//Get only direct parts for the category (without children), as the category is not collapsed
$parts = $category_repo->getParts($category);
}
} else {
//Get all parts
$parts = $this->em->getRepository(Part::class)->findAll();
}
$result = [];
foreach ($parts as $part) {
//If the part is invisible, then skip it
if (!$this->shouldPartBeVisible($part)) {
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;
}
/**
* Determine if the given part should be visible for the EDA.
* @param Category $category
* @return bool
*/
private function shouldCategoryBeVisible(Category $category): bool
{
$eda_info = $category->getEdaInfo();
//If the category visibility is explicitly set, then use it
if ($eda_info->getVisibility() !== null) {
return $eda_info->getVisibility();
}
//try to check if the fields were set
if ($eda_info->getKicadSymbol() !== null
|| $eda_info->getReferencePrefix() !== null) {
return true;
}
//Check if there is any part in this category, which should be visible
$category_repo = $this->em->getRepository(Category::class);
if ($category->getLevel() >= $this->category_depth) {
//Get all parts for the category and its children
$parts = $category_repo->getPartsRecursive($category);
} else {
//Get only direct parts for the category (without children), as the category is not collapsed
$parts = $category_repo->getParts($category);
}
foreach ($parts as $part) {
if ($this->shouldPartBeVisible($part)) {
return true;
}
}
//Otherwise the category should be not visible
return false;
}
/**
* Determine if the given part should be visible for the EDA.
* @param Part $part
* @return bool
*/
private function shouldPartBeVisible(Part $part): bool
{
$eda_info = $part->getEdaInfo();
$category = $part->getCategory();
//If the user set a visibility, then use it
if ($eda_info->getVisibility() !== null) {
return $part->getEdaInfo()->getVisibility();
}
//If the part has a category, then use the category visibility if possible
if ($category && $category->getEdaInfo()->getVisibility() !== null) {
return $category->getEdaInfo()->getVisibility();
}
//If both are null, then we try to determine the visibility based on if fields are set
if ($eda_info->getKicadSymbol() !== null
|| $eda_info->getKicadFootprint() !== null
|| $eda_info->getReferencePrefix() !== null
|| $eda_info->getValue() !== null) {
return true;
}
//Check also if the fields are set for the category (if it exists)
if ($category && (
$category->getEdaInfo()->getKicadSymbol() !== null
|| $category->getEdaInfo()->getReferencePrefix() !== null
)) {
return true;
}
//And on the footprint
if ($part->getFootprint() && $part->getFootprint()->getEdaInfo()->getKicadFootprint() !== null) {
return true;
}
//Otherwise the part should be not visible
return false;
}
/**
* 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

@@ -186,7 +186,8 @@ final class DTOtoEntityConverter
}
//Add other images
foreach ($dto->images ?? [] as $image) {
$images = $this->files_unique($dto->images ?? []);
foreach ($images as $image) {
//Ensure that the image is not the same as the preview image
if ($image->url === $dto->preview_image_url) {
continue;
@@ -195,10 +196,10 @@ final class DTOtoEntityConverter
$entity->addAttachment($this->convertFile($image, $image_type));
}
//Add datasheets
$datasheet_type = $this->getDatasheetType();
foreach ($dto->datasheets ?? [] as $datasheet) {
$datasheets = $this->files_unique($dto->datasheets ?? []);
foreach ($datasheets as $datasheet) {
$entity->addAttachment($this->convertFile($datasheet, $datasheet_type));
}
@@ -210,6 +211,27 @@ final class DTOtoEntityConverter
return $entity;
}
/**
* Returns the given array of files with all duplicates removed.
* @param FileDTO[] $files
* @return FileDTO[]
*/
private function files_unique(array $files): array
{
$unique = [];
//We use the URL and name as unique identifier. If two file DTO have the same URL and name, they are considered equal
//and get filtered out, if it already exists in the array
foreach ($files as $file) {
//Skip already existing files, to preserve the order. The second condition ensure that we keep the version with a name over the one without a name
if (isset($unique[$file->url]) && $unique[$file->url]->name !== null) {
continue;
}
$unique[$file->url] = $file;
}
return array_values($unique);
}
/**
* Get the existing entity of the given class with the given name or create it if it does not exist.
* If the name is null, null is returned.

View File

@@ -44,15 +44,20 @@ namespace App\Services\LabelSystem;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\LabelSystem\LabelSupportedElement;
use App\Repository\LabelProfileRepository;
use App\Services\UserSystem\UserCacheKeyGenerator;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
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);
}
$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;
/** @var LabelProfileRepository $repo */

View File

@@ -88,7 +88,7 @@ class ParameterExtractor
protected function stringToParam(string $input, string $class): ?AbstractParameter
{
$input = trim($input);
$regex = '/^(.*) *(?:=|:) *(.+)/u';
$regex = '/^(.*) *(?:=|:)(?!\/) *(.+)/u';
$matches = [];
preg_match($regex, $input, $matches);

View File

@@ -1,71 +0,0 @@
<?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;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
/**
* Workaround for using snake_case properties with ReflectionExtractor until this PR is merged:
* https://github.com/symfony/symfony/pull/51697
*/
#[AsTaggedItem('property_info.access_extractor', priority: 0)]
class SnakeCasePropertyAccessExtractor implements PropertyAccessExtractorInterface
{
public function __construct(#[Autowire(service: 'property_info.reflection_extractor')]
private readonly PropertyAccessExtractorInterface $reflectionExtractor)
{
//$this->reflectionExtractor = new ReflectionExtractor();
}
public function isReadable(string $class, string $property, array $context = []): ?bool
{
//Null means skip this extractor
return null;
}
/**
* Camelizes a given string.
*/
private function camelize(string $string): string
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
}
public function isWritable(string $class, string $property, array $context = []): ?bool
{
//Check writeablity using a camelized property name
$isWriteable = $this->reflectionExtractor->isWritable($class, $this->camelize($property), $context);
//If we found a writeable property that way, return true
if ($isWriteable === true) {
return true;
}
//Otherwise skip this extractor
return null;
}
}

View File

@@ -22,14 +22,13 @@ declare(strict_types=1);
namespace App\Services\Trees;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\AttachmentContainingDBElementRepository;
use App\Repository\DBElementRepository;
use App\Repository\StructuralDBElementRepository;
use App\Services\UserSystem\UserCacheKeyGenerator;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
@@ -40,8 +39,12 @@ use Symfony\Contracts\Cache\TagAwareCacheInterface;
*/
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,
) {
}
/**
@@ -50,9 +53,9 @@ class NodesListBuilder
*
* @template T of AbstractDBElement
*
* @param string $class_name the class name of the entity you want to retrieve
* @param string $class_name the class name of the entity you want to retrieve
* @phpstan-param class-string<T> $class_name
* @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root
* @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root
*
* @return AbstractDBElement[] a flattened list containing the tree elements
* @phpstan-return list<T>
@@ -86,7 +89,7 @@ class NodesListBuilder
{
$parent_id = $parent instanceof AbstractStructuralDBElement ? $parent->getID() : '0';
// 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;
return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) {
@@ -105,7 +108,7 @@ class NodesListBuilder
* The value is cached for performance reasons.
*
* @template T of AbstractStructuralDBElement
* @param T $element
* @param T $element
* @return AbstractStructuralDBElement[]
*
* @phpstan-return list<T>

View File

@@ -22,9 +22,7 @@ declare(strict_types=1);
namespace App\Services\Trees;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Attachments\AttachmentType;
use App\Entity\ProjectSystem\Project;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@@ -34,10 +32,12 @@ use App\Entity\Parts\Part;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\ProjectSystem\Project;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
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\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

View File

@@ -25,17 +25,18 @@ namespace App\Services\Trees;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\ProjectSystem\Project;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\ProjectSystem\Project;
use App\Helpers\Trees\TreeViewNode;
use App\Helpers\Trees\TreeViewNodeIterator;
use App\Repository\StructuralDBElementRepository;
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use App\Services\EntityURLGenerator;
use App\Services\UserSystem\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use RecursiveIteratorIterator;
@@ -51,25 +52,37 @@ use function count;
*/
class TreeViewGenerator
{
public function __construct(protected EntityURLGenerator $urlGenerator, protected EntityManagerInterface $em, protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator, protected TranslatorInterface $translator, private UrlGeneratorInterface $router,
protected bool $rootNodeExpandedByDefault, protected bool $rootNodeEnabled)
{
public function __construct(
protected EntityURLGenerator $urlGenerator,
protected EntityManagerInterface $em,
protected TagAwareCacheInterface $cache,
protected ElementCacheTagGenerator $tagGenerator,
protected UserCacheKeyGenerator $keyGenerator,
protected TranslatorInterface $translator,
private UrlGeneratorInterface $router,
protected bool $rootNodeExpandedByDefault,
protected bool $rootNodeEnabled,
) {
}
/**
* Gets a TreeView list for the entities of the given class.
*
* @param string $class The class for which the treeView should be generated
* @param AbstractStructuralDBElement|null $parent The root nodes in the tree should have this element as parent (use null, if you want to get all entities)
* @param string $mode The link type that will be generated for the hyperlink section of each node (see EntityURLGenerator for possible values).
* @param string $class The class for which the treeView should be generated
* @param AbstractStructuralDBElement|null $parent The root nodes in the tree should have this element as parent (use null, if you want to get all entities)
* @param string $mode The link type that will be generated for the hyperlink section of each node (see EntityURLGenerator for possible values).
* Set to empty string, to disable href field.
* @param AbstractDBElement|null $selectedElement The element that should be selected. If set to null, no element will be selected.
* @param AbstractDBElement|null $selectedElement The element that should be selected. If set to null, no element will be selected.
*
* @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 = [];
$href_type = $mode;
@@ -110,11 +123,11 @@ class TreeViewGenerator
}
if ($item->getNodes() !== null && $item->getNodes() !== []) {
$item->addTag((string) count($item->getNodes()));
$item->addTag((string)count($item->getNodes()));
}
if ($href_type !== '' && null !== $item->getId()) {
$entity = $this->em->getPartialReference($class, $item->getId());
$entity = $this->em->find($class, $item->getId());
$item->setHref($this->urlGenerator->getURL($entity, $href_type));
}
@@ -155,12 +168,12 @@ class TreeViewGenerator
{
$icon = "fa-fw fa-treeview fa-solid ";
return match ($class) {
Category::class => $icon . 'fa-tags',
StorageLocation::class => $icon . 'fa-cube',
Footprint::class => $icon . 'fa-microchip',
Manufacturer::class => $icon . 'fa-industry',
Supplier::class => $icon . 'fa-truck',
Project::class => $icon . 'fa-archive',
Category::class => $icon.'fa-tags',
StorageLocation::class => $icon.'fa-cube',
Footprint::class => $icon.'fa-microchip',
Manufacturer::class => $icon.'fa-industry',
Supplier::class => $icon.'fa-truck',
Project::class => $icon.'fa-archive',
default => null,
};
}
@@ -170,8 +183,8 @@ class TreeViewGenerator
* Gets a tree of TreeViewNode elements. The root elements has $parent as parent.
* The treeview is generic, that means the href are null and ID values are set.
*
* @param string $class The class for which the tree should be generated
* @param AbstractStructuralDBElement|null $parent the parent the root elements should have
* @param string $class The class for which the tree should be generated
* @param AbstractStructuralDBElement|null $parent the parent the root elements should have
*
* @return TreeViewNode[]
*/
@@ -192,13 +205,12 @@ class TreeViewGenerator
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;
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
$item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]);
return $repo->getGenericNodeTree($parent);
});
}

View File

@@ -20,9 +20,9 @@ declare(strict_types=1);
* 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/>.
*/
namespace App\Services\UserSystem;
use Imagine\Exception\RuntimeException;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\UserAttachment;
@@ -30,16 +30,23 @@ use App\Entity\UserSystem\User;
use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\Attachments\AttachmentURLGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Liip\ImagineBundle\Service\FilterService;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class UserAvatarHelper
{
public const IMG_DEFAULT_AVATAR_PATH = '/img/default_avatar.png';
/**
* Path to the default avatar image (must not start with a slash, or the asset package will not work)
*/
public const IMG_DEFAULT_AVATAR_PATH = 'img/default_avatar.svg';
public function __construct(private readonly bool $use_gravatar, private readonly Packages $packages, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly FilterService $filterService, private readonly EntityManagerInterface $entityManager, private readonly AttachmentSubmitHandler $submitHandler)
{
public function __construct(
private readonly bool $use_gravatar,
private readonly Packages $packages,
private readonly AttachmentURLGenerator $attachmentURLGenerator,
private readonly EntityManagerInterface $entityManager,
private readonly AttachmentSubmitHandler $submitHandler
) {
}
@@ -78,13 +85,8 @@ class UserAvatarHelper
return $this->getGravatar($user, 50); //50px wide picture
}
try {
//Otherwise we can serve the relative path via Asset component
return $this->filterService->getUrlOfFilteredImage(self::IMG_DEFAULT_AVATAR_PATH, 'thumbnail_xs');
} catch (RuntimeException) {
//If the filter fails, we can not serve the thumbnail and fall back to the original image and log an warning
return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH);
}
//Otherwise serve the default image (its an SVG, so we dont need to thumbnail it)
return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH);
}
public function getAvatarMdURL(User $user): string
@@ -100,20 +102,15 @@ class UserAvatarHelper
return $this->getGravatar($user, 150);
}
try {
//Otherwise we can serve the relative path via Asset component
return $this->filterService->getUrlOfFilteredImage(self::IMG_DEFAULT_AVATAR_PATH, 'thumbnail_xs');
} catch (RuntimeException) {
//If the filter fails, we can not serve the thumbnail and fall back to the original image and log an warning
return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH);
}
//Otherwise serve the default image (its an SVG, so we dont need to thumbnail it)
return $this->packages->getUrl(self::IMG_DEFAULT_AVATAR_PATH);
}
/**
* Get either a Gravatar URL or complete image tag for a specified email address.
*
* @param User $user The user for which the gravator should be generated
* @param User $user The user for which the gravator should be generated
* @param int $s Size in pixels, defaults to 80px [ 1 - 2048 ]
* @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
* @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
@@ -131,7 +128,7 @@ class UserAvatarHelper
$url = 'https://www.gravatar.com/avatar/';
$url .= md5(strtolower(trim($email)));
return $url . "?s=$s&d=$d&r=$r";
return $url."?s=$s&d=$d&r=$r";
}
/**

View File

@@ -6,12 +6,12 @@
"version": "v1.6.1"
},
"api-platform/core": {
"version": "3.1",
"version": "3.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.1",
"ref": "adf0c75f4bed8b0043a6680376323404953578c5"
"version": "3.2",
"ref": "696d44adc3c0d4f5d25a2f1c4f3700dd8a5c6db9"
},
"files": [
"config/packages/api_platform.yaml",
@@ -87,12 +87,12 @@
"version": "v0.5.3"
},
"doctrine/doctrine-bundle": {
"version": "2.10",
"version": "2.11",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.10",
"ref": "e025a6cb69b195970543820b2f18ad21724473fa"
"ref": "0db4b12b5df45f5122213b4ecd18733ab7fa7d53"
},
"files": [
"config/packages/doctrine.yaml",
@@ -363,10 +363,10 @@
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.0",
"ref": "f10b8054de1a94a3b9e8806a6453fd5c98491c44"
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
},
"files": [
"./phpstan.dist.neon"
"phpstan.dist.neon"
]
},
"phpstan/phpstan-doctrine": {
@@ -559,12 +559,12 @@
"version": "v4.2.3"
},
"symfony/framework-bundle": {
"version": "6.2",
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.2",
"ref": "af47254c5e4cd543e6af3e4508298ffebbdaddd3"
"version": "6.4",
"ref": "a91c965766ad3ff2ae15981801643330eb42b6a5"
},
"files": [
"config/packages/cache.yaml",
@@ -638,12 +638,12 @@
"version": "v5.3.8"
},
"symfony/phpunit-bridge": {
"version": "6.3",
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "01dfaa98c58f7a7b5a9b30e6edb7074af7ed9819"
"ref": "1f5830c331065b6e4c9d5fa2105e322d29fcd573"
},
"files": [
".env.test",
@@ -705,15 +705,16 @@
"version": "v5.3.4"
},
"symfony/security-bundle": {
"version": "6.2",
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.0",
"ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48"
"version": "6.4",
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
},
"files": [
"config/packages/security.yaml"
"config/packages/security.yaml",
"config/routes/security.yaml"
]
},
"symfony/security-core": {
@@ -752,16 +753,16 @@
"version": "v5.1.0"
},
"symfony/translation": {
"version": "5.3",
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.3",
"ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43"
"branch": "main",
"version": "6.3",
"ref": "64fe617084223633e1dedf9112935d8c95410d3e"
},
"files": [
"./config/packages/translation.yaml",
"./translations/.gitignore"
"config/packages/translation.yaml",
"translations/.gitignore"
]
},
"symfony/translation-contracts": {
@@ -848,18 +849,15 @@
]
},
"symfony/webpack-encore-bundle": {
"version": "1.17",
"version": "2.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.10",
"ref": "eff2e505d4557c967b6710fe06bd947ba555cae5"
"version": "2.0",
"ref": "082d754b3bd54b3fc669f278f1eea955cfd23cf5"
},
"files": [
"assets/app.js",
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/hello_controller.js",
"assets/styles/app.css",
"config/packages/webpack_encore.yaml",
"package.json",
@@ -909,13 +907,17 @@
"version": "3.5.1"
},
"web-auth/webauthn-symfony-bundle": {
"version": "3.3",
"version": "4.7",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "3.0",
"ref": "9926090a80c2cceeffe96e6c3312b397ea55d4a7"
}
"ref": "a5dff33bd46575bea263af94069650af7742dcb6"
},
"files": [
"config/packages/webauthn.yaml",
"config/routes/webauthn_routes.yaml"
]
},
"webmozart/assert": {
"version": "1.4.0"

View File

@@ -6,6 +6,7 @@
{% block additional_controls %}
{{ form_row(form.filetype_filter) }}
{{ form_row(form.alternative_names) }}
{% endblock %}
{% block edit_title %}

View File

@@ -7,6 +7,7 @@
{% 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_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 %}
{% block edit_title %}
@@ -34,4 +35,29 @@
{{ form_row(form.default_description) }}
{{ form_row(form.default_comment) }}
</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.visibility) }}
</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 %}

View File

@@ -19,4 +19,19 @@
{% block additional_controls %}
{{ form_row(form.alternative_names) }}
{% 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

@@ -1,5 +1,5 @@
{% macro datatable(datatable, controller = 'elements/datatables/datatables', state_save_tag = null) %}
<div {{ stimulus_controller(controller, {"stateSaveTag": state_save_tag}) }} data-dt-settings='{{ datatable_settings(datatable)|escape('html_attr') }}' data-dt-url="{{ app.request.requestUri }}">
<div {{ stimulus_controller(controller, {"stateSaveTag": state_save_tag}) }} data-dt-settings='{{ datatable_settings(datatable)|escape('html_attr') }}' data-dt-url="{{ app.request.baseUrl ~ app.request.requestUri }}">
<div {{ stimulus_target(controller, 'dt') }}>
<div class="card-body">
<div class="card">
@@ -25,7 +25,7 @@
data-delete-message="{% trans %}part_list.action.delete-message{% endtrans %}">
<input type="hidden" name="_token" value="{{ csrf_token('table_action') }}">
<input type="hidden" name="redirect_back" value="{{ app.request.requestUri }}">
<input type="hidden" name="redirect_back" value="{{ app.request.baseUrl ~ app.request.requestUri }}">
<input type="hidden" name="ids" {{ stimulus_target('elements/datatables/parts', 'selectIDs') }} value="">

Some files were not shown because too many files have changed in this diff Show More