mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Compare commits
210 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c44535990b | ||
|
b8d5b83eee | ||
|
00da2dedc3 | ||
|
4ce1de079e | ||
|
6b9c125de4 | ||
|
2c4f44e808 | ||
|
2b694731ad | ||
|
7e34535e62 | ||
|
0bb831fe88 | ||
|
42a32ce142 | ||
|
23f58b7bf4 | ||
|
4e9101fded | ||
|
9c700c77a8 | ||
|
cb1f674332 | ||
|
6823d94ffb | ||
|
60ab992360 | ||
|
f9e769a6e3 | ||
|
f802c6c176 | ||
|
dedadf0c10 | ||
|
c8375def1a | ||
|
62ebcde2de | ||
|
594a5779dc | ||
|
c0ef64fb64 | ||
|
48c70c3bb4 | ||
|
68124a340b | ||
|
0b5003fcf6 | ||
|
956ece60af | ||
|
53da45d7d7 | ||
|
57f0432a87 | ||
|
fb535ec6f7 | ||
|
4e1b1a4ffa | ||
|
5b111d80f1 | ||
|
03e1105a8e | ||
|
059a9683db | ||
|
1daf6f01f4 | ||
|
d3b225771c | ||
|
7275db27e7 | ||
|
49ee9131d0 | ||
|
e75e0c4c0b | ||
|
e94d4a7752 | ||
|
95206f427d | ||
|
2d7f88522a | ||
|
f5c17bc7c8 | ||
|
63e222ed40 | ||
|
6963ee3b8d | ||
|
bb5e42bf63 | ||
|
c48f778648 | ||
|
616aad6403 | ||
|
bcc7547d6f | ||
|
5a1a6e9217 | ||
|
eae1fcecab | ||
|
b53989bb9d | ||
|
467d50bd31 | ||
|
1935258978 | ||
|
2b5030c69f | ||
|
6537502696 | ||
|
0ba352ab0b | ||
|
5d3f861728 | ||
|
319b69f6c7 | ||
|
c4ba28e3a0 | ||
|
b38ef8ecea | ||
|
cb0817666d | ||
|
a54c2db9b9 | ||
|
17caf476bf | ||
|
42cb590c75 | ||
|
6fd05e1456 | ||
|
bec45d60e5 | ||
|
019e67a676 | ||
|
f146d88aa5 | ||
|
48be9a8098 | ||
|
29f92d9bd3 | ||
|
ebb977e99f | ||
|
94bcd3d2d3 | ||
|
a9bd4c5606 | ||
|
8d9dde0032 | ||
|
b2d4333fff | ||
|
afd5e2b95b | ||
|
085adf8d71 | ||
|
9b35b60d61 | ||
|
742e57cc5c | ||
|
a5961668fe | ||
|
e7394c165a | ||
|
7be966122f | ||
|
d176b68fd2 | ||
|
1e80be1376 | ||
|
3585b08d4b | ||
|
c51e0eb68f | ||
|
d05c0579a2 | ||
|
5e40519bc5 | ||
|
d13752114c | ||
|
90e1b809fe | ||
|
32b4e6812d | ||
|
a798aa9c24 | ||
|
f1c28b9f46 | ||
|
39bc400376 | ||
|
e287918121 | ||
|
e0bf8e5fbc | ||
|
376c7e7a6f | ||
|
5612a790fb | ||
|
80527e35c3 | ||
|
f592ab6395 | ||
|
d7c741c652 | ||
|
9502f30e1b | ||
|
7286c4bbef | ||
|
a976f97dbb | ||
|
2c9b8c7dea | ||
|
787decf4e2 | ||
|
2fc70b8bdd | ||
|
64491a9772 | ||
|
b724b05de6 | ||
|
d94c4af1be | ||
|
8f0f5a5eb4 | ||
|
edf50a71d1 | ||
|
d0937218b9 | ||
|
3247a97217 | ||
|
edd254ee06 | ||
|
42ecb83155 | ||
|
56f801c058 | ||
|
2d3d05e956 | ||
|
4321e51bf5 | ||
|
be04730906 | ||
|
aa06e1df04 | ||
|
fd7a0156bc | ||
|
1e19ff24ba | ||
|
0f4238291f | ||
|
03a2a10efd | ||
|
04310aa2f8 | ||
|
e8ca11a5cf | ||
|
24137b30a5 | ||
|
4421917333 | ||
|
25c8660c2e | ||
|
190e87390d | ||
|
c9aefdd862 | ||
|
3ad088663f | ||
|
a29e87e5ac | ||
|
de0832bece | ||
|
614697ba84 | ||
|
6bdf3d891a | ||
|
f75704f77c | ||
|
9d09543eb9 | ||
|
a6116398a8 | ||
|
39763b84d5 | ||
|
8502df08fa | ||
|
bf2a776403 | ||
|
052190c69b | ||
|
8826ba6729 | ||
|
39b5240934 | ||
|
ddc1c286d9 | ||
|
22fba37d28 | ||
|
0c627a5636 | ||
|
53dcd24216 | ||
|
4b09a321ad | ||
|
9e85b70c17 | ||
|
9c99217dee | ||
|
afc1dbdd4b | ||
|
20f58fc07d | ||
|
e6b78dd213 | ||
|
63893ffabe | ||
|
c9e519d0b5 | ||
|
273bde90f2 | ||
|
92e4976396 | ||
|
e9efbff912 | ||
|
41089c08f8 | ||
|
9e23e606f8 | ||
|
b3f0fd368a | ||
|
12bd5472e2 | ||
|
ef64779759 | ||
|
b3d8076ddf | ||
|
f775203608 | ||
|
a6083688e4 | ||
|
50689cd4e6 | ||
|
255fcbac1c | ||
|
7f8ffa56e5 | ||
|
9a2a5f30a3 | ||
|
a9f444cbb4 | ||
|
164efb0551 | ||
|
a37b8cbb15 | ||
|
946032a101 | ||
|
a273acbecd | ||
|
0ceee1582e | ||
|
04a0369d56 | ||
|
31a288b44d | ||
|
502dc3aa1c | ||
|
6874d7ca55 | ||
|
da8f669aed | ||
|
0f92a69b03 | ||
|
8faa3251c4 | ||
|
56fc14003c | ||
|
b3499e4ea5 | ||
|
07b1ff9bf5 | ||
|
5bbf24c92e | ||
|
eea8b3e679 | ||
|
e223078af9 | ||
|
b554d0d851 | ||
|
b1ba26e0b9 | ||
|
ca8ad760d7 | ||
|
80129c0a88 | ||
|
7530e62dfa | ||
|
baf8977578 | ||
|
c7bf843312 | ||
|
ce6fee1682 | ||
|
2653fad488 | ||
|
dd54c46a29 | ||
|
724a0e21d3 | ||
|
578277d11f | ||
|
22258e3183 | ||
|
0234463b68 | ||
|
ef412eef92 | ||
|
76ebd22eab | ||
|
5b0ca8e346 |
234 changed files with 16713 additions and 9620 deletions
|
@ -42,6 +42,48 @@ fi
|
|||
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile)
|
||||
service phpPHP_VERSION-fpm start
|
||||
|
||||
|
||||
# Run migrations if automigration is enabled via env variable DB_AUTOMIGRATE
|
||||
if [ "$DB_AUTOMIGRATE" = "true" ]; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(sudo -E -u www-data php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo "The database is not up or not reachable:"
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo "The database is now ready and reachable"
|
||||
fi
|
||||
|
||||
# Check if there are any available migrations to do, by executing doctrine:migrations:up-to-date
|
||||
# and checking if the exit code is 0 (up to date) or 1 (not up to date)
|
||||
if sudo -E -u www-data php bin/console doctrine:migrations:up-to-date --no-interaction; then
|
||||
echo "Database is up to date, no migrations necessary."
|
||||
else
|
||||
echo "Migrations available..."
|
||||
echo "Do backup of database..."
|
||||
|
||||
sudo -E -u www-data mkdir -p /var/www/html/uploads/.automigration-backup/
|
||||
# Backup the database
|
||||
sudo -E -u www-data php bin/console partdb:backup -n --database /var/www/html/uploads/.automigration-backup/backup-$(date +%Y-%m-%d_%H-%M-%S).zip
|
||||
|
||||
# Check if there are any migration files
|
||||
sudo -E -u www-data php bin/console doctrine:migrations:migrate --no-interaction
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint)
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
set -- apache2-foreground "$@"
|
||||
|
|
|
@ -44,6 +44,8 @@
|
|||
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
|
||||
PassEnv PROVIDER_LCSC_ENABLED PROVIDER_LCSC_CURRENCY
|
||||
PassEnv PROVIDER_OEMSECRETS_KEY PROVIDER_OEMSECRETS_COUNTRY_CODE PROVIDER_OEMSECRETS_CURRENCY PROVIDER_OEMSECRETS_ZERO_PRICE PROVIDER_OEMSECRETS_SET_PARAM PROVIDER_OEMSECRETS_SORT_CRITERIA
|
||||
PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT
|
||||
PassEnv PROVIDER_POLLIN_ENABLED
|
||||
PassEnv EDA_KICAD_CATEGORY_DEPTH
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
|
|
24
.env
24
.env
|
@ -143,7 +143,8 @@ PROVIDER_TME_CURRENCY=EUR
|
|||
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
|
||||
# [DEPRECATED] Set this to 1 to get gross prices (including VAT) instead of net prices
|
||||
# With private API keys, this option cannot be used anymore is ignored by Part-DB. The VAT inclusion depends on your TME account settings.
|
||||
PROVIDER_TME_GET_GROSS_PRICES=1
|
||||
|
||||
# Octopart / Nexar Provider:
|
||||
|
@ -215,6 +216,27 @@ PROVIDER_OEMSECRETS_SET_PARAM=1
|
|||
#If unset or set to any other value, no sorting is performed.
|
||||
PROVIDER_OEMSECRETS_SORT_CRITERIA=C
|
||||
|
||||
|
||||
# Reichelt provider:
|
||||
# Reichelt.com offers no official API, so this info provider webscrapes the website to extract info
|
||||
# It could break at any time, use it at your own risk
|
||||
# We dont require an API key for Reichelt, just set this to 1 to enable Reichelt support
|
||||
PROVIDER_REICHELT_ENABLED=0
|
||||
# The country to get prices for
|
||||
PROVIDER_REICHELT_COUNTRY=DE
|
||||
# The language to get results in (en, de, fr, nl, pl, it, es)
|
||||
PROVIDER_REICHELT_LANGUAGE=en
|
||||
# Include VAT in prices (set to 1 to include VAT, 0 to exclude VAT)
|
||||
PROVIDER_REICHELT_INCLUDE_VAT=1
|
||||
# The currency to get prices in (only for countries with countries other than EUR)
|
||||
PROVIDER_REICHELT_CURRENCY=EUR
|
||||
|
||||
# Pollin provider:
|
||||
# Pollin.de offers no official API, so this info provider webscrapes the website to extract info
|
||||
# It could break at any time, use it at your own risk
|
||||
# We dont require an API key for Pollin, just set this to 1 to enable Pollin support
|
||||
PROVIDER_POLLIN_ENABLED=0
|
||||
|
||||
##################################################################################
|
||||
# EDA integration related settings
|
||||
##################################################################################
|
||||
|
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: [ '8.1', '8.2', '8.3' ]
|
||||
php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
|
||||
db-type: [ 'mysql', 'sqlite', 'postgres' ]
|
||||
|
||||
env:
|
||||
|
@ -126,7 +126,7 @@ jobs:
|
|||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
env_vars: PHP_VERSION,DB_TYPE
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ARG BASE_IMAGE=debian:bookworm-slim
|
||||
ARG PHP_VERSION=8.2
|
||||
ARG PHP_VERSION=8.3
|
||||
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
ARG PHP_VERSION
|
||||
|
@ -125,6 +125,7 @@ upload_max_filesize=256M
|
|||
post_max_size=300M
|
||||
opcache.preload_user=www-data
|
||||
opcache.preload=/var/www/html/config/preload.php
|
||||
log_limit=8096
|
||||
EOF
|
||||
|
||||
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.14.3
|
||||
1.17.1
|
||||
|
|
|
@ -128,6 +128,8 @@ const PLACEHOLDERS = [
|
|||
['[[BARCODE_QR]]', 'QR code linking to this element'],
|
||||
['[[BARCODE_C128]]', 'Code 128 barcode linking to this element'],
|
||||
['[[BARCODE_C39]]', 'Code 39 barcode linking to this element'],
|
||||
['[[BARCODE_C93]]', 'Code 93 barcode linking to this element'],
|
||||
['[[BARCODE_DATAMATRIX]]', 'Datamatrix code linking to this element'],
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -69,6 +69,8 @@ Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
|
|||
'QR code linking to this element': 'QR Code verknüpft mit diesem Element',
|
||||
'Code 128 barcode linking to this element': 'Code 128 Barcode verknüpft mit diesem Element',
|
||||
'Code 39 barcode linking to this element': 'Code 39 Barcode verknüpft mit diesem Element',
|
||||
'Code 93 barcode linking to this element': 'Code 93 Barcode verknüpft mit diesem Element',
|
||||
'Datamatrix code linking to this element': 'Datamatrix Code verknüpft mit diesem Element',
|
||||
|
||||
'Location ID': 'Lagerort ID',
|
||||
'Name': 'Name',
|
||||
|
|
|
@ -23,6 +23,12 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
|
@ -46,6 +52,12 @@ export default class extends Controller {
|
|||
}
|
||||
return '<div>' + escape(data.label) + '</div>';
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
"restore_on_backspace": {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ export default class extends Controller {
|
|||
|
||||
const config = {
|
||||
language: language,
|
||||
licenseKey: "GPL",
|
||||
}
|
||||
|
||||
const watchdog = new EditorWatchdog();
|
||||
|
|
|
@ -23,6 +23,12 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
/**
|
||||
* This is the frontend controller for StaticFileAutocompleteType form element.
|
||||
* Basically it loads a text file from the given url (via data-url) and uses it as a source for the autocomplete.
|
||||
|
@ -46,7 +52,13 @@ export default class extends Controller {
|
|||
orderField: 'text',
|
||||
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING'
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
plugins: {
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {}
|
||||
}
|
||||
};
|
||||
|
||||
if (this.element.dataset.url) {
|
||||
|
|
|
@ -24,6 +24,9 @@ import {Controller} from "@hotwired/stimulus";
|
|||
|
||||
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
|
||||
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
|
@ -37,11 +40,15 @@ export default class extends Controller {
|
|||
const allowAdd = this.element.getAttribute("data-allow-add") === "true";
|
||||
const addHint = this.element.getAttribute("data-add-hint") ?? "";
|
||||
|
||||
|
||||
|
||||
|
||||
let settings = {
|
||||
allowEmptyOption: true,
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
create: allowAdd ? this.createItem.bind(this) : false,
|
||||
createFilter: this.createFilter.bind(this),
|
||||
|
||||
// This three options allow us to paste element names with commas: (see issue #538)
|
||||
maxItems: 1,
|
||||
|
@ -81,8 +88,17 @@ export default class extends Controller {
|
|||
//Add callbacks to update validity
|
||||
onInitialize: this.updateValidity.bind(this),
|
||||
onChange: this.updateValidity.bind(this),
|
||||
|
||||
plugins: {
|
||||
"autoselect_typed": {},
|
||||
}
|
||||
};
|
||||
|
||||
//Add clear button plugin, if an empty option is present
|
||||
if (this.element.querySelector("option[value='']") !== null) {
|
||||
settings.plugins["clear_button"] = {};
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
//Do not do a sync here as this breaks the initial rendering of the empty option
|
||||
//this._tomSelect.sync();
|
||||
|
@ -113,6 +129,31 @@ export default class extends Controller {
|
|||
});
|
||||
}
|
||||
|
||||
createFilter(input) {
|
||||
|
||||
//Normalize the input (replace spacing around arrows)
|
||||
if (input.includes("->")) {
|
||||
const inputs = input.split("->");
|
||||
inputs.forEach((value, index) => {
|
||||
inputs[index] = value.trim();
|
||||
});
|
||||
input = inputs.join("->");
|
||||
} else {
|
||||
input = input.trim();
|
||||
}
|
||||
|
||||
const options = this._tomSelect.options;
|
||||
//Iterate over all options and check if the input is already present
|
||||
for (let index in options) {
|
||||
const option = options[index];
|
||||
if (option.path === input) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
updateValidity() {
|
||||
//Mark this input as invalid, if the selected option is disabled
|
||||
|
|
|
@ -23,14 +23,21 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
let settings = {
|
||||
plugins: {
|
||||
remove_button:{
|
||||
}
|
||||
remove_button:{},
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
},
|
||||
persistent: false,
|
||||
selectOnTab: true,
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
//import * as ZXing from "@zxing/library";
|
||||
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "html5-qrcode";
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
@ -50,7 +50,7 @@ export default class extends Controller {
|
|||
});
|
||||
|
||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||
fps: 2,
|
||||
fps: 10,
|
||||
qrbox: qrboxFunction,
|
||||
experimentalFeatures: {
|
||||
//This option improves reading quality on android chrome
|
||||
|
@ -61,6 +61,11 @@ export default class extends Controller {
|
|||
this._scanner.render(this.onScanSuccess.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._scanner.pause();
|
||||
this._scanner.clear();
|
||||
}
|
||||
|
||||
onScanSuccess(decodedText, decodedResult) {
|
||||
//Put our decoded Text into the input box
|
||||
document.getElementById('scan_dialog_input').value = decodedText;
|
||||
|
|
|
@ -25,9 +25,20 @@ import "katex/dist/katex.css";
|
|||
export default class extends Controller {
|
||||
static targets = ["input", "preview"];
|
||||
|
||||
static values = {
|
||||
unit: {type: Boolean, default: false} //Render as upstanding (non-italic) text, useful for units
|
||||
}
|
||||
|
||||
updatePreview()
|
||||
{
|
||||
katex.render(this.inputTarget.value, this.previewTarget, {
|
||||
let value = "";
|
||||
if (this.unitValue) {
|
||||
value = "\\mathrm{" + this.inputTarget.value + "}";
|
||||
} else {
|
||||
value = this.inputTarget.value;
|
||||
}
|
||||
|
||||
katex.render(value, this.previewTarget, {
|
||||
throwOnError: false,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,13 @@ import TomSelect from "tom-select";
|
|||
import katex from "katex";
|
||||
import "katex/dist/katex.css";
|
||||
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller
|
||||
{
|
||||
|
@ -53,7 +60,10 @@ export default class extends Controller
|
|||
connect() {
|
||||
const settings = {
|
||||
plugins: {
|
||||
clear_button:{}
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {}
|
||||
},
|
||||
persistent: false,
|
||||
maxItems: 1,
|
||||
|
|
|
@ -112,3 +112,10 @@ ul.structural_link li a:hover {
|
|||
background-color: var(--bs-success);
|
||||
border-color: var(--bs-success);
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* Katex rendering with same height as text
|
||||
***********************************************/
|
||||
.katex-same-height-as-text .katex {
|
||||
font-size: 1.0em;
|
||||
}
|
|
@ -24,9 +24,8 @@
|
|||
/** Should be the same settings, as in label_style.css */
|
||||
.ck-html-label .ck-content {
|
||||
font-family: "DejaVu Sans Mono", monospace;
|
||||
font-size: 12px;
|
||||
font-size: 12pt;
|
||||
line-height: 1.0;
|
||||
font-size-adjust: 1.5;
|
||||
}
|
||||
|
||||
.ck-html-label .ck-content p {
|
||||
|
|
|
@ -44,4 +44,18 @@ import "./register_events";
|
|||
import "./tristate_checkboxes";
|
||||
|
||||
//Define jquery globally
|
||||
window.$ = window.jQuery = require("jquery")
|
||||
window.$ = window.jQuery = require("jquery");
|
||||
|
||||
//Use the local WASM file for the ZXing library
|
||||
import {
|
||||
setZXingModuleOverrides,
|
||||
} from "barcode-detector/pure";
|
||||
import wasmFile from "../../node_modules/zxing-wasm/dist/reader/zxing_reader.wasm";
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path, prefix) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return wasmFile;
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
});
|
63
assets/tomselect/autoselect_typed/autoselect_typed.js
Normal file
63
assets/tomselect/autoselect_typed/autoselect_typed.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Autoselect Typed plugin for Tomselect
|
||||
*
|
||||
* This plugin allows automatically selecting an option matching the typed text when the Tomselect element goes out of
|
||||
* focus (is blurred) and/or when the delimiter is typed.
|
||||
*
|
||||
* #select_on_blur option
|
||||
* Tomselect natively supports the "createOnBlur" option. This option picks up any remaining text in the input field
|
||||
* and uses it to create a new option and selects that option. It does behave a bit strangely though, in that it will
|
||||
* not select an already existing option when the input is blurred, so if you typed something that matches an option in
|
||||
* the list and then click outside the box (without pressing enter) the entered text is just removed (unless you have
|
||||
* allow duplicates on in which case it will create a new option).
|
||||
* This plugin fixes that, such that Tomselect will first try to select an option matching the remaining uncommitted
|
||||
* text and only when no matching option is found tries to create a new one (if createOnBlur and create is on)
|
||||
*
|
||||
* #select_on_delimiter option
|
||||
* Normally when typing the delimiter (space by default) Tomselect will try to create a new option (and select it) (if
|
||||
* create is on), but if the typed text matches an option (and allow duplicates is off) it refuses to react at all until
|
||||
* you press enter. With this option, the delimiter will also allow selecting an option, not just creating it.
|
||||
*/
|
||||
function select_current_input(self){
|
||||
if(self.isLocked){
|
||||
return
|
||||
}
|
||||
|
||||
const val = self.inputValue()
|
||||
//Do nothing if the input is empty
|
||||
if (!val) {
|
||||
return
|
||||
}
|
||||
|
||||
if (self.options[val]) {
|
||||
self.addItem(val)
|
||||
self.setTextboxValue()
|
||||
}
|
||||
}
|
||||
|
||||
export default function(plugin_options_) {
|
||||
const plugin_options = Object.assign({
|
||||
//Autoselect the typed text when the input element goes out of focus
|
||||
select_on_blur: true,
|
||||
//Autoselect the typed text when the delimiter is typed
|
||||
select_on_delimiter: true,
|
||||
}, plugin_options_);
|
||||
|
||||
const self = this
|
||||
|
||||
if(plugin_options.select_on_blur) {
|
||||
this.hook("before", "onBlur", function () {
|
||||
select_current_input(self)
|
||||
})
|
||||
}
|
||||
|
||||
if(plugin_options.select_on_delimiter) {
|
||||
this.hook("before", "onKeyPress", function (e) {
|
||||
const character = String.fromCharCode(e.keyCode || e.which);
|
||||
if (self.settings.mode === 'multi' && character === self.settings.delimiter) {
|
||||
select_current_input(self)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
93
assets/tomselect/click_to_edit/click_to_edit.js
Normal file
93
assets/tomselect/click_to_edit/click_to_edit.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* click_to_edit plugin for Tomselect
|
||||
*
|
||||
* This plugin allows editing (and selecting text in) any selected item by clicking it.
|
||||
*
|
||||
* Usually, when the user typed some text and created an item in Tomselect that item cannot be edited anymore. To make
|
||||
* a change, the item has to be deleted and retyped completely. There is also generally no way to copy text out of a
|
||||
* tomselect item. The "restore_on_backspace" plugin improves that somewhat, by allowing the user to edit an item after
|
||||
* pressing backspace. However, it is somewhat confusing to first have to focus the field an then hit backspace in order
|
||||
* to copy a piece of text. It may also not be immediately obvious for editing.
|
||||
* This plugin transforms an item into editable text when it is clicked, e.g. when the user tries to place the caret
|
||||
* within an item or when they try to drag across the text to highlight it.
|
||||
* It also plays nice with the remove_button plugin which still removes (deselects) an option entirely.
|
||||
*
|
||||
* It is recommended to also enable the autoselect_typed plugin when using this plugin. Without it, the text in the
|
||||
* input field (i.e. the item that was just clicked) is lost when the user clicks outside the field. Also, when the user
|
||||
* clicks an option (making it text) and then tries to enter another one by entering the delimiter (e.g. space) nothing
|
||||
* happens until enter is pressed or the text is changed from what it was.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return a dom element from either a dom query string, jQuery object, a dom element or html string
|
||||
* https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
|
||||
*
|
||||
* param query should be {}
|
||||
*/
|
||||
const getDom = query => {
|
||||
if (query.jquery) {
|
||||
return query[0];
|
||||
}
|
||||
if (query instanceof HTMLElement) {
|
||||
return query;
|
||||
}
|
||||
if (isHtmlString(query)) {
|
||||
var tpl = document.createElement('template');
|
||||
tpl.innerHTML = query.trim(); // Never return a text node of whitespace as the result
|
||||
return tpl.content.firstChild;
|
||||
}
|
||||
return document.querySelector(query);
|
||||
};
|
||||
const isHtmlString = arg => {
|
||||
if (typeof arg === 'string' && arg.indexOf('<') > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function plugin(plugin_options_) {
|
||||
const self = this
|
||||
|
||||
const plugin_options = Object.assign({
|
||||
//If there is unsubmitted text in the input field, should that text be automatically used to select a matching
|
||||
//element? If this is off, clicking on item1 and then clicking on item2 will result in item1 being deselected
|
||||
auto_select_before_edit: true,
|
||||
//If there is unsubmitted text in the input field, should that text be automatically used to create a matching
|
||||
//element if no matching element was found or auto_select_before_edit is off?
|
||||
auto_create_before_edit: true,
|
||||
//customize this function to change which text the item is replaced with when clicking on it
|
||||
text: option => {
|
||||
return option[self.settings.labelField];
|
||||
}
|
||||
}, plugin_options_);
|
||||
|
||||
|
||||
self.hook('after', 'setupTemplates', () => {
|
||||
const orig_render_item = self.settings.render.item;
|
||||
self.settings.render.item = (data, escape) => {
|
||||
const item = getDom(orig_render_item.call(self, data, escape));
|
||||
|
||||
item.addEventListener('click', evt => {
|
||||
if (self.isLocked) {
|
||||
return;
|
||||
}
|
||||
const val = self.inputValue();
|
||||
|
||||
if (self.options[val]) {
|
||||
self.addItem(val)
|
||||
} else if (self.settings.create) {
|
||||
self.createItem();
|
||||
}
|
||||
const option = self.options[item.dataset.value]
|
||||
self.setTextboxValue(plugin_options.text.call(self, option));
|
||||
self.focus();
|
||||
self.removeItem(item);
|
||||
}
|
||||
);
|
||||
|
||||
return item;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
export { plugin as default };
|
|
@ -4,6 +4,10 @@
|
|||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
//Increase xdebug.max_nesting_level to 1000 if required (see issue #411)
|
||||
//Check if xdebug extension is active, and xdebug.max_nesting_level is set to 256 or lower
|
||||
if (extension_loaded('xdebug') && ((int) ini_get('xdebug.max_nesting_level')) <= 256) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "part-db/part-db-server",
|
||||
"type": "project",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
|
@ -14,9 +15,9 @@
|
|||
"api-platform/core": "^3.1",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"brick/math": "0.12.1 as 0.11.0",
|
||||
"composer/ca-bundle": "^1.3",
|
||||
"composer/ca-bundle": "^1.5",
|
||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||
"doctrine/data-fixtures": "^1.6.6",
|
||||
"doctrine/data-fixtures": "^2.0.0",
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"doctrine/doctrine-bundle": "^2.0",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||
|
@ -39,12 +40,10 @@
|
|||
"nelmio/cors-bundle": "^2.3",
|
||||
"nelmio/security-bundle": "^3.0",
|
||||
"nyholm/psr7": "^1.1",
|
||||
"ocramius/proxy-manager": "2.2.*",
|
||||
"omines/datatables-bundle": "^0.8.0",
|
||||
"omines/datatables-bundle": "^0.9.1",
|
||||
"paragonie/sodium_compat": "^1.21",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"phpstan/phpdoc-parser": "^1.23",
|
||||
"rhukster/dom-sanitizer": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
|
@ -56,6 +55,8 @@
|
|||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "6.4.*",
|
||||
"symfony/console": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
"symfony/dom-crawler": "6.4.*",
|
||||
"symfony/dotenv": "6.4.*",
|
||||
"symfony/expression-language": "6.4.*",
|
||||
"symfony/flex": "^v2.3.1",
|
||||
|
@ -69,7 +70,6 @@
|
|||
"symfony/process": "6.4.*",
|
||||
"symfony/property-access": "6.4.*",
|
||||
"symfony/property-info": "6.4.*",
|
||||
"symfony/proxy-manager-bridge": "6.4.*",
|
||||
"symfony/rate-limiter": "6.4.*",
|
||||
"symfony/runtime": "6.4.*",
|
||||
"symfony/security-bundle": "6.4.*",
|
||||
|
@ -91,24 +91,22 @@
|
|||
"twig/intl-extra": "^3.8",
|
||||
"twig/markdown-extra": "^3.8",
|
||||
"twig/string-extra": "^3.8",
|
||||
"web-auth/webauthn-symfony-bundle": "^4.0.0",
|
||||
"webmozart/assert": "^1.4"
|
||||
"web-auth/webauthn-symfony-bundle": "^4.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^v8.0.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"ekino/phpstan-banned-code": "^v1.0.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
|
||||
"ekino/phpstan-banned-code": "^v3.0.0",
|
||||
"jbtronics/translation-editor-bundle": "^1.0",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "^1.4.7",
|
||||
"phpstan/phpstan-doctrine": "^1.2.11",
|
||||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpstan/phpstan-symfony": "^1.1.7",
|
||||
"phpstan/phpstan": "^2.0.4",
|
||||
"phpstan/phpstan-doctrine": "^2.0.1",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.1",
|
||||
"phpstan/phpstan-symfony": "^2.0.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"rector/rector": "^1.1.1",
|
||||
"rector/rector": "^2.0.4",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
"symfony/debug-bundle": "6.4.*",
|
||||
"symfony/maker-bundle": "^1.13",
|
||||
"symfony/phpunit-bridge": "6.4.*",
|
||||
|
|
3265
composer.lock
generated
3265
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -35,3 +35,7 @@ api_platform:
|
|||
keep_legacy_inflector: false
|
||||
# Need to be true, or some tests will fail
|
||||
use_symfony_listeners: true
|
||||
|
||||
serializer:
|
||||
# Change this to false later, to remove the hydra prefix on the API
|
||||
hydra_prefix: true
|
|
@ -8,7 +8,7 @@ datatables:
|
|||
|
||||
# Set options, as documented at https://datatables.net/reference/option/
|
||||
options:
|
||||
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
|
||||
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated
|
||||
pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
||||
<'card'
|
||||
|
|
|
@ -57,6 +57,7 @@ doctrine:
|
|||
field2: App\Doctrine\Functions\Field2
|
||||
natsort: App\Doctrine\Functions\Natsort
|
||||
array_position: App\Doctrine\Functions\ArrayPosition
|
||||
ilike: App\Doctrine\Functions\ILike
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
|
|
@ -50,7 +50,6 @@ when@prod:
|
|||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
@ -74,7 +73,6 @@ when@docker:
|
|||
type: stream
|
||||
path: "php://stderr"
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
|
|
@ -51,12 +51,16 @@ nelmio_security:
|
|||
img-src:
|
||||
- '*'
|
||||
- 'data:'
|
||||
# Required for be able to load pictures in the QR code scanner
|
||||
- 'blob:'
|
||||
style-src:
|
||||
- 'self'
|
||||
- 'unsafe-inline'
|
||||
- 'data:'
|
||||
script-src:
|
||||
- 'self'
|
||||
# Required for loading the Wasm for the barcode scanner:
|
||||
- 'wasm-unsafe-eval'
|
||||
object-src:
|
||||
- 'self'
|
||||
- 'data:'
|
||||
|
|
|
@ -23,7 +23,7 @@ each other so that it does not matter which one of your 1000 things of Part you
|
|||
A part entity has many fields, which can be used to describe it better. Most of the fields are optional:
|
||||
|
||||
* **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a
|
||||
name you thought of yourself. The name have to be unique in a single category.
|
||||
name you thought of yourself. Each name needs to be unique and must exist in a single category.
|
||||
* **Description**: A short (single-line) description of what this part is/does. For longer information, you should use
|
||||
the comment field or the specifications
|
||||
* **Category** (Required): The category (see there) to which this part belongs to.
|
||||
|
|
|
@ -150,9 +150,9 @@ In the `serverVersion` parameter you can specify the version of the PostgreSQL s
|
|||
|
||||
The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly.
|
||||
|
||||
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter.
|
||||
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter.
|
||||
```shell
|
||||
DATABASE_URL="postgresql://db_user:db_password@localhost/db_name?serverVersion=12.19&charset=utf8&unix_socket=/var/run/postgresql/.s.PGSQL.5432"
|
||||
DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql"
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -7,3 +7,5 @@ has_children: true
|
|||
|
||||
# Installation
|
||||
Below you can find some guides to install Part-DB.
|
||||
|
||||
For the hobbyists without much experience, we recommend the docker installation or direct installation on debian.
|
|
@ -48,6 +48,12 @@ services:
|
|||
# In docker env logs will be redirected to stderr
|
||||
- APP_ENV=docker
|
||||
|
||||
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
|
||||
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
|
||||
# folder (under .automigration-backup), so you can restore it, if the migration fails.
|
||||
# This feature is currently experimental, so use it at your own risk!
|
||||
# - DB_AUTOMIGRATE=true
|
||||
|
||||
# You can configure Part-DB using environment variables
|
||||
# Below you can find the most essential ones predefined
|
||||
# However you can add any other environment configuration you want here
|
||||
|
@ -130,6 +136,12 @@ services:
|
|||
# In docker env logs will be redirected to stderr
|
||||
- APP_ENV=docker
|
||||
|
||||
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
|
||||
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
|
||||
# folder (under .automigration-backup), so you can restore it, if the migration fails.
|
||||
# This feature is currently experimental, so use it at your own risk!
|
||||
# - DB_AUTOMIGRATE=true
|
||||
|
||||
# You can configure Part-DB using environment variables
|
||||
# Below you can find the most essential ones predefined
|
||||
# However you can add add any other environment configuration you want here
|
||||
|
@ -201,6 +213,10 @@ You also have to create the database as described above in step 4.
|
|||
You can run the console commands described in README by
|
||||
executing `docker exec --user=www-data -it partdb bin/console [command]`
|
||||
|
||||
{: .warning }
|
||||
> If you run a root console inside the container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell.
|
||||
> Otherwise Part-DB console might use the wrong configuration to execute commands.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
*Login is not possible. Login page is just reloading and no error message is shown or something like "CSFR token invalid"*:
|
||||
|
|
42
docs/installation/kubernetes.md
Normal file
42
docs/installation/kubernetes.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: Kubernetes / Helm
|
||||
layout: default
|
||||
parent: Installation
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Kubernetes / Helm Charts
|
||||
|
||||
If you are using Kubernetes, you can use the [helm charts](https://helm.sh/) provided in this [repository](https://github.com/Part-DB/helm-charts).
|
||||
|
||||
## Usage
|
||||
|
||||
[Helm](https://helm.sh) must be installed to use the charts. Please refer to
|
||||
Helm's [documentation](https://helm.sh/docs) to get started.
|
||||
|
||||
Once Helm has been set up correctly, add the repo as follows:
|
||||
|
||||
`helm repo add part-db https://part-db.github.io/helm-charts`
|
||||
|
||||
If you had already added this repo earlier, run `helm repo update` to retrieve
|
||||
the latest versions of the packages. You can then run `helm search repo
|
||||
part-db` to see the charts.
|
||||
|
||||
To install the part-db chart:
|
||||
|
||||
helm install my-part-db part-db/part-db
|
||||
|
||||
To uninstall the chart:
|
||||
|
||||
helm delete my-part-db
|
||||
|
||||
This repository is also available at [ArtifactHUB](https://artifacthub.io/packages/search?repo=part-db).
|
||||
|
||||
## Configuration
|
||||
|
||||
See the README in the [chart directory](https://github.com/Part-DB/helm-charts/tree/main/charts/part-db) for more
|
||||
information on the available configuration options.
|
||||
|
||||
## Bugreports
|
||||
|
||||
If you find issues related to the helm charts, please open an issue in the [helm-charts repository](https://github.com/Part-DB/helm-charts).
|
|
@ -53,6 +53,11 @@ server {
|
|||
return 404;
|
||||
}
|
||||
|
||||
# Set Content-Security-Policy for svg files, to block embedded javascript in there
|
||||
location ~* \.svg$ {
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';";
|
||||
}
|
||||
|
||||
error_log /var/log/nginx/parts.error.log;
|
||||
access_log /var/log/nginx/parts.access.log;
|
||||
|
||||
|
|
31
docs/installation/proxmox.md
Normal file
31
docs/installation/proxmox.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: Proxmox VE LXC
|
||||
layout: default
|
||||
parent: Installation
|
||||
nav_order: 6
|
||||
---
|
||||
|
||||
# Proxmox VE LXC
|
||||
|
||||
{: .warning }
|
||||
> The proxmox VE LXC script for Part-DB is developed and maintained by [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/)
|
||||
> and not by the Part-DB developers. Keep in mind that the script is not officially supported by the Part-DB developers.
|
||||
|
||||
If you are using Proxmox VE you can use the scripts provided by [Proxmox VE Helper-Scripts community](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db)
|
||||
to easily install Part-DB in a LXC container.
|
||||
|
||||
## Usage
|
||||
|
||||
To create a new LXC container with Part-DB, you can use the following command in the Proxmox VE shell:
|
||||
|
||||
```bash
|
||||
bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/part-db.sh)"
|
||||
```
|
||||
|
||||
The same command can be used to update an existing Part-DB container.
|
||||
|
||||
See the [helper script website](https://community-scripts.github.io/ProxmoxVE/scripts?id=part-db) for more information.
|
||||
|
||||
## Bugreports
|
||||
|
||||
If you find issues related to the proxmox VE LXC script, please open an issue in the [Proxmox VE Helper-Scripts repository](https://github.com/community-scripts/ProxmoxVE).
|
|
@ -25,6 +25,12 @@ is named `partdb`, you can execute the command `php bin/console cache:clear` wit
|
|||
docker exec --user=www-data partdb php bin/console cache:clear
|
||||
```
|
||||
|
||||
{: .warning }
|
||||
> If you run a root console inside the docker container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell.
|
||||
> Otherwise Part-DB console might use the wrong configuration to execute commands.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
## User management commands
|
||||
|
||||
* `php bin/console partdb:users:list`: List all users of this Part-DB instance
|
||||
|
@ -65,3 +71,9 @@ docker exec --user=www-data partdb php bin/console cache:clear
|
|||
|
||||
* `php bin/console doctrine:migrations:migrate`: Migrate the database to the latest version
|
||||
* `php bin/console doctrine:migrations:up-to-date`: Check if the database is up-to-date
|
||||
|
||||
## Attachment commands
|
||||
|
||||
* `php bin/console partdb:attachments:download`: Download all attachments, which are not already downloaded, to the
|
||||
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote and
|
||||
also makes pictures thumbnails available for the frontend for them
|
|
@ -107,7 +107,7 @@ The following env configuration options are available:
|
|||
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,
|
||||
* `PROVIDER_OCTOPART_COUNTRY`: The country you want to get prices in if available (optional, 2 letter ISO-code,
|
||||
default: `DE`). To get the 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.
|
||||
|
@ -232,6 +232,26 @@ The following env configuration options are available:
|
|||
completeness (prioritizing items with the most detailed information). If set to 'M', it further sorts by manufacturer name.
|
||||
If set to any other value, no sorting is performed.
|
||||
|
||||
### Reichelt
|
||||
|
||||
The reichelt provider uses webscraping from [reichelt.com](https://reichelt.com/) to get part information.
|
||||
This is not an official API and could break at any time. So use it at your own risk.
|
||||
|
||||
The following env configuration options are available:
|
||||
* `PROVIDER_REICHELT_ENABLED`: Set this to `1` to enable the Reichelt provider
|
||||
* `PROVIDER_REICHELT_CURRENCY`: The currency you want to get prices in. Only possible for countries which use Non-EUR (optional, default: `EUR`)
|
||||
* `PROVIDER_REICHELT_COUNTRY`: The country you want to get the prices for (optional, default: `DE`)
|
||||
* `PROVIDER_REICHELT_LANGUAGE`: The language you want to get the descriptions in (optional, default: `en`)
|
||||
* `PROVIDER_REICHELT_INCLUDE_VAT`: If set to `1`, the prices will be gross prices (including tax), otherwise net prices (optional, default: `1`)
|
||||
|
||||
### Pollin
|
||||
|
||||
The pollin provider uses webscraping from [pollin.de](https://www.pollin.de/) to get part information.
|
||||
This is not an official API and could break at any time. So use it at your own risk.
|
||||
|
||||
The following env configuration options are available:
|
||||
* `PROVIDER_POLLIN_ENABLED`: Set this to `1` to enable the Pollin provider
|
||||
|
||||
### Custom provider
|
||||
|
||||
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long
|
||||
|
|
|
@ -117,6 +117,6 @@ For a German keyboard layout, replace `[` with `0`, and `]` with `´`.
|
|||
| Key | Character |
|
||||
|--------------------------------|--------------------|
|
||||
| **Alt + [** (code 219) | © (Copyright char) |
|
||||
| **Alt + Shift + [** (code 219) | (Registered char) |
|
||||
| **Alt + Shift + [** (code 219) | ® (Registered char) |
|
||||
| **Alt + ]** (code 221) | ™ (Trademark char) |
|
||||
| **Alt + Shift + ]** (code 221) | (Degree char) |
|
||||
| **Alt + Shift + ]** (code 221) | ° (Degree char) |
|
||||
|
|
42
migrations/Version20250220215048.php
Normal file
42
migrations/Version20250220215048.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20250220215048 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Split $path property for attachments into $internal_path and $external_path';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
//Create the new columns as nullable (that is easier modifying them)
|
||||
$this->addSql('ALTER TABLE attachments ADD internal_path VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE attachments ADD external_path VARCHAR(255) DEFAULT NULL');
|
||||
|
||||
//Copy the data from path to external_path and remove the path column
|
||||
$this->addSql('UPDATE attachments SET external_path=path');
|
||||
$this->addSql('ALTER TABLE attachments DROP COLUMN path');
|
||||
|
||||
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%MEDIA#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%BASE#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%SECURE#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET internal_path=external_path WHERE external_path LIKE \'#%FOOTPRINTS3D#%%\' ESCAPE \'#\'');
|
||||
$this->addSql('UPDATE attachments SET external_path=NULL WHERE internal_path IS NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('UPDATE attachments SET external_path=internal_path WHERE internal_path IS NOT NULL');
|
||||
$this->addSql('ALTER TABLE attachments DROP COLUMN internal_path');
|
||||
$this->addSql('ALTER TABLE attachments RENAME COLUMN external_path TO path');
|
||||
}
|
||||
}
|
31
migrations/Version20250222165240.php
Normal file
31
migrations/Version20250222165240.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250222165240 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrate the old attachment class discriminator values from legacy Part-DB to the modern format, so that there is just one unified value';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
//Change the old discriminator values to the new ones
|
||||
$this->addSql("UPDATE attachments SET class_name = 'Part' WHERE class_name = 'PartDB\Part'");
|
||||
$this->addSql("UPDATE attachments SET class_name = 'Device' WHERE class_name = 'PartDB\Device'");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
//No down required, as the new format can also be read by older Part-DB version
|
||||
}
|
||||
}
|
81
package.json
81
package.json
|
@ -9,7 +9,7 @@
|
|||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
||||
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
|
||||
"@symfony/webpack-encore": "^4.1.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"core-js": "^3.23.0",
|
||||
"intl-messageformat": "^10.2.5",
|
||||
|
@ -18,7 +18,7 @@
|
|||
"regenerator-runtime": "^0.13.9",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-bundle-analyzer": "^4.3.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-cli": "^5.1.0",
|
||||
"webpack-notifier": "^1.15.0"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
@ -33,50 +33,52 @@
|
|||
"@algolia/autocomplete-js": "^1.17.0",
|
||||
"@algolia/autocomplete-plugin-recent-searches": "^1.17.0",
|
||||
"@algolia/autocomplete-theme-classic": "^1.17.0",
|
||||
"@ckeditor/ckeditor5-alignment": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-block-quote": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-code-block": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-dev-translations": "^39.1.0",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^39.1.0",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-font": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-heading": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-highlight": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-html-embed": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-html-support": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-image": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-indent": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-link": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-list": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-media-embed": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-remove-format": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-source-editing": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-special-characters": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-table": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-upload": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-watchdog": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-word-count": "^41.0.0",
|
||||
"@ckeditor/ckeditor5-alignment": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-block-quote": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-code-block": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-dev-translations": "^43.0.1",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^43.0.1",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-find-and-replace": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-font": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-heading": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-highlight": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-horizontal-line": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-html-embed": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-html-support": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-image": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-indent": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-link": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-list": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-markdown-gfm": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-media-embed": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-remove-format": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-source-editing": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-special-characters": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-table": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-upload": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-watchdog": "^44.0.0",
|
||||
"@ckeditor/ckeditor5-word-count": "^44.0.0",
|
||||
"@jbtronics/bs-treeview": "^1.0.1",
|
||||
"@part-db/html5-qrcode": "^3.1.0",
|
||||
"@zxcvbn-ts/core": "^3.0.2",
|
||||
"@zxcvbn-ts/language-common": "^3.0.3",
|
||||
"@zxcvbn-ts/language-de": "^3.0.1",
|
||||
"@zxcvbn-ts/language-en": "^3.0.1",
|
||||
"@zxcvbn-ts/language-fr": "^3.0.1",
|
||||
"@zxcvbn-ts/language-ja": "^3.0.1",
|
||||
"barcode-detector": "^2.3.1",
|
||||
"bootbox": "^6.0.0",
|
||||
"bootswatch": "^5.1.3",
|
||||
"bs-custom-file-input": "^1.3.4",
|
||||
"clipboard": "^2.0.4",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"compression-webpack-plugin": "^11.1.0",
|
||||
"datatables.net": "^2.0.0",
|
||||
"datatables.net-bs5": "^2.0.0",
|
||||
"datatables.net-buttons-bs5": "^3.0.0",
|
||||
|
@ -86,18 +88,17 @@
|
|||
"datatables.net-select-bs5": "^2.0.0",
|
||||
"dompurify": "^3.0.3",
|
||||
"emoji.json": "^15.0.0",
|
||||
"exports-loader": "^3.0.0",
|
||||
"html5-qrcode": "^2.2.1",
|
||||
"exports-loader": "^5.0.0",
|
||||
"json-formatter-js": "^2.3.4",
|
||||
"jszip": "^3.2.0",
|
||||
"katex": "^0.16.0",
|
||||
"marked": "^12.0.0",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked": "^15.0.4",
|
||||
"marked-gfm-heading-id": "^4.1.1",
|
||||
"marked-mangle": "^1.0.1",
|
||||
"pdfmake": "^0.2.2",
|
||||
"stimulus-use": "^0.52.0",
|
||||
"tom-select": "^2.1.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.0.2"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ parameters:
|
|||
treatPhpDocTypesAsCertain: false
|
||||
|
||||
symfony:
|
||||
container_xml_path: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
|
||||
containerXmlPath: '%rootDir%/../../../var/cache/dev/App_KernelDevDebugContainer.xml'
|
||||
|
||||
doctrine:
|
||||
objectManagerLoader: tests/object-manager.php
|
||||
|
@ -30,11 +30,6 @@ parameters:
|
|||
|
||||
checkFunctionNameCase: false
|
||||
|
||||
checkAlwaysTrueInstanceof: false
|
||||
checkAlwaysTrueCheckTypeFunctionCall: false
|
||||
checkAlwaysTrueStrictComparison: false
|
||||
reportAlwaysTrueInLastCondition: false
|
||||
|
||||
reportMaybesInPropertyPhpDocTypes: false
|
||||
reportMaybesInMethodSignatures: false
|
||||
|
||||
|
@ -43,14 +38,14 @@ parameters:
|
|||
booleansInConditions: false
|
||||
uselessCast: false
|
||||
requireParentConstructorCall: true
|
||||
disallowedConstructs: false
|
||||
overwriteVariablesWithLoop: false
|
||||
closureUsesThis: false
|
||||
matchingInheritedMethodNames: true
|
||||
numericOperandsInArithmeticOperators: true
|
||||
strictCalls: true
|
||||
switchConditionsMatchingType: false
|
||||
noVariableVariables: false
|
||||
disallowedEmpty: false
|
||||
disallowedShortTernary: false
|
||||
|
||||
ignoreErrors:
|
||||
# Ignore errors caused by complex mapping with AbstractStructuralDBElement
|
||||
|
@ -63,3 +58,6 @@ parameters:
|
|||
|
||||
# Ignore doctrine type mapping mismatch
|
||||
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
|
||||
|
||||
# Ignore error of unused WithPermPresetsTrait, as it is used in the migrations which are not analyzed by Phpstan
|
||||
- '#Trait App\\Migration\\WithPermPresetsTrait is used zero times and is not analysed#'
|
||||
|
|
|
@ -118,3 +118,10 @@ DirectoryIndex index.php
|
|||
# RedirectTemp cannot be used instead
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# Set Content-Security-Policy for svg files (and compressed variants), to block embedded javascript in there
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "\.(svg|svg\.gz|svg\.br)$">
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform;
|
||||
|
||||
use ApiPlatform\JsonSchema\Schema;
|
||||
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
/**
|
||||
* This decorator adds the properties given by DocumentedAPIProperty attributes on the classes to the schema.
|
||||
*/
|
||||
#[AsDecorator('api_platform.json_schema.schema_factory')]
|
||||
class AddDocumentedAPIPropertiesJSONSchemaFactory implements SchemaFactoryInterface
|
||||
{
|
||||
|
||||
public function __construct(private readonly SchemaFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildSchema(
|
||||
string $className,
|
||||
string $format = 'json',
|
||||
string $type = Schema::TYPE_OUTPUT,
|
||||
Operation $operation = null,
|
||||
Schema $schema = null,
|
||||
array $serializerContext = null,
|
||||
bool $forceCollection = false
|
||||
): Schema {
|
||||
|
||||
|
||||
$schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
|
||||
|
||||
//Check if there is are DocumentedAPIProperty attributes on the class
|
||||
$reflectionClass = new \ReflectionClass($className);
|
||||
$attributes = $reflectionClass->getAttributes(DocumentedAPIProperty::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var DocumentedAPIProperty $api_property */
|
||||
$api_property = $attribute->newInstance();
|
||||
$this->addPropertyToSchema($schema, $api_property->schemaName, $api_property->property,
|
||||
$api_property, $serializerContext ?? [], $format);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function addPropertyToSchema(Schema $schema, string $definitionName, string $normalizedPropertyName, DocumentedAPIProperty $propertyMetadata, array $serializerContext, string $format): void
|
||||
{
|
||||
$version = $schema->getVersion();
|
||||
$swagger = Schema::VERSION_SWAGGER === $version;
|
||||
|
||||
$propertySchema = [];
|
||||
|
||||
if (false === $propertyMetadata->writeable) {
|
||||
$propertySchema['readOnly'] = true;
|
||||
}
|
||||
if (!$swagger && false === $propertyMetadata->readable) {
|
||||
$propertySchema['writeOnly'] = true;
|
||||
}
|
||||
if (null !== $description = $propertyMetadata->description) {
|
||||
$propertySchema['description'] = $description;
|
||||
}
|
||||
|
||||
$deprecationReason = $propertyMetadata->deprecationReason;
|
||||
|
||||
// see https://github.com/json-schema-org/json-schema-spec/pull/737
|
||||
if (!$swagger && null !== $deprecationReason) {
|
||||
$propertySchema['deprecated'] = true;
|
||||
}
|
||||
|
||||
if (!empty($default = $propertyMetadata->default)) {
|
||||
if ($default instanceof \BackedEnum) {
|
||||
$default = $default->value;
|
||||
}
|
||||
$propertySchema['default'] = $default;
|
||||
}
|
||||
|
||||
if (!empty($example = $propertyMetadata->example)) {
|
||||
$propertySchema['example'] = $example;
|
||||
}
|
||||
|
||||
if (!isset($propertySchema['example']) && isset($propertySchema['default'])) {
|
||||
$propertySchema['example'] = $propertySchema['default'];
|
||||
}
|
||||
|
||||
$propertySchema['type'] = $propertyMetadata->type;
|
||||
$propertySchema['nullable'] = $propertyMetadata->nullable;
|
||||
|
||||
$propertySchema = new \ArrayObject($propertySchema);
|
||||
|
||||
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -21,7 +21,9 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform;
|
||||
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
|
||||
/**
|
||||
* When this attribute is applied to a class, an property will be added to the API documentation using the given parameters.
|
||||
|
@ -64,4 +66,55 @@ final class DocumentedAPIProperty
|
|||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function toAPIProperty(bool $use_swagger = false): ApiProperty
|
||||
{
|
||||
$openApiContext = [];
|
||||
|
||||
if (false === $this->writeable) {
|
||||
$openApiContext['readOnly'] = true;
|
||||
}
|
||||
if (!$use_swagger && false === $this->readable) {
|
||||
$openApiContext['writeOnly'] = true;
|
||||
}
|
||||
if (null !== $description = $this->description) {
|
||||
$openApiContext['description'] = $description;
|
||||
}
|
||||
|
||||
$deprecationReason = $this->deprecationReason;
|
||||
|
||||
// see https://github.com/json-schema-org/json-schema-spec/pull/737
|
||||
if (!$use_swagger && null !== $deprecationReason) {
|
||||
$openApiContext['deprecated'] = true;
|
||||
}
|
||||
|
||||
if (!empty($default = $this->default)) {
|
||||
if ($default instanceof \BackedEnum) {
|
||||
$default = $default->value;
|
||||
}
|
||||
$openApiContext['default'] = $default;
|
||||
}
|
||||
|
||||
if (!empty($example = $this->example)) {
|
||||
$openApiContext['example'] = $example;
|
||||
}
|
||||
|
||||
if (!isset($openApiContext['example']) && isset($openApiContext['default'])) {
|
||||
$openApiContext['example'] = $openApiContext['default'];
|
||||
}
|
||||
|
||||
$openApiContext['type'] = $this->type;
|
||||
$openApiContext['nullable'] = $this->nullable;
|
||||
|
||||
|
||||
|
||||
return new ApiProperty(
|
||||
description: $this->description,
|
||||
readable: $this->readable,
|
||||
writable: $this->writeable,
|
||||
openapiContext: $openApiContext,
|
||||
types: $this->type,
|
||||
property: $this->property
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
/**
|
||||
* This decorator adds the virtual properties defined by the DocumentedAPIProperty attribute to the property metadata
|
||||
* which then get picked up by the openapi schema generator
|
||||
*/
|
||||
#[AsDecorator('api_platform.metadata.property.metadata_factory')]
|
||||
class PropertyMetadataFactory implements PropertyMetadataFactoryInterface
|
||||
{
|
||||
public function __construct(private PropertyMetadataFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
|
||||
{
|
||||
$metadata = $this->decorated->create($resourceClass, $property, $options);
|
||||
|
||||
//Only become active in the context of the openapi schema generation
|
||||
if (!isset($options['schema_type'])) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if (!class_exists($resourceClass)) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$refClass = new ReflectionClass($resourceClass);
|
||||
$attributes = $refClass->getAttributes(DocumentedAPIProperty::class);
|
||||
|
||||
//Look for the DocumentedAPIProperty attribute with the given property name
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var DocumentedAPIProperty $api_property */
|
||||
$api_property = $attribute->newInstance();
|
||||
//If attribute not matches the property name, skip it
|
||||
if ($api_property->property !== $property) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//Return the virtual property
|
||||
return $api_property->toAPIProperty();
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform\DocumentedAPIProperties;
|
||||
|
||||
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
|
||||
use ApiPlatform\Metadata\Property\PropertyNameCollection;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
/**
|
||||
* This decorator adds the virtual property names defined by the DocumentedAPIProperty attribute to the property name collection
|
||||
* which then get picked up by the openapi schema generator
|
||||
*/
|
||||
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
|
||||
class PropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
|
||||
{
|
||||
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $resourceClass, array $options = []): PropertyNameCollection
|
||||
{
|
||||
// Get the default properties from the decorated service
|
||||
$propertyNames = $this->decorated->create($resourceClass, $options);
|
||||
|
||||
//Only become active in the context of the openapi schema generation
|
||||
if (!isset($options['schema_type'])) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
if (!class_exists($resourceClass)) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
$properties = iterator_to_array($propertyNames);
|
||||
|
||||
$refClass = new ReflectionClass($resourceClass);
|
||||
|
||||
foreach ($refClass->getAttributes(DocumentedAPIProperty::class) as $attribute) {
|
||||
/** @var DocumentedAPIProperty $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$properties[] = $instance->property;
|
||||
}
|
||||
|
||||
return new PropertyNameCollection($properties);
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class EntityFilter extends AbstractFilter
|
|||
public function __construct(
|
||||
ManagerRegistry $managerRegistry,
|
||||
private readonly EntityFilterHelper $filter_helper,
|
||||
LoggerInterface $logger = null,
|
||||
?LoggerInterface $logger = null,
|
||||
?array $properties = null,
|
||||
?NameConverterInterface $nameConverter = null
|
||||
) {
|
||||
|
@ -50,7 +50,7 @@ class EntityFilter extends AbstractFilter
|
|||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
Operation $operation = null,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
if (
|
||||
|
|
|
@ -92,12 +92,6 @@ class EntityFilterHelper
|
|||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter using a comma seperated list of element IDs. Use + to include all direct children and ++ to include all children recursively.',
|
||||
'openapi' => [
|
||||
'example' => '',
|
||||
'allowReserved' => false,// if true, query parameters will be not percent-encoded
|
||||
'allowEmptyValue' => true,
|
||||
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
|
||||
],
|
||||
];
|
||||
}
|
||||
return $description;
|
||||
|
|
|
@ -38,7 +38,7 @@ final class LikeFilter extends AbstractFilter
|
|||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
Operation $operation = null,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
// Otherwise filter is applied to order and page as well
|
||||
|
@ -50,7 +50,7 @@ final class LikeFilter extends AbstractFilter
|
|||
}
|
||||
$parameterName = $queryNameGenerator->generateParameterName($property); // Generate a unique parameter name to avoid collisions with other filters
|
||||
$queryBuilder
|
||||
->andWhere(sprintf('o.%s LIKE :%s', $property, $parameterName))
|
||||
->andWhere(sprintf('ILIKE(o.%s, :%s) = TRUE', $property, $parameterName))
|
||||
->setParameter($parameterName, $value);
|
||||
}
|
||||
|
||||
|
@ -67,12 +67,6 @@ final class LikeFilter extends AbstractFilter
|
|||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter using a LIKE SQL expression. Use % as wildcard for multiple characters and _ for single characters. For example, to search for all items containing foo, use foo. To search for all items starting with foo, use foo%. To search for all items ending with foo, use %foo',
|
||||
'openapi' => [
|
||||
'example' => '',
|
||||
'allowReserved' => false,// if true, query parameters will be not percent-encoded
|
||||
'allowEmptyValue' => true,
|
||||
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
|
||||
],
|
||||
];
|
||||
}
|
||||
return $description;
|
||||
|
|
|
@ -38,7 +38,7 @@ class PartStoragelocationFilter extends AbstractFilter
|
|||
public function __construct(
|
||||
ManagerRegistry $managerRegistry,
|
||||
private readonly EntityFilterHelper $filter_helper,
|
||||
LoggerInterface $logger = null,
|
||||
?LoggerInterface $logger = null,
|
||||
?array $properties = null,
|
||||
?NameConverterInterface $nameConverter = null
|
||||
) {
|
||||
|
@ -51,7 +51,7 @@ class PartStoragelocationFilter extends AbstractFilter
|
|||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
Operation $operation = null,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
//Do not check for mapping here, as we are using a virtual property
|
||||
|
|
|
@ -61,10 +61,10 @@ final class TagFilter extends AbstractFilter
|
|||
$expr = $queryBuilder->expr();
|
||||
|
||||
$tmp = $expr->orX(
|
||||
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'),
|
||||
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'),
|
||||
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'),
|
||||
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'),
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_1) = TRUE',
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_2) = TRUE',
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_3) = TRUE',
|
||||
'ILIKE(o.'.$property.', :' . $tag_identifier_prefix . '_4) = TRUE',
|
||||
);
|
||||
|
||||
$queryBuilder->andWhere($tmp);
|
||||
|
@ -89,12 +89,6 @@ final class TagFilter extends AbstractFilter
|
|||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
'required' => false,
|
||||
'description' => 'Filter for tags of a part',
|
||||
'openapi' => [
|
||||
'example' => '',
|
||||
'allowReserved' => false,// if true, query parameters will be not percent-encoded
|
||||
'allowEmptyValue' => true,
|
||||
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
|
||||
],
|
||||
];
|
||||
}
|
||||
return $description;
|
||||
|
|
77
src/ApiPlatform/NormalizePropertyNameCollectionFactory.php
Normal file
77
src/ApiPlatform/NormalizePropertyNameCollectionFactory.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform;
|
||||
|
||||
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
|
||||
use ApiPlatform\Metadata\Property\PropertyNameCollection;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
/**
|
||||
* This decorator removes all camelCase property names from the property name collection, if a snake_case version exists.
|
||||
* This is a fix for https://github.com/Part-DB/Part-DB-server/issues/862, as the openapi schema generator wrongly collects
|
||||
* both camelCase and snake_case property names, which leads to duplicate properties in the schema.
|
||||
* This seems to come from the fact that the openapi schema generator uses no serializerContext, which seems then to collect
|
||||
* the getters too...
|
||||
*/
|
||||
#[AsDecorator('api_platform.metadata.property.name_collection_factory')]
|
||||
class NormalizePropertyNameCollectionFactory implements PropertyNameCollectionFactoryInterface
|
||||
{
|
||||
public function __construct(private readonly PropertyNameCollectionFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $resourceClass, array $options = []): PropertyNameCollection
|
||||
{
|
||||
// Get the default properties from the decorated service
|
||||
$propertyNames = $this->decorated->create($resourceClass, $options);
|
||||
|
||||
//Only become active in the context of the openapi schema generation
|
||||
if (!isset($options['schema_type'])) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
//If we are not in the jsonapi generator (which sets no serializer groups), return the property names as is
|
||||
if (isset($options['serializer_groups'])) {
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
//Remove all camelCase property names from the collection, if a snake_case version exists
|
||||
$properties = iterator_to_array($propertyNames);
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if (str_contains($property, '_')) {
|
||||
$camelized = u($property)->camel()->toString();
|
||||
|
||||
//If the camelized version exists, remove it from the collection
|
||||
$index = array_search($camelized, $properties, true);
|
||||
if ($index !== false) {
|
||||
unset($properties[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new PropertyNameCollection($properties);
|
||||
}
|
||||
}
|
|
@ -73,6 +73,9 @@ class CleanAttachmentsCommand extends Command
|
|||
//Ignore image cache folder
|
||||
$finder->exclude('cache');
|
||||
|
||||
//Ignore automigration folder
|
||||
$finder->exclude('.automigration-backup');
|
||||
|
||||
$fs = new Filesystem();
|
||||
|
||||
$file_list = [];
|
||||
|
|
136
src/Command/Attachments/DownloadAttachmentsCommand.php
Normal file
136
src/Command/Attachments/DownloadAttachmentsCommand.php
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Command\Attachments;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentUpload;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:attachments:download', "Downloads all attachments which have only an external URL to the local filesystem.")]
|
||||
class DownloadAttachmentsCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
private EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setHelp('This command downloads all attachments, which only have an external URL, to the local filesystem, so that you have an offline copy of the attachments.');
|
||||
$this->addOption('--private', null, null, 'If set, the attachments will be downloaded to the private storage.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('attachment')
|
||||
->from(Attachment::class, 'attachment')
|
||||
->where('attachment.external_path IS NOT NULL')
|
||||
->andWhere('attachment.external_path != \'\'')
|
||||
->andWhere('attachment.internal_path IS NULL');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
$attachments = $query->getResult();
|
||||
|
||||
if (count($attachments) === 0) {
|
||||
$io->success('No attachments with external URL found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->note('Found ' . count($attachments) . ' attachments with external URL, that will be downloaded.');
|
||||
|
||||
//If the option --private is set, the attachments will be downloaded to the private storage.
|
||||
$private = $input->getOption('private');
|
||||
if ($private) {
|
||||
if (!$io->confirm('Attachments will be downloaded to the private storage. Continue?')) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
} else {
|
||||
if (!$io->confirm('Attachments will be downloaded to the public storage, where everybody knowing the correct URL can access it. Continue?')){
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar = $io->createProgressBar(count($attachments));
|
||||
$progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% \n%message%");
|
||||
|
||||
$progressBar->setMessage('Starting download...');
|
||||
$progressBar->start();
|
||||
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var Attachment $attachment */
|
||||
$progressBar->setMessage(sprintf('%s (ID: %s) from %s', $attachment->getName(), $attachment->getID(), $attachment->getHost()));
|
||||
$progressBar->advance();
|
||||
|
||||
try {
|
||||
$attachmentUpload = new AttachmentUpload(file: null, downloadUrl: true, private: $private);
|
||||
$this->attachmentSubmitHandler->handleUpload($attachment, $attachmentUpload);
|
||||
|
||||
//Write changes to the database
|
||||
$this->entityManager->flush();
|
||||
} catch (AttachmentDownloadException $e) {
|
||||
$errors[] = [
|
||||
'attachment' => $attachment,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
//Fix the line break after the progress bar
|
||||
$io->newLine();
|
||||
$io->newLine();
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$io->warning('Some attachments could not be downloaded:');
|
||||
foreach ($errors as $error) {
|
||||
$io->warning(sprintf("Attachment %s (ID %s) could not be downloaded from %s:\n%s",
|
||||
$error['attachment']->getName(),
|
||||
$error['attachment']->getID(),
|
||||
$error['attachment']->getExternalPath(),
|
||||
$error['error'])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$io->success('All attachments downloaded successfully.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
90
src/Command/Attachments/SanitizeSVGAttachmentsCommand.php
Normal file
90
src/Command/Attachments/SanitizeSVGAttachmentsCommand.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Command\Attachments;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:attachments:sanitize-svg', "Sanitize uploaded SVG files.")]
|
||||
class SanitizeSVGAttachmentsCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly AttachmentSubmitHandler $attachmentSubmitHandler, ?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setHelp('This command allows to sanitize SVG files uploaded via attachments. This happens automatically since version 1.17.1, this command is intended to be used for older files.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->info('This command will sanitize all uploaded SVG files. This is only required if you have uploaded (untrusted) SVG files before version 1.17.1. If you are running a newer version, you don\'t need to run this command (again).');
|
||||
if (!$io->confirm('Do you want to continue?', false)) {
|
||||
$io->success('Command aborted.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Sanitizing SVG files...');
|
||||
|
||||
//Finding all attachments with svg files
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('a')
|
||||
->from(Attachment::class, 'a')
|
||||
->where('a.internal_path LIKE :pattern ESCAPE \'#\'')
|
||||
->orWhere('a.original_filename LIKE :pattern ESCAPE \'#\'')
|
||||
->setParameter('pattern', '%.svg');
|
||||
|
||||
$attachments = $qb->getQuery()->getResult();
|
||||
$io->note('Found '.count($attachments).' attachments with SVG files.');
|
||||
|
||||
if (count($attachments) === 0) {
|
||||
$io->success('No SVG files found.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Sanitizing SVG files...');
|
||||
$io->progressStart(count($attachments));
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var Attachment $attachment */
|
||||
$io->note('Sanitizing attachment '.$attachment->getId().' ('.($attachment->getFilename() ?? '???').')');
|
||||
$this->attachmentSubmitHandler->sanitizeSVGAttachment($attachment);
|
||||
$io->progressAdvance();
|
||||
|
||||
}
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success('Sanitization finished. All SVG files have been sanitized.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@ class CheckRequirementsCommand extends Command
|
|||
//Checking 32-bit system
|
||||
if (PHP_INT_SIZE === 4) {
|
||||
$io->warning('You are using a 32-bit system. You will have problems with working with dates after the year 2038, therefore a 64-bit system is recommended.');
|
||||
} elseif (PHP_INT_SIZE === 8) {
|
||||
} elseif (PHP_INT_SIZE === 8) { //@phpstan-ignore-line //PHP_INT_SIZE is always 4 or 8
|
||||
if (!$only_issues) {
|
||||
$io->success('You are using a 64-bit system.');
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ class ConvertBBCodeCommand extends Command
|
|||
|
||||
/**
|
||||
* Returns a list which entities and which properties need to be checked.
|
||||
* @return array<class-string<AbstractNamedDBElement>, string[]>
|
||||
*/
|
||||
protected function getTargetsLists(): array
|
||||
{
|
||||
|
@ -109,7 +110,6 @@ class ConvertBBCodeCommand extends Command
|
|||
$class
|
||||
));
|
||||
//Determine which entities of this type we need to modify
|
||||
/** @var EntityRepository $repo */
|
||||
$repo = $this->em->getRepository($class);
|
||||
$qb = $repo->createQueryBuilder('e')
|
||||
->select('e');
|
||||
|
|
|
@ -35,7 +35,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||
#[AsCommand('partdb:users:enable|partdb:user:enable', 'Enables/Disable the login of one or more users')]
|
||||
class UserEnableCommand extends Command
|
||||
{
|
||||
public function __construct(protected EntityManagerInterface $entityManager, string $name = null)
|
||||
public function __construct(protected EntityManagerInterface $entityManager, ?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
|
|
@ -206,12 +206,15 @@ class UsersPermissionsCommand extends Command
|
|||
return '<fg=green>Allow</>';
|
||||
} elseif ($permission_value === false) {
|
||||
return '<fg=red>Disallow</>';
|
||||
} elseif ($permission_value === null && !$inherit) {
|
||||
}
|
||||
// Permission value is null by this point
|
||||
elseif (!$inherit) {
|
||||
return '<fg=blue>Inherit</>';
|
||||
} elseif ($permission_value === null && $inherit) {
|
||||
} elseif ($inherit) {
|
||||
return '<fg=red>Disallow (Inherited)</>';
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line This line is never reached, but PHPstorm complains otherwise
|
||||
return '???';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,7 +221,6 @@ abstract class BaseAdminController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
/** @var AbstractPartsContainingRepository $repo */
|
||||
$repo = $this->entityManager->getRepository($this->entity_class);
|
||||
|
||||
return $this->render($this->twig_template, [
|
||||
|
@ -397,7 +396,7 @@ abstract class BaseAdminController extends AbstractController
|
|||
{
|
||||
if ($entity instanceof AbstractPartsContainingDBElement) {
|
||||
/** @var AbstractPartsContainingRepository $repo */
|
||||
$repo = $this->entityManager->getRepository($this->entity_class);
|
||||
$repo = $this->entityManager->getRepository($this->entity_class); //@phpstan-ignore-line
|
||||
if ($repo->getPartsCount($entity) > 0) {
|
||||
$this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
|
||||
|
||||
|
@ -468,6 +467,11 @@ abstract class BaseAdminController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('read', $entity);
|
||||
$entities = $em->getRepository($this->entity_class)->findAll();
|
||||
|
||||
if (count($entities) === 0) {
|
||||
$this->addFlash('error', 'entity.export.flash.error.no_entities');
|
||||
return $this->redirectToRoute($this->route_base.'_new');
|
||||
}
|
||||
|
||||
return $exporter->exportEntityFromRequest($entities, $request);
|
||||
}
|
||||
|
||||
|
|
|
@ -51,15 +51,15 @@ class AttachmentFileController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('show_private', $attachment);
|
||||
}
|
||||
|
||||
if ($attachment->isExternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
|
||||
if (!$attachment->hasInternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
||||
}
|
||||
|
||||
if (!$helper->isFileExisting($attachment)) {
|
||||
if (!$helper->isInternalFileExisting($attachment)) {
|
||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||
}
|
||||
|
||||
$file_path = $helper->toAbsoluteFilePath($attachment);
|
||||
$file_path = $helper->toAbsoluteInternalFilePath($attachment);
|
||||
$response = new BinaryFileResponse($file_path);
|
||||
|
||||
//Set header content disposition, so that the file will be downloaded
|
||||
|
@ -80,15 +80,15 @@ class AttachmentFileController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('show_private', $attachment);
|
||||
}
|
||||
|
||||
if ($attachment->isExternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
|
||||
if (!$attachment->hasInternal()) {
|
||||
throw $this->createNotFoundException('The file for this attachment is external and not stored locally!');
|
||||
}
|
||||
|
||||
if (!$helper->isFileExisting($attachment)) {
|
||||
if (!$helper->isInternalFileExisting($attachment)) {
|
||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||
}
|
||||
|
||||
$file_path = $helper->toAbsoluteFilePath($attachment);
|
||||
$file_path = $helper->toAbsoluteInternalFilePath($attachment);
|
||||
$response = new BinaryFileResponse($file_path);
|
||||
|
||||
//Set header content disposition, so that the file will be downloaded
|
||||
|
|
|
@ -23,10 +23,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Form\InfoProviderSystem\PartSearchType;
|
||||
use App\Services\InfoProviderSystem\ExistingPartFinder;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -42,7 +45,9 @@ class InfoProviderController extends AbstractController
|
|||
{
|
||||
|
||||
public function __construct(private readonly ProviderRegistry $providerRegistry,
|
||||
private readonly PartInfoRetriever $infoRetriever)
|
||||
private readonly PartInfoRetriever $infoRetriever,
|
||||
private readonly ExistingPartFinder $existingPartFinder
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -72,21 +77,49 @@ class InfoProviderController extends AbstractController
|
|||
//When we are updating a part, use its name as keyword, to make searching easier
|
||||
//However we can only do this, if the form was not submitted yet
|
||||
if ($update_target !== null && !$form->isSubmitted()) {
|
||||
$form->get('keyword')->setData($update_target->getName());
|
||||
//Use the provider reference if available, otherwise use the manufacturer product number
|
||||
$keyword = $update_target->getProviderReference()->getProviderId() ?? $update_target->getManufacturerProductNumber();
|
||||
//Or the name if both are not available
|
||||
if ($keyword === "") {
|
||||
$keyword = $update_target->getName();
|
||||
}
|
||||
|
||||
$form->get('keyword')->setData($keyword);
|
||||
|
||||
//If we are updating a part, which already has a provider, preselect that provider in the form
|
||||
if ($update_target->getProviderReference()->getProviderKey() !== null) {
|
||||
try {
|
||||
$form->get('providers')->setData([$this->providerRegistry->getProviderByKey($update_target->getProviderReference()->getProviderKey())]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
//If the provider is not found, just ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$keyword = $form->get('keyword')->getData();
|
||||
$providers = $form->get('providers')->getData();
|
||||
|
||||
$dtos = [];
|
||||
|
||||
try {
|
||||
$results = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
|
||||
$dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers);
|
||||
} catch (ClientException $e) {
|
||||
$this->addFlash('error', t('info_providers.search.error.client_exception'));
|
||||
$this->addFlash('error',$e->getMessage());
|
||||
//Log the exception
|
||||
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
|
||||
// modify the array to an array of arrays that has a field for a matching local Part
|
||||
// the advantage to use that format even when we don't look for local parts is that we
|
||||
// always work with the same interface
|
||||
$results = array_map(function ($result) {return ['dto' => $result, 'localPart' => null];}, $dtos);
|
||||
if(!$update_target) {
|
||||
foreach ($results as $index => $result) {
|
||||
$results[$index]['localPart'] = $this->existingPartFinder->findFirstExisting($result['dto']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('info_providers/search/part_search.html.twig', [
|
||||
|
|
|
@ -108,8 +108,31 @@ class LabelController extends AbstractController
|
|||
$pdf_data = null;
|
||||
$filename = 'invalid.pdf';
|
||||
|
||||
//Generate PDF either when the form is submitted and valid, or the form was not submit yet, and generate is set
|
||||
if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile instanceof LabelProfile)) {
|
||||
|
||||
//Check if the label should be saved as profile
|
||||
if ($form->get('save_profile')->isClicked() && $this->isGranted('@labels.create_profiles')) { //@phpstan-ignore-line Phpstan does not recognize the isClicked method
|
||||
//Retrieve the profile name from the form
|
||||
$new_name = $form->get('save_profile_name')->getData();
|
||||
//ensure that the name is not empty
|
||||
if ($new_name === '' || $new_name === null) {
|
||||
$form->get('save_profile_name')->addError(new FormError($this->translator->trans('label_generator.profile_name_empty')));
|
||||
goto render;
|
||||
}
|
||||
|
||||
$profile = new LabelProfile();
|
||||
$profile->setName($form->get('save_profile_name')->getData());
|
||||
$profile->setOptions($form_options);
|
||||
$this->em->persist($profile);
|
||||
$this->em->flush();
|
||||
$this->addFlash('success', 'label_generator.profile_saved');
|
||||
|
||||
return $this->redirectToRoute('label_dialog_profile', [
|
||||
'profile' => $profile->getID(),
|
||||
'target_id' => (string) $form->get('target_id')->getData()
|
||||
]);
|
||||
}
|
||||
|
||||
$target_id = (string) $form->get('target_id')->getData();
|
||||
$targets = $this->findObjects($form_options->getSupportedElement(), $target_id);
|
||||
if ($targets !== []) {
|
||||
|
@ -132,6 +155,7 @@ class LabelController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
render:
|
||||
return $this->render('label_system/dialog.html.twig', [
|
||||
'form' => $form,
|
||||
'pdf_data' => $pdf_data,
|
||||
|
@ -152,7 +176,7 @@ class LabelController extends AbstractController
|
|||
{
|
||||
$id_array = $this->rangeParser->parse($ids);
|
||||
|
||||
/** @var DBElementRepository $repo */
|
||||
/** @var DBElementRepository<AbstractDBElement> $repo */
|
||||
$repo = $this->em->getRepository($type->getEntityClass());
|
||||
|
||||
return $repo->getElementsFromIDArray($id_array);
|
||||
|
|
|
@ -229,6 +229,10 @@ class PartController extends AbstractController
|
|||
$dto = $infoRetriever->getDetails($providerKey, $providerId);
|
||||
$new_part = $infoRetriever->dtoToPart($dto);
|
||||
|
||||
if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) {
|
||||
$this->addFlash('warning', t("part.create_from_info_provider.no_category_yet"));
|
||||
}
|
||||
|
||||
return $this->renderPartForm('new', $request, $new_part, [
|
||||
'info_provider_dto' => $dto,
|
||||
]);
|
||||
|
|
|
@ -112,8 +112,9 @@ class PartImportExportController extends AbstractController
|
|||
$ids = $request->query->get('ids', '');
|
||||
$parts = $this->partsTableActionHandler->idStringToArray($ids);
|
||||
|
||||
if ($parts === []) {
|
||||
throw new \RuntimeException('No parts found!');
|
||||
if (count($parts) === 0) {
|
||||
$this->addFlash('error', 'entity.export.flash.error.no_entities');
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
|
||||
//Ensure that we have access to the parts
|
||||
|
|
|
@ -29,6 +29,7 @@ use App\DataTables\PartsDataTable;
|
|||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Exceptions\InvalidRegexException;
|
||||
|
@ -43,8 +44,11 @@ use Symfony\Component\Form\FormInterface;
|
|||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
class PartListsController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly NodesListBuilder $nodesListBuilder, private readonly DataTableFactory $dataTableFactory, private readonly TranslatorInterface $translator)
|
||||
|
@ -60,6 +64,7 @@ class PartListsController extends AbstractController
|
|||
$ids = $request->request->get('ids');
|
||||
$action = $request->request->get('action');
|
||||
$target = $request->request->get('target');
|
||||
$redirectResponse = null;
|
||||
|
||||
if (!$this->isCsrfTokenValid('table_action', $request->request->get('_token'))) {
|
||||
$this->addFlash('error', 'csfr_invalid');
|
||||
|
@ -70,17 +75,36 @@ class PartListsController extends AbstractController
|
|||
if (null === $action || null === $ids) {
|
||||
$this->addFlash('error', 'part.table.actions.no_params_given');
|
||||
} else {
|
||||
$errors = [];
|
||||
|
||||
$parts = $actionHandler->idStringToArray($ids);
|
||||
$redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect);
|
||||
$redirectResponse = $actionHandler->handleAction($action, $parts, $target ? (int) $target : null, $redirect, $errors);
|
||||
|
||||
//Save changes
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'part.table.actions.success');
|
||||
if (count($errors) === 0) {
|
||||
$this->addFlash('success', 'part.table.actions.success');
|
||||
} else {
|
||||
$this->addFlash('error', t('part.table.actions.error', ['%count%' => count($errors)]));
|
||||
//Create a flash message for each error
|
||||
foreach ($errors as $error) {
|
||||
/** @var Part $part */
|
||||
$part = $error['part'];
|
||||
|
||||
$this->addFlash('error',
|
||||
t('part.table.actions.error_detail', [
|
||||
'%part_name%' => $part->getName(),
|
||||
'%part_id%' => $part->getID(),
|
||||
'%message%' => $error['message']
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If the action handler returned a response, we use it, otherwise we redirect back to the previous page.
|
||||
if (isset($redirectResponse) && $redirectResponse instanceof Response) {
|
||||
if ($redirectResponse !== null) {
|
||||
return $redirectResponse;
|
||||
}
|
||||
|
||||
|
@ -131,7 +155,11 @@ class PartListsController extends AbstractController
|
|||
|
||||
$filterForm->handleRequest($formRequest);
|
||||
|
||||
$table = $this->dataTableFactory->createFromType(PartsDataTable::class, array_merge(['filter' => $filter], $additional_table_vars))
|
||||
$table = $this->dataTableFactory->createFromType(
|
||||
PartsDataTable::class,
|
||||
array_merge(['filter' => $filter], $additional_table_vars),
|
||||
['lengthMenu' => PartsDataTable::LENGTH_MENU]
|
||||
)
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
|
|
|
@ -42,10 +42,10 @@ declare(strict_types=1);
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Form\LabelSystem\ScanDialogType;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeRedirector;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeScanResult;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -77,13 +77,21 @@ class ScanController extends AbstractController
|
|||
$mode = $form['mode']->getData();
|
||||
}
|
||||
|
||||
$infoModeData = null;
|
||||
|
||||
if ($input !== null) {
|
||||
try {
|
||||
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||
try {
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
//Perform a redirect if the info mode is not enabled
|
||||
if (!$form['info_mode']->getData()) {
|
||||
try {
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
}
|
||||
} else { //Otherwise retrieve infoModeData
|
||||
$infoModeData = $scan_result->getDecodedForInfoMode();
|
||||
|
||||
}
|
||||
} catch (InvalidArgumentException) {
|
||||
$this->addFlash('error', 'scan.format_unknown');
|
||||
|
@ -92,6 +100,7 @@ class ScanController extends AbstractController
|
|||
|
||||
return $this->render('label_system/scanner/scanner.html.twig', [
|
||||
'form' => $form,
|
||||
'infoModeData' => $infoModeData,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -109,7 +118,7 @@ class ScanController extends AbstractController
|
|||
throw new InvalidArgumentException('Unknown type: '.$type);
|
||||
}
|
||||
//Construct the scan result manually, as we don't have a barcode here
|
||||
$scan_result = new BarcodeScanResult(
|
||||
$scan_result = new LocalBarcodeScanResult(
|
||||
target_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
|
||||
target_id: $id,
|
||||
//The routes are only used on the internal generated QR codes
|
||||
|
|
|
@ -29,6 +29,7 @@ use App\Entity\Parts\Category;
|
|||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\MeasurementUnit;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Form\Type\Helper\StructuralEntityChoiceHelper;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
|
@ -78,6 +79,12 @@ class SelectAPIController extends AbstractController
|
|||
return $this->getResponseForClass(Project::class, false);
|
||||
}
|
||||
|
||||
#[Route(path: '/storage_location', name: 'select_storage_location')]
|
||||
public function locations(): Response
|
||||
{
|
||||
return $this->getResponseForClass(StorageLocation::class, true);
|
||||
}
|
||||
|
||||
#[Route(path: '/export_level', name: 'select_export_level')]
|
||||
public function exportLevel(): Response
|
||||
{
|
||||
|
|
|
@ -61,10 +61,10 @@ class ToolsController extends AbstractController
|
|||
'default_theme' => $this->getParameter('partdb.global_theme'),
|
||||
'enabled_locales' => $this->getParameter('partdb.locale_menu'),
|
||||
'demo_mode' => $this->getParameter('partdb.demo_mode'),
|
||||
'gpdr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
|
||||
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
|
||||
'use_gravatar' => $this->getParameter('partdb.users.use_gravatar'),
|
||||
'email_password_reset' => $this->getParameter('partdb.users.email_pw_reset'),
|
||||
'enviroment' => $this->getParameter('kernel.environment'),
|
||||
'environment' => $this->getParameter('kernel.environment'),
|
||||
'is_debug' => $this->getParameter('kernel.debug'),
|
||||
'email_sender' => $this->getParameter('partdb.mail.sender_email'),
|
||||
'email_sender_name' => $this->getParameter('partdb.mail.sender_name'),
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Parts\Category;
|
||||
|
@ -92,7 +93,7 @@ class TypeaheadController extends AbstractController
|
|||
|
||||
/**
|
||||
* This function map the parameter type to the class, so we can access its repository
|
||||
* @return class-string
|
||||
* @return class-string<AbstractParameter>
|
||||
*/
|
||||
private function typeToParameterClass(string $type): string
|
||||
{
|
||||
|
@ -155,7 +156,7 @@ class TypeaheadController extends AbstractController
|
|||
//Ensure user has the correct permissions
|
||||
$this->denyAccessUnlessGranted('read', $test_obj);
|
||||
|
||||
/** @var ParameterRepository $repository */
|
||||
/** @var ParameterRepository<AbstractParameter> $repository */
|
||||
$repository = $entityManager->getRepository($class);
|
||||
|
||||
$data = $repository->autocompleteParamName($query);
|
||||
|
|
|
@ -240,7 +240,10 @@ class UserSettingsController extends AbstractController
|
|||
$page_need_reload = true;
|
||||
}
|
||||
|
||||
/** @var Form $form We need a form implementation for the next calls */
|
||||
if (!$form instanceof Form) {
|
||||
throw new RuntimeException('Form is not an instance of Form, so we cannot retrieve the clicked button!');
|
||||
}
|
||||
|
||||
//Remove the avatar attachment from the user if requested
|
||||
if ($form->getClickedButton() && 'remove_avatar' === $form->getClickedButton()->getName() && $user->getMasterPictureAttachment() instanceof Attachment) {
|
||||
$em->remove($user->getMasterPictureAttachment());
|
||||
|
|
|
@ -41,7 +41,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
|
|||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
/** @var User $admin_user */
|
||||
$admin_user = $this->getReference(UserFixtures::ADMIN);
|
||||
$admin_user = $this->getReference(UserFixtures::ADMIN, User::class);
|
||||
|
||||
$read_only_token = new ApiToken();
|
||||
$read_only_token->setUser($admin_user);
|
||||
|
|
|
@ -35,7 +35,7 @@ use Doctrine\Persistence\ObjectManager;
|
|||
class LogEntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$this->createCategoryEntries($manager);
|
||||
$this->createDeletedCategory($manager);
|
||||
|
|
|
@ -106,7 +106,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
$partLot2->setComment('Test');
|
||||
$partLot2->setNeedsRefill(true);
|
||||
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
|
||||
$partLot2->setVendorBarcode('lot2_vendor_barcode');
|
||||
$partLot2->setUserBarcode('lot2_vendor_barcode');
|
||||
$part->addPartLot($partLot2);
|
||||
|
||||
$orderdetail = new Orderdetail();
|
||||
|
@ -131,7 +131,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Test2');
|
||||
$attachment->setPath('invalid');
|
||||
$attachment->setInternalPath('invalid');
|
||||
$attachment->setShowInTable(true);
|
||||
$attachment->setAttachmentType($manager->find(AttachmentType::class, 1));
|
||||
$part->addAttachment($attachment);
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
|
@ -41,7 +42,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
|
|||
{
|
||||
$anonymous = new User();
|
||||
$anonymous->setName('anonymous');
|
||||
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY));
|
||||
$anonymous->setGroup($this->getReference(GroupFixtures::READONLY, Group::class));
|
||||
$anonymous->setNeedPwChange(false);
|
||||
$anonymous->setPassword($this->encoder->hashPassword($anonymous, 'test'));
|
||||
$manager->persist($anonymous);
|
||||
|
@ -50,7 +51,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
|
|||
$admin->setName('admin');
|
||||
$admin->setPassword($this->encoder->hashPassword($admin, 'test'));
|
||||
$admin->setNeedPwChange(false);
|
||||
$admin->setGroup($this->getReference(GroupFixtures::ADMINS));
|
||||
$admin->setGroup($this->getReference(GroupFixtures::ADMINS, Group::class));
|
||||
$manager->persist($admin);
|
||||
$this->addReference(self::ADMIN, $admin);
|
||||
|
||||
|
@ -60,7 +61,7 @@ class UserFixtures extends Fixture implements DependentFixtureInterface
|
|||
$user->setEmail('user@invalid.invalid');
|
||||
$user->setFirstName('Test')->setLastName('User');
|
||||
$user->setPassword($this->encoder->hashPassword($user, 'test'));
|
||||
$user->setGroup($this->getReference(GroupFixtures::USERS));
|
||||
$user->setGroup($this->getReference(GroupFixtures::USERS, Group::class));
|
||||
$manager->persist($user);
|
||||
|
||||
$noread = new User();
|
||||
|
|
|
@ -54,7 +54,7 @@ class TwoStepORMAdapter extends ORMAdapter
|
|||
|
||||
private \Closure|null $query_modifier = null;
|
||||
|
||||
public function __construct(ManagerRegistry $registry = null)
|
||||
public function __construct(?ManagerRegistry $registry = null)
|
||||
{
|
||||
parent::__construct($registry);
|
||||
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never {
|
||||
|
|
|
@ -50,8 +50,8 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
{
|
||||
$dataTable->add('dont_matter', RowClassColumn::class, [
|
||||
'render' => function ($value, Attachment $context): string {
|
||||
//Mark attachments with missing files yellow
|
||||
if(!$this->attachmentHelper->isFileExisting($context)){
|
||||
//Mark attachments yellow which have an internal file linked that doesn't exist
|
||||
if($context->hasInternal() && !$this->attachmentHelper->isInternalFileExisting($context)){
|
||||
return 'table-warning';
|
||||
}
|
||||
|
||||
|
@ -64,8 +64,8 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
'className' => 'no-colvis',
|
||||
'render' => function ($value, Attachment $context): string {
|
||||
if ($context->isPicture()
|
||||
&& !$context->isExternal()
|
||||
&& $this->attachmentHelper->isFileExisting($context)) {
|
||||
&& $this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
|
||||
$title = htmlspecialchars($context->getName());
|
||||
if ($context->getFilename()) {
|
||||
$title .= ' ('.htmlspecialchars($context->getFilename()).')';
|
||||
|
@ -93,26 +93,6 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
$dataTable->add('name', TextColumn::class, [
|
||||
'label' => 'attachment.edit.name',
|
||||
'orderField' => 'NATSORT(attachment.name)',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
//Link to external source
|
||||
if ($context->isExternal()) {
|
||||
return sprintf(
|
||||
'<a href="%s" class="link-external">%s</a>',
|
||||
htmlspecialchars((string) $context->getURL()),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->attachmentHelper->isFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
||||
$this->entityURLGenerator->viewURL($context),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
]);
|
||||
|
||||
$dataTable->add('attachment_type', TextColumn::class, [
|
||||
|
@ -136,25 +116,60 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
),
|
||||
]);
|
||||
|
||||
$dataTable->add('filename', TextColumn::class, [
|
||||
'label' => $this->translator->trans('attachment.table.filename'),
|
||||
$dataTable->add('internal_link', TextColumn::class, [
|
||||
'label' => 'attachment.table.internal_file',
|
||||
'propertyPath' => 'filename',
|
||||
'orderField' => 'NATSORT(attachment.original_filename)',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
|
||||
$this->entityURLGenerator->viewURL($context),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
]);
|
||||
|
||||
$dataTable->add('external_link', TextColumn::class, [
|
||||
'label' => 'attachment.table.external_link',
|
||||
'propertyPath' => 'host',
|
||||
'orderField' => 'attachment.external_path',
|
||||
'render' => function ($value, Attachment $context) {
|
||||
if ($context->hasExternal()) {
|
||||
return sprintf(
|
||||
'<a href="%s" class="link-external" title="%s" target="_blank" rel="noopener">%s</a>',
|
||||
htmlspecialchars((string) $context->getExternalPath()),
|
||||
htmlspecialchars((string) $context->getExternalPath()),
|
||||
htmlspecialchars($value),
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
]);
|
||||
|
||||
$dataTable->add('filesize', TextColumn::class, [
|
||||
'label' => $this->translator->trans('attachment.table.filesize'),
|
||||
'render' => function ($value, Attachment $context) {
|
||||
if ($context->isExternal()) {
|
||||
if (!$context->hasInternal()) {
|
||||
return sprintf(
|
||||
'<span class="badge bg-primary">
|
||||
<i class="fas fa-globe fa-fw"></i>%s
|
||||
</span>',
|
||||
$this->translator->trans('attachment.external')
|
||||
$this->translator->trans('attachment.external_only')
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->attachmentHelper->isFileExisting($context)) {
|
||||
return $this->attachmentHelper->getHumanFileSize($context);
|
||||
if ($this->attachmentHelper->isInternalFileExisting($context)) {
|
||||
return sprintf(
|
||||
'<span class="badge bg-secondary">
|
||||
<i class="fas fa-hdd fa-fw"></i> %s
|
||||
</span>',
|
||||
$this->attachmentHelper->getHumanFileSize($context)
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
|
|
|
@ -45,6 +45,9 @@ class AttachmentFilter implements FilterInterface
|
|||
public readonly DateTimeConstraint $lastModified;
|
||||
public readonly DateTimeConstraint $addedDate;
|
||||
|
||||
public readonly TextConstraint $originalFileName;
|
||||
public readonly TextConstraint $externalLink;
|
||||
|
||||
|
||||
public function __construct(NodesListBuilder $nodesListBuilder)
|
||||
{
|
||||
|
@ -55,6 +58,9 @@ class AttachmentFilter implements FilterInterface
|
|||
$this->lastModified = new DateTimeConstraint('attachment.lastModified');
|
||||
$this->addedDate = new DateTimeConstraint('attachment.addedDate');
|
||||
$this->showInTable = new BooleanConstraint('attachment.show_in_table');
|
||||
$this->originalFileName = new TextConstraint('attachment.original_filename');
|
||||
$this->externalLink = new TextConstraint('attachment.external_path');
|
||||
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
|
|
|
@ -45,7 +45,7 @@ abstract class AbstractConstraint implements FilterInterface
|
|||
* @var string The property where this BooleanConstraint should apply to
|
||||
*/
|
||||
protected string $property,
|
||||
string $identifier = null)
|
||||
?string $identifier = null)
|
||||
{
|
||||
$this->identifier = $identifier ?? $this->generateParameterIdentifier($property);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class BooleanConstraint extends AbstractConstraint
|
|||
{
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
/** @var bool|null The value of our constraint */
|
||||
protected ?bool $value = null
|
||||
)
|
||||
|
|
|
@ -34,7 +34,7 @@ class DateTimeConstraint extends AbstractConstraint
|
|||
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
|
|
|
@ -46,7 +46,7 @@ class EntityConstraint extends AbstractConstraint
|
|||
public function __construct(protected ?NodesListBuilder $nodesListBuilder,
|
||||
protected string $class,
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
protected ?AbstractDBElement $value = null,
|
||||
protected ?string $operator = null)
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ class NumberConstraint extends AbstractConstraint
|
|||
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
?string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\QueryBuilder;
|
|||
|
||||
class LessThanDesiredConstraint extends BooleanConstraint
|
||||
{
|
||||
public function __construct(string $property = null, string $identifier = null, ?bool $default_value = null)
|
||||
public function __construct(?string $property = null, ?string $identifier = null, ?bool $default_value = null)
|
||||
{
|
||||
parent::__construct($property ?? '(
|
||||
SELECT COALESCE(SUM(ld_partLot.amount), 0.0)
|
||||
|
|
|
@ -30,7 +30,7 @@ class TagsConstraint extends AbstractConstraint
|
|||
{
|
||||
final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE'];
|
||||
|
||||
public function __construct(string $property, string $identifier = null,
|
||||
public function __construct(string $property, ?string $identifier = null,
|
||||
protected ?string $value = null,
|
||||
protected ?string $operator = '')
|
||||
{
|
||||
|
@ -93,10 +93,10 @@ class TagsConstraint extends AbstractConstraint
|
|||
$expr = $queryBuilder->expr();
|
||||
|
||||
$tmp = $expr->orX(
|
||||
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'),
|
||||
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'),
|
||||
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'),
|
||||
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'),
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_1) = TRUE',
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_2) = TRUE',
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_3) = TRUE',
|
||||
'ILIKE(' . $this->property . ', :' . $tag_identifier_prefix . '_4) = TRUE',
|
||||
);
|
||||
|
||||
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
|
||||
|
@ -133,6 +133,7 @@ class TagsConstraint extends AbstractConstraint
|
|||
return;
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line Keep this check to ensure that everything has the same structure even if we add a new operator
|
||||
if ($this->operator === 'NONE') {
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions)));
|
||||
return;
|
||||
|
|
|
@ -32,7 +32,7 @@ class TextConstraint extends AbstractConstraint
|
|||
/**
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct(string $property, string $identifier = null, /**
|
||||
public function __construct(string $property, ?string $identifier = null, /**
|
||||
* @var string|null The value to compare to
|
||||
*/
|
||||
protected ?string $value = null, /**
|
||||
|
@ -107,7 +107,8 @@ class TextConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
if ($like_value !== null) {
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'LIKE', $like_value);
|
||||
$queryBuilder->andWhere(sprintf('ILIKE(%s, :%s) = TRUE', $this->property, $this->identifier));
|
||||
$queryBuilder->setParameter($this->identifier, $like_value);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ declare(strict_types=1);
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace App\DataTables\Filters;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class PartSearchFilter implements FilterInterface
|
||||
|
@ -132,15 +131,15 @@ class PartSearchFilter implements FilterInterface
|
|||
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
|
||||
}
|
||||
|
||||
return sprintf("%s LIKE :search_query", $field);
|
||||
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
|
||||
}, $fields_to_search);
|
||||
|
||||
//Add Or concatation of the expressions to our query
|
||||
//Add Or concatenation of the expressions to our query
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->orX(...$expressions)
|
||||
);
|
||||
|
||||
//For regex we pass the query as is, for like we add % to the start and end as wildcards
|
||||
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
|
||||
if ($this->regex) {
|
||||
$queryBuilder->setParameter('search_query', $this->keyword);
|
||||
} else {
|
||||
|
|
|
@ -57,6 +57,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||
|
||||
final class PartsDataTable implements DataTableTypeInterface
|
||||
{
|
||||
const LENGTH_MENU = [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]];
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityURLGenerator $urlGenerator,
|
||||
private readonly TranslatorInterface $translator,
|
||||
|
|
|
@ -87,16 +87,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
if(!$context->getPart() instanceof Part) {
|
||||
return htmlspecialchars((string) $context->getName());
|
||||
}
|
||||
if($context->getPart() instanceof Part) {
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
throw new \RuntimeException('This should never happen!');
|
||||
//Part exists if we reach this point
|
||||
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
if($context->getName() !== null && $context->getName() !== '') {
|
||||
$tmp .= '<br><b>'.htmlspecialchars($context->getName()).'</b>';
|
||||
}
|
||||
return $tmp;
|
||||
},
|
||||
])
|
||||
->add('ipn', TextColumn::class, [
|
||||
|
|
71
src/Doctrine/Functions/ILike.php
Normal file
71
src/Doctrine/Functions/ILike.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Doctrine\Functions;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
/**
|
||||
* A platform invariant version of the case-insensitive LIKE operation.
|
||||
* On MySQL and SQLite this is the normal LIKE, but on PostgreSQL it is the ILIKE operator.
|
||||
*/
|
||||
class ILike extends FunctionNode
|
||||
{
|
||||
|
||||
public $value = null;
|
||||
|
||||
public $expr = null;
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
$this->value = $parser->StringPrimary();
|
||||
$parser->match(TokenType::T_COMMA);
|
||||
$this->expr = $parser->StringExpression();
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
||||
|
||||
//
|
||||
if ($platform instanceof AbstractMySQLPlatform || $platform instanceof SQLitePlatform) {
|
||||
$operator = 'LIKE';
|
||||
} elseif ($platform instanceof PostgreSQLPlatform) {
|
||||
//Use the case-insensitive operator, to have the same behavior as MySQL
|
||||
$operator = 'ILIKE';
|
||||
} else {
|
||||
throw new \RuntimeException('Platform ' . gettype($platform) . ' does not support case insensitive like expressions.');
|
||||
}
|
||||
|
||||
return '(' . $this->value->dispatch($sqlWalker) . ' ' . $operator . ' ' . $this->expr->dispatch($sqlWalker) . ')';
|
||||
}
|
||||
}
|
|
@ -44,15 +44,13 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
|
|||
$native_connection = $connection->getNativeConnection();
|
||||
|
||||
//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' )) {
|
||||
if($native_connection instanceof \PDO) {
|
||||
$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);
|
||||
|
||||
//Create a new collation for natural sorting
|
||||
if (method_exists($native_connection, 'sqliteCreateCollation')) {
|
||||
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
||||
}
|
||||
$native_connection->sqliteCreateCollation('NATURAL_CMP', strnatcmp(...));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class DoNotUsePurgerFactory implements PurgerFactory
|
|||
throw new \LogicException('Do not use doctrine:fixtures:load directly. Use partdb:fixtures:load instead!');
|
||||
}
|
||||
|
||||
public function setEntityManager(EntityManagerInterface $em)
|
||||
public function setEntityManager(EntityManagerInterface $em): void
|
||||
{
|
||||
// TODO: Implement setEntityManager() method.
|
||||
}
|
||||
|
|
|
@ -33,23 +33,24 @@ use ApiPlatform\Metadata\Get;
|
|||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\ApiPlatform\DocumentedAPIProperty;
|
||||
use App\ApiPlatform\DocumentedAPIProperties\DocumentedAPIProperty;
|
||||
use App\ApiPlatform\Filter\EntityFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\ApiPlatform\HandleAttachmentsUploadsProcessor;
|
||||
use App\Repository\AttachmentRepository;
|
||||
use App\EntityListeners\AttachmentDeleteListener;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\EntityListeners\AttachmentDeleteListener;
|
||||
use App\Repository\AttachmentRepository;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
use function in_array;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Class Attachment.
|
||||
|
@ -78,11 +79,16 @@ use LogicException;
|
|||
denormalizationContext: ['groups' => ['attachment:write', 'attachment:write:standalone', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||
processor: HandleAttachmentsUploadsProcessor::class,
|
||||
)]
|
||||
#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'media_url', type: 'string', nullable: true,
|
||||
description: 'The URL to the file, where the attachment file can be downloaded. This can be an internal or external URL.',
|
||||
example: '/media/part/2/bc547-6508afa5a79c8.pdf')]
|
||||
#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true,
|
||||
description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.')]
|
||||
//This property is added by the denormalizer in order to resolve the placeholder
|
||||
#[DocumentedAPIProperty(
|
||||
schemaName: 'Attachment-Read', property: 'internal_path', type: 'string', nullable: false,
|
||||
description: 'The URL to the internally saved copy of the file, if one exists',
|
||||
example: '/media/part/2/bc547-6508afa5a79c8.pdf'
|
||||
)]
|
||||
#[DocumentedAPIProperty(
|
||||
schemaName: 'Attachment-Read', property: 'thumbnail_url', type: 'string', nullable: true,
|
||||
description: 'The URL to a thumbnail version of this file. This only exists for internal picture attachments.'
|
||||
)]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name"])]
|
||||
#[ApiFilter(EntityFilter::class, properties: ["attachment_type"])]
|
||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||
|
@ -91,8 +97,8 @@ use LogicException;
|
|||
#[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)]
|
||||
abstract class Attachment extends AbstractNamedDBElement
|
||||
{
|
||||
private const ORM_DISCRIMINATOR_MAP = ['PartDB\Part' => PartAttachment::class, 'Part' => PartAttachment::class,
|
||||
'PartDB\Device' => ProjectAttachment::class, 'Device' => ProjectAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class,
|
||||
private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'Device' => ProjectAttachment::class,
|
||||
'AttachmentType' => AttachmentTypeAttachment::class,
|
||||
'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class,
|
||||
'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class,
|
||||
'Storelocation' => StorageLocationAttachment::class, 'Supplier' => SupplierAttachment::class,
|
||||
|
@ -119,10 +125,6 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
*/
|
||||
final public const MODEL_EXTS = ['x3d'];
|
||||
|
||||
/**
|
||||
* When the path begins with one of the placeholders.
|
||||
*/
|
||||
final public const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%SECURE%'];
|
||||
|
||||
/**
|
||||
* @var array placeholders for attachments which using built in files
|
||||
|
@ -152,10 +154,21 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
protected ?string $original_filename = null;
|
||||
|
||||
/**
|
||||
* @var string The path to the file relative to a placeholder path like %MEDIA%
|
||||
* @var string|null If a copy of the file is stored internally, the path to the file relative to a placeholder
|
||||
* path like %MEDIA%
|
||||
*/
|
||||
#[ORM\Column(name: 'path', type: Types::STRING)]
|
||||
protected string $path = '';
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
protected ?string $internal_path = null;
|
||||
|
||||
|
||||
/**
|
||||
* @var string|null The path to the external source if the file is stored externally or was downloaded from an
|
||||
* external source. Null if there is no external source.
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['attachment:read'])]
|
||||
#[ApiProperty(example: 'http://example.com/image.jpg')]
|
||||
protected ?string $external_path = null;
|
||||
|
||||
/**
|
||||
* @var string the name of this element
|
||||
|
@ -237,7 +250,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
|
||||
/**
|
||||
* Check if this attachment is a picture (analyse the file's extension).
|
||||
* If the link is external, it is assumed that this is true.
|
||||
* If the link is only external and doesn't contain an extension, it is assumed that this is true.
|
||||
*
|
||||
* @return bool * true if the file extension is a picture extension
|
||||
* * otherwise false
|
||||
|
@ -245,54 +258,67 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
#[Groups(['attachment:read'])]
|
||||
public function isPicture(): bool
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
if($this->hasInternal()){
|
||||
|
||||
$extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION);
|
||||
|
||||
return in_array(strtolower($extension), static::PICTURE_EXTS, true);
|
||||
|
||||
}
|
||||
if ($this->hasExternal()) {
|
||||
//Check if we can extract a file extension from the URL
|
||||
$extension = pathinfo(parse_url($this->path, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
|
||||
$extension = pathinfo(parse_url($this->getExternalPath(), PHP_URL_PATH) ?? '', PATHINFO_EXTENSION);
|
||||
|
||||
//If no extension is found or it is known picture extension, we assume that this is a picture extension
|
||||
return $extension === '' || in_array(strtolower($extension), static::PICTURE_EXTS, true);
|
||||
}
|
||||
|
||||
$extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
|
||||
|
||||
return in_array(strtolower($extension), static::PICTURE_EXTS, true);
|
||||
//File doesn't have an internal, nor an external copy. This shouldn't happen, but it certainly isn't a picture...
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this attachment is a 3D model and therefore can be directly shown to user.
|
||||
* If the attachment is external, false is returned (3D Models must be internal).
|
||||
* If no internal copy exists, false is returned (3D Models must be internal).
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
#[SerializedName('3d_model')]
|
||||
public function is3DModel(): bool
|
||||
{
|
||||
//We just assume that 3D Models are internally saved, otherwise we get problems loading them.
|
||||
if ($this->isExternal()) {
|
||||
if (!$this->hasInternal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
|
||||
$extension = pathinfo($this->getInternalPath(), PATHINFO_EXTENSION);
|
||||
|
||||
return in_array(strtolower($extension), static::MODEL_EXTS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the attachment file is externally saved (the database saves an URL).
|
||||
* Checks if this attachment has a path to an external file
|
||||
*
|
||||
* @return bool true, if the file is saved externally
|
||||
* @return bool true, if there is a path to an external file
|
||||
* @phpstan-assert-if-true non-empty-string $this->external_path
|
||||
* @phpstan-assert-if-true non-empty-string $this->getExternalPath())
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
public function isExternal(): bool
|
||||
public function hasExternal(): bool
|
||||
{
|
||||
//When path is empty, this attachment can not be external
|
||||
if ($this->path === '') {
|
||||
return false;
|
||||
}
|
||||
return $this->external_path !== null && $this->external_path !== '';
|
||||
}
|
||||
|
||||
//After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
|
||||
$tmp = explode('/', $this->path);
|
||||
|
||||
return !in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), true);
|
||||
/**
|
||||
* Checks if this attachment has a path to an internal file.
|
||||
* Does not check if the file exists.
|
||||
*
|
||||
* @return bool true, if there is a path to an internal file
|
||||
* @phpstan-assert-if-true non-empty-string $this->internal_path
|
||||
* @phpstan-assert-if-true non-empty-string $this->getInternalPath())
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
public function hasInternal(): bool
|
||||
{
|
||||
return $this->internal_path !== null && $this->internal_path !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -305,8 +331,12 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
#[SerializedName('private')]
|
||||
public function isSecure(): bool
|
||||
{
|
||||
if ($this->internal_path === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
|
||||
$tmp = explode('/', $this->path);
|
||||
$tmp = explode('/', $this->internal_path);
|
||||
|
||||
return '%SECURE%' === $tmp[0];
|
||||
}
|
||||
|
@ -320,7 +350,11 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
#[Groups(['attachment:read'])]
|
||||
public function isBuiltIn(): bool
|
||||
{
|
||||
return static::checkIfBuiltin($this->path);
|
||||
if ($this->internal_path === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return static::checkIfBuiltin($this->internal_path);
|
||||
}
|
||||
|
||||
/********************************************************************************
|
||||
|
@ -332,13 +366,13 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
/**
|
||||
* Returns the extension of the file referenced via the attachment.
|
||||
* For a path like %BASE/path/foo.bar, bar will be returned.
|
||||
* If this attachment is external null is returned.
|
||||
* If this attachment is only external null is returned.
|
||||
*
|
||||
* @return string|null the file extension in lower case
|
||||
*/
|
||||
public function getExtension(): ?string
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
if (!$this->hasInternal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -346,7 +380,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION));
|
||||
return strtolower(pathinfo($this->getInternalPath(), PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -361,52 +395,54 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
|
||||
/**
|
||||
* The URL to the external file, or the path to the built-in file.
|
||||
* The URL to the external file, or the path to the built-in file, but not paths to uploaded files.
|
||||
* Returns null, if the file is not external (and not builtin).
|
||||
* The output of this function is such, that no changes occur when it is fed back into setURL().
|
||||
* Required for the Attachment form field.
|
||||
*/
|
||||
#[Groups(['attachment:read'])]
|
||||
#[SerializedName('url')]
|
||||
public function getURL(): ?string
|
||||
{
|
||||
if (!$this->isExternal() && !$this->isBuiltIn()) {
|
||||
return null;
|
||||
if($this->hasExternal()){
|
||||
return $this->getExternalPath();
|
||||
}
|
||||
|
||||
return $this->path;
|
||||
if($this->isBuiltIn()){
|
||||
return $this->getInternalPath();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hostname where the external file is stored.
|
||||
* Returns null, if the file is not external.
|
||||
* Returns null, if there is no external path.
|
||||
*/
|
||||
public function getHost(): ?string
|
||||
{
|
||||
if (!$this->isExternal()) {
|
||||
if (!$this->hasExternal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parse_url((string) $this->getURL(), PHP_URL_HOST);
|
||||
return parse_url($this->getExternalPath(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filepath, relative to %BASE%.
|
||||
*
|
||||
* @return string A string like %BASE/path/foo.bar
|
||||
*/
|
||||
public function getPath(): string
|
||||
public function getInternalPath(): ?string
|
||||
{
|
||||
return $this->path;
|
||||
return $this->internal_path;
|
||||
}
|
||||
|
||||
public function getExternalPath(): ?string
|
||||
{
|
||||
return $this->external_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename of the attachment.
|
||||
* For a path like %BASE/path/foo.bar, foo.bar will be returned.
|
||||
*
|
||||
* If the path is a URL (can be checked via isExternal()), null will be returned.
|
||||
* If there is no internal copy of the file, null will be returned.
|
||||
*/
|
||||
public function getFilename(): ?string
|
||||
{
|
||||
if ($this->isExternal()) {
|
||||
if (!$this->hasInternal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -415,7 +451,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
return $this->original_filename;
|
||||
}
|
||||
|
||||
return pathinfo($this->getPath(), PATHINFO_BASENAME);
|
||||
return pathinfo($this->getInternalPath(), PATHINFO_BASENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -488,15 +524,12 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the filepath (with relative placeholder) for this attachment.
|
||||
*
|
||||
* @param string $path the new filepath of the attachment
|
||||
*
|
||||
* @return Attachment
|
||||
* Sets the path to a file hosted internally. If you set this path to a file that was not downloaded from the
|
||||
* external source in external_path, make sure to reset external_path.
|
||||
*/
|
||||
public function setPath(string $path): self
|
||||
public function setInternalPath(?string $internal_path): self
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->internal_path = $internal_path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -512,34 +545,60 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the url associated with this attachment.
|
||||
* If the url is empty nothing is changed, to not override the file path.
|
||||
*
|
||||
* @return Attachment
|
||||
* Sets up the paths using a user provided string which might contain an external path or a builtin path. Allows
|
||||
* resetting the external path if an internal path exists. Resets any other paths if a (nonempty) new path is set.
|
||||
*/
|
||||
#[Groups(['attachment:write'])]
|
||||
#[SerializedName('url')]
|
||||
#[ApiProperty(description: 'Set the path of the attachment here.
|
||||
Provide either an external URL, a path to a builtin file (like %FOOTPRINTS%/Active/ICs/IC_DFS.png) or an empty
|
||||
string if the attachment has an internal file associated and you\'d like to reset the external source.
|
||||
If you set a new (nonempty) file path any associated internal file will be removed!')]
|
||||
public function setURL(?string $url): self
|
||||
{
|
||||
//Do nothing if the URL is empty
|
||||
if ($url === null || $url === '') {
|
||||
//Don't allow the user to set an empty external path if the internal path is empty already
|
||||
if (($url === null || $url === "") && !$this->hasInternal()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$url = trim($url);
|
||||
//Escape spaces in URL
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
//Only set if the URL is not empty
|
||||
if ($url !== null && $url !== '') {
|
||||
if (str_contains($url, '%BASE%') || str_contains($url, '%MEDIA%')) {
|
||||
throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
|
||||
}
|
||||
|
||||
$this->path = $url;
|
||||
//Reset internal filename
|
||||
$this->original_filename = null;
|
||||
//The URL field can also contain the special builtin internal paths, so we need to distinguish here
|
||||
if ($this::checkIfBuiltin($url)) {
|
||||
$this->setInternalPath($url);
|
||||
//make sure the external path isn't still pointing to something unrelated
|
||||
$this->setExternalPath(null);
|
||||
} else {
|
||||
$this->setExternalPath($url);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the path to a file hosted on an external server. Setting the external path to a (nonempty) value different
|
||||
* from the the old one _clears_ the internal path, so that the external path reflects where any associated internal
|
||||
* file came from.
|
||||
*/
|
||||
public function setExternalPath(?string $external_path): self
|
||||
{
|
||||
//If we only clear the external path, don't reset the internal path, since that could be confusing
|
||||
if($external_path === null || $external_path === '') {
|
||||
$this->external_path = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$external_path = trim($external_path);
|
||||
//Escape spaces in URL
|
||||
$external_path = str_replace(' ', '%20', $external_path);
|
||||
|
||||
if($this->external_path === $external_path) {
|
||||
//Nothing changed, nothing to do
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->external_path = $external_path;
|
||||
$this->internal_path = null;
|
||||
//Reset internal filename
|
||||
$this->original_filename = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -551,12 +610,17 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
/**
|
||||
* Checks if the given path is a path to a builtin resource.
|
||||
*
|
||||
* @param string $path The path that should be checked
|
||||
* @param string|null $path The path that should be checked
|
||||
*
|
||||
* @return bool true if the path is pointing to a builtin resource
|
||||
*/
|
||||
public static function checkIfBuiltin(string $path): bool
|
||||
public static function checkIfBuiltin(?string $path): bool
|
||||
{
|
||||
//An empty path can't be a builtin
|
||||
if ($path === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
|
||||
$tmp = explode('/', $path);
|
||||
//Builtins must have a %PLACEHOLDER% construction
|
||||
|
|
|
@ -33,7 +33,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
/**
|
||||
* @template-covariant AT of Attachment
|
||||
* @template AT of Attachment
|
||||
*/
|
||||
#[ORM\MappedSuperclass(repositoryClass: AttachmentContainingDBElementRepository::class)]
|
||||
abstract class AttachmentContainingDBElement extends AbstractNamedDBElement implements HasMasterAttachmentInterface, HasAttachmentsInterface
|
||||
|
|
|
@ -33,8 +33,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
/**
|
||||
* This abstract class is used for companies like suppliers or manufacturers.
|
||||
*
|
||||
* @template-covariant AT of Attachment
|
||||
* @template-covariant PT of AbstractParameter
|
||||
* @template AT of Attachment
|
||||
* @template PT of AbstractParameter
|
||||
* @extends AbstractPartsContainingDBElement<AT, PT>
|
||||
*/
|
||||
#[ORM\MappedSuperclass]
|
||||
|
@ -162,7 +162,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
*
|
||||
* @return string the link to the article
|
||||
*/
|
||||
public function getAutoProductUrl(string $partnr = null): string
|
||||
public function getAutoProductUrl(?string $partnr = null): string
|
||||
{
|
||||
if (is_string($partnr)) {
|
||||
return str_replace('%PARTNUMBER%', $partnr, $this->auto_product_url);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue