Compare commits

...

212 Commits

Author SHA1 Message Date
Jan Böhmer
39009a71d5 Bumped version to 1.7.2 2023-09-24 19:54:35 +02:00
Jan Böhmer
0430178fe2 Fixed issue when the data is null. 2023-09-24 15:42:44 +02:00
Jan Böhmer
cf9df883c9 Updated dependencies 2023-09-24 15:36:07 +02:00
Jan Böhmer
198befe2bc Allow to dynamically create elements with purely numeric names in a selector type
Before this was not possible, as this was messed up with the DB ids. Now we prefix the new created values with a special prefix, to mark them as new.

This fixes issue #381
2023-09-24 15:28:35 +02:00
Jan Böhmer
7195bd6cd6 Increased user avatar max file size from 2M to 5M 2023-09-24 14:46:51 +02:00
Jan Böhmer
a5fa2da80c Show the languages from the language selector as preffered in language select on user settings page 2023-09-24 14:45:12 +02:00
Jan Böhmer
593d37f37c Added italien to language selector navbar menu 2023-09-24 14:42:21 +02:00
Jan Böhmer
2ddd6753ca Merge remote-tracking branch 'origin/l10n_master' 2023-09-24 14:40:31 +02:00
Jan Böhmer
9537c4f210 New translations messages.en.xlf (Italian) 2023-09-24 12:50:18 +02:00
Jan Böhmer
e0ce6ba165 New translations messages.en.xlf (Italian) 2023-09-24 11:50:16 +02:00
Jan Böhmer
ee50ce26f8 Merge remote-tracking branch 'origin/master' 2023-09-23 23:08:55 +02:00
Jan Böhmer
94a6de4a90 Fixed wrong literal in italian translation, which caused an exception. 2023-09-23 23:08:46 +02:00
Jan Böhmer
d5902314c3 New Crowdin updates (#378)
* New translations messages.en.xlf (French)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations validators.en.xlf (Italian)

* New translations security.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)
2023-09-23 23:06:06 +02:00
Jan Böhmer
60125534ec New Crowdin updates (#370)
* New translations validators.en.xlf (French)

* New translations messages.en.xlf (German)

* New translations validators.en.xlf (German)

* New translations messages.en.xlf (Italian)

* New translations validators.en.xlf (Italian)

* New translations security.en.xlf (Italian)

* New translations messages.en.xlf (Japanese)

* New translations validators.en.xlf (Japanese)

* New translations messages.en.xlf (Russian)

* New translations validators.en.xlf (Russian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (English)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)

* New translations messages.en.xlf (Italian)
2023-09-18 19:41:01 +02:00
dependabot[bot]
48385cadc9 Bump docker/build-push-action from 4 to 5 (#375)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  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-09-18 19:40:49 +02:00
dependabot[bot]
ba6abe6ca7 Bump docker/setup-buildx-action from 2 to 3 (#374)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  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-09-18 19:40:43 +02:00
dependabot[bot]
79ad243bf4 Bump actions/checkout from 3 to 4 (#361)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  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-09-18 19:40:33 +02:00
dependabot[bot]
5ab21e019d Bump docker/metadata-action from 4 to 5 (#376)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  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-09-18 19:40:23 +02:00
dependabot[bot]
d8469efba2 Bump docker/setup-qemu-action from 2 to 3 (#377)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  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-09-18 19:40:13 +02:00
Jan Böhmer
316b09ddf3 Bumped version to 1.7.1 2023-09-09 23:20:09 +02:00
Jan Böhmer
866ef73774 Upgraded dependencies 2023-09-09 23:19:44 +02:00
Jan Böhmer
138d5c6e0f Merge remote-tracking branch 'origin/l10n_master' 2023-09-09 23:05:29 +02:00
Jan Böhmer
4bed50d894 Allow database migration from legacy versions even if the perms_label column is missing in the groups table
This fixes issue #366 and #67
2023-09-09 23:04:50 +02:00
Jan Böhmer
55943f5d8f Fixed wrong env documentation for TME and digikey provider
This fixes issue #359
2023-08-28 12:08:47 +02:00
Jan Böhmer
133652c296 Fixed PHPstan issues 2023-08-23 22:05:16 +02:00
Jan Böhmer
b9331ac1ef Prevent indexing through search engines, if we are not in demo mode 2023-08-23 22:01:39 +02:00
Jan Böhmer
08f7b2cc87 New translations security.en.xlf (English) 2023-08-23 21:51:37 +02:00
Jan Böhmer
1a2bdaf8e5 New translations validators.en.xlf (English) 2023-08-23 21:51:36 +02:00
Jan Böhmer
d81dec78ae New translations messages.en.xlf (English) 2023-08-23 21:51:35 +02:00
Jan Böhmer
f78bd03521 New translations messages.en.xlf (German) 2023-08-23 21:51:29 +02:00
Jan Böhmer
6aa16272d8 Merge remote-tracking branch 'origin/l10n_master' 2023-08-23 21:24:45 +02:00
Jan Böhmer
e80f7c08ab Bumped version to 1.7.0 2023-08-23 21:24:09 +02:00
Jan Böhmer
675f05f0fb Updated dependencies 2023-08-23 21:23:34 +02:00
Jan Böhmer
b1f23e1684 Added some documentation about the update notification 2023-08-23 21:06:10 +02:00
Jan Böhmer
d612164885 Added that clearing the octopart oauth token is required after changing octopart clientID
See discussion in issue #329. Maybe we will implement a better (more automatic) way to solve this.
2023-08-23 21:00:04 +02:00
Jan Böhmer
b257e1d5f7 New translations messages.en.xlf (German) 2023-08-23 20:51:29 +02:00
Théophile Bornon
f26776ecd5 Fix wrong environment variable name (#355)
For Octopart, the secret must be set inside the PROVIDER_OCTOPART_SECRET instead of PROVIDER_OCTOPART_CLIENT_SECRET
2023-08-23 20:46:26 +02:00
Jan Böhmer
bf4a23652c New translations messages.en.xlf (Chinese Simplified) 2023-08-22 08:00:22 +02:00
Jan Böhmer
e7681aedb1 New translations messages.en.xlf (English) 2023-08-21 23:20:48 +02:00
Jan Böhmer
098fcb29fb Upgraded dependencies 2023-08-21 23:17:42 +02:00
Jan Böhmer
eb46ea19e3 Make update checking mechanism more resilient against connection errors 2023-08-21 23:11:12 +02:00
Jan Böhmer
99ee05a90f Allow to configure update checking utility via env 2023-08-21 22:57:45 +02:00
Jan Böhmer
fd31f983af Fixed positioning of the part row selection checkboxes 2023-08-21 22:52:11 +02:00
Jan Böhmer
80bae4167f Added button to inverse part selection in tables
Fix issue #346
2023-08-21 22:49:02 +02:00
Andy
eaee4af715 Update installation_guide-debian.md (#352)
Fixed a typo: (link to configuration.md)
2023-08-21 17:06:12 +02:00
Jan Böhmer
7d4723c3e4 New translations messages.en.xlf (English) 2023-08-20 23:30:41 +02:00
Jan Böhmer
33a0981981 Added possibility to ignore the checks of withdraw amount when building projects
This fixes #349
2023-08-20 23:23:18 +02:00
Jan Böhmer
b62dc1241d Fix parameter mapping on part creation dialog to fix add builds part
This fixes issue #348
2023-08-20 20:30:38 +02:00
Jan Böhmer
e2270aec38 Upgraded further JS packages 2023-08-20 13:03:09 +02:00
Jan Böhmer
73346fcdaf Upgraded dependencies 2023-08-20 12:51:50 +02:00
Jan Böhmer
7b112512a9 Prevent that an administrator can lockout himself accidentally out of the user interface by using one of the permission presets 2023-08-20 12:42:56 +02:00
Jan Böhmer
0e5613b57b Forbid access to homepage if a user has no allow permission
This allows to block access to everything (even the homepage) for anonymous access. This fixes issue #290
2023-08-20 12:33:08 +02:00
Jan Böhmer
e66ff40733 Use the column order stored in localStorage during the initial datatables ajax call.
This way we still have the right ordering when changing pages. This fixes issue #345
2023-08-20 00:41:44 +02:00
Jan Böhmer
73d61f7440 Fixed PartKeepr import for storagelocation attachments
This should fix issue #334
2023-08-19 23:52:22 +02:00
Jan Böhmer
dedb3071d6 New translations messages.en.xlf (English) 2023-08-05 00:10:52 +02:00
Jan Böhmer
a43ee52086 Fixed static analysis issues 2023-08-05 00:07:42 +02:00
Jan Böhmer
97ccb0cb21 Allow to globally disable update checking/connection with Github 2023-08-04 23:55:41 +02:00
Jan Böhmer
1fb334b0ca Show a notification on homepage and server info page if there is a new version available. 2023-08-04 23:49:26 +02:00
Jan Böhmer
fa4af99525 RELEASE v1.6.1 2023-08-01 16:21:25 +02:00
Jan Böhmer
b3153dac68 Fixed static analysis issue and added test for UserRepository 2023-08-01 16:20:31 +02:00
Jan Böhmer
c981476706 Use proper way of overriding doctrine attributes
In older versions doctrine allowed overriding attributes, by simply redifining them in subclasses. In 2.16 this throws an exception. We now use the proper way using the AttributeOverrides attribute
2023-08-01 15:55:21 +02:00
Jan Böhmer
1a3e5ec705 Updated dependencies
The commit order changes were merged into doctrines main branch, so we can now use the official release instead of the development branch again.
2023-08-01 15:34:37 +02:00
Jan Böhmer
aaff0835a3 Renmed SAMLP_SP_PRIVATE_KEY to SAML_SP_PRIVATE_KEY
Now it matches the documented value and follows the naming schema. The old env name is still valid, to maintain backwards compatibility.

Fixes issue #339
2023-08-01 15:31:40 +02:00
Jan Böhmer
9bf814d4cd Fixed error when StructuralEntityChoice type was used for non structural entities. 2023-08-01 15:14:32 +02:00
Jan Böhmer
b5c0f37f88 Fixed exception on visiting certain group edit pages.
This fixes issue #340
2023-08-01 15:06:57 +02:00
Jan Böhmer
05d73d2f68 RELEASE v1.6.0 2023-08-01 00:22:29 +02:00
Jan Böhmer
ff284d056c Updated dependencies. 2023-08-01 00:22:03 +02:00
Jan Böhmer
2393c759f3 Merge branch 'octopart-ip' 2023-07-31 22:43:19 +02:00
Jan Böhmer
0a983513ea Added documentation for the octopart provider. 2023-07-31 22:43:14 +02:00
Jan Böhmer
c737348cea Dont skip the Sqlite migration. Do nothing instead
Before this migration was never finished and always re executed on subsequent migration attempts.
2023-07-31 22:30:02 +02:00
Jan Böhmer
e8e2258357 Added migration for SQLite 2023-07-31 22:28:19 +02:00
Jan Böhmer
d847b74522 Reenable caching for part provider system. 2023-07-31 22:21:20 +02:00
Jan Böhmer
5750e7dbdf Fixed exception if octopart did not returned an category for a part 2023-07-31 22:21:08 +02:00
Jan Böhmer
ffe76f9d2e Use full path for categories returned by Octopart 2023-07-31 22:17:24 +02:00
Jan Böhmer
5b1ad541a8 Added an caching layer to octopart provider to reduce API queries. 2023-07-31 22:11:46 +02:00
Jan Böhmer
4824a82c3f Fixed error, when octopart search got no results 2023-07-31 01:00:34 +02:00
Jan Böhmer
f7cea1100c Allow to configure various options of the octopart provider 2023-07-31 00:57:33 +02:00
Jan Böhmer
827dd01e28 Get shopping informations from octopart 2023-07-31 00:13:59 +02:00
Jan Böhmer
d969f49ecc Get specifications, mass, footprint and manufacturer status from Octopart 2023-07-31 00:01:19 +02:00
Jan Böhmer
0f336b6f89 Allow for longer manufacturer and supplier URLs 2023-07-31 00:00:57 +02:00
Jan Böhmer
43cc37d10f Added very basic octopart info provider 2023-07-30 23:04:18 +02:00
Jan Böhmer
6a00b8e168 Added possibility to manage client_credentials OAuth tokens 2023-07-30 15:24:46 +02:00
Jan Böhmer
1b3fc2c23c Fixed PHPUnit exception 2023-07-29 19:03:46 +02:00
Jan Böhmer
8e96971b9c Merge remote-tracking branch 'origin/l10n_master' 2023-07-29 18:55:48 +02:00
Jan Böhmer
8e39e330fe Updated dependencies. 2023-07-29 18:55:24 +02:00
Jan Böhmer
15ba6572df Try to detect if an external attachment is an image or not.
Before we assumed that it is a picture in all cases.
2023-07-29 18:50:18 +02:00
Jan Böhmer
1c222ff293 Improved attachment name generation from DTO url
We dont include the query paramaters in the name anymore
2023-07-29 18:38:18 +02:00
Jan Böhmer
2fb1ec7f8a New translations messages.en.xlf (German) 2023-07-29 18:10:26 +02:00
Jan Böhmer
70f1db9619 New translations messages.en.xlf (German) 2023-07-29 17:10:30 +02:00
Jan Böhmer
11be65678e Fixed order by FIELD emulation on sqlite via INSTR
We need to search for the comma token separators at both ends of the search term, otherwise the ordering will be wrong.
2023-07-29 17:06:35 +02:00
Jan Böhmer
80ed064cd6 Emulate the field function in SQLIte by using an string search, instead of our PHP function callback 2023-07-29 16:50:47 +02:00
Jan Böhmer
62b1e33616 Fixed field ordering on SQLite 2023-07-29 16:42:27 +02:00
Jan Böhmer
52f2ef6d30 New translations messages.en.xlf (German) 2023-07-28 23:30:26 +02:00
Jan Böhmer
8ace78a873 New translations messages.en.xlf (German) 2023-07-28 11:50:42 +02:00
Jan Böhmer
5c30210534 Fixed some stuff on the emulated Field2 function and added tests 2023-07-27 00:38:17 +02:00
Jan Böhmer
1d03b6c38d Added tests for sqlite emulated functions 2023-07-26 23:39:53 +02:00
Jan Böhmer
d3ead8742e Implement a special field2 function, to migitiate the argument count limit in sqlite
This fixes issue #332 on SQLite DBs
2023-07-26 23:23:25 +02:00
Jan Böhmer
ed6b0057b7 Make the manufacturer field optional on part manufacturer data during Partkeepr import
This should fix issue #333
2023-07-26 22:55:18 +02:00
Jan Böhmer
90fbcb88d8 Updated dependencies 2023-07-23 23:22:00 +02:00
Jan Böhmer
be2ed52d93 Fixed phpstan issues 2023-07-23 23:04:45 +02:00
Jan Böhmer
f1af19a52a Fixed wrong background color for structural links 2023-07-23 23:02:04 +02:00
Jan Böhmer
4c4501073c Use a two step approach to retrieve part tables
This allows us to combine the fast filtering with the fetch joins, which should speed up the datatables a lot.
2023-07-23 21:48:38 +02:00
Jan Böhmer
b8b9a416ac Fixed wrong type of the supplier filter 2023-07-23 21:33:50 +02:00
Jan Böhmer
fc7e436ca9 Improved image and attachment viewing on part info page 2023-07-23 01:32:28 +02:00
Jan Böhmer
1de1eebc59 Fixed PHPstan issues 2023-07-23 01:20:38 +02:00
Jan Böhmer
a4d411656b Added a field extension for SQLite and let the database sort the elements by the given ID order directly 2023-07-23 01:19:48 +02:00
Jan Böhmer
1ec4266f96 Fixed sorting for element choice type and added tests 2023-07-23 01:01:29 +02:00
Jan Böhmer
61f02d693f Added info about alternative names to info provider documentation 2023-07-23 00:29:16 +02:00
Jan Böhmer
cd80552ce7 Fixed PHPstan issues 2023-07-23 00:14:29 +02:00
Jan Böhmer
d59b8817c3 Do not use fetch join, as even with the N+1 problem the queries are faster than with the very complex and slow expressions needed for the fetch Join pagination 2023-07-22 23:51:06 +02:00
Jan Böhmer
8ce5f4a796 Do not cache entities directly in NodesListBuilder but cache only the IDs instead
Otherwise the doctrine proxies break, and we get issues with loading the preview_images in structural Elements.
2023-07-20 23:20:46 +02:00
Jan Böhmer
2e8cb35acc Use ManyToOne instead of OneToOne for master_picture_attachment like before 2023-07-20 21:54:13 +02:00
Jan Böhmer
c0f626e9bd Fixed phpstan issue 2023-07-19 23:01:29 +02:00
Jan Böhmer
2ddfe48aba Do the doctrine definitions of the master preview attachment in the sub classes
This makes lazy fetching works and saves some db queries.
2023-07-19 22:49:40 +02:00
Jan Böhmer
2c6de84c9a Only check every 10 minutes if the user needs to setup a 2FA method enforced by its group
That saves us 3 database queries on many requests.
2023-07-19 00:56:28 +02:00
Jan Böhmer
87cf4c2d08 Do a extra lazy fetch on webauthn and u2f keys property of a user
In the most cases we just need the count of them and not the exact details. This just gives as a bit better performance on most requests
2023-07-19 00:20:38 +02:00
Jan Böhmer
3a8c835880 Lazily sort info providers list, to prevent unneccesarry work on most container calls 2023-07-19 00:11:19 +02:00
Jan Böhmer
e2dbd3d873 Fixed bug, that prevented all related history entries for an element
As the query param had the same name for each iteration, it got overwritten every time
2023-07-19 00:01:42 +02:00
Jan Böhmer
55aabddd41 Allow to sort by user in log data table 2023-07-18 23:45:52 +02:00
Jan Böhmer
eb07820523 Fixed issue that prevented undo changes from being logged 2023-07-18 23:42:01 +02:00
Jan Böhmer
8116217019 Fixed exception when undo/revert to a change which contained a change to a enum property 2023-07-18 23:34:45 +02:00
Jan Böhmer
6fb1845ff7 Use fixed version of jbtronics/2fa-webauthn-bundle 2023-07-17 23:46:12 +02:00
Jan Böhmer
75325f0ed8 Added some trademark disclaimer to Part-DB documentation 2023-07-17 23:28:24 +02:00
Jan Böhmer
0577f9e166 Added some documentation about caching 2023-07-17 23:25:22 +02:00
Jan Böhmer
52c6884e28 Allow slashes in digikey product ids 2023-07-17 23:21:30 +02:00
Jan Böhmer
2be76a488f Added documentation for part provider system 2023-07-17 23:14:35 +02:00
Jan Böhmer
686535fe42 New translations messages.en.xlf (English) 2023-07-17 00:53:09 +02:00
Jan Böhmer
5c17aee1e4 Properly escape group table name
Otherwise MySQL 8 will not be able to use this schema
2023-07-17 00:49:25 +02:00
Jan Böhmer
9b35ac3a99 Updated yarn dependencies. 2023-07-17 00:45:46 +02:00
Jan Böhmer
59b78e850f Merge branch 'part_info_provider_integration' 2023-07-17 00:43:51 +02:00
Jan Böhmer
3a8c5a788f Fixed phpstan issues 2023-07-17 00:43:35 +02:00
Jan Böhmer
afcbbe0f43 Fixed phpunit tests 2023-07-17 00:34:00 +02:00
Jan Böhmer
d10d29e590 Do not enable the create part from provider permission automatically
This allows users to create new datastructures, which is maybe not wanted. Besides it has to be configured first.
2023-07-17 00:20:38 +02:00
Jan Böhmer
7b61cb3163 Added more env variables to configure providers 2023-07-17 00:19:02 +02:00
Jan Böhmer
4c1c6701b3 Test availability of the info provider pages 2023-07-16 23:56:30 +02:00
Jan Böhmer
f423fdf7f8 Fixed bug in DB schema, which prevented the creation of parts without info provider reference 2023-07-16 23:48:55 +02:00
Jan Böhmer
a5995a2ce8 Centralized logic for part creation form 2023-07-16 23:46:20 +02:00
Jan Böhmer
c810b6772c Show the value returned by the provider on part creation page.
This makes it easier to check or assign a element manually
2023-07-16 23:19:02 +02:00
Jan Böhmer
b74ab18a6d Added possibility to define alternative names on data structures
This can be used to find elements, based on the data returned by info providers
2023-07-16 22:59:46 +02:00
Jan Böhmer
edc54aaf91 Added migrations for sqlite and new additional_names field 2023-07-16 20:47:25 +02:00
Jan Böhmer
b3b205cd6e Added permissions to control access to info providers and oauth tokens 2023-07-16 20:33:24 +02:00
Jan Böhmer
2fe4def775 New translations messages.en.xlf (Chinese Simplified) 2023-07-16 20:30:24 +02:00
Jan Böhmer
7bbf612394 Fixed title of info providers list 2023-07-16 20:09:20 +02:00
Jan Böhmer
97ab1f0492 Improved search page 2023-07-16 20:05:11 +02:00
Jan Böhmer
81bfcdd158 New translations messages.en.xlf (Chinese Simplified) 2023-07-16 19:30:23 +02:00
Jan Böhmer
6862d318f0 Cache the DTO objects returned by the info providers
This saves API requests
2023-07-16 19:05:26 +02:00
Jan Böhmer
412fa3f0bf Get datasheets and category from digikey 2023-07-16 18:35:44 +02:00
Jan Böhmer
01d9109c45 Improved digikey provider 2023-07-16 17:10:48 +02:00
Jan Böhmer
dd914d1d64 New translations messages.en.xlf (Chinese Simplified) 2023-07-16 16:56:03 +02:00
Jan Böhmer
5cf3624a3a New translations messages.en.xlf (Chinese Simplified) 2023-07-16 13:20:24 +02:00
Jan Böhmer
c7ff8c2dd1 New translations messages.en.xlf (Chinese Simplified) 2023-07-16 12:20:29 +02:00
Jan Böhmer
83c202d675 New translations messages.en.xlf (Chinese Simplified) 2023-07-16 11:20:25 +02:00
Jan Böhmer
f7648e3311 Added an button to connect the oauth providers from WebUI 2023-07-16 03:18:33 +02:00
Jan Böhmer
c203de082e Added proper OAuth authentication for digikey and other providers 2023-07-16 03:07:53 +02:00
Jan Böhmer
a95ba1acc4 Add a reference to the used info provider to a part 2023-07-16 01:24:49 +02:00
Jan Böhmer
db325525e4 New translations messages.en.xlf (Chinese Simplified) 2023-07-15 22:30:25 +02:00
Jan Böhmer
db97114fb4 Use preview image and other additional images provided by the info provider 2023-07-15 21:41:35 +02:00
Jan Böhmer
b18a300f10 New translations validators.en.xlf (Chinese Simplified) 2023-07-15 21:30:27 +02:00
Jan Böhmer
61ffb857ee New translations messages.en.xlf (Chinese Simplified) 2023-07-15 21:30:26 +02:00
Jan Böhmer
701212239d Use an experimental doctrine/orm version to fix some issues persisting attachments while simutanously creating a new attachment type
The circular reference between attachmentTypeAttachment and attachmentType seems to confuse doctrine. This is fixed in the experimental version
2023-07-15 21:17:10 +02:00
Jan Böhmer
422fa01c6f Use the initial element for database if the value was not changed. 2023-07-15 21:00:45 +02:00
Jan Böhmer
62820b4dd9 New translations validators.en.xlf (Chinese Simplified) 2023-07-15 20:30:54 +02:00
Jan Böhmer
8ea92ef330 Added tests for DTOConverter 2023-07-15 18:18:35 +02:00
Jan Böhmer
de82249d8d Provide footprint information on TMEProvider 2023-07-15 01:52:46 +02:00
Jan Böhmer
94a26ae75a Allow to extract ranges from paramaters 2023-07-15 01:41:29 +02:00
Jan Böhmer
f9fdae9de9 Added an TME data provider 2023-07-15 01:01:20 +02:00
Jan Böhmer
0cb46039dd Allow to retrieve price and shopping informations from info providers 2023-07-14 00:09:22 +02:00
Jan Böhmer
c4439cc9db Mark newly created entities better in structural entity selector 2023-07-12 23:58:40 +02:00
Jan Böhmer
6cd9640b30 Allow to automatically find or create entities from database based on info providers 2023-07-12 23:43:16 +02:00
Jan Böhmer
f9bce3dfdb Merge branch 'master' into part_info_provider_integration 2023-07-11 22:04:06 +02:00
Jan Böhmer
8eb0c997ed Updated dependencies 2023-07-11 22:01:32 +02:00
Jan Böhmer
a8f96e06bd Automatically whitelist the SAML IIDP domain for external redirect
This fixes issue #318
2023-07-11 21:50:55 +02:00
Jan Böhmer
9a2945927f New translations security.en.xlf (Chinese Simplified) 2023-07-10 23:36:28 +02:00
Jan Böhmer
89f9249ec6 New translations messages.en.xlf (Chinese Simplified) 2023-07-10 23:36:27 +02:00
Jan Böhmer
24f572253f New translations messages.en.xlf (Chinese Simplified) 2023-07-10 22:36:25 +02:00
Jan Böhmer
4fc9c19893 Removed useless tabs in code blocks in installation guide 2023-07-10 13:26:07 +02:00
Jan Böhmer
f6fcd730a8 Added how you checkout a stable release in the installation guide
Related to discussion #317
2023-07-10 13:20:44 +02:00
Jan Böhmer
716a56979d Added basic possibilty to create parts based on infoProviders 2023-07-09 23:31:40 +02:00
Jan Böhmer
538476be99 Added a info provider for element14/Farnell 2023-07-09 18:51:54 +02:00
Jan Böhmer
93a170a893 Added basic search system in info providers 2023-07-09 17:55:41 +02:00
Jan Böhmer
e0301f096f Added an very basic system to configure info providers 2023-07-09 14:27:41 +02:00
Jan Böhmer
9e3cb4d694 Use enum for manufacturing status 2023-07-08 23:49:47 +02:00
Jan Böhmer
49b76c3e43 New translations messages.en.xlf (English) 2023-07-08 23:16:14 +02:00
Jan Böhmer
9962784991 New translations messages.en.xlf (Chinese Simplified) 2023-07-08 23:16:12 +02:00
Jan Böhmer
6336b38cfc Bumped version to 1.6.0-dev 2023-07-08 23:07:33 +02:00
Jan Böhmer
2362835275 Merge branch 'user_impersonator' 2023-07-08 23:07:12 +02:00
Jan Böhmer
8a4ede9d43 Write to event log, when a user gets impersonated 2023-07-08 23:07:06 +02:00
Jan Böhmer
cc1595e048 Added button to user admin to impersonate a certain user 2023-07-08 22:43:41 +02:00
Jan Böhmer
ca16763423 Bumped version to 1.5.1 2023-07-08 21:11:55 +02:00
Jan Böhmer
b6dd5bb881 Fixed ordering columns of tables when columns were reordered 2023-07-08 20:16:52 +02:00
Jan Böhmer
f8e299ec56 Added new env option to show all parts on a page by default
Related to discussion #312
2023-07-08 19:33:23 +02:00
Jan Böhmer
91e9c6e048 Use bootstrap popover for title attribute in datatables 2023-07-08 19:08:00 +02:00
Jan Böhmer
b941b97eee Show full paths of elements on hover in part tables
Related to discussion #312
2023-07-08 19:02:43 +02:00
Jan Böhmer
d38ac652fc Do not cut QR code on small label pages
Fixes issue #314
2023-07-08 18:46:29 +02:00
Jan Böhmer
bdcf3b71ce Fixed exception when parameter constraint unit field is empty 2023-07-08 18:39:44 +02:00
Jan Böhmer
ddbf8b7725 Fixed phpstan issue 2023-07-04 00:35:57 +02:00
Jan Böhmer
a6fd4547a7 Bumped version to 1.5.1 2023-07-04 00:31:37 +02:00
Jan Böhmer
d20153c569 Added basic logic for impersonation 2023-07-04 00:31:13 +02:00
Jan Böhmer
15e072a2ff Fixed exception when the calculated minimum_order_price is null
This fixes issue #311
2023-07-03 23:41:39 +02:00
Jan Böhmer
f98e20aa84 Fixed errors importing partkeepr databases 2023-07-03 23:33:45 +02:00
Jan Böhmer
e7a1b33ae6 Allow to set the exchange rate of a currency to null (not existing) after it was set once 2023-07-03 22:15:58 +02:00
Jan Böhmer
2d5f23271f Force that an currency has an iso currency code
Otherwise it will crash a lot of formatter code (and a currency which is not existing is not really useful)
2023-07-03 22:11:12 +02:00
Jan Böhmer
059110ae7a Improved styling of a info level flash toast in darkmode 2023-07-03 22:01:39 +02:00
Jan Böhmer
63726b09d6 New translations security.en.xlf (English) 2023-06-30 00:26:50 +02:00
Jan Böhmer
f78d42cc30 New translations validators.en.xlf (English) 2023-06-30 00:26:49 +02:00
Jan Böhmer
5d1c807a86 New translations messages.en.xlf (English) 2023-06-30 00:26:48 +02:00
Jan Böhmer
fad077aad9 New translations security.en.xlf (Chinese Simplified) 2023-06-30 00:26:47 +02:00
Jan Böhmer
24f47bee73 New translations validators.en.xlf (Chinese Simplified) 2023-06-30 00:26:46 +02:00
Jan Böhmer
d93b7b2cb2 New translations messages.en.xlf (Chinese Simplified) 2023-06-30 00:26:45 +02:00
Jan Böhmer
ab03111a84 New translations security.en.xlf (German) 2023-06-30 00:26:38 +02:00
Jan Böhmer
1ba03b69f6 New translations validators.en.xlf (German) 2023-06-30 00:26:37 +02:00
Jan Böhmer
9957f64628 New translations messages.en.xlf (German) 2023-06-30 00:26:36 +02:00
221 changed files with 29761 additions and 6148 deletions

View File

@@ -28,13 +28,19 @@
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
PassEnv DEFAULT_LANG DEFAULT_TIMEZONE BASE_CURRENCY INSTANCE_NAME ALLOW_ATTACHMENT_DOWNLOADS USE_GRAVATAR MAX_ATTACHMENT_FILE_SIZE DEFAULT_URI CHECK_FOR_UPDATES
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
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 SAMLP_SP_PRIVATE_KEY
# 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 TABLE_DEFAULT_PAGE_SIZE
PassEnv PROVIDER_DIGIKEY_CLIENT_ID PROVIDER_DIGIKEY_SECRET PROVIDER_DIGIKEY_CURRENCY PROVIDER_DIGIKEY_LANGUAGE PROVIDER_DIGIKEY_COUNTRY
PassEnv PROVIDER_ELEMENT14_KEY PROVIDER_ELEMENT14_STORE_ID
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
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to

61
.env
View File

@@ -44,6 +44,9 @@ DEFAULT_URI="https://partdb.changeme.invalid/"
# Leave this empty, to make all change reasons optional
ENFORCE_CHANGE_COMMENTS_FOR=""
# Disable that if you do not want that Part-DB connects to GitHub to check for available updates, or if your server can not connect to the internet
CHECK_FOR_UPDATES=1
###################################################################################
# Email settings
###################################################################################
@@ -84,6 +87,62 @@ ERROR_PAGE_ADMIN_EMAIL=''
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
ERROR_PAGE_SHOW_HELP=1
##################################################################################
# Part table settings
##################################################################################
# The default page size for the part table (set to -1 to show all parts on one page)
TABLE_DEFAULT_PAGE_SIZE=50
##################################################################################
# Info provider settings
##################################################################################
# Digikey Provider:
# You can get your client id and secret from https://developer.digikey.com/
PROVIDER_DIGIKEY_CLIENT_ID=
PROVIDER_DIGIKEY_SECRET=
# The currency to get prices in
PROVIDER_DIGIKEY_CURRENCY=EUR
# The language to get results in (en, de, fr, it, es, zh, ja, ko)
PROVIDER_DIGIKEY_LANGUAGE=en
# The country to get results for
PROVIDER_DIGIKEY_COUNTRY=DE
# Farnell Provider:
# You can get your API key from https://partner.element14.com/
PROVIDER_ELEMENT14_KEY=
# Configure the store domain you want to use. This decides the language and currency of results. You can get a list of available stores from https://partner.element14.com/docs/Product_Search_API_REST__Description
PROVIDER_ELEMENT14_STORE_ID=de.farnell.com
# TME Provider:
# You can get your API key from https://developers.tme.eu/en/
PROVIDER_TME_KEY=
PROVIDER_TME_SECRET=
# The currency to get prices in
PROVIDER_TME_CURRENCY=EUR
# The language to get results in (en, de, pl)
PROVIDER_TME_LANGUAGE=en
# The country to get results for
PROVIDER_TME_COUNTRY=DE
# Set this to 1 to get gross prices (including VAT) instead of net prices
PROVIDER_TME_GET_GROSS_PRICES=1
# Octopart / Nexar Provider:
# You can get your API key from https://nexar.com/api
PROVIDER_OCTOPART_CLIENT_ID=
PROVIDER_OCTOPART_SECRET=
# The currency and country to get prices for (you have to set both to get meaningful results)
# 3 letter ISO currency code (e.g. EUR, USD, GBP)
PROVIDER_OCTOPART_CURRENCY=EUR
# 2 letter ISO country code (e.g. DE, US, GB)
PROVIDER_OCTOPART_COUNTRY=DE
# The number of results to get from Octopart while searching (please note that this counts towards your API limits)
PROVIDER_OCTOPART_SEARCH_LIMIT=10
# Set to false to include non authorized offers in the results
PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1
###################################################################################
# SAML Single sign on-settings
###################################################################################
@@ -116,7 +175,7 @@ SAML_SP_ENTITY_ID="https://partdb.changeme.invalid/sp"
# The public certificate of the SAML SP
SAML_SP_X509_CERT="MIIC..."
# The private key of the SAML SP
SAMLP_SP_PRIVATE_KEY="MIIE..."
SAML_SP_PRIVATE_KEY="MIIE..."
######################################################################################

View File

@@ -19,7 +19,7 @@ jobs:
APP_ENV: prod
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache

View File

@@ -17,11 +17,11 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
-
name: Docker meta
id: docker_meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
@@ -49,12 +49,12 @@ jobs:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,arm'
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
@@ -65,7 +65,7 @@ jobs:
-
name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache

View File

@@ -38,7 +38,7 @@ jobs:
if: matrix.db-type == 'sqlite'
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -51,6 +51,7 @@ Two-factor authentication is supported (Google Authenticator and Webauthn/U2F ke
* Support for multiple currencies and automatic update of exchange rates supported
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
* Automatic thumbnail generation for pictures
* Use cloud providers (like Octopart, Digikey, farnell or TME) to automatically get part information, datasheets and prices for parts
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,

View File

@@ -1 +1 @@
1.5.0
1.7.2

View File

@@ -65,14 +65,19 @@ export default class extends Controller {
localStorage.setItem( this.getStateSaveKey(), JSON.stringify(data) );
}
stateLoadCallback(settings) {
const data = JSON.parse( localStorage.getItem(this.getStateSaveKey()) );
stateLoadCallback() {
const json = localStorage.getItem(this.getStateSaveKey());
if(json === null || json === undefined) {
return null;
}
const data = JSON.parse(json);
if (data) {
//Do not save the start value (current page), as we want to always start at the first page on a page reload
data.start = 0;
//50 is the default length supplied by datatables, reset it to that value
data.length = 50;
delete data.start;
//Reset the data length to the default value by deleting the length property
delete data.length;
}
return data;
@@ -90,6 +95,19 @@ export default class extends Controller {
//Add url info, as the one available in the history is not enough, as Turbo may have not changed it yet
settings.url = this.element.dataset.dtUrl;
//Add initial_order info to the settings, so that the order on the initial page load is the one saved in the state
const saved_state = this.stateLoadCallback();
if (saved_state !== null) {
const raw_order = saved_state.order;
settings.initial_order = raw_order.map((order) => {
return {
column: order[0],
dir: order[1]
}
});
}
let options = {
colReorder: true,
responsive: true,
@@ -221,4 +239,16 @@ export default class extends Controller {
return this.element.dataset.select ?? false;
}
invertSelection() {
//Do nothing if the datatable is not selectable
if(!this.isSelectable()) {
return;
}
//Invert the selected rows on the datatable
const selected_rows = this._dt.rows({selected: true});
this._dt.rows().select();
selected_rows.deselect();
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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)
*
* 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 * as bootbox from "bootbox";
import "../../css/components/bootbox_extensions.css";
export default class extends Controller
{
static values = {
message: String,
title: String
}
connect()
{
this._confirmed = false;
this.element.addEventListener('click', this._onClick.bind(this));
}
_onClick(event)
{
//If a user has not already confirmed the deletion, just let turbo do its work
if (this._confirmed) {
this._confirmed = false;
return;
}
event.preventDefault();
event.stopPropagation();
const that = this;
bootbox.confirm({
title: this.titleValue,
message: this.messageValue,
callback: (result) => {
if (result) {
//Set a flag to prevent the dialog from popping up again and allowing turbo to submit the form
that._confirmed = true;
//Click the link
that.element.click();
} else {
that._confirmed = false;
}
}
});
}
}

View File

@@ -22,6 +22,8 @@ import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
import {Controller} from "@hotwired/stimulus";
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
export default class extends Controller {
_tomSelect;
@@ -40,8 +42,7 @@ export default class extends Controller {
allowEmptyOption: true,
selectOnTab: true,
maxOptions: null,
create: allowAdd,
createFilter: /\D/, //Must contain a non-digit character, otherwise they would be recognized as DB ID
create: allowAdd ? this.createItem.bind(this) : false,
searchField: [
{field: "text", weight : 2},
@@ -68,6 +69,15 @@ export default class extends Controller {
this._tomSelect.sync();
}
createItem(input, callback) {
callback({
//$%$ is a special value prefix, that is used to identify items, that are not yet in the DB
value: '$%$' + input,
text: input,
not_in_db_yet: true,
});
}
updateValidity() {
//Mark this input as invalid, if the selected option is disabled
@@ -97,14 +107,27 @@ export default class extends Controller {
}
if (data.short) {
return '<div><b>' + escape(data.short) + '</b></div>';
let short = escape(data.short)
//Make text italic, if the item is not yet in the DB
if (data.not_in_db_yet) {
short = '<i>' + short + '</i>';
}
return '<div><b>' + short + '</b></div>';
}
let name = "";
if (data.parent) {
name += escape(data.parent) + "&nbsp;→&nbsp;";
}
name += "<b>" + escape(data.text) + "</b>";
if (data.not_in_db_yet) {
//Not yet added items are shown italic and with a badge
name += "<i><b>" + escape(data.text) + "</b></i>" + "<span class='ms-3 badge bg-info badge-info'>" + trans(ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB) + "</span>";
} else {
name += "<b>" + escape(data.text) + "</b>";
}
return '<div>' + (data.image ? "<img class='structural-entity-select-image' style='margin-right: 5px;' ' src='" + data.image + "'/>" : "") + name + '</div>';
}

View File

@@ -0,0 +1,65 @@
/*
* 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)
*
* 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";
/**
* This controller is used on a checkbox, which toggles the max value of all number input fields
*/
export default class extends Controller {
_checkbox;
getCheckbox() {
if (this._checkbox) {
return this._checkbox;
}
//Find the checkbox inside the controller element
this._checkbox = this.element.querySelector('input[type="checkbox"]');
return this._checkbox;
}
connect() {
//Add event listener to the checkbox
this.getCheckbox().addEventListener('change', this.toggleInputLimits.bind(this));
}
toggleInputLimits() {
//Find all input fields with the data-toggle-input-limits-target="max"
const inputFields = document.querySelectorAll("input[type='number']");
inputFields.forEach((inputField) => {
//Ensure that the input field has either a max or a data-max attribute
if (!inputField.hasAttribute('max') && !inputField.hasAttribute('data-max')) {
return;
}
//If the checkbox is checked, rename the max attribute to data-max
if (this.getCheckbox().checked) {
inputField.setAttribute('data-max', inputField.getAttribute('max'));
inputField.removeAttribute('max');
} else {
//If the checkbox is not checked, rename the data-max attribute back to max
inputField.setAttribute('max', inputField.getAttribute('data-max'));
inputField.removeAttribute('data-max');
}
});
}
}

View File

@@ -67,7 +67,6 @@ ul.structural_link {
padding-bottom: 7px;
padding-left: 0;
list-style: none;
background-color: inherit;
}
/* Display list items side by side */

View File

@@ -91,7 +91,7 @@ th.select-checkbox {
/** Fix datatables select-checkbox position */
table.dataTable tr.selected td.select-checkbox:after
{
margin-top: -25px !important;
margin-top: -20px !important;
}

View File

@@ -47,7 +47,8 @@
method: config.method,
data: {
_dt: config.name,
_init: true
_init: true,
order: config.initial_order ?? undefined,
}
}).done(function(data) {
var baseState;
@@ -72,6 +73,17 @@
}
} else {
request._dt = config.name;
//Try to resolve the original column index when the column was reordered (using the ColReorder plugin)
//Only do this when _ColReorder_iOrigCol is available
if (settings.aoColumns && settings.aoColumns.length && settings.aoColumns[0]._ColReorder_iOrigCol !== undefined) {
if (request.order && request.order.length) {
request.order.forEach(function (order) {
order.column = settings.aoColumns[order.column]._ColReorder_iOrigCol;
});
}
}
$.ajax(typeof config.url === 'function' ? config.url(dt) : config.url, {
method: config.method,
data: request

View File

@@ -59,13 +59,16 @@ class RegisterEventHelper {
}
registerTooltips() {
this.registerLoadHandler(() => {
const handler = () => {
$(".tooltip").remove();
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
$('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i.fas[title]')
$('a[title], label[title], button[title]:not([data-bs-toggle="dropdown"]), p[title], span[title], h6[title], h3[title], i[title], small[title]')
//@ts-ignore
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
});
};
this.registerLoadHandler(handler);
document.addEventListener('dt:loaded', handler);
}
registerSpecialCharInput() {

View File

@@ -18,7 +18,7 @@
"doctrine/dbal": "^3.4.6",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.9",
"doctrine/orm": "^2.16",
"dompdf/dompdf": "dev-master#87bea32efe0b0db309e1d31537201f64d5508280 as v2.0.3",
"erusev/parsedown": "^1.7",
"florianv/swap": "^4.0",
@@ -27,6 +27,7 @@
"jbtronics/2fa-webauthn": "^v2.0.0",
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
"jfcherng/php-diff": "^6.14",
"knpuniversity/oauth2-client-bundle": "^2.15",
"league/csv": "^9.8.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
@@ -37,7 +38,7 @@
"ocramius/proxy-manager": "2.2.*",
"omines/datatables-bundle": "^0.7.2",
"part-db/label-fonts": "^1.0",
"php-translation/symfony-bundle": "^0.13.0",
"php-translation/symfony-bundle": "^0.14.0",
"phpdocumentor/reflection-docblock": "^5.2",
"s9e/text-formatter": "^2.1",
"scheb/2fa-backup-code": "^6.8.0",
@@ -68,7 +69,7 @@
"symfony/serializer": "6.3.*",
"symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*",
"symfony/ux-translator": "2.x-dev",
"symfony/ux-translator": "^2.10",
"symfony/ux-turbo": "^2.0",
"symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*",
@@ -94,7 +95,7 @@
"phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan-symfony": "^1.1.7",
"psalm/plugin-symfony": "^v5.0.1",
"rector/rector": "^0.17.0",
"rector/rector": "^0.18.0",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*",
@@ -103,7 +104,7 @@
"symfony/phpunit-bridge": "6.3.*",
"symfony/stopwatch": "6.3.*",
"symfony/web-profiler-bundle": "6.3.*",
"symplify/easy-coding-standard": "^11.0",
"symplify/easy-coding-standard": "^12.0",
"vimeo/psalm": "^5.6.0"
},
"suggest": {

2211
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,4 +30,5 @@ return [
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
Jbtronics\DompdfFontLoaderBundle\DompdfFontLoaderBundle::class => ['all' => true],
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
];

View File

@@ -20,3 +20,6 @@ framework:
tree.cache:
adapter: cache.app
tags: true
info_provider.cache:
adapter: cache.app

View File

@@ -9,7 +9,7 @@ datatables:
# Set options, as documented at https://datatables.net/reference/option/
options:
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
pageLength: 50
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
#dom: "<'row' <'col-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pif>>"
dom: " <'row'<'col mb-2 input-group' B l> <'col mb-2' <'pull-end' p>>>
<'card'

View File

@@ -38,6 +38,8 @@ doctrine:
string_functions:
regexp: DoctrineExtensions\Query\Mysql\Regexp
ifnull: DoctrineExtensions\Query\Mysql\IfNull
field: DoctrineExtensions\Query\Mysql\Field
field2: App\Doctrine\Functions\Field2
when@test:
doctrine:

View File

@@ -4,6 +4,9 @@ framework:
csrf_protection: true
handle_all_throwables: true
# We set this header by ourself, 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
# TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278)
http_method_override: true

View File

@@ -0,0 +1,5 @@
framework:
http_client:
default_options:
headers:
'User-Agent': 'Part-DB'

View File

@@ -0,0 +1,38 @@
knpu_oauth2_client:
clients:
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
ip_digikey_oauth:
type: generic
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
client_id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%'
client_secret: '%env(PROVIDER_DIGIKEY_SECRET)%'
redirect_route: 'oauth_client_check'
redirect_params: {name: 'ip_digikey_oauth'}
provider_options:
urlAuthorize: 'https://api.digikey.com/v1/oauth2/authorize'
urlAccessToken: 'https://api.digikey.com/v1/oauth2/token'
urlResourceOwnerDetails: ''
# Sandbox
#urlAuthorize: 'https://sandbox-api.digikey.com/v1/oauth2/authorize'
#urlAccessToken: 'https://sandbox-api.digikey.com/v1/oauth2/token'
#urlResourceOwnerDetails: ''
ip_octopart_oauth:
type: generic
provider_class: '\League\OAuth2\Client\Provider\GenericProvider'
client_id: '%env(PROVIDER_OCTOPART_CLIENT_ID)%'
client_secret: '%env(PROVIDER_OCTOPART_SECRET)%'
redirect_route: 'oauth_client_check'
redirect_params: { name: 'ip_octopart_oauth' }
provider_options:
urlAuthorize: 'https://identity.nexar.com/connect/authorize'
urlAccessToken: 'https://identity.nexar.com/connect/token'
urlResourceOwnerDetails: ''

View File

@@ -1,5 +1,9 @@
# See https://github.com/SAML-Toolkits/php-saml for more information about the SAML settings
# Define a parameter here, so we can access it later in the default fallback
parameters:
saml.sp.privateKey: '%env(string:SAML_SP_PRIVATE_KEY)%'
nbgrp_onelogin_saml:
onelogin_settings:
default:
@@ -22,7 +26,9 @@ nbgrp_onelogin_saml:
url: '%partdb.default_uri%logout'
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
x509cert: '%env(string:SAML_SP_X509_CERT)%'
privateKey: '%env(string:SAMLP_SP_PRIVATE_KEY)%'
# Before the env variable was wrongly named "SAMLP_SP_PRIVATE_KEY".
# For compatibility reasons we keep it and only fallback to the new name if the old one is not set. This may be removed in the future.
privateKey: '%env(string:default:saml.sp.privateKey:string:SAMLP_SP_PRIVATE_KEY)%'
# Optional settings
#baseurl: 'http://myapp.com'

View File

@@ -12,6 +12,13 @@ nelmio_security:
external_redirects:
abort: true
log: true
allow_list:
# Whitelist the domain of the SAML IDP, so we can redirect to it during the SAML login process
- '%env(string:key:host:url:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%'
# Whitelist the info provider APIs (OAuth redirects)
- 'digikey.com'
- 'nexar.com'
# forces Microsoft's XSS-Protection with
# its block mode

View File

@@ -21,6 +21,9 @@ security:
user_checker: App\Security\UserChecker
entry_point: form_login
# Enable user impersonation
switch_user: { role: CAN_SWITCH_USER }
two_factor:
auth_form_path: 2fa_login
check_path: 2fa_login_check

View File

@@ -11,7 +11,7 @@ parameters:
partdb.banner: '%env(trim:string:BANNER)%' # The info text shown in the homepage, if empty config/banner.md is used
partdb.default_currency: '%env(string:BASE_CURRENCY)%' # The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country
partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme
partdb.locale_menu: ['en', 'de', 'fr', 'ru', 'ja'] # The languages that are shown in user drop down menu
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja'] # The languages that are shown in user drop down menu
partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all.
partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails
@@ -23,6 +23,8 @@ parameters:
partdb.users.use_gravatar: '%env(bool:USE_GRAVATAR)%' # Set to false, if no Gravatar images should be used for user profiles.
partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured.
partdb.check_for_updates: '%env(bool:CHECK_FOR_UPDATES)' # Set to false, if Part-DB should not contact the GitHub API to check for updates
######################################################################################################################
# Mail settings
######################################################################################################################
@@ -48,6 +50,11 @@ parameters:
######################################################################################################################
partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled
######################################################################################################################
# Table settings
######################################################################################################################
partdb.table.default_page_size: '%env(int:TABLE_DEFAULT_PAGE_SIZE)%' # The default number of entries shown per page in tables
######################################################################################################################
# Sidebar
######################################################################################################################
@@ -119,6 +126,8 @@ parameters:
env(EMAIL_SENDER_NAME): 'Part-DB Mailer'
env(ALLOW_EMAIL_PW_RESET): 0
env(TABLE_DEFAULT_PAGE_SIZE): 50
env(TRUSTED_PROXIES): '127.0.0.1' #By default trust only our own server
env(TRUSTED_HOSTS): '' # Trust all host names by default

View File

@@ -139,6 +139,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
ic_logos:
label: "perm.tools.ic_logos"
info_providers:
label: "perm.part.info_providers"
operations:
create_parts:
label: "perm.part.info_providers.create_parts"
alsoSet: ['parts.create']
groups:
label: "perm.groups"
group: "system"
@@ -190,6 +197,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
set_password:
label: "perm.users.set_password"
alsoSet: 'read'
impersonate:
label: "perm.users.impersonate"
alsoSet: ['set_password']
change_user_settings:
label: "perm.users.change_user_settings"
show_history:
@@ -239,6 +249,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
alsoSet: 'show_logs'
server_infos:
label: "perm.server_infos"
manage_oauth_tokens:
label: "Manage OAuth tokens"
show_updates:
label: "perm.system.show_available_updates"
attachments:
label: "perm.part.attachments"

View File

@@ -24,6 +24,9 @@ services:
App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface:
tags: ['app.label_placeholder_provider']
App\Services\InfoProviderSystem\Providers\InfoProviderInterface:
tags: ['app.info_provider']
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
@@ -234,6 +237,46 @@ services:
$rootNodeExpandedByDefault: '%partdb.sidebar.root_expanded%'
$rootNodeEnabled: '%partdb.sidebar.root_node_enable%'
####################################################################################################################
# Part info provider system
####################################################################################################################
App\Services\InfoProviderSystem\ProviderRegistry:
arguments:
$providers: !tagged_iterator 'app.info_provider'
App\Services\InfoProviderSystem\Providers\Element14Provider:
arguments:
$api_key: '%env(string:PROVIDER_ELEMENT14_KEY)%'
$store_id: '%env(string:PROVIDER_ELEMENT14_STORE_ID)%'
App\Services\InfoProviderSystem\Providers\DigikeyProvider:
arguments:
$clientId: '%env(string:PROVIDER_DIGIKEY_CLIENT_ID)%'
$currency: '%env(string:PROVIDER_DIGIKEY_CURRENCY)%'
$language: '%env(string:PROVIDER_DIGIKEY_LANGUAGE)%'
$country: '%env(string:PROVIDER_DIGIKEY_COUNTRY)%'
App\Services\InfoProviderSystem\Providers\TMEClient:
arguments:
$secret: '%env(string:PROVIDER_TME_SECRET)%'
$token: '%env(string:PROVIDER_TME_KEY)%'
App\Services\InfoProviderSystem\Providers\TMEProvider:
arguments:
$currency: '%env(string:PROVIDER_TME_CURRENCY)%'
$country: '%env(string:PROVIDER_TME_COUNTRY)%'
$language: '%env(string:PROVIDER_TME_LANGUAGE)%'
$get_gross_prices: '%env(bool:PROVIDER_TME_GET_GROSS_PRICES)%'
App\Services\InfoProviderSystem\Providers\OctopartProvider:
arguments:
$clientId: '&env(string:PROVIDER_OCTOPART_CLIENT_ID)%'
$secret: '%env(string:PROVIDER_OCTOPART_SECRET)%'
$country: '%env(string:PROVIDER_OCTOPART_COUNTRY)%'
$currency: '%env(string:PROVIDER_OCTOPART_CURRENCY)%'
$search_limit: '%env(int:PROVIDER_OCTOPART_SEARCH_LIMIT)%'
$onlyAuthorizedSellers: '%env(bool:PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS)%'
####################################################################################################################
# Symfony overrides
####################################################################################################################
@@ -272,6 +315,10 @@ services:
arguments:
$project_dir: '%kernel.project_dir%'
App\Services\System\UpdateAvailableManager:
arguments:
$check_for_updates: '%partdb.check_for_updates%'
####################################################################################################################
# Monolog
####################################################################################################################

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

View File

@@ -36,6 +36,7 @@ The following configuration options can only be changed by the server administra
* `datastructure_edit`: Edit operation of a existing datastructure (e.g. category, manufacturer, ...)
* `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...)
* `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...)
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new versions, or if your server can not connect to the internet.
### E-Mail settings
* `MAILER_DSN`: You can configure the mail provider which should be used for email delivery (see https://symfony.com/doc/current/components/mailer.html for full documentation). If you just want to use an SMTP mail account, you can use the following syntax `MAILER_DSN=smtp://user:password@smtp.mailserver.invalid:587`
@@ -43,6 +44,9 @@ The following configuration options can only be changed by the server administra
* `EMAIL_SENDER_NAME`: Similar to `EMAIL_SENDER_EMAIL` but this allows you to specify the name from which the mails are sent from.
* `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you wan to allow users to reset their password via an email notification. You have to configure the mailprovider first before via the MAILER_DSN setting.
### Table related settings
* `TABLE_DEFAULT_PAGE_SIZE`: The default page size for tables. This is the number of rows which are shown per page. Set to `-1` to disable pagination and show all rows at once.
### History/Eventlog related settings
The following options are used to configure, which (and how much) data is written to the system log:
* `HISTORY_SAVE_CHANGED_FIELDS`: When this option is set to true, the name of the fields which are changed, are saved to the DB (so for example it is logged that a user has changed, that the user has changed the name and description of the field, but not the data/content of these changes)
@@ -71,6 +75,9 @@ You can find more advanced settings in the `config/packages/hslavich_onelogin_sa
* `SAML_SP_X509_CERT`: The public X.509 certificate of your SAML SP (here Part-DB). This is the value you have configured for the Part-DB client in your IdP. It should start with `MIIC` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which you can setup here and in the `SAML_SP_PRIVATE_KEY` setting.
* `SAML_SP_PRIVATE_KEY`: The private key of your SAML SP (here Part-DB), corresponding the public key specified in `SAML_SP_X509_CERT`. This is the value you have configured for the Part-DB client in your IdP. It should start with `MIIE` and end with `=`. IdPs like keycloak allows you to generate a public/private key pair for the client which you can setup here and in the `SAML_SP_X509_CERT` setting.
### Information provider settings
The settings prefixes with `PROVIDER_*` are used to configure the information providers.
See the [information providers]({% link usage/information_provider_system.md %}) page for more information.
### Other / less used options
* `TRUSTED_PROXIES`: Set the IP addresses (or IP blocks) of trusted reverse proxies here. This is needed to get correct IP informations (see [here](https://symfony.com/doc/current/deployment/proxies.html) for more info).

View File

@@ -37,6 +37,7 @@ It is installed on a web server and so can be accessed with any browser without
* Support for multiple currencies and automatic update of exchange rates supported
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
* Easy migration from an existing PartKeepr instance (see [here]({%link partkeepr_migration.md %}))
* 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 %}))
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

@@ -24,7 +24,7 @@ sudo apt install git curl zip ca-certificates software-properties-common apt-tra
### Install PHP and apache2
Part-DB is written in [PHP](https://php.net) and therefore needs an PHP interpreter to run. Part-DB needs PHP 8.1 or higher, however it is recommended to use the most recent version of PHP for performance reasons and future compatibility.
As Debian 11 does not ship PHP 8.1 in it's default repositories, we have to add a repository for it. You can skip this step if your distribution is shipping a recent version of PHP or you want to use the built-in PHP version.
As Debian 11 does not ship PHP 8.1 in it's default repositories, we have to add a repository for it. You can skip this step if your distribution is shipping a recent version of PHP or you want to use the built-in PHP version. If you are using Debian 12, you can skip this step, as PHP 8.1 is already included in the default repositories.
```bash
# Add sury repository for PHP 8.1
sudo curl -sSL https://packages.sury.org/php/README.txt | sudo bash -x
@@ -74,6 +74,17 @@ We now have all prerequisites installed and can start to install Part-DB. We wil
git clone https://github.com/Part-DB/Part-DB-symfony.git /var/www/partdb
```
By default you are now on the latest development version. In most cases you want to use the latest stable version. You can switch to the latest stable version (tagged) by running the following command:
```bash
# This finds the latest release/tag and checks it out
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
```
Alternatively you can checkout a specific version by running (see [GitHub Relases page](https://github.com/Part-DB/Part-DB-server/releases) for a list of available versions):
```bash
# This checks out the version 1.5.2
git checkout v1.5.2
```
Change ownership of the files to the apache user:
```bash
chown -R www-data:www-data /var/www/partdb
@@ -90,7 +101,7 @@ The basic configuration of Part-DB is done by a `.env.local` file in the main di
cp .env .env.local
```
In your `.env.local` you can configure Part-DB according to your wishes. A full list of configuration options can be found [here]({% link configuration.md %}.
In your `.env.local` you can configure Part-DB according to your wishes. A full list of configuration options can be found [here](../configuration.md).
Other configuration options like the default language or default currency can be found in `config/parameters.yaml`.
Please check that the `partdb.default_currency` value in `config/parameters.yaml` matches your mainly used currency, as this can not be changed after creating price informations.
@@ -184,6 +195,10 @@ If you want to update your existing Part-DB installation, you just have to run t
cd /var/www/partdb
# Pull latest Part-DB version from GitHub
git pull
# Checkout the latest version (or use a specific version, like described above)
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
# Apply correct permission
chown -R www-data:www-data .
# Install new composer dependencies

View File

@@ -143,7 +143,7 @@ The reverse is also possible: If you have existing SAML users and want them to b
> It is recommended that you let the original admin user (ID: 2) be a local user, so you can still login to Part-DB if the SAML identity provider is not available.
## Advanced SAML configuration
You can find some more advanced SAML configuration options in the `config/packages/hslavich_onelogin_saml.yaml` file. Refer to the file for more information.
You can find some more advanced SAML configuration options in the `config/packages/nbgrp_onelogin_saml.yaml` file. Refer to the file for more information.
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.

View File

@@ -0,0 +1,142 @@
---
title: Information provider system
layout: default
parent: Usage
---
# Information provider system
Part-DB can create parts based on information from external sources: For example with the right setup you can just search for a part number
and Part-DB will query selected distributors and manufacturers for the part and create a part with the information it found.
This way your Part-DB parts automatically get datasheet links, prices, parameters and more, with just a few clicks.
## Usage
Before you can use the information provider system, you have to configure at least one information provider, which act as data source.
See below for a list of available information providers and available configuration options.
For many providers it is enough, to setup the API keys in the env configuration, some require an additional OAuth connection.
You can list all enabled information providers in the browser at `https://your-partdb-instance.tld/tools/info_providers/providers` (you need the right permission for it, see below).
To use the information provider system, your user need to have the right permissions. Go to the permission management page of
a user or a group and assign the permissions of the "Info providers" group in the "Miscellaneous" tab.
If you have the required permission you will find in the sidebar in the "Tools" section the entry "Create part from info provider".
Click this and you will land on a search page. Enter the part number you want to search for and select the information providers you want to use.
After you click Search, you will be presented with the results and can select the result that fits best.
With a click on the blue plus button, you will be redirected to the part creation page with the information already filled in.
![image]({% link assets/usage/information_provider_system/animation.gif %})
## Alternative names
Part-DB tries to automatically find existing elements from your database for the information it got from the providers for fields like manufacturer, footprint, etc.
For this it searches for a element with the same name (case-insensitive) as the information it got from the provider. So e.g. if the provider returns "EXAMPLE CORP" as manufacturer,
Part-DB will automatically select the element with the name "Example Corp" from your database.
As the names of these fields differ from provider to provider (and maybe not even normalized for the same provider), you
can define multiple alternative names for an element (on their editing page).
For example if define a manufacturer "Example Corp" with the alternative names "Example Corp.", "Example Corp", "Example Corp. Inc." and "Example Corporation",
then the provider can return any of these names and Part-DB will still automatically select the right element.
If Part-DB finds no matching element, it will automatically create a new one, when you do not change the value before saving.
## Attachment types
The information provider system uses attachment types to differentiate between datasheets and image attachments.
For this it will create a "Datasheet" and "Image" attachment type on the first run. You can change the names of these
types in the attachment type settings (as long as you keep the "Datasheet"/"Image" in the alternative names field).
If you already have attachment types for images and datasheets and want the information provider system to use them, you can
add the alternative names "Datasheet" and "Image" to the alternative names field of the attachment types.
## Data providers
The system tries to be as flexible as possible, so many different information sources can be used.
Each information source is called am "info provider" and handles the communication with the external source.
The providers are just a driver which handles the communication with the different external sources and converts them into a common format Part-DB understands.
That way it is pretty easy to create new providers as they just need to do very little work.
Normally the providers utilize an API of a service, and you need to create a account at the provider and get an API key.
Also there are limits on how many requests you can do per day or months, depending on the provider and your contract with them.
The following providers are currently available and shipped with Part-DB:
(All trademarks are property of their respective owners. Part-DB is not affiliated with any of the companies.)
### Ocotpart
The Octopart provider uses the [Octopart / Nexar API](https://nexar.com/api) to search for parts and getting informations.
To use it you have to create an account at Nexar and create a new application on the [Nexar Portal](https://portal.nexar.com/).
The name does not matter, but it is important that the application has access to the "Supply" scope.
In the Authorization tab, you will find the client ID and client secret, which you have to enter in the Part-DB env configuration (see below).
Please note that the Nexar API in the free plan is limited to 1000 results per month.
That means if you search for a keyword and results in 10 parts, then 10 will be substracted from your monthly limit. You can see your current usage on the Nexar portal.
Part-DB caches the search results internally, so if you have searched for a part before, it will not count against your monthly limit again, when you create it from the search results.
Following env configuration options are available:
* `PROVIDER_OCTOPART_CLIENT_ID`: The client ID you got from Nexar (mandatory)
* `PROVIDER_OCTOPART_SECRET`: The client secret you got from Nexar (mandatory)
* `PROVIDER_OCTOPART_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`). If an offer is only available in a certain currency,
Part-DB will save the prices in their native currency, and you can use Part-DB currency conversion feature to convert it to your preferred currency.
* `PROVIDER_OCOTPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code, default: `DE`). To get correct prices, you have to set this and the currency setting to the correct value.
* `PROVIDER_OCTOPART_SEARCH_LIMIT`: The maximum number of results to return per search (optional, default: `10`). This affects how quickly your monthly limit is used up.
* `PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS`: If set to `true`, only offers from [authorized sellers](https://octopart.com/authorized) will be returned (optional, default: `false`).
**Attention**: If you change the octopart clientID after you have already used the provider, you have to remove the OAuth token in the Part-DB database. Remove the entry in the table `oauth_tokens` with the name `ip_octopart_oauth`.
### Digi-Key
The Digi-Key provider uses the [Digi-Key API](https://developer.digikey.com/) to search for parts and getting shopping information from [Digi-Key](https://www.digikey.com/).
To use it you have to create an account at Digi-Key and get an API key on the [Digi-Key API page](https://developer.digikey.com/).
You must create an organization there and create a "Production app". Most settings are not important, you just have to grant access to the "Product Information" API.
You will get an Client ID and a Client Secret, which you have to enter in the Part-DB env configuration (see below).
Following env configuration options are available:
* `PROVIDER_DIGIKEY_CLIENT_ID`: The client ID you got from Digi-Key (mandatory)
* `PROVIDER_DIGIKEY_SECRET`: The client secret you got from Digi-Key (mandatory)
* `PROVIDER_DIGIKEY_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`)
* `PROVIDER_DIGIKEY_LANGUAGE`: The language you want to get the descriptions in (optional, default: `en`)
* `PROVIDER_DIGIKEY_COUNTRY`: The country you want to get the prices for (optional, default: `DE`)
The Digi-Key provider needs an additional OAuth connection. To do this, go to the information provider list (`https://your-partdb-instance.tld/tools/info_providers/providers`),
go the Digi-Key provider (in the disabled page) and click on the "Connect OAuth" button. You will be redirected to Digi-Key, where you have to login and grant access to the app.
To do this your user needs the "Manage OAuth tokens" permission from the "System" section in the "System" tab.
The OAuth connection should only be needed once, but if you have any problems with the provider, just click the button again, to establish a new connection.
### TME
The TME provider use the API of [TME](https://www.tme.eu/) to search for parts and getting shopping information from them.
To use it you have to create an account at TME and get an API key on the [TME API page](https://developers.tme.eu/en/).
You have to generate a new anonymous key there and enter the key and secret in the Part-DB env configuration (see below).
Following env configuration options are available:
* `PROVIDER_TME_KEY`: The API key you got from TME (mandatory)
* `PROVIDER_TME_SECRET`: The API secret you got from TME (mandatory)
* `PROVIDER_TME_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`)
* `PROVIDER_TME_LANGUAGE`: The language you want to get the descriptions in (`en`, `de` and `pl`) (optional, default: `en`)
* `PROVIDER_TME_COUNTRY`: The country you want to get the prices for (optional, default: `DE`)
* `PROVIDER_TME_GET_GROSS_PRICES`: If this is set to `1` the prices will be gross prices (including tax), otherwise net prices (optional, default: `0`)
### Farnell / Element14 / Newark
The Farnell provider uses the [Farnell API](https://partner.element14.com/) to search for parts and getting shopping information from [Farnell](https://www.farnell.com/).
You have to create an account at Farnell and get an API key on the [Farnell API page](https://partner.element14.com/).
Register a new application there (settings does not matter, as long as you select the "Product Search API") and you will get an API key.
Following env configuration options are available:
* `PROVIDER_ELEMENT14_KEY`: The API key you got from Farnell (mandatory)
* `PROVIDER_ELEMENT14_STORE_ID`: The store ID you want to use. This decides the language of results, currency and country of prices (optional, default: `de.farnell.com`, see [here](https://partner.element14.com/docs/Product_Search_API_REST__Description) for availailable values)
### Custom provider
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long as it is a valid Symfony service, it will be automatically loaded and can be used.
Besides some metadata functions, you have to implement the `searchByKeyword()` and `getDetails()` functions, which do the actual API requests and return the information to Part-DB.
See the existing providers for examples.
If you created a new provider, feel free to create a pull request to add it to the Part-DB core.
## Result caching
To reduce the number of API calls against the providers, the results are cached:
* The search results (exact search term) are cached for 7 days
* The product details are cached for 4 days
If you need a fresh result, you can clear the cache by running `php .\bin\console cache:pool:clear info_provider.cache` on the command line.
The default `php bin/console cache:clear` also clears the result cache, as it clears all caches.

View File

@@ -72,4 +72,11 @@ See the configuration reference for more information.
## Personal stocks and stock locations
For makerspaces and universities with a lot of users, where each user can have his own stock, which only he should be able to access, you can assign
the user as "owner" of a part lot. This way, only him is allowed to add or remove parts from this lot.
the user as "owner" of a part lot. This way, only him is allowed to add or remove parts from this lot.
## Update notfications
Part-DB can show you a notification that there is a newer version than currently installed available. The notification will be shown on the homepage and the server info page.
It is only be shown to users which has the `Show available Part-DB updates` permission.
For the notification to work, Part-DB queries the GitHub API every 2 days to check for new releases. No data is sent to GitHub besides the metadata required for the connection (so the public IP address of your computer running Part-DB).
If you don't want Part-DB to query the GitHub API, or if your server can not reach the internet, you can disable the update notifications by setting the `CHECK_FOR_UPDATES` option to `false`.

View File

@@ -69,6 +69,12 @@ final class Version20190902140506 extends AbstractMultiPlatformMigration
//For attachments
$this->addSql('DELETE FROM `attachements` WHERE attachements.class_name = "Part" AND (SELECT COUNT(parts.id) FROM parts WHERE parts.id = attachements.element_id) = 0;');
//Add perms_labels column to groups table if not existing (it was not created in certain legacy versions)
//This prevents the migration failing (see https://github.com/Part-DB/Part-DB-server/issues/366 and https://github.com/Part-DB/Part-DB-server/issues/67)
if (!$this->doesColumnExist('groups', 'perms_labels')) {
$this->addSql('ALTER TABLE `groups` ADD `perms_labels` SMALLINT NOT NULL AFTER `perms_tools`');
}
/**************************************************************************************************************
* Doctrine generated SQL
**************************************************************************************************************/

View File

@@ -38,7 +38,7 @@ final class Version20230417211732 extends AbstractMultiPlatformMigration
public function sqLiteUp(Schema $schema): void
{
//As legacy database can only be migrated to MySQL, we don't need to implement this method.
$this->skipIf(true, 'Not needed for SQLite');
//Dont skip here, as this causes this migration always to be executed. Do nothing instead.
}
public function sqLiteDown(Schema $schema): void

View File

@@ -0,0 +1,351 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
final class Version20230716184033 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Create the structure needed for the information provider system';
}
public function mySQLUp(Schema $schema): void
{
$this->addSql('CREATE TABLE oauth_tokens (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, UNIQUE INDEX oauth_tokens_unique_name (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE attachment_types ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE currencies ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE footprints ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE `groups` ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE manufacturers ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE measurement_units ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE parts ADD provider_reference_provider_key VARCHAR(255) DEFAULT NULL, ADD provider_reference_provider_id VARCHAR(255) DEFAULT NULL, ADD provider_reference_provider_url VARCHAR(255) DEFAULT NULL, ADD provider_reference_last_updated DATETIME DEFAULT NULL');
$this->addSql('ALTER TABLE projects ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE storelocations ADD alternative_names LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE suppliers ADD alternative_names LONGTEXT DEFAULT NULL');
}
public function mySQLDown(Schema $schema): void
{
$this->addSql('DROP TABLE oauth_tokens');
$this->addSql('ALTER TABLE `attachment_types` DROP alternative_names');
$this->addSql('ALTER TABLE `categories` DROP alternative_names');
$this->addSql('ALTER TABLE currencies DROP alternative_names');
$this->addSql('ALTER TABLE `footprints` DROP alternative_names');
$this->addSql('ALTER TABLE `groups` DROP alternative_names');
$this->addSql('ALTER TABLE `manufacturers` DROP alternative_names');
$this->addSql('ALTER TABLE `measurement_units` DROP alternative_names');
$this->addSql('ALTER TABLE `parts` DROP provider_reference_provider_key, DROP provider_reference_provider_id, DROP provider_reference_provider_url, DROP provider_reference_last_updated');
$this->addSql('ALTER TABLE projects DROP alternative_names');
$this->addSql('ALTER TABLE `storelocations` DROP alternative_names');
$this->addSql('ALTER TABLE `suppliers` DROP alternative_names');
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)');
$this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)');
$this->addSql('ALTER TABLE attachment_types ADD COLUMN alternative_names CLOB DEFAULT NULL');
$this->addSql('ALTER TABLE categories ADD COLUMN alternative_names CLOB DEFAULT NULL');
$this->addSql('CREATE TEMPORARY TABLE __temp__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM currencies');
$this->addSql('DROP TABLE currencies');
$this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal)
, iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM __temp__currencies');
$this->addSql('DROP TABLE __temp__currencies');
$this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)');
$this->addSql('CREATE INDEX currency_idx_name ON currencies (name)');
$this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)');
$this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)');
$this->addSql('ALTER TABLE footprints ADD COLUMN alternative_names CLOB DEFAULT NULL');
$this->addSql('CREATE TEMPORARY TABLE __temp__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM groups');
$this->addSql('DROP TABLE groups');
$this->addSql('CREATE TABLE groups (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL --(DC2Type:json)
, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO groups (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM __temp__groups');
$this->addSql('DROP TABLE __temp__groups');
$this->addSql('CREATE INDEX group_idx_parent_name ON groups (parent_id, name)');
$this->addSql('CREATE INDEX group_idx_name ON groups (name)');
$this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON groups (parent_id)');
$this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON groups (id_preview_attachment)');
$this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM log');
$this->addSql('DROP TABLE log');
$this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, datetime DATETIME NOT NULL, level TINYINT NOT NULL --(DC2Type:tinyint)
, target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL --(DC2Type:json)
, type SMALLINT NOT NULL, username VARCHAR(255) NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO log (id, id_user, datetime, level, target_id, target_type, extra, type, username) SELECT id, id_user, datetime, level, target_id, target_type, extra, type, username FROM __temp__log');
$this->addSql('DROP TABLE __temp__log');
$this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)');
$this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)');
$this->addSql('CREATE INDEX log_idx_type ON log (type)');
$this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)');
$this->addSql('ALTER TABLE manufacturers ADD COLUMN alternative_names CLOB DEFAULT NULL');
$this->addSql('ALTER TABLE measurement_units ADD COLUMN alternative_names CLOB DEFAULT NULL');
$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, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn 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, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION 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 VARCHAR(255) 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, ipn VARCHAR(100) DEFAULT 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_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL 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, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn FROM __temp__parts');
$this->addSql('DROP TABLE __temp__parts');
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)');
$this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)');
$this->addSql('CREATE INDEX parts_idx_name ON parts (name)');
$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 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 TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM pricedetails');
$this->addSql('DROP TABLE pricedetails');
$this->addSql('CREATE TABLE pricedetails (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL --(DC2Type:big_decimal)
, price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO pricedetails (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails');
$this->addSql('DROP TABLE __temp__pricedetails');
$this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON pricedetails (orderdetails_id)');
$this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON pricedetails (id_currency)');
$this->addSql('CREATE INDEX pricedetails_idx_min_discount ON pricedetails (min_discount_quantity)');
$this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON pricedetails (min_discount_quantity, price_related_quantity)');
$this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries');
$this->addSql('DROP TABLE project_bom_entries');
$this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal)
, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AFC547992F180363 FOREIGN KEY (id_device) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AFC54799C22F6CC4 FOREIGN KEY (id_part) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries');
$this->addSql('DROP TABLE __temp__project_bom_entries');
$this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)');
$this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)');
$this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)');
$this->addSql('ALTER TABLE projects ADD COLUMN alternative_names CLOB DEFAULT NULL');
$this->addSql('ALTER TABLE storelocations ADD COLUMN alternative_names CLOB DEFAULT NULL');
$this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM suppliers');
$this->addSql('DROP TABLE suppliers');
$this->addSql('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL --(DC2Type:big_decimal)
, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, alternative_names CLOB DEFAULT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO suppliers (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__suppliers');
$this->addSql('DROP TABLE __temp__suppliers');
$this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON suppliers (default_currency_id)');
$this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON suppliers (parent_id)');
$this->addSql('CREATE INDEX supplier_idx_name ON suppliers (name)');
$this->addSql('CREATE INDEX supplier_idx_parent_name ON suppliers (parent_id, name)');
$this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON suppliers (id_preview_attachment)');
$this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile FROM users');
$this->addSql('DROP TABLE users');
$this->addSql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL --(DC2Type:json)
, google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL --(DC2Type:json)
, backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL --(DC2Type:json)
, saml_user BOOLEAN NOT NULL, about_me CLOB NOT NULL, show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL, CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO users (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, last_modified, datetime_added, permissions_data, saml_user, about_me, show_email_on_profile FROM __temp__users');
$this->addSql('DROP TABLE __temp__users');
$this->addSql('CREATE INDEX user_idx_username ON users (name)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON users (name)');
$this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON users (group_id)');
$this->addSql('CREATE INDEX IDX_1483A5E938248176 ON users (currency_id)');
$this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON users (id_preview_attachment)');
$this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui FROM webauthn_keys');
$this->addSql('DROP TABLE webauthn_keys');
$this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64)
, type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array)
, attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path)
, aaguid CLOB NOT NULL --(DC2Type:aaguid)
, credential_public_key CLOB NOT NULL --(DC2Type:base64)
, user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, other_ui CLOB DEFAULT NULL --(DC2Type:array)
, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, name, last_modified, datetime_added, other_ui FROM __temp__webauthn_keys');
$this->addSql('DROP TABLE __temp__webauthn_keys');
$this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)');
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql('DROP TABLE oauth_tokens');
$this->addSql('CREATE TEMPORARY TABLE __temp__attachment_types AS SELECT id, parent_id, id_preview_attachment, filetype_filter, comment, not_selectable, name, last_modified, datetime_added FROM "attachment_types"');
$this->addSql('DROP TABLE "attachment_types"');
$this->addSql('CREATE TABLE "attachment_types" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, filetype_filter CLOB NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_EFAED719727ACA70 FOREIGN KEY (parent_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EFAED719EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "attachment_types" (id, parent_id, id_preview_attachment, filetype_filter, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, filetype_filter, comment, not_selectable, name, last_modified, datetime_added FROM __temp__attachment_types');
$this->addSql('DROP TABLE __temp__attachment_types');
$this->addSql('CREATE INDEX IDX_EFAED719727ACA70 ON "attachment_types" (parent_id)');
$this->addSql('CREATE INDEX IDX_EFAED719EA7100A1 ON "attachment_types" (id_preview_attachment)');
$this->addSql('CREATE INDEX attachment_types_idx_name ON "attachment_types" (name)');
$this->addSql('CREATE INDEX attachment_types_idx_parent_name ON "attachment_types" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__categories AS SELECT id, parent_id, id_preview_attachment, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment, comment, not_selectable, name, last_modified, datetime_added 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, 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, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP 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, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, partname_hint, partname_regex, disable_footprints, disable_manufacturers, disable_autodatasheets, disable_properties, default_description, default_comment, comment, not_selectable, name, last_modified, datetime_added 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__currencies AS SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM currencies');
$this->addSql('DROP TABLE currencies');
$this->addSql('CREATE TABLE currencies (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, exchange_rate NUMERIC(11, 5) DEFAULT NULL --
(DC2Type:big_decimal)
, iso_code VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_37C44693EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO currencies (id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, exchange_rate, iso_code, comment, not_selectable, name, last_modified, datetime_added FROM __temp__currencies');
$this->addSql('DROP TABLE __temp__currencies');
$this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)');
$this->addSql('CREATE INDEX IDX_37C44693EA7100A1 ON currencies (id_preview_attachment)');
$this->addSql('CREATE INDEX currency_idx_name ON currencies (name)');
$this->addSql('CREATE INDEX currency_idx_parent_name ON currencies (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__footprints AS SELECT id, parent_id, id_footprint_3d, id_preview_attachment, comment, not_selectable, name, last_modified, datetime_added 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_footprint_3d INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_A34D68A2727ACA70 FOREIGN KEY (parent_id) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A232A38C34 FOREIGN KEY (id_footprint_3d) REFERENCES "attachments" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_A34D68A2EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "footprints" (id, parent_id, id_footprint_3d, id_preview_attachment, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_footprint_3d, id_preview_attachment, comment, not_selectable, name, last_modified, datetime_added FROM __temp__footprints');
$this->addSql('DROP TABLE __temp__footprints');
$this->addSql('CREATE INDEX IDX_A34D68A2727ACA70 ON "footprints" (parent_id)');
$this->addSql('CREATE INDEX IDX_A34D68A232A38C34 ON "footprints" (id_footprint_3d)');
$this->addSql('CREATE INDEX IDX_A34D68A2EA7100A1 ON "footprints" (id_preview_attachment)');
$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__groups AS SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM "groups"');
$this->addSql('DROP TABLE "groups"');
$this->addSql('CREATE TABLE "groups" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, enforce_2fa BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL --
(DC2Type:json)
, CONSTRAINT FK_F06D3970727ACA70 FOREIGN KEY (parent_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F06D3970EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "groups" (id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data) SELECT id, parent_id, id_preview_attachment, enforce_2fa, comment, not_selectable, name, last_modified, datetime_added, permissions_data FROM __temp__groups');
$this->addSql('DROP TABLE __temp__groups');
$this->addSql('CREATE INDEX IDX_F06D3970727ACA70 ON "groups" (parent_id)');
$this->addSql('CREATE INDEX IDX_F06D3970EA7100A1 ON "groups" (id_preview_attachment)');
$this->addSql('CREATE INDEX group_idx_name ON "groups" (name)');
$this->addSql('CREATE INDEX group_idx_parent_name ON "groups" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__log AS SELECT id, id_user, username, datetime, level, target_id, target_type, extra, type FROM log');
$this->addSql('DROP TABLE log');
$this->addSql('CREATE TABLE log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_user INTEGER DEFAULT NULL, username VARCHAR(255) NOT NULL, datetime DATETIME NOT NULL, level TINYINT NOT NULL --
(DC2Type:tinyint)
, target_id INTEGER NOT NULL, target_type SMALLINT NOT NULL, extra CLOB NOT NULL --
(DC2Type:json)
, type SMALLINT NOT NULL, CONSTRAINT FK_8F3F68C56B3CA4B FOREIGN KEY (id_user) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO log (id, id_user, username, datetime, level, target_id, target_type, extra, type) SELECT id, id_user, username, datetime, level, target_id, target_type, extra, type FROM __temp__log');
$this->addSql('DROP TABLE __temp__log');
$this->addSql('CREATE INDEX IDX_8F3F68C56B3CA4B ON log (id_user)');
$this->addSql('CREATE INDEX log_idx_type ON log (type)');
$this->addSql('CREATE INDEX log_idx_type_target ON log (type, target_type, target_id)');
$this->addSql('CREATE INDEX log_idx_datetime ON log (datetime)');
$this->addSql('CREATE TEMPORARY TABLE __temp__manufacturers AS SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM "manufacturers"');
$this->addSql('DROP TABLE "manufacturers"');
$this->addSql('CREATE TABLE "manufacturers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_94565B12727ACA70 FOREIGN KEY (parent_id) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_94565B12EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "manufacturers" (id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__manufacturers');
$this->addSql('DROP TABLE __temp__manufacturers');
$this->addSql('CREATE INDEX IDX_94565B12727ACA70 ON "manufacturers" (parent_id)');
$this->addSql('CREATE INDEX IDX_94565B12EA7100A1 ON "manufacturers" (id_preview_attachment)');
$this->addSql('CREATE INDEX manufacturer_name ON "manufacturers" (name)');
$this->addSql('CREATE INDEX manufacturer_idx_parent_name ON "manufacturers" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__measurement_units AS SELECT id, parent_id, id_preview_attachment, unit, is_integer, use_si_prefix, comment, not_selectable, name, last_modified, datetime_added FROM "measurement_units"');
$this->addSql('DROP TABLE "measurement_units"');
$this->addSql('CREATE TABLE "measurement_units" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, unit VARCHAR(255) DEFAULT NULL, is_integer BOOLEAN NOT NULL, use_si_prefix BOOLEAN NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_F5AF83CF727ACA70 FOREIGN KEY (parent_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_F5AF83CFEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "measurement_units" (id, parent_id, id_preview_attachment, unit, is_integer, use_si_prefix, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, unit, is_integer, use_si_prefix, comment, not_selectable, name, last_modified, datetime_added FROM __temp__measurement_units');
$this->addSql('DROP TABLE __temp__measurement_units');
$this->addSql('CREATE INDEX IDX_F5AF83CF727ACA70 ON "measurement_units" (parent_id)');
$this->addSql('CREATE INDEX IDX_F5AF83CFEA7100A1 ON "measurement_units" (id_preview_attachment)');
$this->addSql('CREATE INDEX unit_idx_name ON "measurement_units" (name)');
$this->addSql('CREATE INDEX unit_idx_parent_name ON "measurement_units" (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 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 VARCHAR(255) 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, 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) 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 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)');
$this->addSql('CREATE TEMPORARY TABLE __temp__pricedetails AS SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM "pricedetails"');
$this->addSql('DROP TABLE "pricedetails"');
$this->addSql('CREATE TABLE "pricedetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_currency INTEGER DEFAULT NULL, orderdetails_id INTEGER NOT NULL, price NUMERIC(11, 5) NOT NULL --
(DC2Type:big_decimal)
, price_related_quantity DOUBLE PRECISION NOT NULL, min_discount_quantity DOUBLE PRECISION NOT NULL, manual_input BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_C68C4459398D64AA FOREIGN KEY (id_currency) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_C68C44594A01DDC7 FOREIGN KEY (orderdetails_id) REFERENCES "orderdetails" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "pricedetails" (id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added) SELECT id, id_currency, orderdetails_id, price, price_related_quantity, min_discount_quantity, manual_input, last_modified, datetime_added FROM __temp__pricedetails');
$this->addSql('DROP TABLE __temp__pricedetails');
$this->addSql('CREATE INDEX IDX_C68C4459398D64AA ON "pricedetails" (id_currency)');
$this->addSql('CREATE INDEX IDX_C68C44594A01DDC7 ON "pricedetails" (orderdetails_id)');
$this->addSql('CREATE INDEX pricedetails_idx_min_discount ON "pricedetails" (min_discount_quantity)');
$this->addSql('CREATE INDEX pricedetails_idx_min_discount_price_qty ON "pricedetails" (min_discount_quantity, price_related_quantity)');
$this->addSql('CREATE TEMPORARY TABLE __temp__project_bom_entries AS SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM project_bom_entries');
$this->addSql('DROP TABLE project_bom_entries');
$this->addSql('CREATE TABLE project_bom_entries (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_device INTEGER DEFAULT NULL, id_part INTEGER DEFAULT NULL, price_currency_id INTEGER DEFAULT NULL, quantity DOUBLE PRECISION NOT NULL, mountnames CLOB NOT NULL, name VARCHAR(255) DEFAULT NULL, comment CLOB NOT NULL, price NUMERIC(11, 5) DEFAULT NULL --
(DC2Type:big_decimal)
, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_1AA2DD312F180363 FOREIGN KEY (id_device) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD31C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1AA2DD313FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO project_bom_entries (id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added) SELECT id, id_device, id_part, price_currency_id, quantity, mountnames, name, comment, price, last_modified, datetime_added FROM __temp__project_bom_entries');
$this->addSql('DROP TABLE __temp__project_bom_entries');
$this->addSql('CREATE INDEX IDX_1AA2DD312F180363 ON project_bom_entries (id_device)');
$this->addSql('CREATE INDEX IDX_1AA2DD31C22F6CC4 ON project_bom_entries (id_part)');
$this->addSql('CREATE INDEX IDX_1AA2DD313FFDCD60 ON project_bom_entries (price_currency_id)');
$this->addSql('CREATE TEMPORARY TABLE __temp__projects AS SELECT id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added FROM projects');
$this->addSql('DROP TABLE projects');
$this->addSql('CREATE TABLE projects (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, order_quantity INTEGER NOT NULL, status VARCHAR(64) DEFAULT NULL, order_only_missing_parts BOOLEAN NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_5C93B3A4727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_5C93B3A4EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO projects (id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, id_preview_attachment, order_quantity, status, order_only_missing_parts, description, comment, not_selectable, name, last_modified, datetime_added FROM __temp__projects');
$this->addSql('DROP TABLE __temp__projects');
$this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)');
$this->addSql('CREATE INDEX IDX_5C93B3A4EA7100A1 ON projects (id_preview_attachment)');
$this->addSql('CREATE TEMPORARY TABLE __temp__storelocations AS SELECT id, parent_id, storage_type_id, id_owner, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, part_owner_must_match, comment, not_selectable, name, last_modified, datetime_added FROM "storelocations"');
$this->addSql('DROP TABLE "storelocations"');
$this->addSql('CREATE TABLE "storelocations" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, storage_type_id INTEGER DEFAULT NULL, id_owner INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, is_full BOOLEAN NOT NULL, only_single_part BOOLEAN NOT NULL, limit_to_existing_parts BOOLEAN NOT NULL, part_owner_must_match BOOLEAN DEFAULT 0 NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_7517020727ACA70 FOREIGN KEY (parent_id) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020B270BFF1 FOREIGN KEY (storage_type_id) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_751702021E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_7517020EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "storelocations" (id, parent_id, storage_type_id, id_owner, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, part_owner_must_match, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, storage_type_id, id_owner, id_preview_attachment, is_full, only_single_part, limit_to_existing_parts, part_owner_must_match, comment, not_selectable, name, last_modified, datetime_added FROM __temp__storelocations');
$this->addSql('DROP TABLE __temp__storelocations');
$this->addSql('CREATE INDEX IDX_7517020727ACA70 ON "storelocations" (parent_id)');
$this->addSql('CREATE INDEX IDX_7517020B270BFF1 ON "storelocations" (storage_type_id)');
$this->addSql('CREATE INDEX IDX_751702021E5A74C ON "storelocations" (id_owner)');
$this->addSql('CREATE INDEX IDX_7517020EA7100A1 ON "storelocations" (id_preview_attachment)');
$this->addSql('CREATE INDEX location_idx_name ON "storelocations" (name)');
$this->addSql('CREATE INDEX location_idx_parent_name ON "storelocations" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__suppliers AS SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM "suppliers"');
$this->addSql('DROP TABLE "suppliers"');
$this->addSql('CREATE TABLE "suppliers" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, parent_id INTEGER DEFAULT NULL, default_currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, shipping_costs NUMERIC(11, 5) DEFAULT NULL --
(DC2Type:big_decimal)
, address VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, fax_number VARCHAR(255) NOT NULL, email_address VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, auto_product_url VARCHAR(255) NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_AC28B95C727ACA70 FOREIGN KEY (parent_id) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CECD792C0 FOREIGN KEY (default_currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_AC28B95CEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "suppliers" (id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added) SELECT id, parent_id, default_currency_id, id_preview_attachment, shipping_costs, address, phone_number, fax_number, email_address, website, auto_product_url, comment, not_selectable, name, last_modified, datetime_added FROM __temp__suppliers');
$this->addSql('DROP TABLE __temp__suppliers');
$this->addSql('CREATE INDEX IDX_AC28B95C727ACA70 ON "suppliers" (parent_id)');
$this->addSql('CREATE INDEX IDX_AC28B95CECD792C0 ON "suppliers" (default_currency_id)');
$this->addSql('CREATE INDEX IDX_AC28B95CEA7100A1 ON "suppliers" (id_preview_attachment)');
$this->addSql('CREATE INDEX supplier_idx_name ON "suppliers" (name)');
$this->addSql('CREATE INDEX supplier_idx_parent_name ON "suppliers" (parent_id, name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__users AS SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM "users"');
$this->addSql('DROP TABLE "users"');
$this->addSql('CREATE TABLE "users" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, group_id INTEGER DEFAULT NULL, currency_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, disabled BOOLEAN NOT NULL, config_theme VARCHAR(255) DEFAULT NULL, pw_reset_token VARCHAR(255) DEFAULT NULL, config_instock_comment_a CLOB NOT NULL, config_instock_comment_w CLOB NOT NULL, about_me CLOB NOT NULL, trusted_device_cookie_version INTEGER NOT NULL, backup_codes CLOB NOT NULL --
(DC2Type:json)
, google_authenticator_secret VARCHAR(255) DEFAULT NULL, config_timezone VARCHAR(255) DEFAULT NULL, config_language VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, show_email_on_profile BOOLEAN DEFAULT 0 NOT NULL, department VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, need_pw_change BOOLEAN NOT NULL, password VARCHAR(255) DEFAULT NULL, name VARCHAR(180) NOT NULL, settings CLOB NOT NULL --
(DC2Type:json)
, backup_codes_generation_date DATETIME DEFAULT NULL, pw_reset_expires DATETIME DEFAULT NULL, saml_user BOOLEAN NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, permissions_data CLOB NOT NULL --
(DC2Type:json)
, CONSTRAINT FK_1483A5E9FE54D947 FOREIGN KEY (group_id) REFERENCES "groups" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E938248176 FOREIGN KEY (currency_id) REFERENCES currencies (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_1483A5E9EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "users" (id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data) SELECT id, group_id, currency_id, id_preview_attachment, disabled, config_theme, pw_reset_token, config_instock_comment_a, config_instock_comment_w, about_me, trusted_device_cookie_version, backup_codes, google_authenticator_secret, config_timezone, config_language, email, show_email_on_profile, department, last_name, first_name, need_pw_change, password, name, settings, backup_codes_generation_date, pw_reset_expires, saml_user, last_modified, datetime_added, permissions_data FROM __temp__users');
$this->addSql('DROP TABLE __temp__users');
$this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E95E237E06 ON "users" (name)');
$this->addSql('CREATE INDEX IDX_1483A5E9FE54D947 ON "users" (group_id)');
$this->addSql('CREATE INDEX IDX_1483A5E938248176 ON "users" (currency_id)');
$this->addSql('CREATE INDEX IDX_1483A5E9EA7100A1 ON "users" (id_preview_attachment)');
$this->addSql('CREATE INDEX user_idx_username ON "users" (name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__webauthn_keys AS SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added FROM webauthn_keys');
$this->addSql('DROP TABLE webauthn_keys');
$this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --
(DC2Type:base64)
, type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --
(DC2Type:array)
, attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --
(DC2Type:trust_path)
, aaguid CLOB NOT NULL --
(DC2Type:aaguid)
, credential_public_key CLOB NOT NULL --
(DC2Type:base64)
, user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, other_ui CLOB DEFAULT NULL --
(DC2Type:array)
, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO webauthn_keys (id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added) SELECT id, user_id, public_key_credential_id, type, transports, attestation_type, trust_path, aaguid, credential_public_key, user_handle, counter, other_ui, name, last_modified, datetime_added FROM __temp__webauthn_keys');
$this->addSql('DROP TABLE __temp__webauthn_keys');
$this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)');
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use App\Migration\AbstractMultiPlatformMigration;
use Doctrine\DBAL\Schema\Schema;
final class Version20230730131708 extends AbstractMultiPlatformMigration
{
public function getDescription(): string
{
return 'Allow longer fields for manufacturer and supplier product urls, allow client credentials grant to have null refresh token';
}
public function mySQLUp(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE oauth_tokens CHANGE token token LONGTEXT DEFAULT NULL, CHANGE refresh_token refresh_token LONGTEXT DEFAULT NULL');
$this->addSql('ALTER TABLE orderdetails CHANGE supplier_product_url supplier_product_url LONGTEXT NOT NULL');
$this->addSql('ALTER TABLE parts CHANGE manufacturer_product_url manufacturer_product_url LONGTEXT NOT NULL');
}
public function mySQLDown(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE oauth_tokens CHANGE token token VARCHAR(255) DEFAULT NULL, CHANGE refresh_token refresh_token VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE `orderdetails` CHANGE supplier_product_url supplier_product_url VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE `parts` CHANGE manufacturer_product_url manufacturer_product_url VARCHAR(255) NOT NULL');
}
public function sqLiteUp(Schema $schema): void
{
$this->addSql('CREATE TEMPORARY TABLE __temp__oauth_tokens AS SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM oauth_tokens');
$this->addSql('DROP TABLE oauth_tokens');
$this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token CLOB DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, refresh_token CLOB DEFAULT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)');
$this->addSql('INSERT INTO oauth_tokens (id, token, expires_at, refresh_token, name, last_modified, datetime_added) SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM __temp__oauth_tokens');
$this->addSql('DROP TABLE __temp__oauth_tokens');
$this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM orderdetails');
$this->addSql('DROP TABLE orderdetails');
$this->addSql('CREATE TABLE orderdetails (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url CLOB NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES parts (id) ON UPDATE NO ACTION ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES suppliers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO orderdetails (id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added) SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM __temp__orderdetails');
$this->addSql('DROP TABLE __temp__orderdetails');
$this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON orderdetails (supplierpartnr)');
$this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON orderdetails (part_id)');
$this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON orderdetails (id_supplier)');
$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, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, 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, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION 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, ipn VARCHAR(100) DEFAULT 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_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL 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, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, 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, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, 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_6940A7FEE8AE70D9 ON parts (built_project_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)');
$this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)');
$this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)');
$this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)');
$this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)');
$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 UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)');
$this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)');
$this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)');
}
public function sqLiteDown(Schema $schema): void
{
$this->addSql('CREATE TEMPORARY TABLE __temp__oauth_tokens AS SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM oauth_tokens');
$this->addSql('DROP TABLE oauth_tokens');
$this->addSql('CREATE TABLE oauth_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, token VARCHAR(255) DEFAULT NULL, expires_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, refresh_token VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL)');
$this->addSql('INSERT INTO oauth_tokens (id, token, expires_at, refresh_token, name, last_modified, datetime_added) SELECT id, token, expires_at, refresh_token, name, last_modified, datetime_added FROM __temp__oauth_tokens');
$this->addSql('DROP TABLE __temp__oauth_tokens');
$this->addSql('CREATE UNIQUE INDEX oauth_tokens_unique_name ON oauth_tokens (name)');
$this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM "orderdetails"');
$this->addSql('DROP TABLE "orderdetails"');
$this->addSql('CREATE TABLE "orderdetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO "orderdetails" (id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added) SELECT id, part_id, id_supplier, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added FROM __temp__orderdetails');
$this->addSql('DROP TABLE __temp__orderdetails');
$this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON "orderdetails" (part_id)');
$this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON "orderdetails" (id_supplier)');
$this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON "orderdetails" (supplierpartnr)');
$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 VARCHAR(255) 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

@@ -30,38 +30,38 @@
"build": "encore production --progress"
},
"dependencies": {
"@ckeditor/ckeditor5-alignment": "^38.0.1",
"@ckeditor/ckeditor5-autoformat": "^38.0.1",
"@ckeditor/ckeditor5-basic-styles": "^38.0.1",
"@ckeditor/ckeditor5-block-quote": "^38.0.1",
"@ckeditor/ckeditor5-code-block": "^38.0.1",
"@ckeditor/ckeditor5-dev-utils": "^38.0.1",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
"@ckeditor/ckeditor5-editor-classic": "^38.0.1",
"@ckeditor/ckeditor5-essentials": "^38.0.1",
"@ckeditor/ckeditor5-find-and-replace": "^38.0.1",
"@ckeditor/ckeditor5-font": "^38.0.1",
"@ckeditor/ckeditor5-heading": "^38.0.1",
"@ckeditor/ckeditor5-highlight": "^38.0.1",
"@ckeditor/ckeditor5-horizontal-line": "^38.0.1",
"@ckeditor/ckeditor5-html-embed": "^38.0.1",
"@ckeditor/ckeditor5-html-support": "^38.0.1",
"@ckeditor/ckeditor5-image": "^38.0.1",
"@ckeditor/ckeditor5-indent": "^38.0.1",
"@ckeditor/ckeditor5-link": "^38.0.1",
"@ckeditor/ckeditor5-list": "^38.0.1",
"@ckeditor/ckeditor5-markdown-gfm": "^38.0.1",
"@ckeditor/ckeditor5-media-embed": "^38.0.1",
"@ckeditor/ckeditor5-paragraph": "^38.0.1",
"@ckeditor/ckeditor5-paste-from-office": "^38.0.1",
"@ckeditor/ckeditor5-remove-format": "^38.0.1",
"@ckeditor/ckeditor5-source-editing": "^38.0.1",
"@ckeditor/ckeditor5-special-characters": "^38.0.1",
"@ckeditor/ckeditor5-table": "^38.0.1",
"@ckeditor/ckeditor5-theme-lark": "^38.0.1",
"@ckeditor/ckeditor5-upload": "^38.0.1",
"@ckeditor/ckeditor5-watchdog": "^38.0.1",
"@ckeditor/ckeditor5-word-count": "^38.0.1",
"@ckeditor/ckeditor5-alignment": "^39.0.1",
"@ckeditor/ckeditor5-autoformat": "^39.0.1",
"@ckeditor/ckeditor5-basic-styles": "^39.0.1",
"@ckeditor/ckeditor5-block-quote": "^39.0.1",
"@ckeditor/ckeditor5-code-block": "^39.0.1",
"@ckeditor/ckeditor5-dev-translations": "^38.4.0",
"@ckeditor/ckeditor5-dev-utils": "^38.4.0",
"@ckeditor/ckeditor5-editor-classic": "^39.0.1",
"@ckeditor/ckeditor5-essentials": "^39.0.1",
"@ckeditor/ckeditor5-find-and-replace": "^39.0.1",
"@ckeditor/ckeditor5-font": "^39.0.1",
"@ckeditor/ckeditor5-heading": "^39.0.1",
"@ckeditor/ckeditor5-highlight": "^39.0.1",
"@ckeditor/ckeditor5-horizontal-line": "^39.0.1",
"@ckeditor/ckeditor5-html-embed": "^39.0.1",
"@ckeditor/ckeditor5-html-support": "^39.0.1",
"@ckeditor/ckeditor5-image": "^39.0.1",
"@ckeditor/ckeditor5-indent": "^39.0.1",
"@ckeditor/ckeditor5-link": "^39.0.1",
"@ckeditor/ckeditor5-list": "^39.0.1",
"@ckeditor/ckeditor5-markdown-gfm": "^39.0.1",
"@ckeditor/ckeditor5-media-embed": "^39.0.1",
"@ckeditor/ckeditor5-paragraph": "^39.0.1",
"@ckeditor/ckeditor5-paste-from-office": "^39.0.1",
"@ckeditor/ckeditor5-remove-format": "^39.0.1",
"@ckeditor/ckeditor5-source-editing": "^39.0.1",
"@ckeditor/ckeditor5-special-characters": "^39.0.1",
"@ckeditor/ckeditor5-table": "^39.0.1",
"@ckeditor/ckeditor5-theme-lark": "^39.0.1",
"@ckeditor/ckeditor5-upload": "^39.0.1",
"@ckeditor/ckeditor5-watchdog": "^39.0.1",
"@ckeditor/ckeditor5-word-count": "^39.0.1",
"@jbtronics/bs-treeview": "^1.0.1",
"@zxcvbn-ts/core": "^3.0.2",
"@zxcvbn-ts/language-common": "^3.0.3",
@@ -81,13 +81,13 @@
"datatables.net-responsive-bs5": "^2.2.3",
"datatables.net-select-bs5": "^1.2.7",
"dompurify": "^3.0.3",
"emoji.json": "^14.0.0",
"emoji.json": "^15.0.0",
"exports-loader": "^3.0.0",
"html5-qrcode": "^2.2.1",
"json-formatter-js": "^2.3.4",
"jszip": "^3.2.0",
"katex": "^0.16.0",
"marked": "^5.1.0",
"marked": "^7.0.4",
"marked-gfm-heading-id": "^3.0.4",
"marked-mangle": "^1.0.1",
"pdfmake": "^0.2.2",

View File

@@ -25,6 +25,7 @@ namespace App\Controller;
use App\DataTables\LogDataTable;
use App\Entity\Parts\Part;
use App\Services\Misc\GitVersionInfo;
use App\Services\System\UpdateAvailableManager;
use Doctrine\ORM\EntityManagerInterface;
use const DIRECTORY_SEPARATOR;
use Omines\DataTablesBundle\DataTableFactory;
@@ -62,8 +63,11 @@ class HomepageController extends AbstractController
}
#[Route(path: '/', name: 'homepage')]
public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager): Response
public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager,
UpdateAvailableManager $updateAvailableManager): Response
{
$this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS');
if ($this->isGranted('@tools.lastActivity')) {
$table = $this->dataTable->createFromType(
LogDataTable::class,
@@ -97,6 +101,9 @@ class HomepageController extends AbstractController
'git_commit' => $versionInfo->getGitCommitHash(),
'show_first_steps' => $show_first_steps,
'datatable' => $table,
'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
'new_version' => $updateAvailableManager->getLatestVersionString(),
'new_version_url' => $updateAvailableManager->getLatestVersionUrl(),
]);
}
}

View File

@@ -0,0 +1,85 @@
<?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\Exceptions\AttachmentDownloadException;
use App\Form\InfoProviderSystem\PartSearchType;
use App\Form\Part\PartBaseType;
use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\InfoProviderSystem\PartInfoRetriever;
use App\Services\InfoProviderSystem\ProviderRegistry;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\Parts\PartFormHelper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
#[Route('/tools/info_providers')]
class InfoProviderController extends AbstractController
{
public function __construct(private readonly ProviderRegistry $providerRegistry,
private readonly PartInfoRetriever $infoRetriever)
{
}
#[Route('/providers', name: 'info_providers_list')]
public function listProviders(): Response
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
return $this->render('info_providers/providers_list/providers_list.html.twig', [
'active_providers' => $this->providerRegistry->getActiveProviders(),
'disabled_providers' => $this->providerRegistry->getDisabledProviders(),
]);
}
#[Route('/search', name: 'info_providers_search')]
public function search(Request $request): Response
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
$form = $this->createForm(PartSearchType::class);
$form->handleRequest($request);
$results = null;
if ($form->isSubmitted() && $form->isValid()) {
$keyword = $form->get('keyword')->getData();
$providers = $form->get('providers')->getData();
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
}
return $this->render('info_providers/search/part_search.html.twig', [
'form' => $form,
'results' => $results,
]);
}
}

View File

@@ -0,0 +1,67 @@
<?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\Services\OAuth\OAuthTokenManager;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use function Symfony\Component\Translation\t;
#[Route('/oauth/client')]
class OAuthClientController extends AbstractController
{
public function __construct(private readonly ClientRegistry $clientRegistry, private readonly OAuthTokenManager $tokenManager)
{
}
#[Route('/{name}/connect', name: 'oauth_client_connect')]
public function connect(string $name): Response
{
$this->denyAccessUnlessGranted('@system.manage_oauth_tokens');
return $this->clientRegistry
->getClient($name) // key used in config/packages/knpu_oauth2_client.yaml
->redirect([], []);
}
#[Route('/{name}/check', name: 'oauth_client_check')]
public function check(string $name, Request $request): Response
{
$this->denyAccessUnlessGranted('@system.manage_oauth_tokens');
$client = $this->clientRegistry->getClient($name);
$access_token = $client->getAccessToken();
$this->tokenManager->saveToken($name, $access_token);
$this->addFlash('success', t('oauth_client.flash.connection_successful'));
return $this->redirectToRoute('homepage');
}
}

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\InfoProviderSystem\PartInfoRetriever;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\LogSystem\HistoryHelper;
use App\Services\LogSystem\TimeTravel;
@@ -63,7 +64,11 @@ use function Symfony\Component\Translation\t;
#[Route(path: '/part')]
class PartController extends AbstractController
{
public function __construct(protected PricedetailHelper $pricedetailHelper, protected PartPreviewGenerator $partPreviewGenerator, protected EventCommentHelper $commentHelper)
public function __construct(protected PricedetailHelper $pricedetailHelper,
protected PartPreviewGenerator $partPreviewGenerator,
private readonly TranslatorInterface $translator,
private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em,
protected EventCommentHelper $commentHelper)
{
}
@@ -121,65 +126,15 @@ class PartController extends AbstractController
}
#[Route(path: '/{id}/edit', name: 'part_edit')]
public function edit(Part $part, Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
AttachmentSubmitHandler $attachmentSubmitHandler): Response
public function edit(Part $part, Request $request): Response
{
$this->denyAccessUnlessGranted('edit', $part);
$form = $this->createForm(PartBaseType::class, $part);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Upload passed files
$attachments = $form['attachments'];
foreach ($attachments as $attachment) {
/** @var FormInterface $attachment */
$options = [
'secure_attachment' => $attachment['secureFile']->getData(),
'download_url' => $attachment['downloadURL']->getData(),
];
try {
$attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
} catch (AttachmentDownloadException $attachmentDownloadException) {
$this->addFlash(
'error',
$translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
);
}
}
$this->commentHelper->setMessage($form['log_comment']->getData());
$em->persist($part);
$em->flush();
$this->addFlash('success', 'part.edited_flash');
//Redirect to clone page if user wished that...
//@phpstan-ignore-next-line
if ('save_and_clone' === $form->getClickedButton()->getName()) {
return $this->redirectToRoute('part_clone', ['id' => $part->getID()]);
}
//@phpstan-ignore-next-line
if ('save_and_new' === $form->getClickedButton()->getName()) {
return $this->redirectToRoute('part_new');
}
//Reload form, so the SIUnitType entries use the new part unit
$form = $this->createForm(PartBaseType::class, $part);
} elseif ($form->isSubmitted() && !$form->isValid()) {
$this->addFlash('error', 'part.edited_flash.invalid');
}
return $this->render('parts/edit/edit_part_info.html.twig',
[
'part' => $part,
'form' => $form,
]);
return $this->renderPartForm('edit', $request, $part);
}
#[Route(path: '/{id}/delete', name: 'part_delete', methods: ['DELETE'])]
public function delete(Request $request, Part $part, EntityManagerInterface $entityManager): RedirectResponse
public function delete(Request $request, Part $part): RedirectResponse
{
$this->denyAccessUnlessGranted('delete', $part);
@@ -188,10 +143,10 @@ class PartController extends AbstractController
$this->commentHelper->setMessage($request->request->get('log_comment', null));
//Remove part
$entityManager->remove($part);
$this->em->remove($part);
//Flush changes
$entityManager->flush();
$this->em->flush();
$this->addFlash('success', 'part.deleted');
}
@@ -205,7 +160,7 @@ class PartController extends AbstractController
public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper,
#[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null,
#[MapEntity(mapping: ['id' => 'project_id'])] ?Project $project = null): Response
#[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response
{
if ($part instanceof Part) {
@@ -262,7 +217,39 @@ class PartController extends AbstractController
$new_part->addOrderdetail($orderdetail);
}
$form = $this->createForm(PartBaseType::class, $new_part);
return $this->renderPartForm('new', $request, $new_part);
}
#[Route('/from_info_provider/{providerKey}/{providerId}/create', name: 'info_providers_create_part', requirements: ['providerId' => '.+'])]
public function createFromInfoProvider(Request $request, string $providerKey, string $providerId, PartInfoRetriever $infoRetriever): Response
{
$this->denyAccessUnlessGranted('@info_providers.create_parts');
$dto = $infoRetriever->getDetails($providerKey, $providerId);
$new_part = $infoRetriever->dtoToPart($dto);
return $this->renderPartForm('new', $request, $new_part, [
'info_provider_dto' => $dto,
]);
}
/**
* This function provides a common implementation for methods, which use the part form.
* @param Request $request
* @param Part $data
* @param array $form_options
* @return Response
*/
private function renderPartForm(string $mode, Request $request, Part $data, array $form_options = []): Response
{
//Ensure that mode is either 'new' or 'edit
if (!in_array($mode, ['new', 'edit'], true)) {
throw new \InvalidArgumentException('Invalid mode given');
}
$new_part = $data;
$form = $this->createForm(PartBaseType::class, $new_part, $form_options);
$form->handleRequest($request);
@@ -277,20 +264,24 @@ class PartController extends AbstractController
];
try {
$attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
$this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
} catch (AttachmentDownloadException $attachmentDownloadException) {
$this->addFlash(
'error',
$translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
$this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
);
}
}
$this->commentHelper->setMessage($form['log_comment']->getData());
$em->persist($new_part);
$em->flush();
$this->addFlash('success', 'part.created_flash');
$this->em->persist($new_part);
$this->em->flush();
if ($mode === 'new') {
$this->addFlash('success', 'part.created_flash');
} else if ($mode === 'edit') {
$this->addFlash('success', 'part.edited_flash');
}
//If a redirect URL was given, redirect there
if ($request->query->get('_redirect')) {
@@ -314,13 +305,21 @@ class PartController extends AbstractController
$this->addFlash('error', 'part.created_flash.invalid');
}
return $this->render('parts/edit/new_part.html.twig',
$template = '';
if ($mode === 'new') {
$template = 'parts/edit/new_part.html.twig';
} else if ($mode === 'edit') {
$template = 'parts/edit/edit_part_info.html.twig';
}
return $this->render($template,
[
'part' => $new_part,
'form' => $form,
]);
}
#[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])]
public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response
{

View File

@@ -170,7 +170,7 @@ class SecurityController extends AbstractController
$this->addFlash('success', 'pw_reset.new_pw.success');
$repo = $em->getRepository(User::class);
$u = $repo->findOneBy(['name' => $data['username']]);
$u = $repo->findByUsername($data['username']);
$event = new SecurityEvent($u);
/** @var EventDispatcher $eventDispatcher */
$eventDispatcher->dispatch($event, SecurityEvents::PASSWORD_RESET);

View File

@@ -28,6 +28,7 @@ use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Attachments\BuiltinAttachmentsFinder;
use App\Services\Misc\GitVersionInfo;
use App\Services\Misc\DBInfoHelper;
use App\Services\System\UpdateAvailableManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@@ -47,7 +48,7 @@ class ToolsController extends AbstractController
#[Route(path: '/server_infos', name: 'tools_server_infos')]
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper,
AttachmentSubmitHandler $attachmentSubmitHandler): Response
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response
{
$this->denyAccessUnlessGranted('@system.server_infos');
@@ -90,6 +91,11 @@ class ToolsController extends AbstractController
'db_size' => $DBInfoHelper->getDatabaseSize(),
'db_name' => $DBInfoHelper->getDatabaseName() ?? 'Unknown',
'db_user' => $DBInfoHelper->getDatabaseUsername() ?? 'Unknown',
//New version section
'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
'new_version' => $updateAvailableManager->getLatestVersionString(),
'new_version_url' => $updateAvailableManager->getLatestVersionUrl(),
]);
}

View File

@@ -46,6 +46,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route(path: '/user')]
class UserController extends BaseAdminController
@@ -79,7 +80,8 @@ class UserController extends BaseAdminController
*/
#[Route(path: '/{id}/edit/{timestamp}', requirements: ['id' => '\d+'], name: 'user_edit')]
#[Route(path: '/{id}/', requirements: ['id' => '\d+'])]
public function edit(User $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper, PermissionSchemaUpdater $permissionSchemaUpdater, ?string $timestamp = null): Response
public function edit(User $entity, Request $request, EntityManagerInterface $em, PermissionPresetsHelper $permissionPresetsHelper,
PermissionSchemaUpdater $permissionSchemaUpdater, ValidatorInterface $validator, ?string $timestamp = null): Response
{
//Do an upgrade of the permission schema if needed (so the user can see the permissions a user get on next request (even if it was not done yet)
$permissionSchemaUpdater->userUpgradeSchemaRecursively($entity);
@@ -108,7 +110,7 @@ class UserController extends BaseAdminController
$this->addFlash('success', 'user.edit.reset_success');
} else {
$this->addFlash('danger', 'csfr_invalid');
$this->addFlash('error', 'csfr_invalid');
}
}
@@ -120,15 +122,25 @@ class UserController extends BaseAdminController
$permissionPresetsHelper->applyPreset($entity, $preset);
$em->flush();
//Ensure that the user is valid after applying the preset
$errors = $validator->validate($entity);
if (count($errors) > 0) {
$this->addFlash('error', 'validator.noLockout');
//Refresh the entity to remove the changes
$em->refresh($entity);
} else {
$em->flush();
$this->addFlash('success', 'user.edit.permission_success');
$this->addFlash('success', 'user.edit.permission_success');
//We need to stop the execution here, or our permissions changes will be overwritten by the form values
return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]);
//We need to stop the execution here, or our permissions changes will be overwritten by the form values
return $this->redirectToRoute('user_edit', ['id' => $entity->getID()]);
}
} else {
$this->addFlash('error', 'csfr_invalid');
}
$this->addFlash('danger', 'csfr_invalid');
}
return $this->_edit($entity, $request, $em, $timestamp);

View File

@@ -46,6 +46,7 @@ use App\Entity\Attachments\PartAttachment;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\ManufacturingStatus;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
@@ -83,7 +84,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$part->setTags('test, Test, Part2');
$part->setMass(100.2);
$part->setNeedsReview(true);
$part->setManufacturingStatus('active');
$part->setManufacturingStatus(ManufacturingStatus::ACTIVE);
$manager->persist($part);
/** Part with orderdetails, storelocations and Attachments */

View File

@@ -54,6 +54,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
$user = new User();
$user->setName('user');
$user->setNeedPwChange(false);
$user->setEmail('user@invalid.invalid');
$user->setFirstName('Test')->setLastName('User');
$user->setPassword($this->encoder->hashPassword($user, 'test'));
$user->setGroup($this->getReference(GroupFixtures::USERS));
@@ -66,6 +67,9 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
$manager->persist($noread);
$manager->flush();
//Ensure that the anonymous user has the ID 0
$manager->getRepository(User::class)->changeID($anonymous, User::ID_ANONYMOUS);
}
public function getDependencies(): array

View File

@@ -0,0 +1,69 @@
<?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\DataTables\Adapters;
use App\DataTables\Events\ORMPostQueryEvent;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\QueryBuilder;
use Omines\DataTablesBundle\Adapter\AdapterQuery;
use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents;
use Omines\DataTablesBundle\Column\AbstractColumn;
/**
* This class is very similar to the original ORMAdapter, but instead of getting each line one by one, it gets all the results at once,
* which can save some time in combination with fetch hints.
*/
class FetchResultsAtOnceORMAdapter extends ORMAdapter
{
protected function getResults(AdapterQuery $query): \Traversable
{
/** @var QueryBuilder $builder */
$builder = $query->get('qb');
$state = $query->getState();
// Apply definitive view state for current 'page' of the table
foreach ($state->getOrderBy() as list($column, $direction)) {
/** @var AbstractColumn $column */
if ($column->isOrderable()) {
$builder->addOrderBy($column->getOrderField(), $direction);
}
}
if (null !== $state->getLength()) {
$builder
->setFirstResult($state->getStart())
->setMaxResults($state->getLength())
;
}
$q = $builder->getQuery();
$event = new ORMAdapterQueryEvent($q);
$state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY);
foreach ($q->getResult() as $item) {
yield $item;
}
}
}

View File

@@ -0,0 +1,191 @@
<?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\DataTables\Adapters;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr\Select;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\CountOutputWalker;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;
use Omines\DataTablesBundle\Adapter\AdapterQuery;
use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents;
use Omines\DataTablesBundle\Column\AbstractColumn;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* This adapter fetches entities from the database in two steps:
* In the first step, it uses a simple query (filter_query option), which returns only the ids of the main entity,
* to get the count of results, doing filtering and pagination.
*
* In the next step the IDs are passed to the detail_query callback option, which then can do a more complex queries (like fetch join)
* to get the entities and related stuff in an efficient way.
* This way we save the overhead of the fetch join query for the count and counting, which can be very slow, cause
* no indexes can be used.
*/
class TwoStepORMAdapater extends ORMAdapter
{
private \Closure $detailQueryCallable;
private bool $use_simple_total = false;
public function __construct(ManagerRegistry $registry = null)
{
parent::__construct($registry);
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids) {
throw new \RuntimeException('You need to set the detail_query option to use the TwoStepORMAdapter');
};
}
protected function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setRequired('filter_query');
$resolver->setDefault('query', function (Options $options) {
return $options['filter_query'];
});
$resolver->setRequired('detail_query');
$resolver->setAllowedTypes('detail_query', \Closure::class);
/*
* Add the possibility to replace the query for total entity count through a very simple one, to improve performance.
* You can only use this option, if you did not apply any criteria to your total count.
*/
$resolver->setDefault('simple_total_query', false);
}
protected function afterConfiguration(array $options): void
{
parent::afterConfiguration($options);
$this->detailQueryCallable = $options['detail_query'];
$this->use_simple_total = $options['simple_total_query'];
}
protected function prepareQuery(AdapterQuery $query): void
{
//Like the parent class, but we add the possibility to use the simple total query
$state = $query->getState();
$query->set('qb', $builder = $this->createQueryBuilder($state));
$query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias());
// Provide default field mappings if needed
foreach ($state->getDataTable()->getColumns() as $column) {
if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) {
$column->setOption('field', "{$rootAlias}.{$name}");
}
}
/** @var Query\Expr\From $fromClause */
$fromClause = $builder->getDQLPart('from')[0];
$identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}";
// Use simpler (faster) total count query if the user wanted so...
if ($this->use_simple_total) {
$query->setTotalRows($this->getSimpleTotalCount($builder));
} else {
$query->setTotalRows($this->getCount($builder, $identifier));
}
// Get record count after filtering
$this->buildCriteria($builder, $state);
$query->setFilteredRows($this->getCount($builder, $identifier));
// Perform mapping of all referred fields and implied fields
$aliases = $this->getAliases($query);
$query->set('aliases', $aliases);
$query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases));
}
protected function getCount(QueryBuilder $queryBuilder, $identifier): int
{
//Check if the queryBuilder is having a HAVING clause, which would make the count query invalid
if (empty($queryBuilder->getDQLPart('having'))) {
//If not, we can use the simple count query
return parent::getCount($queryBuilder, $identifier);
}
//Otherwise Use the paginator, which uses a subquery to get the right count even with HAVING clauses
return (new Paginator($queryBuilder, false))->count();
}
protected function getResults(AdapterQuery $query): \Traversable
{
//Very similar to the parent class
/** @var QueryBuilder $builder */
$builder = $query->get('qb');
$state = $query->getState();
// Apply definitive view state for current 'page' of the table
foreach ($state->getOrderBy() as list($column, $direction)) {
/** @var AbstractColumn $column */
if ($column->isOrderable()) {
$builder->addOrderBy($column->getOrderField(), $direction);
}
}
if (null !== $state->getLength()) {
$builder
->setFirstResult($state->getStart())
->setMaxResults($state->getLength())
;
}
$id_query = $builder->getQuery();
$event = new ORMAdapterQueryEvent($id_query);
$state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY);
//In the first step we only get the ids of the main entity...
$ids = $id_query->getArrayResult();
//Which is then passed to the detailQuery, which filters for the entities based on the IDs
$detail_qb = $this->manager->createQueryBuilder();
$this->detailQueryCallable->__invoke($detail_qb, $ids);
$detail_query = $detail_qb->getQuery();
//We pass the results of the detail query to the datatable for view rendering
foreach ($detail_query->getResult() as $item) {
yield $item;
}
}
protected function getSimpleTotalCount(QueryBuilder $queryBuilder): int
{
/** The paginator count queries can be rather slow, so when query for total count (100ms or longer),
* just return the entity count.
*/
/** @var Query\Expr\From $from_expr */
$from_expr = $queryBuilder->getDQLPart('from')[0];
return $this->manager->getRepository($from_expr->getFrom())->count([]);
}
}

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\DataTables\Column;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Services\EntityURLGenerator;
use Omines\DataTablesBundle\Column\AbstractColumn;
use Symfony\Component\OptionsResolver\Options;
@@ -70,8 +71,9 @@ class EntityColumn extends AbstractColumn
if ($entity instanceof AbstractNamedDBElement) {
if (null !== $entity->getID()) {
return sprintf(
'<a href="%s">%s</a>',
'<a href="%s" title="%s">%s</a>',
$this->urlGenerator->listPartsURL($entity),
$entity instanceof AbstractStructuralDBElement ? htmlspecialchars($entity->getFullPath()) : htmlspecialchars($entity->getName()),
htmlspecialchars($entity->getName())
);
}

View File

@@ -31,10 +31,14 @@ class EnumColumn extends AbstractColumn
{
/**
* @phpstan-return T
* @phpstan-return T|null
*/
public function normalize($value): UnitEnum
public function normalize($value): ?UnitEnum
{
if ($value === null) {
return null;
}
if (is_a($value, $this->getEnumClass())) {
return $value;
}

View File

@@ -41,9 +41,9 @@ class LessThanDesiredConstraint extends BooleanConstraint
//If value is true, we want to filter for parts with stock < desired stock
if ($this->value) {
$queryBuilder->andHaving('amountSum < part.minamount');
$queryBuilder->andHaving('amountSum < minamount');
} else {
$queryBuilder->andHaving('amountSum >= part.minamount');
$queryBuilder->andHaving('amountSum >= minamount');
}
}
}

View File

@@ -156,6 +156,7 @@ class LogDataTable implements DataTableTypeInterface
$dataTable->add('user', TextColumn::class, [
'label' => 'log.user',
'orderField' => 'user.name',
'render' => function ($value, AbstractLogEntry $context): string {
$user = $context->getUser();
@@ -292,9 +293,13 @@ class LogDataTable implements DataTableTypeInterface
$target_type = LogTargetType::fromElementClass($element);
$target_id = $element->getID();
$builder->orWhere('log.target_type = :filter_target_type AND log.target_id = :filter_target_id');
$builder->setParameter('filter_target_type', $target_type);
$builder->setParameter('filter_target_id', $target_id);
//We have to create unique parameter names for each element
$target_type_var = 'filter_target_type_' . uniqid('', false);
$target_id_var = 'filter_target_id_' . uniqid('', false);
$builder->orWhere("log.target_type = :$target_type_var AND log.target_id = :$target_id_var");
$builder->setParameter($target_type_var, $target_type);
$builder->setParameter($target_id_var, $target_id);
}
}
}

View File

@@ -22,9 +22,19 @@ declare(strict_types=1);
namespace App\DataTables;
use App\DataTables\Adapters\FetchResultsAtOnceORMAdapter;
use App\DataTables\Adapters\TwoStepORMAdapater;
use App\DataTables\Column\EnumColumn;
use App\Doctrine\Helpers\FieldHelper;
use App\Entity\Parts\ManufacturingStatus;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapterEvents;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Storelocation;
use App\DataTables\Adapters\CustomFetchJoinORMAdapter;
use App\DataTables\Column\EntityColumn;
use App\DataTables\Column\IconLinkColumn;
use App\DataTables\Column\LocaleDateTimeColumn;
@@ -41,12 +51,9 @@ use App\DataTables\Helpers\PartDataTableHelper;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Services\Formatters\AmountFormatter;
use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\EntityURLGenerator;
use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\QueryBuilder;
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
use Omines\DataTablesBundle\Column\MapColumn;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
@@ -144,8 +151,9 @@ final class PartsDataTable implements DataTableTypeInterface
continue;
}
$tmp[] = sprintf(
'<a href="%s">%s</a>',
'<a href="%s" title="%s">%s</a>',
$this->urlGenerator->listPartsURL($lot->getStorageLocation()),
htmlspecialchars($lot->getStorageLocation()->getFullPath()),
htmlspecialchars($lot->getStorageLocation()->getName())
);
}
@@ -226,18 +234,17 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.favorite'),
'visible' => false,
])
->add('manufacturing_status', MapColumn::class, [
->add('manufacturing_status', EnumColumn::class, [
'label' => $this->translator->trans('part.table.manufacturingStatus'),
'visible' => false,
'default' => $this->translator->trans('m_status.unknown'),
'map' => [
'' => $this->translator->trans('m_status.unknown'),
'announced' => $this->translator->trans('m_status.announced'),
'active' => $this->translator->trans('m_status.active'),
'nrfnd' => $this->translator->trans('m_status.nrfnd'),
'eol' => $this->translator->trans('m_status.eol'),
'discontinued' => $this->translator->trans('m_status.discontinued'),
],
'class' => ManufacturingStatus::class,
'render' => function(?ManufacturingStatus $status, Part $context): string {
if (!$status) {
return '';
}
return $this->translator->trans($status->toTranslationKey());
} ,
])
->add('manufacturer_product_number', TextColumn::class, [
'label' => $this->translator->trans('part.table.mpn'),
@@ -265,12 +272,11 @@ final class PartsDataTable implements DataTableTypeInterface
])
->addOrderBy('name')
->createAdapter(CustomFetchJoinORMAdapter::class, [
'simple_total_query' => true,
'query' => function (QueryBuilder $builder): void {
$this->getQuery($builder);
},
->createAdapter(TwoStepORMAdapater::class, [
'filter_query' => $this->getFilterQuery(...),
'detail_query' => $this->getDetailQuery(...),
'entity' => Part::class,
'hydrate' => Query::HYDRATE_OBJECT,
'criteria' => [
function (QueryBuilder $builder) use ($options): void {
$this->buildCriteria($builder, $options);
@@ -280,10 +286,58 @@ final class PartsDataTable implements DataTableTypeInterface
]);
}
private function getQuery(QueryBuilder $builder): void
private function getFilterQuery(QueryBuilder $builder): void
{
//Distinct is very slow here, do not add this here (also I think this is not needed here, as the id column is always distinct)
$builder->select('part')
/* In the filter query we only select the IDs. The fetching of the full entities is done in the detail query.
* We only need to join the entities here, so we can filter by them.
* The filter conditions are added to this QB in the buildCriteria method.
*/
$builder
->select('part.id')
->addSelect('part.minamount AS HIDDEN minamount')
//Calculate amount sum using a subquery, so we can filter and sort by it
->addSelect(
'(
SELECT IFNULL(SUM(partLot.amount), 0.0)
FROM '. PartLot::class. ' partLot
WHERE partLot.part = part.id
AND partLot.instock_unknown = false
AND (partLot.expiration_date IS NULL OR partLot.expiration_date > CURRENT_DATE())
) AS HIDDEN amountSum'
)
->from(Part::class, 'part')
->leftJoin('part.category', 'category')
->leftJoin('part.master_picture_attachment', 'master_picture_attachment')
->leftJoin('part.partLots', 'partLots')
->leftJoin('partLots.storage_location', 'storelocations')
->leftJoin('part.footprint', 'footprint')
->leftJoin('footprint.master_picture_attachment', 'footprint_attachment')
->leftJoin('part.manufacturer', 'manufacturer')
->leftJoin('part.orderdetails', 'orderdetails')
->leftJoin('orderdetails.supplier', 'suppliers')
->leftJoin('part.attachments', 'attachments')
->leftJoin('part.partUnit', 'partUnit')
->leftJoin('part.parameters', 'parameters')
//This must be the only group by, or the paginator will not work correctly
->addGroupBy('part.id')
;
}
private function getDetailQuery(QueryBuilder $builder, array $filter_results): void
{
$ids = array_map(fn($row) => $row['id'], $filter_results);
/*
* In this query we take the IDs which were filtered, paginated and sorted in the filter query, and fetch the
* full entities.
* We can do complex fetch joins, as we do not need to filter or sort here (which would kill the performance).
* The only condition should be for the IDs.
* It is important that elements are ordered the same way, as the IDs are passed, or ordering will be wrong.
*/
$builder
->select('part')
->addSelect('category')
->addSelect('footprint')
->addSelect('manufacturer')
@@ -318,6 +372,9 @@ final class PartsDataTable implements DataTableTypeInterface
->leftJoin('part.partUnit', 'partUnit')
->leftJoin('part.parameters', 'parameters')
->where('part.id IN (:ids)')
->setParameter('ids', $ids)
//We have to group by all elements, or only the first sub elements of an association is fetched! (caused issue #190)
->addGroupBy('part')
->addGroupBy('partLots')
@@ -333,6 +390,9 @@ final class PartsDataTable implements DataTableTypeInterface
->addGroupBy('partUnit')
->addGroupBy('parameters')
;
//Get the results in the same order as the IDs were passed
FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids');
}
private function buildCriteria(QueryBuilder $builder, array $options): void

View File

@@ -0,0 +1,81 @@
<?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\Functions;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/**
* Basically the same as the original Field function, but uses FIELD2 for the SQL query.
*/
class Field2 extends FunctionNode
{
private $field = null;
private $values = [];
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
// Do the field.
$this->field = $parser->ArithmeticPrimary();
// Add the strings to the values array. FIELD must
// be used with at least 1 string not including the field.
$lexer = $parser->getLexer();
while (count($this->values) < 1 ||
$lexer->lookahead['type'] != Lexer::T_CLOSE_PARENTHESIS) {
$parser->match(Lexer::T_COMMA);
$this->values[] = $parser->ArithmeticPrimary();
}
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
$query = 'FIELD2(';
$query .= $this->field->dispatch($sqlWalker);
$query .= ', ';
for ($i = 0; $i < count($this->values); $i++) {
if ($i > 0) {
$query .= ', ';
}
$query .= $this->values[$i]->dispatch($sqlWalker);
}
$query .= ')';
return $query;
}
}

View File

@@ -0,0 +1,103 @@
<?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\Helpers;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\ORM\QueryBuilder;
/**
* The purpose of this class is to provide help with using the FIELD functions in Doctrine, which depends on the database platform.
*/
final class FieldHelper
{
/**
* Add an ORDER BY FIELD expression to the query builder. The correct FIELD function is used depending on the database platform.
* In this function an already bound paramater is used. If you want to a not already bound value, use the addOrderByFieldValues function.
* @param QueryBuilder $qb The query builder to apply the order by to
* @param string $field_expr The expression to compare with the values
* @param string|int $bound_param The already bound parameter to use for the values
* @param string|null $order The order direction (ASC or DESC)
* @return QueryBuilder
*/
public static function addOrderByFieldParam(QueryBuilder $qb, string $field_expr, string|int $bound_param, ?string $order = null): QueryBuilder
{
$db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform();
//If we are on MySQL, we can just use the FIELD function
if ($db_platform instanceof AbstractMySQLPlatform) {
$param = (is_numeric($bound_param) ? '?' : ":") . (string) $bound_param;
$qb->orderBy("FIELD($field_expr, $param)", $order);
} else {
//Retrieve the values from the bound parameter
$param = $qb->getParameter($bound_param);
if ($param === null) {
throw new \InvalidArgumentException("The bound parameter $bound_param does not exist.");
}
//Generate a unique key from the field_expr
$key = 'field2_' . (string) $bound_param;
self::addSqliteOrderBy($qb, $field_expr, $key, $param->getValue(), $order);
}
return $qb;
}
private static function addSqliteOrderBy(QueryBuilder $qb, string $field_expr, string $key, array $values, ?string $order = null): void
{
//Otherwise we emulate it using
$qb->orderBy("LOCATE(CONCAT(',', $field_expr, ','), :$key)", $order);
//The string must be padded with a comma on both sides, otherwise the search using INSTR will fail
$qb->setParameter($key, ',' .implode(',', $values) . ',');
}
/**
* Add an ORDER BY FIELD expression to the query builder. The correct FIELD function is used depending on the database platform.
* In this function the values are passed as an array. If you want to reuse an existing bound parameter, use the addOrderByFieldParam function.
* @param QueryBuilder $qb The query builder to apply the order by to
* @param string $field_expr The expression to compare with the values
* @param array $values The values to compare with the expression as array
* @param string|null $order The order direction (ASC or DESC)
* @return QueryBuilder
*/
public static function addOrderByFieldValues(QueryBuilder $qb, string $field_expr, array $values, ?string $order = null): QueryBuilder
{
$db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform();
$key = 'field2_' . md5($field_expr);
//If we are on MySQL, we can just use the FIELD function
if ($db_platform instanceof AbstractMySQLPlatform) {
$qb->orderBy("FIELD($field_expr, :field_arr)", $order);
} else {
//Generate a unique key from the field_expr
//Otherwise we have to it using the FIELD2 function
self::addSqliteOrderBy($qb, $field_expr, $key, $values, $order);
}
$qb->setParameter($key, $values);
return $qb;
}
}

View File

@@ -47,14 +47,64 @@ class SQLiteRegexExtension
//Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation
if($native_connection instanceof \PDO && method_exists($native_connection, 'sqliteCreateFunction' )) {
$native_connection->sqliteCreateFunction('REGEXP', function ($pattern, $value) {
try {
return (mb_ereg($pattern, $value)) ? 1 : 0;
} catch (\ErrorException $e) {
throw InvalidRegexException::fromMBRegexError($e);
}
});
$native_connection->sqliteCreateFunction('REGEXP', self::regexp(...), 2, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD', self::field(...), -1, \PDO::SQLITE_DETERMINISTIC);
$native_connection->sqliteCreateFunction('FIELD2', self::field2(...), 2, \PDO::SQLITE_DETERMINISTIC);
}
}
}
/**
* This function emulates the MySQL regexp function for SQLite
* @param string $pattern
* @param string $value
* @return int
*/
final public static function regexp(string $pattern, string $value): int
{
try {
return (mb_ereg($pattern, $value)) ? 1 : 0;
} catch (\ErrorException $e) {
throw InvalidRegexException::fromMBRegexError($e);
}
}
/**
* Very similar to the field function, but takes the array values as a comma separated string.
* This is needed as SQLite has a pretty low argument count limit.
* @param string|int|null $value
* @param string $imploded_array
* @return int
*/
final public static function field2(string|int|null $value, string $imploded_array): int
{
$array = explode(',', $imploded_array);
return self::field($value, ...$array);
}
/**
* This function emulates the MySQL field function for SQLite
* This function returns the index (position) of the first argument in the subsequent arguments.
* If the first argument is not found or is NULL, 0 is returned.
* @param string|int|null $value
* @param mixed ...$array
* @return int
*/
final public static function field(string|int|null $value, ...$array): int
{
if ($value === null) {
return 0;
}
//We are loose with the types here
//@phpstan-ignore-next-line
$index = array_search($value, $array, false);
if ($index === false) {
return 0;
}
return $index + 1;
}
}

View File

@@ -97,7 +97,6 @@ abstract class Attachment extends AbstractNamedDBElement
*/
#[Assert\NotBlank(message: 'validator.attachment.name_not_blank')]
#[Groups(['simple', 'extended', 'full'])]
#[ORM\Column(type: Types::STRING)]
protected string $name = '';
/**
@@ -144,9 +143,17 @@ abstract class Attachment extends AbstractNamedDBElement
*/
public function isPicture(): bool
{
//We can not check if an external link is a picture, so just assume this is false
if ($this->isExternal()) {
return true;
//Check if we can extract a file extension from the URL
$extension = pathinfo(parse_url($this->path, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
//If no extension is found or it is known picture extension, we assume that this is a picture extension
if ($extension === '' || in_array(strtolower($extension), static::PICTURE_EXTS, true)) {
return true;
}
//Otherwise we assume that the file is not a picture
return false;
}
$extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);

View File

@@ -26,6 +26,7 @@ use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\MasterAttachmentTrait;
use App\Entity\Contracts\HasAttachmentsInterface;
use App\Entity\Contracts\HasMasterAttachmentInterface;
use App\Repository\AttachmentContainingDBElementRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@@ -34,7 +35,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
/**
* @template-covariant AT of Attachment
*/
#[ORM\MappedSuperclass]
#[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)]
abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface
{
use MasterAttachmentTrait;

View File

@@ -63,6 +63,10 @@ class AttachmentType extends AbstractStructuralDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: AttachmentTypeAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, AttachmentTypeParameter>
*/
#[Assert\Valid]
@@ -70,9 +74,6 @@ class AttachmentType extends AbstractStructuralDBElement
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
protected Collection $parameters;
/**
* @var Collection<Attachment>
*/
/**
* @var Collection<Attachment>
*/

View File

@@ -124,6 +124,12 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
*/
private array $full_path_strings = [];
/**
* Alternative names (semicolon-separated) for this element, which can be used for searching (especially for info provider system)
*/
#[ORM\Column(type: Types::TEXT, nullable: true, options: ['default' => null])]
private ?string $alternative_names = "";
public function __construct()
{
parent::__construct();
@@ -413,4 +419,34 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
return $this;
}
/**
* Returns a comma separated list of alternative names.
* @return string|null
*/
public function getAlternativeNames(): ?string
{
if ($this->alternative_names === null) {
return null;
}
//Remove trailing comma
return rtrim($this->alternative_names, ',');
}
/**
* Sets a comma separated list of alternative names.
* @return $this
*/
public function setAlternativeNames(?string $new_value): self
{
//Add a trailing comma, if not already there (makes it easier to find in the database)
if (is_string($new_value) && substr($new_value, -1) !== ',') {
$new_value .= ',';
}
$this->alternative_names = $new_value;
return $this;
}
}

View File

@@ -33,10 +33,12 @@ trait MasterAttachmentTrait
{
/**
* @var Attachment|null
* Mapping is done in the subclasses (e.g. Part), like with the attachments.
* If this is done here (which is possible in theory), the attachment is not lazy loaded anymore, which causes unnecessary overhead.
*
* !!! If you change this name, you have to change it in the fetchHint in the AttachmentContainingDBElementRepository (getElementsAndPreviewAttachmentByIDs()) too !!!
*/
#[Assert\Expression('value == null or value.isPicture()', message: 'part.master_attachment.must_be_picture')]
#[ORM\ManyToOne(targetEntity: Attachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/**

View File

@@ -41,6 +41,8 @@ declare(strict_types=1);
namespace App\Entity\LabelSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\LabelProfileRepository;
use App\EntityListeners\TreeCacheInvalidationListener;
use Doctrine\DBAL\Types\Types;
@@ -68,6 +70,10 @@ class LabelProfile extends AttachmentContainingDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: AttachmentTypeAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/**
* @var LabelOptions
*/

View File

@@ -64,6 +64,7 @@ class SecurityEventLogEntry extends AbstractLogEntry
6 => SecurityEvents::GOOGLE_DISABLED,
7 => SecurityEvents::TRUSTED_DEVICE_RESET,
8 => SecurityEvents::TFA_ADMIN_RESET,
9 => SecurityEvents::USER_IMPERSONATED,
];
public function __construct(string $type, string $ip_address, bool $anonymize = true)

152
src/Entity/OAuthToken.php Normal file
View File

@@ -0,0 +1,152 @@
<?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;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use League\OAuth2\Client\Token\AccessTokenInterface;
/**
* This entity represents a OAuth token pair (access and refresh token), for an application
*/
#[ORM\Entity()]
#[ORM\Table(name: 'oauth_tokens')]
#[ORM\UniqueConstraint(name: 'oauth_tokens_unique_name', columns: ['name'])]
#[ORM\Index(columns: ['name'], name: 'oauth_tokens_name_idx')]
class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
{
/** @var string|null The short-term usable OAuth2 token */
#[ORM\Column(type: 'text', nullable: true)]
private ?string $token = null;
/** @var \DateTimeInterface The date when the token expires */
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeInterface $expires_at = null;
/** @var string|null The refresh token for the OAuth2 auth */
#[ORM\Column(type: 'text', nullable: true)]
private ?string $refresh_token = null;
/**
* The default expiration time for a authorization token, if no expiration time is given
*/
private const DEFAULT_EXPIRATION_TIME = 3600;
public function __construct(string $name, ?string $refresh_token, ?string $token = null, \DateTimeInterface $expires_at = null)
{
//If token is given, you also have to give the expires_at date
if ($token !== null && $expires_at === null) {
throw new \InvalidArgumentException('If you give a token, you also have to give the expires_at date');
}
//If no refresh_token is given, the token is a client credentials grant token, which must have a token
if ($refresh_token === null && $token === null) {
throw new \InvalidArgumentException('If you give no refresh_token, you have to give a token!');
}
$this->name = $name;
$this->refresh_token = $refresh_token;
$this->expires_at = $expires_at;
$this->token = $token;
}
public static function fromAccessToken(AccessTokenInterface $accessToken, string $name): self
{
return new self(
$name,
$accessToken->getRefreshToken(),
$accessToken->getToken(),
self::unixTimestampToDatetime($accessToken->getExpires() ?? time() + self::DEFAULT_EXPIRATION_TIME)
);
}
private static function unixTimestampToDatetime(int $timestamp): \DateTimeInterface
{
return \DateTimeImmutable::createFromFormat('U', (string)$timestamp);
}
public function getToken(): ?string
{
return $this->token;
}
public function getExpirationDate(): ?\DateTimeInterface
{
return $this->expires_at;
}
public function getRefreshToken(): string
{
return $this->refresh_token;
}
public function isExpired(): bool
{
//null token is always expired
if ($this->token === null) {
return true;
}
if ($this->expires_at === null) {
return false;
}
return $this->expires_at->getTimestamp() < time();
}
/**
* Returns true if this token is a client credentials grant token (meaning it has no refresh token), and
* needs to be refreshed via the client credentials grant.
* @return bool
*/
public function isClientCredentialsGrant(): bool
{
return $this->refresh_token === null;
}
public function replaceWithNewToken(AccessTokenInterface $accessToken): void
{
$this->token = $accessToken->getToken();
$this->refresh_token = $accessToken->getRefreshToken();
//If no expiration date is given, we set it to the default expiration time
$this->expires_at = self::unixTimestampToDatetime($accessToken->getExpires() ?? time() + self::DEFAULT_EXPIRATION_TIME);
}
public function getExpires()
{
return $this->expires_at->getTimestamp();
}
public function hasExpired()
{
return $this->isExpired();
}
public function getValues()
{
return [];
}
}

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\Parts\CategoryRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\Common\Collections\ArrayCollection;
@@ -118,6 +120,10 @@ class Category extends AbstractPartsContainingDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: CategoryAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, CategoryParameter>
*/
#[Assert\Valid]

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\Parts\FootprintRepository;
use App\Entity\Base\AbstractStructuralDBElement;
use Doctrine\Common\Collections\ArrayCollection;
@@ -59,6 +61,10 @@ class Footprint extends AbstractPartsContainingDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: FootprintAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/**
* @var FootprintAttachment|null
*/

View File

@@ -0,0 +1,155 @@
<?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 App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Embeddable;
/**
* This class represents a reference to a info provider inside a part.
*/
#[Embeddable]
class InfoProviderReference
{
/** @var string|null The key referencing the provider used to get this part, or null if it was not provided by a data provider */
#[Column(type: 'string', nullable: true)]
private ?string $provider_key = null;
/** @var string|null The id of this part inside the provider system or null if the part was not provided by a data provider */
#[Column(type: 'string', nullable: true)]
private ?string $provider_id = null;
/**
* @var string|null The url of this part inside the provider system or null if this info is not existing
*/
#[Column(type: 'string', nullable: true)]
private ?string $provider_url = null;
#[Column(type: Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])]
private ?\DateTimeInterface $last_updated = null;
/**
* Constructing is forbidden from outside.
*/
private function __construct()
{
}
/**
* Returns the key usable to identify the provider, which provided this part. Returns null, if the part was not created by a provider.
* @return string|null
*/
public function getProviderKey(): ?string
{
return $this->provider_key;
}
/**
* Returns the id of this part inside the provider system or null if the part was not provided by a data provider.
* @return string|null
*/
public function getProviderId(): ?string
{
return $this->provider_id;
}
/**
* Returns the url of this part inside the provider system or null if this info is not existing.
* @return string|null
*/
public function getProviderUrl(): ?string
{
return $this->provider_url;
}
/**
* Gets the time, when the part was last time updated by the provider.
* @return \DateTimeInterface|null
*/
public function getLastUpdated(): ?\DateTimeInterface
{
return $this->last_updated;
}
/**
* Returns true, if this part was created based on infos from a provider.
* Or false, if this part was created by a user manually.
* @return bool
*/
public function isProviderCreated(): bool
{
return $this->provider_key !== null;
}
/**
* Creates a new instance, without any provider information.
* Use this for parts, which are created by a user manually.
* @return InfoProviderReference
*/
public static function noProvider(): self
{
$ref = new InfoProviderReference();
$ref->provider_key = null;
$ref->provider_id = null;
$ref->provider_url = null;
$ref->last_updated = null;
return $ref;
}
/**
* Creates a reference to an info provider based on the given parameters.
* @param string $provider_key
* @param string $provider_id
* @param string|null $provider_url
* @return self
*/
public static function providerReference(string $provider_key, string $provider_id, ?string $provider_url = null): self
{
$ref = new InfoProviderReference();
$ref->provider_key = $provider_key;
$ref->provider_id = $provider_id;
$ref->provider_url = $provider_url;
$ref->last_updated = new \DateTimeImmutable();
return $ref;
}
/**
* Creates a reference to an info provider based on the given Part DTO
* @param SearchResultDTO $dto
* @return self
*/
public static function fromPartDTO(SearchResultDTO $dto): self
{
$ref = new InfoProviderReference();
$ref->provider_key = $dto->provider_key;
$ref->provider_id = $dto->provider_id;
$ref->provider_url = $dto->provider_url;
$ref->last_updated = new \DateTimeImmutable();
return $ref;
}
}

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\Parts\ManufacturerRepository;
use App\Entity\Base\AbstractStructuralDBElement;
use Doctrine\Common\Collections\ArrayCollection;
@@ -59,6 +61,10 @@ class Manufacturer extends AbstractCompany
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: ManufacturerAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, ManufacturerParameter>
*/
#[Assert\Valid]

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\Entity\Parts;
enum ManufacturingStatus: string
{
/** Part has been announced, but is not in production yet */
case ANNOUNCED = 'announced';
/** Part is in production and will be for the foreseeable future */
case ACTIVE = 'active';
/** Not recommended for new designs. */
case NRFND = 'nrfnd';
/** End of life: Part will become discontinued soon */
case EOL = 'eol';
/** Part is obsolete/discontinued by the manufacturer. */
case DISCONTINUED = 'discontinued';
/** Status not set */
case NOT_SET = '';
public function toTranslationKey(): string
{
return match ($this) {
self::ANNOUNCED => 'm_status.announced',
self::ACTIVE => 'm_status.active',
self::NRFND => 'm_status.nrfnd',
self::EOL => 'm_status.eol',
self::DISCONTINUED => 'm_status.discontinued',
self::NOT_SET => '',
};
}
}

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\Parts\MeasurementUnitRepository;
use Doctrine\DBAL\Types\Types;
use App\Entity\Base\AbstractStructuralDBElement;
@@ -90,6 +92,10 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: MeasurementUnitAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, MeasurementUnitParameter>
*/
#[Assert\Valid]

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\PartRepository;
use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\Attachment;
@@ -83,10 +84,10 @@ class Part extends AttachmentContainingDBElement
* Overridden properties
* (They are defined here and not in a trait, to avoid conflicts).
****************************************************************/
/**
* @var string The name of this part
*/
#[ORM\Column(type: Types::STRING)]
protected string $name = '';
/**
@@ -102,7 +103,7 @@ class Part extends AttachmentContainingDBElement
* @var Attachment|null
*/
#[Assert\Expression('value == null or value.isPicture()', message: 'part.master_attachment.must_be_picture')]
#[ORM\ManyToOne(targetEntity: Attachment::class)]
#[ORM\ManyToOne(targetEntity: PartAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
@@ -114,6 +115,9 @@ class Part extends AttachmentContainingDBElement
$this->orderdetails = new ArrayCollection();
$this->parameters = new ArrayCollection();
$this->project_bom_entries = new ArrayCollection();
//By default, the part has no provider
$this->providerReference = InfoProviderReference::noProvider();
}
public function __clone()
@@ -139,6 +143,9 @@ class Part extends AttachmentContainingDBElement
foreach ($parameters as $parameter) {
$this->addParameter(clone $parameter);
}
//Deep clone info provider
$this->providerReference = clone $this->providerReference;
}
parent::__clone();
}

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Repository\PartLotRepository;
use Doctrine\DBAL\Types\Types;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\TimestampTrait;
@@ -79,7 +80,7 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* @var Storelocation|null The storelocation of this lot
*/
#[Groups(['simple', 'extended', 'full', 'import'])]
#[ORM\ManyToOne(targetEntity: Storelocation::class)]
#[ORM\ManyToOne(targetEntity: Storelocation::class, fetch: 'EAGER')]
#[ORM\JoinColumn(name: 'id_store_location')]
#[Selectable()]
protected ?Storelocation $storage_location = null;

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts\PartTraits;
use App\Entity\Parts\InfoProviderReference;
use Doctrine\DBAL\Types\Types;
use App\Entity\Parts\Part;
use Doctrine\ORM\Mapping as ORM;
@@ -63,6 +64,12 @@ trait AdvancedPropertyTrait
#[ORM\Column(type: Types::STRING, length: 100, nullable: true, unique: true)]
protected ?string $ipn = null;
/**
* @var InfoProviderReference The reference to the info provider, that provided the information about this part
*/
#[ORM\Embedded(class: InfoProviderReference::class, columnPrefix: 'provider_reference_')]
protected InfoProviderReference $providerReference;
/**
* Checks if this part is marked, for that it needs further review.
*/
@@ -150,5 +157,27 @@ trait AdvancedPropertyTrait
return $this;
}
/**
* Returns the reference to the info provider, that provided the information about this part.
* @return InfoProviderReference
*/
public function getProviderReference(): InfoProviderReference
{
return $this->providerReference;
}
/**
* Sets the reference to the info provider, that provided the information about this part.
* @param InfoProviderReference $providerReference
* @return Part
*/
public function setProviderReference(InfoProviderReference $providerReference): Part
{
$this->providerReference = $providerReference;
return $this;
}
}

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts\PartTraits;
use App\Entity\Parts\ManufacturingStatus;
use Doctrine\DBAL\Types\Types;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
@@ -49,7 +50,7 @@ trait ManufacturerTrait
*/
#[Assert\Url]
#[Groups(['full', 'import'])]
#[ORM\Column(type: Types::STRING)]
#[ORM\Column(type: Types::TEXT)]
protected string $manufacturer_product_url = '';
/**
@@ -60,12 +61,11 @@ trait ManufacturerTrait
protected string $manufacturer_product_number = '';
/**
* @var string|null The production status of this part. Can be one of the specified ones.
* @var ManufacturingStatus|null The production status of this part. Can be one of the specified ones.
*/
#[Assert\Choice(['announced', 'active', 'nrfnd', 'eol', 'discontinued', ''])]
#[Groups(['extended', 'full', 'import'])]
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
protected ?string $manufacturing_status = '';
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, enumType: ManufacturingStatus::class)]
protected ?ManufacturingStatus $manufacturing_status = ManufacturingStatus::NOT_SET;
/**
* Get the link to the website of the article on the manufacturers website
@@ -113,9 +113,9 @@ trait ManufacturerTrait
* * "eol": Part will become discontinued soon
* * "discontinued": Part is obsolete/discontinued by the manufacturer.
*
* @return string
* @return ManufacturingStatus|null
*/
public function getManufacturingStatus(): ?string
public function getManufacturingStatus(): ?ManufacturingStatus
{
return $this->manufacturing_status;
}
@@ -124,9 +124,9 @@ trait ManufacturerTrait
* Sets the manufacturing status for this part
* See getManufacturingStatus() for valid values.
*
* @return Part
* @return $this
*/
public function setManufacturingStatus(string $manufacturing_status): self
public function setManufacturingStatus(ManufacturingStatus $manufacturing_status): self
{
$this->manufacturing_status = $manufacturing_status;

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\Attachment;
use App\Repository\Parts\StorelocationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\Common\Collections\ArrayCollection;
@@ -109,6 +110,10 @@ class Storelocation extends AbstractPartsContainingDBElement
#[ORM\OneToMany(targetEntity: StorelocationAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: StorelocationAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/********************************************************************************
*
* Getters

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\Parts;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\Parts\SupplierRepository;
use App\Entity\PriceInformations\Orderdetail;
use Doctrine\Common\Collections\ArrayCollection;
@@ -91,6 +93,10 @@ class Supplier extends AbstractCompany
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: SupplierAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, SupplierParameter>
*/
#[Assert\Valid]

View File

@@ -22,6 +22,9 @@ declare(strict_types=1);
namespace App\Entity\PriceInformations;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\CurrencyRepository;
use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\CurrencyAttachment;
use App\Entity\Base\AbstractStructuralDBElement;
@@ -42,7 +45,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @extends AbstractStructuralDBElement<CurrencyAttachment, CurrencyParameter>
*/
#[UniqueEntity('iso_code')]
#[ORM\Entity]
#[ORM\Entity(repositoryClass: CurrencyRepository::class)]
#[ORM\Table(name: 'currencies')]
#[ORM\Index(name: 'currency_idx_name', columns: ['name'])]
#[ORM\Index(name: 'currency_idx_parent_name', columns: ['parent_id', 'name'])]
@@ -62,6 +65,7 @@ class Currency extends AbstractStructuralDBElement
* @var string the 3-letter ISO code of the currency
*/
#[Assert\Currency]
#[Assert\NotBlank]
#[Groups(['extended', 'full', 'import'])]
#[ORM\Column(type: Types::STRING)]
protected string $iso_code = "";
@@ -82,6 +86,10 @@ class Currency extends AbstractStructuralDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: CurrencyAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, CurrencyParameter>
*/
#[Assert\Valid]
@@ -113,12 +121,12 @@ class Currency extends AbstractStructuralDBElement
*
* @return string
*/
public function getIsoCode(): ?string
public function getIsoCode(): string
{
return $this->iso_code;
}
public function setIsoCode(?string $iso_code): self
public function setIsoCode(string $iso_code): self
{
$this->iso_code = $iso_code;
@@ -156,12 +164,15 @@ class Currency extends AbstractStructuralDBElement
*/
public function setExchangeRate(?BigDecimal $exchange_rate): self
{
if (!$exchange_rate instanceof BigDecimal) {
//If the exchange rate is null, set it to null and return.
if ($exchange_rate === null) {
$this->exchange_rate = null;
return $this;
}
$tmp = $exchange_rate->toScale(self::PRICE_SCALE, RoundingMode::HALF_UP);
//Only change the object, if the value changes, so that doctrine does not detect it as changed.
if ((string) $tmp !== (string) $this->exchange_rate) {
//Or if the current exchange rate is currently null, as we can not compare it then
if ($this->exchange_rate === null || $exchange_rate->compareTo($this->exchange_rate) !== 0) {
$this->exchange_rate = $exchange_rate;
}

View File

@@ -75,7 +75,7 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N
*/
#[Assert\Url]
#[Groups(['full', 'import'])]
#[ORM\Column(type: Types::STRING)]
#[ORM\Column(type: Types::TEXT)]
protected string $supplier_product_url = '';
/**

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\ProjectSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\Parts\DeviceRepository;
use App\Validator\Constraints\UniqueObjectCollection;
use Doctrine\DBAL\Types\Types;
@@ -93,6 +95,10 @@ class Project extends AbstractStructuralDBElement
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: ProjectAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var Collection<int, ProjectParameter>
*/
#[ORM\OneToMany(targetEntity: ProjectParameter::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)]

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\UserSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Validator\Constraints\NoLockout;
use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\GroupAttachment;
@@ -76,6 +78,10 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: GroupAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
#[Groups(['full'])]
#[ORM\Embedded(class: PermissionData::class, columnPrefix: 'permissions_')]
#[ValidPermission()]

View File

@@ -43,7 +43,7 @@ final class PermissionData implements \JsonSerializable
/**
* The current schema version of the permission data
*/
public const CURRENT_SCHEMA_VERSION = 2;
public const CURRENT_SCHEMA_VERSION = 3;
/**
* Creates a new Permission Data Instance using the given data.

View File

@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\UserSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Repository\UserRepository;
use App\EntityListeners\TreeCacheInvalidationListener;
use App\Validator\Constraints\NoLockout;
@@ -67,6 +69,10 @@ use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface
#[ORM\EntityListeners([TreeCacheInvalidationListener::class])]
#[ORM\Table('`users`')]
#[ORM\Index(name: 'user_idx_username', columns: ['name'])]
#[ORM\AttributeOverrides([
new ORM\AttributeOverride(name: 'name', column: new ORM\Column(type: Types::STRING, length: 180, unique: true))
])]
#[NoLockout()]
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface,
BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface, SamlUserInterface
@@ -123,11 +129,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
#[ORM\Column(type: Types::JSON)]
protected ?array $backupCodes = [];
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
protected ?int $id = null;
/**
* @var Group|null the group this user belongs to
* DO NOT PUT A fetch eager here! Otherwise, you can not unset the group of a user! This seems to be some kind of bug in doctrine. Maybe this is fixed in future versions.
@@ -210,7 +211,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
#[Assert\NotBlank]
#[Assert\Regex('/^[\w\.\+\-\$]+$/', message: 'user.invalid_username')]
#[ORM\Column(type: Types::STRING, length: 180, unique: true)]
protected string $name = '';
/**
@@ -222,10 +222,14 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/**
* @var Collection<int, UserAttachment>
*/
#[ORM\OneToMany(targetEntity: UserAttachment::class, mappedBy: 'element', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'element', targetEntity: UserAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['name' => 'ASC'])]
protected Collection $attachments;
#[ORM\ManyToOne(targetEntity: UserAttachment::class)]
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
protected ?Attachment $master_picture_attachment = null;
/** @var \DateTimeInterface|null The time when the backup codes were generated
*/
#[Groups(['full'])]
@@ -234,13 +238,13 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
/** @var Collection<int, LegacyU2FKeyInterface>
*/
#[ORM\OneToMany(targetEntity: U2FKey::class, mappedBy: 'user', cascade: ['REMOVE'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'user', targetEntity: U2FKey::class, cascade: ['REMOVE'], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
protected Collection $u2fKeys;
/**
* @var Collection<int, WebauthnKey>
*/
#[ORM\OneToMany(targetEntity: WebauthnKey::class, mappedBy: 'user', cascade: ['REMOVE'], orphanRemoval: true)]
#[ORM\OneToMany(mappedBy: 'user', targetEntity: WebauthnKey::class, cascade: ['REMOVE'], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
protected Collection $webauthn_keys;
/**

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
#[AsEventListener]
class DisallowSearchEngineIndexingRequestListener
{
private const HEADER_NAME = 'X-Robots-Tag';
private readonly bool $enabled;
public function __construct(#[Autowire(param: 'partdb.demo_mode')] bool $demo_mode)
{
// Disable this listener in demo mode
$this->enabled = !$demo_mode;
}
public function __invoke(ResponseEvent $event): void
{
//Skip if disabled
if (!$this->enabled) {
return;
}
if (!$event->getResponse()->headers->has(self::HEADER_NAME)) {
$event->getResponse()->headers->set(self::HEADER_NAME, 'noindex');
}
}
}

View File

@@ -29,6 +29,7 @@ use App\Entity\LogSystem\CollectionElementDeleted;
use App\Entity\LogSystem\ElementCreatedLogEntry;
use App\Entity\LogSystem\ElementDeletedLogEntry;
use App\Entity\LogSystem\ElementEditedLogEntry;
use App\Entity\OAuthToken;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\PartLot;
use App\Entity\PriceInformations\Orderdetail;
@@ -243,7 +244,7 @@ class EventLoggerSubscriber implements EventSubscriber
$changeSet = $uow->getEntityChangeSet($entity);
//Skip log entry, if only the lastModified field has changed...
if (isset($changeSet['lastModified']) && count($changeSet)) {
if (count($changeSet) === 1 && isset($changeSet['lastModified'])) {
return;
}
@@ -344,6 +345,11 @@ class EventLoggerSubscriber implements EventSubscriber
*/
protected function validEntity(object $entity): bool
{
//Dont log OAuthTokens
if ($entity instanceof OAuthToken) {
return false;
}
//Dont log logentries itself!
return $entity instanceof AbstractDBElement && !$entity instanceof AbstractLogEntry;
}

View File

@@ -48,6 +48,7 @@ use App\Events\SecurityEvents;
use App\Services\LogSystem\EventLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
/**
* This subscriber writes entries to log if a security related event happens (e.g. the user changes its password).
@@ -70,6 +71,7 @@ final class SecurityEventLoggerSubscriber implements EventSubscriberInterface
SecurityEvents::GOOGLE_DISABLED => 'google_disabled',
SecurityEvents::GOOGLE_ENABLED => 'google_enabled',
SecurityEvents::TFA_ADMIN_RESET => 'tfa_admin_reset',
SecurityEvents::USER_IMPERSONATED => 'user_impersonated',
];
}
@@ -103,6 +105,11 @@ final class SecurityEventLoggerSubscriber implements EventSubscriberInterface
$this->addLog(SecurityEvents::U2F_REMOVED, $event);
}
public function user_impersonated(SecurityEvent $event): void
{
$this->addLog(SecurityEvents::USER_IMPERSONATED, $event);
}
public function u2f_added(SecurityEvent $event): void
{
$this->addLog(SecurityEvents::U2F_ADDED, $event);

View File

@@ -0,0 +1,64 @@
<?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\EventSubscriber;
use App\Entity\UserSystem\User;
use App\Events\SecurityEvent;
use App\Events\SecurityEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class SwitchUserEventSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly EventDispatcherInterface $eventDispatcher)
{
}
public static function getSubscribedEvents()
{
return [
'security.switch_user' => 'onSwitchUser',
];
}
public function onSwitchUser(SwitchUserEvent $event): void
{
$target_user = $event->getTargetUser();
// We can only handle User objects
if (!$target_user instanceof User) {
return;
}
//We are only interested in impersonation (not unimpersonation)
if (!$event->getToken() instanceof SwitchUserToken) {
return;
}
$security_event = new SecurityEvent($target_user);
$this->eventDispatcher->dispatch($security_event, SecurityEvents::USER_IMPERSONATED);
}
}

View File

@@ -26,6 +26,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
@@ -78,8 +79,13 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface
return;
}
//If the user is impersonated, we don't need to redirect him
if ($this->security->isGranted('IS_IMPERSONATOR')) {
return;
}
//Abort if we dont need to redirect the user.
if (!$user->isNeedPwChange() && !static::TFARedirectNeeded($user)) {
if (!$user->isNeedPwChange() && !self::TFARedirectNeeded($user, $request)) {
return;
}
@@ -106,7 +112,7 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface
$flashBag->add('warning', 'user.pw_change_needed.flash');
}
if (static::TFARedirectNeeded($user)) {
if (self::TFARedirectNeeded($user, $request)) {
$flashBag->add('warning', 'user.2fa_needed.flash');
}
@@ -117,16 +123,35 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface
* Check if a redirect because of a missing 2FA method is needed.
* That is the case if the group of the user enforces 2FA, but the user has neither Google Authenticator nor an
* U2F key setup.
* The result is cached for some minutes in the session to prevent unnecessary database queries.
*
* @param User $user the user for which should be checked if it needs to be redirected
*
* @return bool true if the user needs to be redirected
*/
public static function TFARedirectNeeded(User $user): bool
public static function TFARedirectNeeded(User $user, Request $request): bool
{
//Check if when we have checked the user the last time
$session = $request->getSession();
$last_check = $session->get('tfa_redirect_check', 0);
//If we have checked the user already in the last 10 minutes, we don't need to check it again
if ($last_check > time() - 600) {
return false;
}
//Otherwise we check the user again
$tfa_enabled = $user->isWebAuthnAuthenticatorEnabled() || $user->isGoogleAuthenticatorEnabled();
return $user->getGroup() instanceof Group && $user->getGroup()->isEnforce2FA() && !$tfa_enabled;
$result = $user->getGroup() instanceof Group && $user->getGroup()->isEnforce2FA() && !$tfa_enabled;
//If no redirect is needed, we set the last check time to now
if (!$result) {
$session->set('tfa_redirect_check', time());
}
return $result;
}
public static function getSubscribedEvents(): array

View File

@@ -52,4 +52,6 @@ class SecurityEvents
final public const GOOGLE_DISABLED = 'security.google_disabled';
final public const TRUSTED_DEVICE_RESET = 'security.trusted_device_reset';
final public const TFA_ADMIN_RESET = 'security.2fa_admin_reset';
final public const USER_IMPERSONATED = 'security.user_impersonated';
}

View File

@@ -22,6 +22,9 @@ declare(strict_types=1);
namespace App\Form\AdminPages;
use App\Entity\PriceInformations\Currency;
use App\Entity\ProjectSystem\Project;
use App\Entity\UserSystem\Group;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
@@ -111,6 +114,19 @@ class BaseEntityAdminForm extends AbstractType
);
}
if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) {
$builder->add('alternative_names', TextType::class, [
'required' => false,
'label' => 'entity.edit.alternative_names.label',
'help' => 'entity.edit.alternative_names.help',
'empty_data' => null,
'attr' => [
'class' => 'tagsinput',
'data-controller' => 'elements--tagsinput',
]
]);
}
$this->additionalFormElements($builder, $options, $entity);
//Attachment section

View File

@@ -42,7 +42,7 @@ class CurrencyAdminForm extends BaseEntityAdminForm
$is_new = null === $entity->getID();
$builder->add('iso_code', CurrencyType::class, [
'required' => false,
'required' => true,
'label' => 'currency.edit.iso_code',
'preferred_choices' => ['EUR', 'USD', 'GBP', 'JPY', 'CNY'],
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),

View File

@@ -50,10 +50,12 @@ class ParameterConstraintType extends AbstractType
$builder->add('unit', SearchType::class, [
'required' => false,
'empty_data' => '',
]);
$builder->add('symbol', SearchType::class, [
'required' => false
'required' => false,
'empty_data' => '',
]);
$builder->add('value_text', TextConstraintType::class, [

View File

@@ -30,6 +30,7 @@ use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Form\Filters\Constraints\BooleanConstraintType;
use App\Form\Filters\Constraints\ChoiceConstraintType;
use App\Form\Filters\Constraints\DateTimeConstraintType;
@@ -172,7 +173,7 @@ class PartFilterType extends AbstractType
$builder->add('supplier', StructuralEntityConstraintType::class, [
'label' => 'supplier.label',
'entity_class' => Manufacturer::class
'entity_class' => Supplier::class
]);
$builder->add('orderdetailsCount', NumberConstraintType::class, [

View File

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

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Form\InfoProviderSystem;
use App\Services\InfoProviderSystem\ProviderRegistry;
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
use Hoa\Compiler\Llk\Rule\Choice;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProviderSelectType extends AbstractType
{
public function __construct(private readonly ProviderRegistry $providerRegistry)
{
}
public function getParent(): string
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choices' => $this->providerRegistry->getActiveProviders(),
'choice_label' => ChoiceList::label($this, fn (?InfoProviderInterface $choice) => $choice?->getProviderInfo()['name']),
'choice_value' => ChoiceList::value($this, fn(?InfoProviderInterface $choice) => $choice?->getProviderKey()),
'multiple' => true,
]);
}
}

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