mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-20 16:52:41 +01:00
Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39009a71d5 | ||
|
|
0430178fe2 | ||
|
|
cf9df883c9 | ||
|
|
198befe2bc | ||
|
|
7195bd6cd6 | ||
|
|
a5fa2da80c | ||
|
|
593d37f37c | ||
|
|
2ddd6753ca | ||
|
|
9537c4f210 | ||
|
|
e0ce6ba165 | ||
|
|
ee50ce26f8 | ||
|
|
94a6de4a90 | ||
|
|
d5902314c3 | ||
|
|
60125534ec | ||
|
|
48385cadc9 | ||
|
|
ba6abe6ca7 | ||
|
|
79ad243bf4 | ||
|
|
5ab21e019d | ||
|
|
d8469efba2 | ||
|
|
316b09ddf3 | ||
|
|
866ef73774 | ||
|
|
138d5c6e0f | ||
|
|
4bed50d894 | ||
|
|
55943f5d8f | ||
|
|
133652c296 | ||
|
|
b9331ac1ef | ||
|
|
08f7b2cc87 | ||
|
|
1a2bdaf8e5 | ||
|
|
d81dec78ae | ||
|
|
f78bd03521 | ||
|
|
6aa16272d8 | ||
|
|
e80f7c08ab | ||
|
|
675f05f0fb | ||
|
|
b1f23e1684 | ||
|
|
d612164885 | ||
|
|
b257e1d5f7 | ||
|
|
f26776ecd5 | ||
|
|
bf4a23652c | ||
|
|
e7681aedb1 | ||
|
|
098fcb29fb | ||
|
|
eb46ea19e3 | ||
|
|
99ee05a90f | ||
|
|
fd31f983af | ||
|
|
80bae4167f | ||
|
|
eaee4af715 | ||
|
|
7d4723c3e4 | ||
|
|
33a0981981 | ||
|
|
b62dc1241d | ||
|
|
e2270aec38 | ||
|
|
73346fcdaf | ||
|
|
7b112512a9 | ||
|
|
0e5613b57b | ||
|
|
e66ff40733 | ||
|
|
73d61f7440 | ||
|
|
dedb3071d6 | ||
|
|
a43ee52086 | ||
|
|
97ccb0cb21 | ||
|
|
1fb334b0ca | ||
|
|
fa4af99525 | ||
|
|
b3153dac68 | ||
|
|
c981476706 | ||
|
|
1a3e5ec705 | ||
|
|
aaff0835a3 | ||
|
|
9bf814d4cd | ||
|
|
b5c0f37f88 | ||
|
|
05d73d2f68 | ||
|
|
ff284d056c | ||
|
|
2393c759f3 | ||
|
|
0a983513ea | ||
|
|
c737348cea | ||
|
|
e8e2258357 | ||
|
|
d847b74522 | ||
|
|
5750e7dbdf | ||
|
|
ffe76f9d2e | ||
|
|
5b1ad541a8 | ||
|
|
4824a82c3f | ||
|
|
f7cea1100c | ||
|
|
827dd01e28 | ||
|
|
d969f49ecc | ||
|
|
0f336b6f89 | ||
|
|
43cc37d10f | ||
|
|
6a00b8e168 | ||
|
|
1b3fc2c23c | ||
|
|
8e96971b9c | ||
|
|
8e39e330fe | ||
|
|
15ba6572df | ||
|
|
1c222ff293 | ||
|
|
2fb1ec7f8a | ||
|
|
70f1db9619 | ||
|
|
11be65678e | ||
|
|
80ed064cd6 | ||
|
|
62b1e33616 | ||
|
|
52f2ef6d30 | ||
|
|
8ace78a873 | ||
|
|
5c30210534 | ||
|
|
1d03b6c38d | ||
|
|
d3ead8742e | ||
|
|
ed6b0057b7 | ||
|
|
90fbcb88d8 | ||
|
|
be2ed52d93 | ||
|
|
f1af19a52a | ||
|
|
4c4501073c | ||
|
|
b8b9a416ac | ||
|
|
fc7e436ca9 | ||
|
|
1de1eebc59 | ||
|
|
a4d411656b | ||
|
|
1ec4266f96 | ||
|
|
61f02d693f | ||
|
|
cd80552ce7 | ||
|
|
d59b8817c3 | ||
|
|
8ce5f4a796 | ||
|
|
2e8cb35acc | ||
|
|
c0f626e9bd | ||
|
|
2ddfe48aba | ||
|
|
2c6de84c9a | ||
|
|
87cf4c2d08 | ||
|
|
3a8c835880 | ||
|
|
e2dbd3d873 | ||
|
|
55aabddd41 | ||
|
|
eb07820523 | ||
|
|
8116217019 | ||
|
|
6fb1845ff7 | ||
|
|
75325f0ed8 | ||
|
|
0577f9e166 | ||
|
|
52c6884e28 | ||
|
|
2be76a488f | ||
|
|
686535fe42 | ||
|
|
5c17aee1e4 | ||
|
|
9b35ac3a99 | ||
|
|
59b78e850f | ||
|
|
3a8c5a788f | ||
|
|
afcbbe0f43 | ||
|
|
d10d29e590 | ||
|
|
7b61cb3163 | ||
|
|
4c1c6701b3 | ||
|
|
f423fdf7f8 | ||
|
|
a5995a2ce8 | ||
|
|
c810b6772c | ||
|
|
b74ab18a6d | ||
|
|
edc54aaf91 | ||
|
|
b3b205cd6e | ||
|
|
2fe4def775 | ||
|
|
7bbf612394 | ||
|
|
97ab1f0492 | ||
|
|
81bfcdd158 | ||
|
|
6862d318f0 | ||
|
|
412fa3f0bf | ||
|
|
01d9109c45 | ||
|
|
dd914d1d64 | ||
|
|
5cf3624a3a | ||
|
|
c7ff8c2dd1 | ||
|
|
83c202d675 | ||
|
|
f7648e3311 | ||
|
|
c203de082e | ||
|
|
a95ba1acc4 | ||
|
|
db325525e4 | ||
|
|
db97114fb4 | ||
|
|
b18a300f10 | ||
|
|
61ffb857ee | ||
|
|
701212239d | ||
|
|
422fa01c6f | ||
|
|
62820b4dd9 | ||
|
|
8ea92ef330 | ||
|
|
de82249d8d | ||
|
|
94a26ae75a | ||
|
|
f9fdae9de9 | ||
|
|
0cb46039dd | ||
|
|
c4439cc9db | ||
|
|
6cd9640b30 | ||
|
|
f9bce3dfdb | ||
|
|
8eb0c997ed | ||
|
|
a8f96e06bd | ||
|
|
9a2945927f | ||
|
|
89f9249ec6 | ||
|
|
24f572253f | ||
|
|
4fc9c19893 | ||
|
|
f6fcd730a8 | ||
|
|
716a56979d | ||
|
|
538476be99 | ||
|
|
93a170a893 | ||
|
|
e0301f096f | ||
|
|
9e3cb4d694 | ||
|
|
49b76c3e43 | ||
|
|
9962784991 | ||
|
|
6336b38cfc | ||
|
|
2362835275 | ||
|
|
8a4ede9d43 | ||
|
|
cc1595e048 | ||
|
|
ca16763423 | ||
|
|
b6dd5bb881 | ||
|
|
f8e299ec56 | ||
|
|
91e9c6e048 | ||
|
|
b941b97eee | ||
|
|
d38ac652fc | ||
|
|
bdcf3b71ce | ||
|
|
ddbf8b7725 | ||
|
|
a6fd4547a7 | ||
|
|
d20153c569 | ||
|
|
15e072a2ff | ||
|
|
f98e20aa84 | ||
|
|
e7a1b33ae6 | ||
|
|
2d5f23271f | ||
|
|
059110ae7a | ||
|
|
63726b09d6 | ||
|
|
f78d42cc30 | ||
|
|
5d1c807a86 | ||
|
|
fad077aad9 | ||
|
|
24f47bee73 | ||
|
|
d93b7b2cb2 | ||
|
|
ab03111a84 | ||
|
|
1ba03b69f6 | ||
|
|
9957f64628 |
@@ -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
61
.env
@@ -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..."
|
||||
|
||||
|
||||
######################################################################################
|
||||
|
||||
2
.github/workflows/assets_artifact_build.yml
vendored
2
.github/workflows/assets_artifact_build.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/docker_build.yml
vendored
10
.github/workflows/docker_build.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
72
assets/controllers/elements/link_confirm_controller.js
Normal file
72
assets/controllers/elements/link_confirm_controller.js
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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) + " → ";
|
||||
}
|
||||
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>';
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
2211
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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],
|
||||
];
|
||||
|
||||
@@ -20,3 +20,6 @@ framework:
|
||||
tree.cache:
|
||||
adapter: cache.app
|
||||
tags: true
|
||||
|
||||
info_provider.cache:
|
||||
adapter: cache.app
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
5
config/packages/http_client.yaml
Normal file
5
config/packages/http_client.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
framework:
|
||||
http_client:
|
||||
default_options:
|
||||
headers:
|
||||
'User-Agent': 'Part-DB'
|
||||
38
config/packages/knpu_oauth2_client.yaml
Normal file
38
config/packages/knpu_oauth2_client.yaml
Normal 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: ''
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
####################################################################################################################
|
||||
|
||||
BIN
docs/assets/usage/information_provider_system/animation.gif
Normal file
BIN
docs/assets/usage/information_provider_system/animation.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
142
docs/usage/information_provider_system.md
Normal file
142
docs/usage/information_provider_system.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
**************************************************************************************************************/
|
||||
|
||||
@@ -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
|
||||
|
||||
351
migrations/Version20230716184033.php
Normal file
351
migrations/Version20230716184033.php
Normal 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)');
|
||||
}
|
||||
}
|
||||
102
migrations/Version20230730131708.php
Normal file
102
migrations/Version20230730131708.php
Normal 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)');
|
||||
}
|
||||
}
|
||||
68
package.json
68
package.json
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
85
src/Controller/InfoProviderController.php
Normal file
85
src/Controller/InfoProviderController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
67
src/Controller/OAuthClientController.php
Normal file
67
src/Controller/OAuthClientController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
69
src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php
Normal file
69
src/DataTables/Adapters/FetchResultsAtOnceORMAdapter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
src/DataTables/Adapters/TwoStepORMAdapater.php
Normal file
191
src/DataTables/Adapters/TwoStepORMAdapater.php
Normal 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([]);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
81
src/Doctrine/Functions/Field2.php
Normal file
81
src/Doctrine/Functions/Field2.php
Normal 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;
|
||||
}
|
||||
}
|
||||
103
src/Doctrine/Helpers/FieldHelper.php
Normal file
103
src/Doctrine/Helpers/FieldHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
152
src/Entity/OAuthToken.php
Normal 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 [];
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
155
src/Entity/Parts/InfoProviderReference.php
Normal file
155
src/Entity/Parts/InfoProviderReference.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
53
src/Entity/Parts/ManufacturingStatus.php
Normal file
53
src/Entity/Parts/ManufacturingStatus.php
Normal 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 => '',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
64
src/EventSubscriber/SwitchUserEventSubscriber.php
Normal file
64
src/EventSubscriber/SwitchUserEventSubscriber.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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, [
|
||||
|
||||
47
src/Form/InfoProviderSystem/PartSearchType.php
Normal file
47
src/Form/InfoProviderSystem/PartSearchType.php
Normal 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
57
src/Form/InfoProviderSystem/ProviderSelectType.php
Normal file
57
src/Form/InfoProviderSystem/ProviderSelectType.php
Normal 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
Reference in New Issue
Block a user