Compare commits

...

237 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
Jan Böhmer
76295b73c8 Bumped to release 1.9.0 2023-11-25 20:07:51 +01:00
Jan Böhmer
8c00769757 Merge remote-tracking branch 'origin/l10n_master' 2023-11-25 20:07:17 +01:00
Jan Böhmer
773d0e9d20 New translations messages.en.xlf (English) 2023-11-25 20:05:34 +01:00
Jan Böhmer
d14f596479 New translations messages.en.xlf (German) 2023-11-25 20:05:23 +01:00
Jan Böhmer
700ed42ce5 Removed unused translation 2023-11-25 20:00:08 +01:00
Jan Böhmer
dc2369c71e Remove additional colon in translation 2023-11-25 19:42:32 +01:00
Jan Böhmer
5fc760f6ad Run phpunit tests against PHP 8.3 2023-11-25 19:42:05 +01:00
Jan Böhmer
ffb5d3e790 Added an checkbox in the withdrawal/move dialog, that if checked automatically deletes a part lot if it becomes empty during the operation
Fixes issue #327
2023-11-25 19:38:21 +01:00
Jan Böhmer
512947e0d0 New translations messages.en.xlf (English) 2023-11-25 19:20:46 +01:00
Jan Böhmer
9e69a09a19 Fixed translation of stock change type in log table 2023-11-25 19:16:54 +01:00
Jan Böhmer
b447a69dae Allow to specify an informational field during a part stock operation, where the user can specify, when this operation was really performed
Fixes issue #416
2023-11-25 19:10:18 +01:00
Jan Böhmer
d52e6b5881 New translations messages.en.xlf (German) 2023-11-25 14:00:18 +01:00
Jan Böhmer
6cff19358a Dont show the permission reset warning during database migration anymore
This only affects database which were older than the new permission system  and hopefully everybody should have updated by now. The message could irritate new users, so it was removed
2023-11-25 13:21:39 +01:00
Jan Böhmer
a6d476f953 New translations messages.en.xlf (German) 2023-11-25 13:00:19 +01:00
Jan Böhmer
aba73174ab Fixed wrong link in docs. 2023-11-25 01:48:32 +01:00
Jan Böhmer
83d43d931c Added a SAML_BEHIND_PROXY env, which must be set if Part-DB is behind a reverse proxy when using SAML
The php-saml library use its own logic to determine the current path, and we need to set the use_proxy_vars so that  it respects the proxy FORWARDED headers.

This should fix issue #436
2023-11-25 01:45:08 +01:00
Jan Böhmer
64cebaba77 New translations messages.en.xlf (English) 2023-11-25 01:20:54 +01:00
Jan Böhmer
07535c26a6 Fixed static analysis issue 2023-11-25 01:19:48 +01:00
Jan Böhmer
aab1dcf8e6 Fixed APIPlatform test issues which were introduced with the upgrade to api platform core v3.2.6 2023-11-25 01:17:35 +01:00
Jan Böhmer
4b88de9316 Show a notice above the permission editor, that permissions can depend on each other
Related to issue #435
2023-11-25 00:42:17 +01:00
Jan Böhmer
84c111ac7c Show a warning flash message, if permissions were corrected and missing permissions were set
Related to issue #435
2023-11-25 00:36:31 +01:00
Jan Böhmer
2feeb1c868 New translations messages.en.xlf (English) 2023-11-25 00:01:18 +01:00
Jan Böhmer
17000da97e Updated dependencies 2023-11-24 23:55:21 +01:00
Jan Böhmer
5b09cbf1ac Merge branch 'merge_system' 2023-11-24 23:51:35 +01:00
Jan Böhmer
07088c94e7 Implemented logic for not (yet) used EntityMerger service 2023-11-24 23:48:39 +01:00
Jan Böhmer
1da5e7ccd7 Properly merge the PartAssociations pointing towards the parts 2023-11-24 23:36:09 +01:00
Jan Böhmer
b9956e38b8 Dont concat if one of the strings is empty during part merge 2023-11-24 23:16:26 +01:00
Jan Böhmer
36879dd7da Test merge and update from info provider controller endpoints 2023-11-24 23:13:15 +01:00
Jan Böhmer
099ea63740 Show a notice flash to remember user to review changes 2023-11-24 19:45:15 +01:00
Jan Böhmer
615defa84a Give the update part from info provider menu a own design 2023-11-24 19:35:44 +01:00
Jan Böhmer
3eeeb01ad1 Added possibility to search the info providers to update an existing part 2023-11-24 19:28:30 +01:00
Jan Böhmer
73f6d79925 Added an modal form on the part info page, to merge a part into another one 2023-11-22 22:50:25 +01:00
Jan Böhmer
b0f5d9b55f Added an form for merging two parts together 2023-11-22 20:11:38 +01:00
Jan Böhmer
50069c7611 Fixed tests 2023-11-22 17:14:24 +01:00
Jan Böhmer
c86694ab8f Merge the remaining fields of a Part 2023-11-21 19:41:18 +01:00
Jan Böhmer
478d5e2a3a Merge tags and bool fields of parts 2023-11-21 00:18:10 +01:00
Jan Böhmer
e7b766906d Try to avoid duplications during merging of parts and merge more fields. 2023-11-21 00:02:17 +01:00
Jan Böhmer
c5435df6f9 Add a blue dot to the collection type delete buttons in forms to indicate that this element was not yet saved to DB yet 2023-11-20 23:40:14 +01:00
Jan Böhmer
e8f4cd9fec Merge remote-tracking branch 'origin/l10n_master' 2023-11-20 00:08:20 +01:00
Jan Böhmer
378d695a24 New translations validators.en.xlf (German) 2023-11-20 00:06:55 +01:00
Jan Böhmer
a4b16f7f09 New translations messages.en.xlf (German) 2023-11-20 00:06:54 +01:00
Jan Böhmer
1fe3a614c9 New translations validators.en.xlf (German) 2023-11-20 00:01:40 +01:00
Jan Böhmer
773e393f55 New translations messages.en.xlf (German) 2023-11-20 00:01:39 +01:00
Jan Böhmer
87626589a3 Added very basic controller to merge info provider data into the part 2023-11-19 23:47:46 +01:00
Jan Böhmer
01784a9d1f Started implementing building blocks for the merge system 2023-11-19 23:17:48 +01:00
Jan Böhmer
f99323f9b3 New translations messages.en.xlf (English) 2023-11-19 22:22:04 +01:00
Jan Böhmer
83ad99215f Added optional "stocked amount" and storage locations columns for the BOM list
This fixes issue #429
2023-11-19 22:13:25 +01:00
Jan Böhmer
958d59a0ff Save search setting checkboxes state to localStorage to persist it
Related to issue #424
2023-11-19 21:49:16 +01:00
Jan Böhmer
de8a68c70d Expliticly declare a conversion from the custom TinyInt doctrine type to int
This hopefully fixes issue #434
2023-11-19 21:03:43 +01:00
Jan Böhmer
5f87d5b1ac Updated dependencies 2023-11-19 21:02:43 +01:00
Jan Böhmer
c2ea880dad New translations validators.en.xlf (Italian) 2023-11-18 09:40:18 +01:00
Jan Böhmer
7eba4254e6 New translations messages.en.xlf (Italian) 2023-11-18 09:40:17 +01:00
Jan Böhmer
76bb3eae9d New translations messages.en.xlf (Italian) 2023-11-18 08:40:18 +01:00
Jan Böhmer
3da656c08b Fixed (false positive) test in APIDocsAvailabilityTest
API Platform seems to have slightly changed the serialization process of errors and the requested HTML format was not available for error serialization
2023-11-17 23:59:32 +01:00
Jan Böhmer
b6dc3eb1a2 New translations validators.en.xlf (English) 2023-11-17 23:51:17 +01:00
Jan Böhmer
fefa65941b New translations messages.en.xlf (English) 2023-11-17 23:51:16 +01:00
Jan Böhmer
74d75c6e1f Upgraded dependencies 2023-11-17 23:44:28 +01:00
Jan Böhmer
01ed3eeecd Merge branch 'part_associations' 2023-11-17 23:37:26 +01:00
Jan Böhmer
9a3b9b84bc Fixed deprecation 2023-11-17 23:32:41 +01:00
Jan Böhmer
90a1ffa2ac Fixed issues with wrong Groups attribute definition 2023-11-17 23:30:36 +01:00
Jan Böhmer
5442aa5e07 Added test for vendor barcode scanner 2023-11-17 23:29:06 +01:00
Jan Böhmer
0ab604d468 Added migrations for sqlite 2023-11-17 23:26:45 +01:00
Jan Böhmer
0b178b46f2 Allow to scan barcodes, whose content where defined in the vendor_barcode field 2023-11-17 23:23:54 +01:00
Jan Böhmer
d12bde2b1e Reveal the invalid field in a collapse if a validation error occurs 2023-11-17 23:15:18 +01:00
Jan Böhmer
96a771e7ac Allow to edit the vendor_barcode field of a PartLot 2023-11-17 23:05:47 +01:00
Jan Böhmer
3e6b80d1cf Added possibility to access PartAssociations via API 2023-11-17 22:12:19 +01:00
Jan Böhmer
4d7d196a3c Added some documentation and tests to PartAssociations 2023-11-17 18:36:49 +01:00
Jan Böhmer
4e1f6277c6 Added a translation for the part association in LogFilterType 2023-11-17 18:28:24 +01:00
Jan Böhmer
626c4dd5d6 Use a custom delete confirmation message for part associations 2023-11-16 22:38:01 +01:00
Jan Böhmer
c8bd800b9f Hide the custom type input field if not needed to improve the UX 2023-11-16 22:33:31 +01:00
d-buchmann
0fa03d8bb0 Associate project BOM entries with correct project when cloning (#433) 2023-11-16 12:56:42 +01:00
Jan Böhmer
22606f01d2 New translations messages.en.xlf (Chinese Simplified) 2023-11-16 10:10:33 +01:00
Jan Böhmer
3c2e535117 New translations messages.en.xlf (Chinese Simplified) 2023-11-16 09:10:25 +01:00
Jan Böhmer
7f612bc371 Added ability to view part relations on a part info page 2023-11-15 00:44:45 +01:00
Jan Böhmer
cc2332a83a Added some constraints to ensure validity of the PartAssociation entities 2023-11-14 23:50:09 +01:00
Jan Böhmer
c7892cb9e2 Added a seperate field for specifying the other relation 2023-11-14 23:35:18 +01:00
Jan Böhmer
5bd2d9b344 Restrict size of part images, if the part selector element is very wide
This improves UX
2023-11-13 23:05:25 +01:00
Jan Böhmer
81f8b365e9 Improved the association edit panel 2023-11-13 23:01:59 +01:00
Jan Böhmer
8ab9cf1417 Added very basic possibility to add an association 2023-11-13 00:11:58 +01:00
Jan Böhmer
b7cfdebad5 Added data field for vendor PartLot barcodes 2023-11-12 22:06:05 +01:00
Jan Böhmer
0447a7e6b3 Added basic data structures for part associations 2023-11-12 21:53:45 +01:00
Jan Böhmer
6d67ee8106 New translations security.en.xlf (English) 2023-11-12 00:40:45 +01:00
Jan Böhmer
2d7058329c New translations validators.en.xlf (English) 2023-11-12 00:40:44 +01:00
Jan Böhmer
9e58baa574 New translations messages.en.xlf (English) 2023-11-12 00:40:43 +01:00
Jan Böhmer
6d8cb9cc08 New translations messages.en.xlf (French) 2023-11-12 00:40:28 +01:00
Jan Böhmer
5cfccab671 Allow to scan IPN barcodes using the built in barcode scanner
This improves issue #373
2023-11-12 00:36:13 +01:00
Jan Böhmer
3953e36921 Include the ipv6 localhost in default TRUSTED_PROXIES
Related to issue #428
2023-11-11 23:22:30 +01:00
Jan Böhmer
7163df6d46 Fixed paths generated using app.request.requestUri when in a reverse proxy with a subpath
Related to issue #428
2023-11-11 23:14:22 +01:00
Jan Böhmer
5f86253b94 Add documentation on how to put Part-DB into a subpath of a reverse proxy
This fixes issue #428
2023-11-11 23:03:29 +01:00
Jan Böhmer
93d0f97cfd Updated dependencies 2023-11-11 18:35:49 +01:00
Jan Böhmer
9732b71f85 Fixed other placeholders besides the IPN Barcodes 2023-11-05 21:42:39 +01:00
Jan Böhmer
cf11320789 Keep query parameters when switching language via navbar 2023-11-05 21:32:45 +01:00
Jan Böhmer
5e326bca12 Added label placeholders to add a barcode version of the IPN to a label
This fixes issue #373
2023-11-05 21:24:21 +01:00
Jan Böhmer
3c52e57a44 Updated dependencies 2023-11-04 20:44:32 +01:00
Jan Böhmer
2002b9d5d3 Merge remote-tracking branch 'origin/master' 2023-11-03 23:07:58 +01:00
Jan Böhmer
323c70393d Use demo.part-db.de in links to demo as this now has TLS encryption too. 2023-11-03 23:07:51 +01:00
Jan Böhmer
eabd03dc53 Fixed variable access curly brackets deprecations in migrations 2023-11-03 23:04:55 +01:00
Jan Böhmer
3ac82cf76a The frontend dependencies can now detect their path automatically.
Therefore it does not need to be configured in the webpack.config.js before compilation. This should help to make things like issue #426 easier to resolve.
2023-11-03 23:01:23 +01:00
Pyromane
1409d19922 Update installation_docker.md (#425)
For the database additionally "restart: unless-stopped" is specified, so that this is also available after a restart of the host.
2023-10-31 13:32:43 +01:00
Jan Böhmer
bdcd51d533 Refactored barcode scan functions
This is preparatory work for issue #373
2023-10-26 22:23:43 +02:00
Jan Böhmer
563edb1731 Added ipn to possible columns in project BOM view
This fixes issue #418
2023-10-26 17:55:03 +02:00
dependabot[bot]
cd7013f776 Bump actions/setup-node from 3 to 4 (#420)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  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-10-26 17:44:42 +02:00
Jan Böhmer
783a00ca2f Added test for price related quantity in DTOToEntityConverter 2023-10-26 17:41:26 +02:00
Jan Böhmer
e233940f1f Merge remote-tracking branch 'd-buchmann/d-buchmann-patch-1' 2023-10-26 17:38:10 +02:00
Jan Böhmer
717a9fb0a3 Fixed API Platform deprecations 2023-10-25 20:40:41 +02:00
buchmann
5144b75ed7 Fix image type alt name if newly generated 2023-10-25 15:56:40 +02:00
buchmann
aeed7c0802 fix line breaks 2023-10-25 14:29:18 +02:00
buchmann
2b470e6cdd Merge commit 'f8ccd5bc22bbc25188077dc20f0049f7068852ea' into d-buchmann-patch-1 2023-10-25 14:10:46 +02:00
d-buchmann
e6870c61ee Update DTOtoEntityConverter.php 2023-10-25 14:01:46 +02:00
d-buchmann
f8ccd5bc22 Add price_related_quantity to PriceDTO.php 2023-10-25 13:58:59 +02:00
222 changed files with 45327 additions and 4300 deletions

View File

@@ -27,14 +27,14 @@
# 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
PassEnv DEMO_MODE NO_URL_REWRITE_AVAILABLE FIXER_API_KEY BANNER
# In old version the SAML sp private key env, was wrongly named SAMLP_SP_PRIVATE_KEY, keep it for backward compatibility
PassEnv SAML_ENABLED SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAML_SP_PRIVATE_KEY SAMLP_SP_PRIVATE_KEY
PassEnv SAML_ENABLED SAML_BEHIND_PROXY SAML_ROLE_MAPPING SAML_UPDATE_GROUP_ON_LOGIN SAML_IDP_ENTITY_ID SAML_IDP_SINGLE_SIGN_ON_SERVICE SAML_IDP_SINGLE_LOGOUT_SERVICE SAML_IDP_X509_CERT SAML_SP_ENTITY_ID SAML_SP_X509_CERT SAML_SP_PRIVATE_KEY SAMLP_SP_PRIVATE_KEY
PassEnv TABLE_DEFAULT_PAGE_SIZE TABLE_PARTS_DEFAULT_COLUMNS
PassEnv PROVIDER_DIGIKEY_CLIENT_ID PROVIDER_DIGIKEY_SECRET PROVIDER_DIGIKEY_CURRENCY PROVIDER_DIGIKEY_LANGUAGE PROVIDER_DIGIKEY_COUNTRY
@@ -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

28
.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,12 +168,25 @@ 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
SAML_BEHIND_PROXY=0
# A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID }
# The first match is used, so the order is important! Put the group mapping with the most privileges first.
# Please not to only use single quotes to enclose the JSON string
@@ -214,7 +238,7 @@ APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
# Set the trusted IPs here, when using an reverse proxy
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'

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
@@ -57,7 +57,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '18'
@@ -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

@@ -13,12 +13,11 @@ on:
jobs:
phpunit:
name: PHPUnit and coverage Test (PHP ${{ matrix.php-versions }}, ${{ matrix.db-type }})
# Ubuntu 20.04 ships MySQL 8.0 which causes problems with login, so we just use ubuntu 18.04 for now...
runs-on: ubuntu-22.04
strategy:
matrix:
php-versions: [ '8.1', '8.2' ]
php-versions: [ '8.1', '8.2', '8.3' ]
db-type: [ 'mysql', 'sqlite' ]
env:
@@ -27,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
@@ -46,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
@@ -87,7 +87,7 @@ jobs:
run: composer install --prefer-dist --no-progress
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '18'
@@ -108,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

@@ -9,7 +9,7 @@
![Docker Build Status](https://github.com/Part-DB/Part-DB-symfony/workflows/Docker%20Image%20Build/badge.svg)
[![Crowdin](https://badges.crowdin.net/e/8325196085d4bee8c04b75f7c915452a/localized.svg)](https://part-db.crowdin.com/part-db)
**[Documentation](https://docs.part-db.de/)** | **[Demo](https://part-db.herokuapp.com)** | **[Docker Image](https://hub.docker.com/r/jbtronics/part-db1)**
**[Documentation](https://docs.part-db.de/)** | **[Demo](https://demo.part-db.de/)** | **[Docker Image](https://hub.docker.com/r/jbtronics/part-db1)**
# Part-DB
@@ -24,8 +24,8 @@ for everybody.
## Demo
If you want to test Part-DB without installing it, you can use [this](https://part-db.herokuapp.com) Heroku instance.
(Or this link for the [German Version](https://part-db.herokuapp.com/de/)).
If you want to test Part-DB without installing it, you can use [this](https://demo.part-db.de/) Heroku instance.
(Or this link for the [German Version](https://demo.part-db.de/de/)).
You can log in with username: *user* and password: *user*.
@@ -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.
@@ -101,24 +103,20 @@ for a detailed guide how to install Part-DB.**
In bigger instances with concurrent accesses, MySQL is more performant. This can not be changed easily later, so
choose wisely.
4. Install composer dependencies and generate autoload files: `composer install -o --no-dev`
5. If you have put Part-DB into a subdirectory on your server (like `part-db/`), you have to edit the file
`webpack.config.js` and uncomment the lines (remove the `//` before the lines) `.setPublicPath('/part-db/build')` (
line 43) and
`.setManifestKeyPrefix('build/')` (line 44). You have to replace `/part-db` with your own path on line 44.
6. Install client side dependencies and build it: `yarn install` and `yarn build`
7. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup`
8. Upgrade database to new scheme (or create it, when it was empty): `php bin/console doctrine:migrations:migrate` and
5. Install client side dependencies and build it: `yarn install` and `yarn build`
6. _Optional_ (speeds up first load): Warmup cache: `php bin/console cache:warmup`
7. Upgrade database to new scheme (or create it, when it was empty): `php bin/console doctrine:migrations:migrate` and
follow the instructions given. During the process the password for the admin is user is shown. Copy it. **Caution**:
This steps tamper with your database and could potentially destroy it. So make sure to make a backup of your
database.
9. You can configure Part-DB via `config/parameters.yaml`. You should check if settings match your expectations, after
8. You can configure Part-DB via `config/parameters.yaml`. You should check if settings match your expectations, after
you installed/upgraded Part-DB. Check if `partdb.default_currency` matches your mainly used currency (this can not be
changed after creating price information).
Run `php bin/console cache:clear` when you changed something.
10. Access Part-DB in your browser (under the URL you put it) and login with user *admin*. Password is the one outputted
during DB setup.
If you can not remember the password, set a new one with `php bin/console app:set-password admin`. You can create
new users with the admin user and start using Part-DB.
9. Access Part-DB in your browser (under the URL you put it) and login with user *admin*. Password is the one outputted
during DB setup.
If you can not remember the password, set a new one with `php bin/console app:set-password admin`. You can create
new users with the admin user and start using Part-DB.
When you want to upgrade to a newer version, then just copy the new files into the folder
and repeat the steps 4. to 7.

View File

@@ -1 +1 @@
1.8.2
1.10.2

View File

@@ -85,6 +85,9 @@ const PLACEHOLDERS = [
['[[COMMENT_T]]', 'Comment (plain text)'],
['[[LAST_MODIFIED]]', 'Last modified datetime'],
['[[CREATION_DATE]]', 'Creation datetime'],
['[[IPN_BARCODE_QR]]', 'IPN as QR code'],
['[[IPN_BARCODE_C128]]', 'IPN as Code 128 barcode'],
['[[IPN_BARCODE_C39]]', 'IPN as Code 39 barcode'],
]
},
{

View File

@@ -48,6 +48,9 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
'Comment (plain text)': 'Kommentar (Nur-Text)',
'Last modified datetime': 'Zuletzt geändert',
'Creation datetime': 'Erstellt',
'IPN as QR code': 'IPN als QR Code',
'IPN as Code 128 barcode': 'IPN als Code 128 Barcode',
'IPN as Code 39 barcode': 'IPN als Code 39 Barcode',
'Lot ID': 'Lot ID',
'Lot name': 'Lot Name',

View File

@@ -43,7 +43,8 @@ export default class extends Controller
const message = this.element.dataset.deleteMessage;
const title = this.element.dataset.deleteTitle;
const form = this.element;
//Use event target, to find the form, where the submit button was clicked
const form = event.target;
const submitter = event.submitter;
const that = this;

View File

@@ -0,0 +1,67 @@
/*
* 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";
export default class extends Controller
{
static values = {
id: String
}
connect() {
this.loadState()
this.element.addEventListener('change', () => {
this.saveState()
});
}
loadState() {
let storageKey = this.getStorageKey();
let value = localStorage.getItem(storageKey);
if (value === null) {
return;
}
if (value === 'true') {
this.element.checked = true
}
if (value === 'false') {
this.element.checked = false
}
}
saveState() {
let storageKey = this.getStorageKey();
if (this.element.checked) {
localStorage.setItem(storageKey, 'true');
} else {
localStorage.setItem(storageKey, 'false');
}
}
getStorageKey() {
if (this.hasIdValue) {
return 'persistent_checkbox_' + this.idValue
}
return 'persistent_checkbox_' + this.element.id;
}
}

View File

@@ -27,7 +27,7 @@ export default class extends Controller {
}
let tmp = '<div class="row m-0">' +
"<div class='col-2 p-0 d-flex align-items-center'>" +
"<div class='col-2 p-0 d-flex align-items-center' style='max-width: 80px;'>" +
(data.image ? "<img class='typeahead-image' src='" + data.image + "'/>" : "") +
"</div>" +
"<div class='col-10'>" +

View File

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

View File

@@ -0,0 +1,44 @@
/*
* 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";
export default class extends Controller {
static targets = [ "display", "select" ]
connect()
{
this.update();
this.selectTarget.addEventListener('change', this.update.bind(this));
}
update()
{
//If the select value is 0, then we show the input field
if( this.selectTarget.value === '0')
{
this.displayTarget.classList.remove('d-none');
}
else
{
this.displayTarget.classList.add('d-none');
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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";
export default class extends Controller
{
static targets = ['link', 'mode', 'otherSelect'];
static values = {
targetId: Number,
};
connect() {
}
update() {
const link = this.linkTarget;
const other_select = this.otherSelectTarget;
//Extract the mode using the mode radio buttons (we filter the array to get the checked one)
const mode = (this.modeTargets.filter((e)=>e.checked))[0].value;
if (other_select.value === '') {
link.classList.add('disabled');
return;
}
//Extract href template from data attribute on link target
let href = link.getAttribute('data-href-template');
let target, other;
if (mode === '1') {
target = this.targetIdValue;
other = other_select.value;
} else if (mode === '2') {
target = other_select.value;
other = this.targetIdValue;
} else {
throw 'Invalid mode';
}
//Replace placeholder with actual target id
href = href.replace('__target__', target);
//Replace placeholder with selected value of the select (the event sender)
href = href.replace('__other__', other);
//Assign new href to link
link.setAttribute('href', href);
//Make link clickable
link.classList.remove('disabled');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
"use strict";
import {Tab, Dropdown} from "bootstrap";
import {Tab, Dropdown, Collapse} from "bootstrap";
import tab from "bootstrap/js/src/tab";
/**
@@ -54,6 +54,7 @@ class TabRememberHelper {
const first_element = merged[0] ?? null;
if(first_element) {
this.revealElementOnTab(first_element);
this.revealElementInCollapse(first_element);
}
}
@@ -62,10 +63,20 @@ class TabRememberHelper {
* @param event
*/
onInvalid(event) {
this.revealElementInCollapse(event.target);
this.revealElementOnTab(event.target);
this.revealElementInDropdown(event.target);
}
revealElementInCollapse(element) {
let collapse = element.closest('.collapse');
if(collapse) {
let bs_collapse = Collapse.getOrCreateInstance(collapse);
bs_collapse.show();
}
}
revealElementInDropdown(element) {
let dropdown = element.closest('.dropdown-menu');

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,34 +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/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",
@@ -89,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",
@@ -101,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"
},
@@ -160,7 +163,7 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.3.*"
"require": "6.4.*"
}
}
}

3337
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +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']
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
json: ['application/vnd.openapi+json']
swagger:
api_keys:
@@ -22,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

@@ -5,6 +5,7 @@ parameters:
saml.sp.privateKey: '%env(string:SAML_SP_PRIVATE_KEY)%'
nbgrp_onelogin_saml:
use_proxy_vars: '%env(bool:SAML_BEHIND_PROXY)%'
onelogin_settings:
default:
# Basic settings
@@ -31,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

@@ -20,6 +20,7 @@ twig:
avatar_helper: '@App\Services\UserSystem\UserAvatarHelper'
available_themes: '%partdb.available_themes%'
saml_enabled: '%partdb.saml.enabled%'
part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator'
when@test:
twig:

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
@@ -137,6 +151,8 @@ want to edit it on docker, you have to map the file to a volume.
* `SAML_ENABLED`: When this is set to 1, SAML SSO is enabled and the SSO Login button is shown in the login form. You
have to configure the SAML settings below, before you can use this feature.
* `SAML_BEHIND_PROXY`: Set this to 1, if Part-DB is behind a reverse proxy. See [here]({% link installation/reverse-proxy.md %})
for more information. Otherwise, leave it to 0 (default.)
* `SAML_ROLE_MAPPING`: A [JSON](https://en.wikipedia.org/wiki/JSON) encoded map which specifies how Part-DB should
convert the user roles given by SAML attribute `group` should be converted to a Part-DB group (specified by ID). You
can use a wildcard `*` to map all otherwise unmapped roles to a certain group.

View File

@@ -12,8 +12,8 @@ It is installed on a web server and so can be accessed with any browser without
{: .important-title }
> Demo
>
> If you want to test Part-DB without installing it, you can use [this](https://part-db.herokuapp.com) Heroku instance.
> (Or this link for the [German Version](https://part-db.herokuapp.com/de/)).
> If you want to test Part-DB without installing it, you can use [this](https://demo.part-db.de/) Heroku instance.
> (Or this link for the [German Version](https://demo.part-db.de/de/)).
>
> You can log in with username: **user** and password: **user**, to change/create data.
>
@@ -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
@@ -150,6 +154,7 @@ services:
database:
container_name: partdb_database
image: mysql:8.0
restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password
environment:
# Change this Password

View File

@@ -20,4 +20,21 @@ TRUSTED_PROXIES=192.168.2.10
```
Set the `DEFAULT_URI` environment variable to the URL of your Part-DB installation, available from the outside (so via
the reverse proxy).
the reverse proxy).
## Part-DB in a subpath via reverse proxy
If you put Part-DB into a subpath via the reverse proxy, you have to configure your webserver to include `X-Forwarded-Prefix` in the request headers.
For example if you put Part-DB behind a reverse proxy with the URL `https://example.com/partdb`, you have to set the `X-Forwarded-Prefix` header to `/partdb`.
In apache, you can do this by adding the following line to your virtual host configuration:
```
RequestHeader set X-Forwarded-Prefix "/partdb"
```
and in nginx, you can do this by adding the following line to your server configuration:
```
proxy_set_header X-Forwarded-Prefix "/partdb";
```

View File

@@ -230,3 +230,8 @@ Normally you don't have to change anything here.
Please note that this file is not saved by the Part-DB backup tool, so you have to save it manually if you want to keep
your changes. On docker containers you have to configure a volume mapping for it.
## SAML behind a reverse proxy
If you are running Part-DB behind a reverse proxy, configure the `TRUSTED_PROXIES` environment and other reverse proxy
settings as described in the [reverse proxy guide]({% link installation/reverse-proxy.md %}).
If you want to use SAML you also need to set `SAML_BEHIND_PROXY` to `true` to enable the SAML proxy mode.

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

@@ -160,7 +160,7 @@ EOD;
21840,21840,21840,21840,21840,21520,21520,21520,20480,21520,20480,
20480,20480,20480,20480,21504,20480),
(
2,'admin', '${admin_pw}','','',
2,'admin', '$admin_pw','','',
'','',1,1,21845,21845,21845,21,85,21,349525,21845,21845,21845,21845
,21845,21845,21845,21845,21845,21845,21845,21845,21845,21845,21845,
21845,21845,21845,21845,21845,21845);

View File

@@ -234,8 +234,8 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration
'orderdetails', 'pricedetails', 'storelocations', 'suppliers', ];
foreach ($tables as $table) {
$this->addSql("UPDATE ${table} SET datetime_added = NOW() WHERE datetime_added = '0000-00-00 00:00:00'");
$this->addSql("UPDATE ${table} SET last_modified = datetime_added WHERE last_modified = '0000-00-00 00:00:00'");
$this->addSql("UPDATE $table SET datetime_added = NOW() WHERE datetime_added = '0000-00-00 00:00:00'");
$this->addSql("UPDATE $table SET last_modified = datetime_added WHERE last_modified = '0000-00-00 00:00:00'");
}
//Set the dbVersion to a high value, to prevent the old Part-DB versions to upgrade DB!

View File

@@ -83,9 +83,12 @@ final class Version20221114193325 extends AbstractMultiPlatformMigration impleme
//Reset the permissions of the admin user, to allow admin permissions (like the admins group)
$this->addSql("UPDATE `users` SET permissions_data = '$admin' WHERE id = 2;");
//This warning should not be needed, anymore, as almost everybody should have updated to the new version by now, and this warning would just irritate new users of the software
/*
$this->logger->warning('<bg=cyan;fg=black>!!! All permissions were reset! Please change them to the desired state, immediately !!!</>');
$this->logger->warning('<bg=cyan;fg=black>!!! For security reasons all users (except the admin user) were disabled. Login with admin user and reenable other users after checking their permissions !!!</>');
$this->logger->warning('<bg=cyan;fg=black>!!! For more infos see: https://github.com/Part-DB/Part-DB-symfony/discussions/193 !!!</>');
*/
}
public function mySQLUp(Schema $schema): void

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231114223101 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Add schema for part associations and vendor barcodes';
}
public function mySQLUp(Schema $schema): void
{
$this->addSql('CREATE TABLE part_association (id INT AUTO_INCREMENT NOT NULL, owner_id INT NOT NULL, other_id INT NOT NULL, type SMALLINT NOT NULL, other_type VARCHAR(255) DEFAULT NULL, comment LONGTEXT DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_61B952E07E3C61F9 (owner_id), INDEX IDX_61B952E0998D9879 (other_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE part_association ADD CONSTRAINT FK_61B952E07E3C61F9 FOREIGN KEY (owner_id) REFERENCES `parts` (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE part_association ADD CONSTRAINT FK_61B952E0998D9879 FOREIGN KEY (other_id) REFERENCES `parts` (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE part_lots ADD vendor_barcode VARCHAR(255) DEFAULT NULL');
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
}
public function mySQLDown(Schema $schema): void
{
$this->addSql('ALTER TABLE part_association DROP FOREIGN KEY FK_61B952E07E3C61F9');
$this->addSql('ALTER TABLE part_association DROP FOREIGN KEY FK_61B952E0998D9879');
$this->addSql('DROP TABLE part_association');
$this->addSql('DROP INDEX part_lots_idx_barcode ON part_lots');
$this->addSql('ALTER TABLE part_lots DROP vendor_barcode');
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql('CREATE TABLE part_association (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, owner_id INTEGER NOT NULL, other_id INTEGER NOT NULL, type SMALLINT NOT NULL, other_type VARCHAR(255) DEFAULT NULL, comment CLOB DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_61B952E07E3C61F9 FOREIGN KEY (owner_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_61B952E0998D9879 FOREIGN KEY (other_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_61B952E07E3C61F9 ON part_association (owner_id)');
$this->addSql('CREATE INDEX IDX_61B952E0998D9879 ON part_association (other_id)');
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM part_lots');
$this->addSql('DROP TABLE part_lots');
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES storelocations (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM __temp__part_lots');
$this->addSql('DROP TABLE __temp__part_lots');
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
$this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)');
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql('DROP TABLE part_association');
$this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM part_lots');
$this->addSql('DROP TABLE part_lots');
$this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO part_lots (id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added) SELECT id, id_store_location, id_part, id_owner, description, comment, expiration_date, instock_unknown, amount, needs_refill, last_modified, datetime_added FROM __temp__part_lots');
$this->addSql('DROP TABLE __temp__part_lots');
$this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)');
$this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)');
$this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)');
$this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)');
$this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)');
}
}

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

View File

@@ -26,7 +26,7 @@ parameters:
checkUninitializedProperties: true
checkFunctionNameCase: true
checkFunctionNameCase: false
checkAlwaysTrueInstanceof: false
checkAlwaysTrueCheckTypeFunctionCall: false

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

@@ -26,6 +26,7 @@ namespace App\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\State\PartDBInfoProvider;
@@ -35,7 +36,7 @@ use App\State\PartDBInfoProvider;
#[ApiResource(
uriTemplate: '/info.{_format}',
description: 'Basic information about Part-DB like version, title, etc.',
operations: [new Get(openapiContext: ['summary' => 'Get basic information about the installed Part-DB instance.'])],
operations: [new Get(openapi: new Operation(summary: 'Get basic information about the installed Part-DB instance.'))],
provider: PartDBInfoProvider::class
)]
#[ApiFilter(PropertyFilter::class)]

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

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\Parts\Part;
use App\Exceptions\AttachmentDownloadException;
use App\Form\InfoProviderSystem\PartSearchType;
use App\Form\Part\PartBaseType;
@@ -32,13 +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
{
@@ -61,7 +67,8 @@ class InfoProviderController extends AbstractController
}
#[Route('/search', name: 'info_providers_search')]
public function search(Request $request): Response
#[Route('/update/{target}', name: 'info_providers_update_part_search')]
public function search(Request $request, #[MapEntity(id: 'target')] ?Part $update_target, LoggerInterface $exceptionLogger): Response
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
@@ -70,16 +77,30 @@ class InfoProviderController extends AbstractController
$results = null;
//When we are updating a part, use its name as keyword, to make searching easier
//However we can only do this, if the form was not submitted yet
if ($update_target !== null && !$form->isSubmitted()) {
$form->get('keyword')->setData($update_target->getName());
}
if ($form->isSubmitted() && $form->isValid()) {
$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', [
'form' => $form,
'results' => $results,
'update_target' => $update_target
]);
}
}

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

@@ -36,6 +36,7 @@ use App\Exceptions\AttachmentDownloadException;
use App\Form\Part\PartBaseType;
use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\Attachments\PartPreviewGenerator;
use App\Services\EntityMergers\Mergers\PartMerger;
use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\LogSystem\HistoryHelper;
@@ -233,6 +234,48 @@ class PartController extends AbstractController
]);
}
#[Route('/{target}/merge/{other}', name: 'part_merge')]
public function merge(Request $request, Part $target, Part $other, PartMerger $partMerger): Response
{
$this->denyAccessUnlessGranted('edit', $target);
$this->denyAccessUnlessGranted('delete', $other);
//Save the old name of the target part for the template
$target_name = $target->getName();
$this->addFlash('notice', t('part.merge.flash.please_review'));
$merged = $partMerger->merge($target, $other);
return $this->renderPartForm('merge', $request, $merged, [], [
'tname_before' => $target_name,
'other_part' => $other,
]);
}
#[Route(path: '/{id}/from_info_provider/{providerKey}/{providerId}/update', name: 'info_providers_update_part', requirements: ['providerId' => '.+'])]
public function updateFromInfoProvider(Part $part, Request $request, string $providerKey, string $providerId,
PartInfoRetriever $infoRetriever, PartMerger $partMerger): Response
{
$this->denyAccessUnlessGranted('edit', $part);
$this->denyAccessUnlessGranted('@info_providers.create_parts');
//Save the old name of the target part for the template
$old_name = $part->getName();
$dto = $infoRetriever->getDetails($providerKey, $providerId);
$provider_part = $infoRetriever->dtoToPart($dto);
$part = $partMerger->merge($part, $provider_part);
$this->addFlash('notice', t('part.merge.flash.please_review'));
return $this->renderPartForm('update_from_ip', $request, $part, [
'info_provider_dto' => $dto,
], [
'tname_before' => $old_name
]);
}
/**
* This function provides a common implementation for methods, which use the part form.
* @param Request $request
@@ -240,10 +283,10 @@ class PartController extends AbstractController
* @param array $form_options
* @return Response
*/
private function renderPartForm(string $mode, Request $request, Part $data, array $form_options = []): Response
private function renderPartForm(string $mode, Request $request, Part $data, array $form_options = [], array $merge_infos = []): Response
{
//Ensure that mode is either 'new' or 'edit
if (!in_array($mode, ['new', 'edit'], true)) {
if (!in_array($mode, ['new', 'edit', 'merge', 'update_from_ip'], true)) {
throw new \InvalidArgumentException('Invalid mode given');
}
@@ -276,6 +319,12 @@ class PartController extends AbstractController
$this->commentHelper->setMessage($form['log_comment']->getData());
$this->em->persist($new_part);
//When we are in merge mode, we have to remove the other part
if ($mode === 'merge') {
$this->em->remove($merge_infos['other_part']);
}
$this->em->flush();
if ($mode === 'new') {
$this->addFlash('success', 'part.created_flash');
@@ -310,12 +359,18 @@ class PartController extends AbstractController
$template = 'parts/edit/new_part.html.twig';
} else if ($mode === 'edit') {
$template = 'parts/edit/edit_part_info.html.twig';
} else if ($mode === 'merge') {
$template = 'parts/edit/merge_parts.html.twig';
} else if ($mode === 'update_from_ip') {
$template = 'parts/edit/update_from_ip.html.twig';
}
return $this->render($template,
[
'part' => $new_part,
'form' => $form,
'merge_old_name' => $merge_infos['tname_before'] ?? null,
'merge_other' => $merge_infos['other_part'] ?? null
]);
}
@@ -345,23 +400,41 @@ class PartController extends AbstractController
$amount = (float) $request->request->get('amount');
$comment = $request->request->get('comment');
$action = $request->request->get('action');
$delete_lot_if_empty = $request->request->getBoolean('delete_lot_if_empty', false);
$timestamp = null;
$timestamp_str = $request->request->getString('timestamp', '');
//Try to parse the timestamp
if($timestamp_str !== '') {
$timestamp = new DateTime($timestamp_str);
}
//Ensure that the timestamp is not in the future
if($timestamp !== null && $timestamp > new DateTime("+20min")) {
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":
case "remove":
$this->denyAccessUnlessGranted('withdraw', $partLot);
$withdrawAddHelper->withdraw($partLot, $amount, $comment);
$withdrawAddHelper->withdraw($partLot, $amount, $comment, $timestamp, $delete_lot_if_empty);
break;
case "add":
$this->denyAccessUnlessGranted('add', $partLot);
$withdrawAddHelper->add($partLot, $amount, $comment);
$withdrawAddHelper->add($partLot, $amount, $comment, $timestamp);
break;
case "move":
$this->denyAccessUnlessGranted('move', $partLot);
$this->denyAccessUnlessGranted('move', $targetLot);
$withdrawAddHelper->move($partLot, $targetLot, $amount, $comment);
$withdrawAddHelper->move($partLot, $targetLot, $amount, $comment, $timestamp, $delete_lot_if_empty);
break;
default:
throw new \RuntimeException("Unknown action!");

View File

@@ -42,8 +42,10 @@ declare(strict_types=1);
namespace App\Controller;
use App\Form\LabelSystem\ScanDialogType;
use App\Services\LabelSystem\Barcodes\BarcodeNormalizer;
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper;
use App\Services\LabelSystem\Barcodes\BarcodeRedirector;
use App\Services\LabelSystem\Barcodes\BarcodeScanResult;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
use Doctrine\ORM\EntityNotFoundException;
use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -55,7 +57,7 @@ use Symfony\Component\Routing\Annotation\Route;
#[Route(path: '/scan')]
class ScanController extends AbstractController
{
public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeNormalizer $barcodeNormalizer)
public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer)
{
}
@@ -69,14 +71,14 @@ class ScanController extends AbstractController
if ($input === null && $form->isSubmitted() && $form->isValid()) {
$input = $form['input']->getData();
$mode = $form['mode']->getData();
}
if ($input !== null) {
try {
[$type, $id] = $this->barcodeNormalizer->normalizeBarcodeContent($input);
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
try {
return $this->redirect($this->barcodeParser->getRedirectURL($type, $id));
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
} catch (EntityNotFoundException) {
$this->addFlash('success', 'scan.qr_not_found');
}
@@ -95,10 +97,23 @@ class ScanController extends AbstractController
*/
public function scanQRCode(string $type, int $id): Response
{
$type = strtolower($type);
try {
$this->addFlash('success', 'scan.qr_success');
return $this->redirect($this->barcodeParser->getRedirectURL($type, $id));
if (!isset(BarcodeScanHelper::QR_TYPE_MAP[$type])) {
throw new InvalidArgumentException('Unknown type: '.$type);
}
//Construct the scan result manually, as we don't have a barcode here
$scan_result = new BarcodeScanResult(
target_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
target_id: $id,
//The routes are only used on the internal generated QR codes
source_type: BarcodeSourceType::INTERNAL
);
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
} catch (EntityNotFoundException) {
$this->addFlash('success', 'scan.qr_not_found');

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

@@ -83,6 +83,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$part->setManufacturer($manager->find(Manufacturer::class, 1));
$part->setTags('test, Test, Part2');
$part->setMass(100.2);
$part->setIpn('IPN123');
$part->setNeedsReview(true);
$part->setManufacturingStatus(ManufacturingStatus::ACTIVE);
$manager->persist($part);
@@ -102,6 +103,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$partLot2->setComment('Test');
$partLot2->setNeedsRefill(true);
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
$partLot2->setVendorBarcode('lot2_vendor_barcode');
$part->addPartLot($partLot2);
$orderdetail = new Orderdetail();

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

@@ -20,14 +20,17 @@ 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\DataTables\Helpers;
use App\Entity\Parts\StorageLocation;
use App\Entity\ProjectSystem\Project;
use App\Entity\Attachments\Attachment;
use App\Entity\Parts\Part;
use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Attachments\PartPreviewGenerator;
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@@ -35,8 +38,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class PartDataTableHelper
{
public function __construct(private readonly PartPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator, private readonly EntityURLGenerator $entityURLGenerator, private readonly TranslatorInterface $translator)
{
public function __construct(
private readonly PartPreviewGenerator $previewGenerator,
private readonly AttachmentURLGenerator $attachmentURLGenerator,
private readonly EntityURLGenerator $entityURLGenerator,
private readonly TranslatorInterface $translator,
private readonly AmountFormatter $amountFormatter,
) {
}
public function renderName(Part $context): string
@@ -45,14 +53,16 @@ class PartDataTableHelper
//Depending on the part status we show a different icon (the later conditions have higher priority)
if ($context->isFavorite()) {
$icon = sprintf('<i class="fa-solid fa-star fa-fw me-1" title="%s"></i>', $this->translator->trans('part.favorite.badge'));
$icon = sprintf('<i class="fa-solid fa-star fa-fw me-1" title="%s"></i>',
$this->translator->trans('part.favorite.badge'));
}
if ($context->isNeedsReview()) {
$icon = sprintf('<i class="fa-solid fa-ambulance fa-fw me-1" title="%s"></i>', $this->translator->trans('part.needs_review.badge'));
$icon = sprintf('<i class="fa-solid fa-ambulance fa-fw me-1" title="%s"></i>',
$this->translator->trans('part.needs_review.badge'));
}
if ($context->getBuiltProject() instanceof Project) {
$icon = sprintf('<i class="fa-solid fa-box-archive fa-fw me-1" title="%s"></i>',
$this->translator->trans('part.info.projectBuildPart.hint') . ': ' . $context->getBuiltProject()->getName());
$this->translator->trans('part.info.projectBuildPart.hint').': '.$context->getBuiltProject()->getName());
}
@@ -85,4 +95,62 @@ class PartDataTableHelper
$title
);
}
public function renderStorageLocations(Part $context): string
{
$tmp = [];
foreach ($context->getPartLots() as $lot) {
//Ignore lots without storelocation
if (!$lot->getStorageLocation() instanceof StorageLocation) {
continue;
}
$tmp[] = sprintf(
'<a href="%s" title="%s">%s</a>',
$this->entityURLGenerator->listPartsURL($lot->getStorageLocation()),
htmlspecialchars($lot->getStorageLocation()->getFullPath()),
htmlspecialchars($lot->getStorageLocation()->getName())
);
}
return implode('<br>', $tmp);
}
public function renderAmount(Part $context): string
{
$amount = $context->getAmountSum();
$expiredAmount = $context->getExpiredAmountSum();
$ret = '';
if ($context->isAmountUnknown()) {
//When all amounts are unknown, we show a question mark
if ($amount === 0.0) {
$ret .= sprintf('<b class="text-primary" title="%s">?</b>',
$this->translator->trans('part_lots.instock_unknown'));
} else { //Otherwise mark it with greater equal and the (known) amount
$ret .= sprintf('<b class="text-primary" title="%s">≥</b>',
$this->translator->trans('part_lots.instock_unknown')
);
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
}
} else {
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
}
//If we have expired lots, we show them in parentheses behind
if ($expiredAmount > 0) {
$ret .= sprintf(' <span title="%s" class="text-muted">(+%s)</span>',
$this->translator->trans('part_lots.is_expired'),
htmlspecialchars($this->amountFormatter->format($expiredAmount, $context->getPartUnit())));
}
//When the amount is below the minimum amount, we highlight the number red
if ($context->isNotEnoughInstock()) {
$ret = sprintf('<b class="text-danger" title="%s">%s</b>',
$this->translator->trans('part.info.amount.less_than_desired'),
$ret);
}
return $ret;
}
}

View File

@@ -139,7 +139,7 @@ class LogDataTable implements DataTableTypeInterface
if ($context instanceof PartStockChangedLogEntry) {
$text .= sprintf(
' (<i>%s</i>)',
$this->translator->trans('log.part_stock_changed.' . $context->getInstockChangeType()->toExtraShortType())
$this->translator->trans($context->getInstockChangeType()->toTranslationKey())
);
}

View File

@@ -139,63 +139,11 @@ final class PartsDataTable implements DataTableTypeInterface
->add('storelocation', TextColumn::class, [
'label' => $this->translator->trans('part.table.storeLocations'),
'orderField' => 'storelocations.name',
'render' => function ($value, Part $context): string {
$tmp = [];
foreach ($context->getPartLots() as $lot) {
//Ignore lots without storelocation
if (!$lot->getStorageLocation() instanceof StorageLocation) {
continue;
}
$tmp[] = sprintf(
'<a href="%s" title="%s">%s</a>',
$this->urlGenerator->listPartsURL($lot->getStorageLocation()),
htmlspecialchars($lot->getStorageLocation()->getFullPath()),
htmlspecialchars($lot->getStorageLocation()->getName())
);
}
return implode('<br>', $tmp);
},
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
], alias: 'storage_location')
->add('amount', TextColumn::class, [
'label' => $this->translator->trans('part.table.amount'),
'render' => function ($value, Part $context) {
$amount = $context->getAmountSum();
$expiredAmount = $context->getExpiredAmountSum();
$ret = '';
if ($context->isAmountUnknown()) {
//When all amounts are unknown, we show a question mark
if ($amount === 0.0) {
$ret .= sprintf('<b class="text-primary" title="%s">?</b>',
$this->translator->trans('part_lots.instock_unknown'));
} else { //Otherwise mark it with greater equal and the (known) amount
$ret .= sprintf('<b class="text-primary" title="%s">≥</b>',
$this->translator->trans('part_lots.instock_unknown')
);
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
}
} else {
$ret .= htmlspecialchars($this->amountFormatter->format($amount, $context->getPartUnit()));
}
//If we have expired lots, we show them in parentheses behind
if ($expiredAmount > 0) {
$ret .= sprintf(' <span title="%s" class="text-muted">(+%s)</span>',
$this->translator->trans('part_lots.is_expired'),
htmlspecialchars($this->amountFormatter->format($expiredAmount, $context->getPartUnit())));
}
//When the amount is below the minimum amount, we highlight the number red
if ($context->isNotEnoughInstock()) {
$ret = sprintf('<b class="text-danger" title="%s">%s</b>',
$this->translator->trans('part.info.amount.less_than_desired'),
$ret);
}
return $ret;
},
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
'orderField' => 'amountSum'
])
->add('minamount', TextColumn::class, [

View File

@@ -100,7 +100,16 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
throw new \Exception('This should never happen!');
},
])
->add('ipn', TextColumn::class, [
'label' => $this->translator->trans('part.table.ipn'),
'orderField' => 'part.ipn',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if($context->getPart() instanceof Part) {
return $context->getPart()->getIpn();
}
}
])
->add('description', MarkdownColumn::class, [
'label' => $this->translator->trans('part.table.description'),
'data' => function (ProjectBOMEntry $context) {
@@ -142,6 +151,28 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
},
])
->add('instockAmount', TextColumn::class, [
'label' => 'project.bom.instockAmount',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if ($context->getPart()) {
return $this->partDataTableHelper->renderAmount($context->getPart());
}
return '';
}
])
->add('storageLocations', TextColumn::class, [
'label' => 'part.table.storeLocations',
'visible' => false,
'render' => function ($value, ProjectBOMEntry $context) {
if ($context->getPart()) {
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
}
return '';
}
])
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),

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

@@ -41,6 +41,20 @@ class TinyIntType extends Type
return 'tinyint';
}
/**
* {@inheritDoc}
*
* @param T $value
*
* @return (T is null ? null : int)
*
* @template T
*/
public function convertToPHPValue($value, AbstractPlatform $platform): ?int
{
return $value === null ? null : (int) $value;
}
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
//We use the comment, so that doctrine migrations can properly detect, that nothing has changed and no migration is needed.

View File

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Parts\Footprint;
@@ -70,7 +71,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/attachment_types/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of an attachment type.'],
new GetCollection(openapi: new Operation(summary: 'Retrieves the children elements of an attachment type.'),
security: 'is_granted("@attachment_types.read")')
],
uriVariables: [

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

@@ -29,6 +29,7 @@ use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartAssociation;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
@@ -63,6 +64,8 @@ enum LogTargetType: int
case PARAMETER = 18;
case LABEL_PROFILE = 19;
case PART_ASSOCIATION = 20;
/**
* Returns the class name of the target type or null if the target type is NONE.
* @return string|null
@@ -90,6 +93,7 @@ enum LogTargetType: int
self::MEASUREMENT_UNIT => MeasurementUnit::class,
self::PARAMETER => AbstractParameter::class,
self::LABEL_PROFILE => LabelProfile::class,
self::PART_ASSOCIATION => PartAssociation::class,
};
}

View File

@@ -39,6 +39,11 @@ enum PartStockChangeType: string
};
}
public function toTranslationKey(): string
{
return 'log.part_stock_changed.' . $this->value;
}
public static function fromExtraShortType(string $value): self
{
return match ($value) {

View File

@@ -41,8 +41,10 @@ class PartStockChangedLogEntry extends AbstractLogEntry
* @param float $new_total_part_instock The new total instock of the part.
* @param string $comment The comment associated with the change.
* @param PartLot|null $move_to_target The target lot if the type is TYPE_MOVE.
* @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards.
*/
protected function __construct(PartStockChangeType $type, PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?PartLot $move_to_target = null)
protected function __construct(PartStockChangeType $type, PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?PartLot $move_to_target = null,
?\DateTimeInterface $action_timestamp = null)
{
parent::__construct();
@@ -62,6 +64,11 @@ class PartStockChangedLogEntry extends AbstractLogEntry
$this->extra['c'] = mb_strimwidth($comment, 0, self::COMMENT_MAX_LENGTH, '...');
}
if ($action_timestamp instanceof \DateTimeInterface) {
//The action timestamp is saved as an ISO 8601 string
$this->extra['a'] = $action_timestamp->format(\DateTimeInterface::ATOM);
}
if ($move_to_target instanceof PartLot) {
if ($type !== PartStockChangeType::MOVE) {
throw new \InvalidArgumentException('The move_to_target parameter can only be set if the type is "move"!');
@@ -78,11 +85,12 @@ class PartStockChangedLogEntry extends AbstractLogEntry
* @param float $new_stock The new stock of the lot.
* @param float $new_total_part_instock The new total instock of the part.
* @param string $comment The comment associated with the change.
* @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards.
* @return self
*/
public static function add(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment): self
public static function add(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self
{
return new self(PartStockChangeType::ADD, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment);
return new self(PartStockChangeType::ADD, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp);
}
/**
@@ -92,11 +100,12 @@ class PartStockChangedLogEntry extends AbstractLogEntry
* @param float $new_stock The new stock of the lot.
* @param float $new_total_part_instock The new total instock of the part.
* @param string $comment The comment associated with the change.
* @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards.
* @return self
*/
public static function withdraw(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment): self
public static function withdraw(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self
{
return new self(PartStockChangeType::WITHDRAW, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment);
return new self(PartStockChangeType::WITHDRAW, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp);
}
/**
@@ -107,10 +116,12 @@ class PartStockChangedLogEntry extends AbstractLogEntry
* @param float $new_total_part_instock The new total instock of the part.
* @param string $comment The comment associated with the change.
* @param PartLot $move_to_target The target lot.
* @param \DateTimeInterface|null $action_timestamp The optional timestamp, where the action happened. Useful if the action happened in the past, and the log entry is created afterwards.
* @return self
*/
public static function move(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, PartLot $move_to_target): self
public static function move(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, PartLot $move_to_target, ?\DateTimeInterface $action_timestamp = null): self
{
return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target);
return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target, action_timestamp: $action_timestamp);
}
/**
@@ -169,4 +180,18 @@ class PartStockChangedLogEntry extends AbstractLogEntry
{
return $this->extra['m'] ?? null;
}
/**
* Returns the timestamp when this action was performed and not when the log entry was created.
* This is useful if the action happened in the past, and the log entry is created afterwards.
* If the timestamp is not set, null is returned.
* @return \DateTimeInterface|null
*/
public function getActionTimestamp(): ?\DateTimeInterface
{
if (!empty($this->extra['a'])) {
return \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $this->extra['a']);
}
return null;
}
}

View File

@@ -0,0 +1,46 @@
<?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;
/**
* The values of this enums are used to describe how two parts are associated with each other.
*/
enum AssociationType: int
{
/** A user definable association type, which can be described in the comment field */
case OTHER = 0;
/** The owning part is compatible with the other part */
case COMPATIBLE = 1;
/** The owning part supersedes the other part (owner is newer version) */
case SUPERSEDES = 2;
/**
* Returns the translation key for this association type.
* @return string
*/
public function getTranslationKey(): string
{
return 'part_association.type.' . strtolower($this->name);
}
}

View File

@@ -34,9 +34,12 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
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;
@@ -46,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;
@@ -72,8 +76,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/categories/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a category.'],
security: 'is_granted("@categories.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a category.'),
security: 'is_granted("@categories.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Category::class)
@@ -182,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
{
@@ -275,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

@@ -34,10 +34,14 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
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;
@@ -46,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;
@@ -72,8 +77,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/footprints/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a footprint.'],
security: 'is_granted("@footprints.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a footprint.'),
security: 'is_granted("@footprints.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Footprint::class)
@@ -134,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
@@ -163,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

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
@@ -71,8 +72,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/manufacturers/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a manufacturer.'],
security: 'is_granted("@manufacturers.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a manufacturer.'),
security: 'is_granted("@manufacturers.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)

View File

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
@@ -75,8 +76,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/footprints/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a MeasurementUnit.'],
security: 'is_granted("@measurement_units.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a MeasurementUnit.'),
security: 'is_granted("@measurement_units.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: MeasurementUnit::class)

View File

@@ -41,6 +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;
@@ -58,6 +61,7 @@ use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Jfcherng\Diff\Utility\Arr;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
@@ -81,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")'),
@@ -90,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"])]
@@ -100,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;
@@ -112,6 +114,8 @@ class Part extends AttachmentContainingDBElement
use OrderTrait;
use ParametersTrait;
use ProjectTrait;
use AssociationTrait;
use EDATrait;
/** @var Collection<int, PartParameter>
*/
@@ -165,8 +169,12 @@ class Part extends AttachmentContainingDBElement
$this->parameters = new ArrayCollection();
$this->project_bom_entries = new ArrayCollection();
$this->associated_parts_as_owner = new ArrayCollection();
$this->associated_parts_as_other = new ArrayCollection();
//By default, the part has no provider
$this->providerReference = InfoProviderReference::noProvider();
$this->eda_info = new EDAPartInfo();
}
public function __clone()
@@ -193,8 +201,16 @@ class Part extends AttachmentContainingDBElement
$this->addParameter(clone $parameter);
}
//Deep clone the owned part associations (the owned ones make not much sense without the owner)
$ownedAssociations = $this->associated_parts_as_owner;
$this->associated_parts_as_owner = new ArrayCollection();
foreach ($ownedAssociations as $association) {
$this->addAssociatedPartsAsOwner(clone $association);
}
//Deep clone info provider
$this->providerReference = clone $this->providerReference;
$this->eda_info = clone $this->eda_info;
}
parent::__clone();
}

View File

@@ -0,0 +1,234 @@
<?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;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
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;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\TimestampTrait;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* This entity describes a part association, which is a semantic connection between two parts.
* For example, a part association can be used to describe that a part is a replacement for another part.
*/
#[ORM\Entity(repositoryClass: DBElementRepository::class)]
#[ORM\HasLifecycleCallbacks]
#[UniqueEntity(fields: ['other', 'owner', 'type'], message: 'validator.part_association.already_exists')]
#[ApiResource(
operations: [
new Get(security: 'is_granted("read", object)'),
new GetCollection(security: 'is_granted("@parts.read")'),
new Post(securityPostDenormalize: 'is_granted("create", object)'),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
],
normalizationContext: ['groups' => ['part_assoc:read', 'part_assoc:read:standalone', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['part_assoc:write', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
)]
#[ApiFilter(PropertyFilter::class)]
#[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 implements TimeStampableInterface
{
use TimestampTrait;
/**
* @var AssociationType The type of this association (how the two parts are related)
*/
#[ORM\Column(type: Types::SMALLINT, enumType: AssociationType::class)]
#[Groups(['part_assoc:read', 'part_assoc:write'])]
protected AssociationType $type = AssociationType::OTHER;
/**
* @var string|null A user definable association type, which can be described in the comment field, which
* is used if the type is OTHER
*/
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[Assert\Expression("this.getType().value !== 0 or this.getOtherType() !== null",
message: 'validator.part_association.must_set_an_value_if_type_is_other')]
#[Groups(['part_assoc:read', 'part_assoc:write'])]
protected ?string $other_type = null;
/**
* @var string|null A comment describing this association further.
*/
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Groups(['part_assoc:read', 'part_assoc:write'])]
protected ?string $comment = null;
/**
* @var Part|null The part which "owns" this association, e.g. the part which is a replacement for another part
*/
#[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'associated_parts_as_owner')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
#[Groups(['part_assoc:read:standalone', 'part_assoc:write'])]
protected ?Part $owner = null;
/**
* @var Part|null The part which is "owned" by this association, e.g. the part which is replaced by another part
*/
#[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'associated_parts_as_other')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
#[Assert\Expression("this.getOwner() !== this.getOther()",
message: 'validator.part_association.part_cannot_be_associated_with_itself')]
#[Groups(['part_assoc:read', 'part_assoc:write'])]
protected ?Part $other = null;
/**
* Returns the (semantic) relation type of this association as an AssociationType enum value.
* If the type is set to OTHER, then the other_type field value is used for the user defined type.
* @return AssociationType
*/
public function getType(): AssociationType
{
return $this->type;
}
/**
* Sets the (semantic) relation type of this association as an AssociationType enum value.
* @param AssociationType $type
* @return $this
*/
public function setType(AssociationType $type): PartAssociation
{
$this->type = $type;
return $this;
}
/**
* Returns a comment, which describes this association further.
* @return string|null
*/
public function getComment(): ?string
{
return $this->comment;
}
/**
* Sets a comment, which describes this association further.
* @param string|null $comment
* @return $this
*/
public function setComment(?string $comment): PartAssociation
{
$this->comment = $comment;
return $this;
}
/**
* Returns the part which "owns" this association, e.g. the part which is a replacement for another part.
* @return Part|null
*/
public function getOwner(): ?Part
{
return $this->owner;
}
/**
* Sets the part which "owns" this association, e.g. the part which is a replacement for another part.
* @param Part|null $owner
* @return $this
*/
public function setOwner(?Part $owner): PartAssociation
{
$this->owner = $owner;
return $this;
}
/**
* Returns the part which is "owned" by this association, e.g. the part which is replaced by another part.
* @return Part|null
*/
public function getOther(): ?Part
{
return $this->other;
}
/**
* Sets the part which is "owned" by this association, e.g. the part which is replaced by another part.
* @param Part|null $other
* @return $this
*/
public function setOther(?Part $other): PartAssociation
{
$this->other = $other;
return $this;
}
/**
* Returns the user defined association type, which is used if the type is set to OTHER.
* @return string|null
*/
public function getOtherType(): ?string
{
return $this->other_type;
}
/**
* Sets the user defined association type, which is used if the type is set to OTHER.
* @param string|null $other_type
* @return $this
*/
public function setOtherType(?string $other_type): PartAssociation
{
$this->other_type = $other_type;
return $this;
}
/**
* Returns the translation key for the type of this association.
* If the type is set to OTHER, then the other_type field value is used.
* @return string
*/
public function getTypeTranslationKey(): string
{
if ($this->type === AssociationType::OTHER) {
return $this->other_type ?? 'Unknown';
}
return $this->type->getTranslationKey();
}
}

View File

@@ -27,6 +27,7 @@ use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
@@ -47,6 +48,7 @@ use App\Validator\Constraints\ValidPartLot;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -60,9 +62,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'part_lots')]
#[ORM\Index(name: 'part_lots_idx_instock_un_expiration_id_part', columns: ['instock_unknown', 'expiration_date', 'id_part'])]
#[ORM\Index(name: 'part_lots_idx_needs_refill', columns: ['needs_refill'])]
#[ORM\Index(columns: ['instock_unknown', 'expiration_date', 'id_part'], name: 'part_lots_idx_instock_un_expiration_id_part')]
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
#[ValidPartLot]
#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
#[ApiResource(
operations: [
new Get(security: 'is_granted("read", object)'),
@@ -144,6 +148,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
#[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'partLots')]
#[ORM\JoinColumn(name: 'id_part', nullable: false, onDelete: 'CASCADE')]
#[Groups(['part_lot:read:standalone', 'part_lot:write'])]
#[ApiProperty(writableLink: false)]
protected ?Part $part = null;
/**
@@ -152,8 +157,16 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'id_owner', onDelete: 'SET NULL')]
#[Groups(['part_lot:read', 'part_lot:write'])]
#[ApiProperty(writableLink: false)]
protected ?User $owner = null;
/**
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
*/
#[ORM\Column(type: Types::STRING, nullable: true)]
#[Groups(['part_lot:read', 'part_lot:write'])]
protected ?string $vendor_barcode = null;
public function __clone()
{
if ($this->id) {
@@ -354,6 +367,29 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
return $this->description;
}
/**
* The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor), or
* null if no barcode is set.
* @return string|null
*/
public function getVendorBarcode(): ?string
{
return $this->vendor_barcode;
}
/**
* Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor).
* @param string|null $vendor_barcode
* @return $this
*/
public function setVendorBarcode(?string $vendor_barcode): PartLot
{
$this->vendor_barcode = $vendor_barcode;
return $this;
}
#[Assert\Callback]
public function validate(ExecutionContextInterface $context, $payload): void
{

View File

@@ -0,0 +1,111 @@
<?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\Parts\Part;
use App\Entity\Parts\PartAssociation;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints\Valid;
use Doctrine\ORM\Mapping as ORM;
trait AssociationTrait
{
/**
* @var Collection<PartAssociation> All associations where this part is the owner
*/
#[Valid]
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: PartAssociation::class,
cascade: ['persist', 'remove'], orphanRemoval: true)]
#[Groups(['part:read', 'part:write'])]
protected Collection $associated_parts_as_owner;
/**
* @var Collection<PartAssociation> All associations where this part is the owned/other part
*/
#[Valid]
#[ORM\OneToMany(mappedBy: 'other', targetEntity: PartAssociation::class,
cascade: ['persist', 'remove'], orphanRemoval: true)]
#[Groups(['part:read'])]
protected Collection $associated_parts_as_other;
/**
* Returns all associations where this part is the owner.
* @return Collection<PartAssociation>
*/
public function getAssociatedPartsAsOwner(): Collection
{
return $this->associated_parts_as_owner;
}
/**
* Add a new association where this part is the owner.
* @param PartAssociation $association
* @return $this
*/
public function addAssociatedPartsAsOwner(PartAssociation $association): self
{
//Ensure that the association is really owned by this part
$association->setOwner($this);
$this->associated_parts_as_owner->add($association);
return $this;
}
/**
* Remove an association where this part is the owner.
* @param PartAssociation $association
* @return $this
*/
public function removeAssociatedPartsAsOwner(PartAssociation $association): self
{
$this->associated_parts_as_owner->removeElement($association);
return $this;
}
/**
* Returns all associations where this part is the owned/other part.
* If you want to modify the association, do it on the owning part
* @return Collection<PartAssociation>
*/
public function getAssociatedPartsAsOther(): Collection
{
return $this->associated_parts_as_other;
}
/**
* Returns all associations where this part is the owned or other part.
* @return Collection<PartAssociation>
*/
public function getAssociatedPartsAll(): Collection
{
return new ArrayCollection(
array_merge(
$this->associated_parts_as_owner->toArray(),
$this->associated_parts_as_other->toArray()
)
);
}
}

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

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
@@ -71,8 +72,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/storage_locations/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a storage location.'],
security: 'is_granted("@storelocations.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a storage location.'),
security: 'is_granted("@storelocations.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Manufacturer::class)

View File

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
@@ -75,8 +76,10 @@ use Symfony\Component\Validator\Constraints as Assert;
)]
#[ApiResource(
uriTemplate: '/suppliers/{id}/children.{_format}',
operations: [new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a supplier'],
security: 'is_granted("@manufacturers.read")')],
operations: [new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a supplier.'),
security: 'is_granted("@manufacturers.read")'
)],
uriVariables: [
'id' => new Link(fromClass: Supplier::class, fromProperty: 'children')
],

View File

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
@@ -75,8 +76,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/currencies/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a currency.'],
security: 'is_granted("@currencies.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a currency.'),
security: 'is_granted("@currencies.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Currency::class)

View File

@@ -34,6 +34,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use Doctrine\DBAL\Types\Types;
@@ -73,8 +74,10 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
uriTemplate: '/parts/{id}/orderdetails.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the orderdetails of a part.'],
security: 'is_granted("@parts.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the orderdetails of a part.'),
security: 'is_granted("@parts.read")'
)
],
uriVariables: [
'id' => new Link(toProperty: 'part', fromClass: Part::class)

View File

@@ -33,6 +33,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
@@ -74,8 +75,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiResource(
uriTemplate: '/projects/{id}/children.{_format}',
operations: [
new GetCollection(openapiContext: ['summary' => 'Retrieves the children elements of a project.'],
security: 'is_granted("@projects.read")')
new GetCollection(
openapi: new Operation(summary: 'Retrieves the children elements of a project.'),
security: 'is_granted("@projects.read")'
)
],
uriVariables: [
'id' => new Link(fromProperty: 'children', fromClass: Project::class)
@@ -183,7 +186,7 @@ class Project extends AbstractStructuralDBElement
//Set master attachment is needed
foreach ($bom_entries as $bom_entry) {
$clone = clone $bom_entry;
$this->bom_entries->add($clone);
$this->addBomEntry($clone);
}
}

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