mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-27 20:15:40 +01:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0b6b10c27b | ||
|
|
6225d2c9b3 | ||
|
|
01fc6524a4 | ||
|
|
2575e6a160 | ||
|
|
484ba5ebd7 | ||
|
|
b42d98e9f8 | ||
|
|
65b2f045ac | ||
|
|
5e76451d46 | ||
|
|
a873ad3316 | ||
|
|
b1e03f49ee | ||
|
|
011e23f8e6 | ||
|
|
646cd8cf22 | ||
|
|
52ac8a70d5 | ||
|
|
e020334b73 | ||
|
|
7698e83f0b | ||
|
|
dd56f5e0c8 | ||
|
|
92c32eef74 |
@@ -43,6 +43,7 @@
|
||||
PassEnv PROVIDER_OCTOPART_CLIENT_ID PROVIDER_OCTOPART_SECRET PROVIDER_OCTOPART_CURRENCY PROVIDER_OCTOPART_COUNTRY PROVIDER_OCTOPART_SEARCH_LIMIT PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS
|
||||
PassEnv PROVIDER_MOUSER_KEY PROVIDER_MOUSER_SEARCH_OPTION PROVIDER_MOUSER_SEARCH_LIMIT PROVIDER_MOUSER_SEARCH_WITH_SIGNUP_LANGUAGE
|
||||
PassEnv 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 EDA_KICAD_CATEGORY_DEPTH
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
|
||||
3
.env
3
.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:
|
||||
|
||||
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
|
||||
|
||||
@@ -53,6 +53,7 @@ export default class extends Controller {
|
||||
|
||||
const config = {
|
||||
language: language,
|
||||
licenseKey: "GPL",
|
||||
}
|
||||
|
||||
const watchdog = new EditorWatchdog();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -108,8 +108,8 @@ body {
|
||||
.back-to-top {
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
bottom: 60px;
|
||||
right: 40px;
|
||||
display:none;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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": {
|
||||
@@ -16,7 +17,7 @@
|
||||
"brick/math": "0.12.1 as 0.11.0",
|
||||
"composer/ca-bundle": "^1.3",
|
||||
"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,9 @@
|
||||
"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",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^6.8.0",
|
||||
@@ -69,7 +67,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,21 +88,20 @@
|
||||
"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.*",
|
||||
|
||||
2495
composer.lock
generated
2495
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,14 @@ 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-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pif>>"
|
||||
dom: " <'row'<'col mb-2 input-group' B l> <'col mb-2' <'pull-end' p>>>
|
||||
<'card'
|
||||
rt
|
||||
<'card-footer card-footer-table text-muted' i >
|
||||
>
|
||||
<'row'<'col mt-2 input-group' B l> <'col mt-2' <'pull-right' p>>>"
|
||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
||||
<'card'
|
||||
rt
|
||||
<'card-footer card-footer-table text-muted' i >
|
||||
>
|
||||
<'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>"
|
||||
pagingType: 'simple_numbers'
|
||||
searching: true
|
||||
stateSave: true
|
||||
|
||||
@@ -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:'
|
||||
|
||||
@@ -11,7 +11,7 @@ parameters:
|
||||
partdb.banner: '%env(trim:string:BANNER)%' # The info text shown in the homepage, if empty config/banner.md is used
|
||||
partdb.default_currency: '%env(string:BASE_CURRENCY)%' # The currency that is used inside the DB (and is assumed when no currency is set). This can not be changed later, so be sure to set it the currency used in your country
|
||||
partdb.global_theme: '' # The theme to use globally (see public/build/themes/ for choices, use name without .css). Set to '' for default bootstrap theme
|
||||
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh'] # The languages that are shown in user drop down menu
|
||||
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu
|
||||
partdb.enforce_change_comments_for: '%env(csv:ENFORCE_CHANGE_COMMENTS_FOR)%' # The actions for which a change comment is required (e.g. "part_edit", "part_create", etc.). If this is empty, change comments are not required at all.
|
||||
|
||||
partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails
|
||||
|
||||
@@ -23,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.
|
||||
@@ -239,4 +239,4 @@ replaced with data for the actual thing.
|
||||
|
||||
You do not have to define a label profile to generate labels (you can just set the settings on the fly in the label
|
||||
dialog), however, if you want to generate many labels, it is recommended to save the settings as a label profile, to save
|
||||
it for later usage. This ensures that all generated labels look the same.
|
||||
it for later usage. This ensures that all generated labels look the same.
|
||||
|
||||
@@ -6,4 +6,6 @@ has_children: true
|
||||
---
|
||||
|
||||
# Installation
|
||||
Below you can find some guides to install Part-DB.
|
||||
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.
|
||||
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).
|
||||
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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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) |
|
||||
|
||||
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
|
||||
@@ -62,4 +57,7 @@ parameters:
|
||||
- '#Part::getParameters\(\) should return .*AbstractParameter#'
|
||||
|
||||
# Ignore doctrine type mapping mismatch
|
||||
- '#Property .* type mapping mismatch: property can contain .* but database expects .*#'
|
||||
- '#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#'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
102
src/ApiPlatform/Filter/TagFilter.php
Normal file
102
src/ApiPlatform/Filter/TagFilter.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform\Filter;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Due to their nature, tags are stored in a single string, separated by commas, which requires some more complex search logic.
|
||||
* This filter allows to easily search for tags in a part entity.
|
||||
*/
|
||||
final class TagFilter extends AbstractFilter
|
||||
{
|
||||
|
||||
protected function filterProperty(
|
||||
string $property,
|
||||
$value,
|
||||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = []
|
||||
): void {
|
||||
// Ignore filter if property is not enabled or mapped
|
||||
if (
|
||||
!$this->isPropertyEnabled($property, $resourceClass) ||
|
||||
!$this->isPropertyMapped($property, $resourceClass)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Escape any %, _ or \ in the tag
|
||||
$value = addcslashes($value, '%_\\');
|
||||
|
||||
$tag_identifier_prefix = $queryNameGenerator->generateParameterName($property);
|
||||
|
||||
$expr = $queryBuilder->expr();
|
||||
|
||||
$tmp = $expr->orX(
|
||||
'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);
|
||||
|
||||
//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)
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $value . ',%');
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $value);
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_3', $value . ',%');
|
||||
$queryBuilder->setParameter($tag_identifier_prefix . '_4', $value);
|
||||
}
|
||||
|
||||
public function getDescription(string $resourceClass): array
|
||||
{
|
||||
if (!$this->properties) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$description = [];
|
||||
foreach (array_keys($this->properties) as $property) {
|
||||
$description[(string)$property] = [
|
||||
'property' => $property,
|
||||
'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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -83,6 +83,19 @@ class SetPasswordCommand extends Command
|
||||
|
||||
while (!$success) {
|
||||
$pw1 = $io->askHidden('Please enter new password:');
|
||||
|
||||
if ($pw1 === null) {
|
||||
$io->error('No password entered! Please try again.');
|
||||
|
||||
//If we are in non-interactive mode, we can not ask again
|
||||
if (!$input->isInteractive()) {
|
||||
$io->warning('Non-interactive mode detected. No password can be entered that way! If you are using docker exec, please use -it flag.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$pw2 = $io->askHidden('Please confirm:');
|
||||
if ($pw1 !== $pw2) {
|
||||
$io->error('The entered password did not match! Please try again.');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,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');
|
||||
@@ -80,7 +81,7 @@ class PartListsController extends AbstractController
|
||||
}
|
||||
|
||||
//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 +132,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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -85,15 +85,18 @@ class TagsConstraint extends AbstractConstraint
|
||||
*/
|
||||
protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Orx
|
||||
{
|
||||
//Escape any %, _ or \ in the tag
|
||||
$tag = addcslashes($tag, '%_\\');
|
||||
|
||||
$tag_identifier_prefix = uniqid($this->identifier . '_', false);
|
||||
|
||||
$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)
|
||||
@@ -130,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;
|
||||
|
||||
@@ -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,
|
||||
@@ -137,7 +139,8 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||
])
|
||||
->add('storelocation', TextColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.storeLocations'),
|
||||
'orderField' => 'NATSORT(_storelocations.name)',
|
||||
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
|
||||
'orderField' => 'NATSORT(MIN(_storelocations.name))',
|
||||
'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
|
||||
], alias: 'storage_location')
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
||||
$url = str_replace(' ', '%20', $url);
|
||||
|
||||
//Only set if the URL is not empty
|
||||
if ($url !== null && $url !== '') {
|
||||
if ($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!');
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -31,8 +31,8 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
/**
|
||||
* @template-covariant AT of Attachment
|
||||
* @template-covariant PT of AbstractParameter
|
||||
* @template AT of Attachment
|
||||
* @template PT of AbstractParameter
|
||||
* @extends AbstractStructuralDBElement<AT, PT>
|
||||
*/
|
||||
#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)]
|
||||
|
||||
@@ -53,8 +53,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
*
|
||||
* @see \App\Tests\Entity\Base\AbstractStructuralDBElementTest
|
||||
*
|
||||
* @template-covariant AT of Attachment
|
||||
* @template-covariant PT of AbstractParameter
|
||||
* @template AT of Attachment
|
||||
* @template PT of AbstractParameter
|
||||
* @template-use ParametersTrait<PT>
|
||||
* @extends AttachmentContainingDBElement<AT>
|
||||
* @uses ParametersTrait<PT>
|
||||
|
||||
@@ -22,6 +22,8 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
@@ -34,7 +36,7 @@ enum LabelSupportedElement: string
|
||||
|
||||
/**
|
||||
* Returns the entity class for the given element type
|
||||
* @return string
|
||||
* @return class-string<AbstractDBElement>
|
||||
*/
|
||||
public function getEntityClass(): string
|
||||
{
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace App\Entity\LogSystem;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Events\SecurityEvents;
|
||||
use App\Helpers\IPAnonymizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
/**
|
||||
* This log entry is created when something security related to a user happens.
|
||||
@@ -127,14 +127,14 @@ class SecurityEventLogEntry extends AbstractLogEntry
|
||||
* Sets the IP address used to log in the user.
|
||||
*
|
||||
* @param string $ip the IP address used to log in the user
|
||||
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
|
||||
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIPAddress(string $ip, bool $anonymize = true): self
|
||||
{
|
||||
if ($anonymize) {
|
||||
$ip = IpUtils::anonymize($ip);
|
||||
$ip = IPAnonymizer::anonymize($ip);
|
||||
}
|
||||
$this->extra['i'] = $ip;
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Helpers\IPAnonymizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
|
||||
/**
|
||||
* This log entry is created when a user logs in.
|
||||
@@ -52,14 +53,14 @@ class UserLoginLogEntry extends AbstractLogEntry
|
||||
* Sets the IP address used to log in the user.
|
||||
*
|
||||
* @param string $ip the IP address used to log in the user
|
||||
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
|
||||
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIPAddress(string $ip, bool $anonymize = true): self
|
||||
{
|
||||
if ($anonymize) {
|
||||
$ip = IpUtils::anonymize($ip);
|
||||
$ip = IPAnonymizer::anonymize($ip);
|
||||
}
|
||||
|
||||
$this->extra['i'] = $ip;
|
||||
|
||||
@@ -22,8 +22,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Helpers\IPAnonymizer;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
#[ORM\Entity]
|
||||
class UserLogoutLogEntry extends AbstractLogEntry
|
||||
@@ -49,14 +49,14 @@ class UserLogoutLogEntry extends AbstractLogEntry
|
||||
* Sets the IP address used to log in the user.
|
||||
*
|
||||
* @param string $ip the IP address used to log in the user
|
||||
* @param bool $anonymize Anonymize the IP address (remove last block) to be GPDR compliant
|
||||
* @param bool $anonymize Anonymize the IP address (remove last block) to be GDPR compliant
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIPAddress(string $ip, bool $anonymize = true): self
|
||||
{
|
||||
if ($anonymize) {
|
||||
$ip = IpUtils::anonymize($ip);
|
||||
$ip = IPAnonymizer::anonymize($ip);
|
||||
}
|
||||
|
||||
$this->extra['i'] = $ip;
|
||||
|
||||
@@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\Parts;
|
||||
|
||||
use App\ApiPlatform\Filter\TagFilter;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
||||
@@ -97,7 +98,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
#[ApiFilter(EntityFilter::class, properties: ["category", "footprint", "manufacturer", "partUnit"])]
|
||||
#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "tags", "manufacturer_product_number"])]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
|
||||
#[ApiFilter(TagFilter::class, properties: ["tags"])]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])]
|
||||
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
|
||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||
|
||||
@@ -68,7 +68,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
|
||||
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
|
||||
#[ValidPartLot]
|
||||
#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
|
||||
#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: 'is_granted("read", object)'),
|
||||
@@ -166,10 +166,10 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
||||
/**
|
||||
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
|
||||
#[Groups(['part_lot:read', 'part_lot:write'])]
|
||||
#[Length(max: 255)]
|
||||
protected ?string $vendor_barcode = null;
|
||||
protected ?string $user_barcode = null;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
@@ -185,7 +185,6 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
||||
*
|
||||
* @return bool|null True, if the part lot is expired. Returns null, if no expiration date was set.
|
||||
*
|
||||
* @throws Exception If an error with the DateTime occurs
|
||||
*/
|
||||
public function isExpired(): ?bool
|
||||
{
|
||||
@@ -376,19 +375,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
|
||||
* null if no barcode is set.
|
||||
* @return string|null
|
||||
*/
|
||||
public function getVendorBarcode(): ?string
|
||||
public function getUserBarcode(): ?string
|
||||
{
|
||||
return $this->vendor_barcode;
|
||||
return $this->user_barcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor).
|
||||
* @param string|null $vendor_barcode
|
||||
* @param string|null $user_barcode
|
||||
* @return $this
|
||||
*/
|
||||
public function setVendorBarcode(?string $vendor_barcode): PartLot
|
||||
public function setUserBarcode(?string $user_barcode): PartLot
|
||||
{
|
||||
$this->vendor_barcode = $vendor_barcode;
|
||||
$this->user_barcode = $user_barcode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ class Supplier extends AbstractCompany
|
||||
protected ?AbstractStructuralDBElement $parent = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Orderdetail>|Orderdetail[]
|
||||
* @var Collection<int, Orderdetail>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'supplier', targetEntity: Orderdetail::class)]
|
||||
protected Collection $orderdetails;
|
||||
|
||||
@@ -333,7 +333,6 @@ class Project extends AbstractStructuralDBElement
|
||||
{
|
||||
//If this project has subprojects, and these have builds part, they must be included in the BOM
|
||||
foreach ($this->getChildren() as $child) {
|
||||
/** @var $child Project */
|
||||
if (!$child->getBuildPart() instanceof Part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
||||
protected ?string $password = null;
|
||||
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Regex('/^[\w\.\+\-\$]+$/', message: 'user.invalid_username')]
|
||||
#[Assert\Regex('/^[\w\.\+\-\$]+[\w\.\+\-\$\@]*$/', message: 'user.invalid_username')]
|
||||
#[Groups(['user:read'])]
|
||||
protected string $name = '';
|
||||
|
||||
@@ -893,8 +893,6 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
||||
* @param string[] $codes An array containing the backup codes
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws Exception If an error with the datetime occurs
|
||||
*/
|
||||
public function setBackupCodes(array $codes): self
|
||||
{
|
||||
|
||||
@@ -1,57 +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\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
|
||||
|
||||
/**
|
||||
* This class fixes the wrong pathes generated by webpack using the auto publicPath mode.
|
||||
* Basically it replaces the wrong /auto/ part of the path with the correct /build/ in all encore entrypoints.
|
||||
*/
|
||||
class WebpackAutoPathSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
RenderAssetTagEvent::class => 'onRenderAssetTag'
|
||||
];
|
||||
}
|
||||
|
||||
public function onRenderAssetTag(RenderAssetTagEvent $event): void
|
||||
{
|
||||
if ($event->isScriptTag()) {
|
||||
$event->setAttribute('src', $this->resolveAuto($event->getUrl()));
|
||||
}
|
||||
if ($event->isLinkTag()) {
|
||||
$event->setAttribute('href', $this->resolveAuto($event->getUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveAuto(string $path): string
|
||||
{
|
||||
//Replace the first occurence of /auto/ with /build/ to get the correct path
|
||||
return preg_replace('/\/auto\//', '/build/', $path, 1);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,22 @@ class LabelDialogType extends AbstractType
|
||||
'label' => false,
|
||||
'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'],
|
||||
]);
|
||||
|
||||
$builder->add('save_profile_name', TextType::class, [
|
||||
'required' => false,
|
||||
'attr' =>[
|
||||
'placeholder' => 'label_generator.save_profile_name',
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('save_profile', SubmitType::class, [
|
||||
'label' => 'label_generator.save_profile',
|
||||
'disabled' => !$this->security->isGranted('@labels.create_profiles'),
|
||||
'attr' => [
|
||||
'class' => 'btn btn-outline-success'
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('update', SubmitType::class, [
|
||||
'label' => 'label_generator.update',
|
||||
]);
|
||||
|
||||
@@ -41,8 +41,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\LabelSystem;
|
||||
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@@ -55,6 +56,8 @@ class ScanDialogType extends AbstractType
|
||||
{
|
||||
$builder->add('input', TextType::class, [
|
||||
'label' => 'scan_dialog.input',
|
||||
//Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters
|
||||
'trim' => false,
|
||||
'attr' => [
|
||||
'autofocus' => true,
|
||||
'id' => 'scan_dialog_input',
|
||||
@@ -71,9 +74,14 @@ class ScanDialogType extends AbstractType
|
||||
null => 'scan_dialog.mode.auto',
|
||||
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
|
||||
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn',
|
||||
BarcodeSourceType::VENDOR => 'scan_dialog.mode.vendor',
|
||||
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
|
||||
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp'
|
||||
},
|
||||
]);
|
||||
|
||||
$builder->add('info_mode', CheckboxType::class, [
|
||||
'label' => 'scan_dialog.info_mode',
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
|
||||
@@ -101,6 +101,8 @@ class PartBaseType extends AbstractType
|
||||
'dto_value' => $dto?->category,
|
||||
'label' => 'part.edit.category',
|
||||
'disable_not_selectable' => true,
|
||||
//Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity)
|
||||
'required' => !$new_part,
|
||||
])
|
||||
->add('footprint', StructuralEntityType::class, [
|
||||
'class' => Footprint::class,
|
||||
|
||||
@@ -103,10 +103,12 @@ class PartLotType extends AbstractType
|
||||
'help' => 'part_lot.owner.help',
|
||||
]);
|
||||
|
||||
$builder->add('vendor_barcode', TextType::class, [
|
||||
'label' => 'part_lot.edit.vendor_barcode',
|
||||
$builder->add('user_barcode', TextType::class, [
|
||||
'label' => 'part_lot.edit.user_barcode',
|
||||
'help' => 'part_lot.edit.vendor_barcode.help',
|
||||
'required' => false,
|
||||
//Do not remove whitespace chars on the beginning and end of the string
|
||||
'trim' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class ExponentialNumberType extends AbstractType
|
||||
return NumberType::class;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
//We want to allow the full precision of the number, so disable rounding
|
||||
@@ -47,7 +47,7 @@ class ExponentialNumberType extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->resetViewTransformers();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class StructuralEntityChoiceHelper
|
||||
|
||||
/**
|
||||
* Generates the choice attributes for the given AbstractStructuralDBElement.
|
||||
* @return array|string[]
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function generateChoiceAttr(AbstractNamedDBElement $choice, Options|array $options): array
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Type\Helper;
|
||||
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Repository\StructuralDBElementRepository;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -33,6 +34,9 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @template T of AbstractStructuralDBElement
|
||||
*/
|
||||
class StructuralEntityChoiceLoader extends AbstractChoiceLoader
|
||||
{
|
||||
private ?string $additional_element = null;
|
||||
@@ -90,10 +94,14 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @var class-string<T> $class */
|
||||
$class = $this->options['class'];
|
||||
/** @var StructuralDBElementRepository $repo */
|
||||
|
||||
/** @var StructuralDBElementRepository<T> $repo */
|
||||
$repo = $this->entityManager->getRepository($class);
|
||||
|
||||
|
||||
$entities = $repo->getNewEntityFromPath($value, '->');
|
||||
|
||||
$results = [];
|
||||
|
||||
@@ -99,7 +99,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
|
||||
*
|
||||
* @return mixed The value in the transformed representation
|
||||
*
|
||||
* @throws TransformationFailedException when the transformation fails
|
||||
*/
|
||||
public function transform(mixed $value)
|
||||
{
|
||||
@@ -142,8 +141,6 @@ final class TriStateCheckboxType extends AbstractType implements DataTransformer
|
||||
* @param mixed $value The value in the transformed representation
|
||||
*
|
||||
* @return mixed The value in the original representation
|
||||
*
|
||||
* @throws TransformationFailedException when the transformation fails
|
||||
*/
|
||||
public function reverseTransform(mixed $value)
|
||||
{
|
||||
|
||||
@@ -36,6 +36,9 @@ class FilenameSanatizer
|
||||
*/
|
||||
public static function sanitizeFilename(string $filename): string
|
||||
{
|
||||
//Convert to ASCII
|
||||
$filename = iconv('UTF-8', 'ASCII//TRANSLIT', $filename);
|
||||
|
||||
$filename = preg_replace(
|
||||
'~
|
||||
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
|
||||
49
src/Helpers/IPAnonymizer.php
Normal file
49
src/Helpers/IPAnonymizer.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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\Helpers;
|
||||
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
|
||||
/**
|
||||
* Utils to assist with IP anonymization.
|
||||
* The IPUtils::anonymize has a certain edgecase with local-link addresses, which is handled here.
|
||||
* See: https://github.com/Part-DB/Part-DB-server/issues/782
|
||||
*/
|
||||
final class IPAnonymizer
|
||||
{
|
||||
public static function anonymize(string $ip): string
|
||||
{
|
||||
/**
|
||||
* If the IP contains a % symbol, then it is a local-link address with scoping according to RFC 4007
|
||||
* In that case, we only care about the part before the % symbol, as the following functions, can only work with
|
||||
* the IP address itself. As the scope can leak information (containing interface name), we do not want to
|
||||
* include it in our anonymized IP data.
|
||||
*/
|
||||
if (str_contains($ip, '%')) {
|
||||
$ip = substr($ip, 0, strpos($ip, '%'));
|
||||
}
|
||||
|
||||
return IpUtils::anonymize($ip);
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@ class AttachmentRepository extends DBElementRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('attachment');
|
||||
$qb->select('COUNT(attachment)')
|
||||
->where('attachment.path LIKE :http')
|
||||
->orWhere('attachment.path LIKE :https');
|
||||
->where('ILIKE(attachment.path, :http) = TRUE')
|
||||
->orWhere('ILIKE(attachment.path, :https) = TRUE');
|
||||
$qb->setParameter('http', 'http://%');
|
||||
$qb->setParameter('https', 'https://%');
|
||||
$query = $qb->getQuery();
|
||||
|
||||
@@ -44,7 +44,7 @@ class ParameterRepository extends DBElementRepository
|
||||
->select('parameter.name')
|
||||
->addSelect('parameter.symbol')
|
||||
->addSelect('parameter.unit')
|
||||
->where('parameter.name LIKE :name');
|
||||
->where('ILIKE(parameter.name, :name) = TRUE');
|
||||
if ($exact) {
|
||||
$qb->setParameter('name', $name);
|
||||
} else {
|
||||
|
||||
@@ -81,10 +81,10 @@ class PartRepository extends NamedDBElementRepository
|
||||
->leftJoin('part.category', 'category')
|
||||
->leftJoin('part.footprint', 'footprint')
|
||||
|
||||
->where('part.name LIKE :query')
|
||||
->orWhere('part.description LIKE :query')
|
||||
->orWhere('category.name LIKE :query')
|
||||
->orWhere('footprint.name LIKE :query')
|
||||
->where('ILIKE(part.name, :query) = TRUE')
|
||||
->orWhere('ILIKE(part.description, :query) = TRUE')
|
||||
->orWhere('ILIKE(category.name, :query) = TRUE')
|
||||
->orWhere('ILIKE(footprint.name, :query) = TRUE')
|
||||
;
|
||||
|
||||
$qb->setParameter('query', '%'.$query.'%');
|
||||
|
||||
@@ -151,7 +151,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
|
||||
}
|
||||
if (null === $entity) {
|
||||
$class = $this->getClassName();
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
/** @var TEntityClass $entity */
|
||||
$entity = new $class;
|
||||
$entity->setName($name);
|
||||
$entity->setParent($parent);
|
||||
@@ -265,7 +265,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
|
||||
}
|
||||
|
||||
$class = $this->getClassName();
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
/** @var TEntityClass $entity */
|
||||
$entity = new $class;
|
||||
$entity->setName($name);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace App\Security;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -50,13 +51,20 @@ class EnsureSAMLUserForSAMLLoginChecker implements EventSubscriberInterface
|
||||
$token = $event->getAuthenticationToken();
|
||||
$user = $token->getUser();
|
||||
|
||||
//If we are using SAML, we need to check that the user is a SAML user.
|
||||
if ($token instanceof SamlToken) {
|
||||
if ($user instanceof User && !$user->isSamlUser()) {
|
||||
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml', [], 'security'));
|
||||
}
|
||||
} elseif ($user instanceof User && $user->isSamlUser()) {
|
||||
//Ensure that you can not login locally with a SAML user (even if this should not happen, as the password is not set)
|
||||
//Do not check for anonymous users
|
||||
if (!$user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Do not allow SAML users to login as local user
|
||||
if ($token instanceof SamlToken && !$user->isSamlUser()) {
|
||||
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_local_user_per_saml',
|
||||
[], 'security'));
|
||||
}
|
||||
|
||||
//Do not allow local users to login as SAML user via local username and password
|
||||
if ($token instanceof UsernamePasswordToken && $user->isSamlUser()) {
|
||||
//Ensure that you can not login locally with a SAML user (even though this should not happen, as the password is not set)
|
||||
throw new CustomUserMessageAccountStatusException($this->translator->trans('saml.error.cannot_login_saml_user_locally', [], 'security'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,10 @@ final class UserChecker implements UserCheckerInterface
|
||||
|
||||
/**
|
||||
* Checks the user account before authentication.
|
||||
*
|
||||
* @throws AccountStatusException
|
||||
*/
|
||||
public function checkPreAuth(UserInterface $user): void
|
||||
{
|
||||
// TODO: Implement checkPreAuth() method.
|
||||
//We don't need to check the user before authentication, just implemented to fulfill the interface
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,7 +94,14 @@ class PartNormalizer implements NormalizerInterface, DenormalizerInterface, Norm
|
||||
|
||||
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
|
||||
{
|
||||
return !isset($context[self::ALREADY_CALLED]) && is_array($data) && is_a($type, Part::class, true);
|
||||
//Only denormalize if we are doing a file import operation
|
||||
if (!($context['partdb_import'] ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//Only make the denormalizer available on import operations
|
||||
return !isset($context[self::ALREADY_CALLED])
|
||||
&& is_array($data) && is_a($type, Part::class, true);
|
||||
}
|
||||
|
||||
private function normalizeKeys(array &$data): array
|
||||
|
||||
@@ -69,6 +69,15 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
||||
&& in_array('import', $context['groups'] ?? [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of AbstractStructuralDBElement
|
||||
* @param $data
|
||||
* @phpstan-param class-string<T> $type
|
||||
* @param string|null $format
|
||||
* @param array $context
|
||||
* @return AbstractStructuralDBElement|null
|
||||
* @phpstan-return T|null
|
||||
*/
|
||||
public function denormalize($data, string $type, string $format = null, array $context = []): ?AbstractStructuralDBElement
|
||||
{
|
||||
//Do not use API Platform's denormalizer
|
||||
@@ -85,7 +94,7 @@ class StructuralElementDenormalizer implements DenormalizerInterface, Denormaliz
|
||||
$deserialized_entity = $this->denormalizer->denormalize($data, $type, $format, $context);
|
||||
|
||||
//Check if we already have the entity in the database (via path)
|
||||
/** @var StructuralDBElementRepository $repo */
|
||||
/** @var StructuralDBElementRepository<T> $repo */
|
||||
$repo = $this->entityManager->getRepository($type);
|
||||
|
||||
$path = $deserialized_entity->getFullPath(AbstractStructuralDBElement::PATH_DELIMITER_ARROW);
|
||||
|
||||
@@ -54,7 +54,7 @@ class StructuralElementFromNameDenormalizer implements DenormalizerInterface
|
||||
public function denormalize($data, string $type, string $format = null, array $context = []): AbstractStructuralDBElement|null
|
||||
{
|
||||
//Retrieve the repository for the given type
|
||||
/** @var StructuralDBElementRepository $repo */
|
||||
/** @var StructuralDBElementRepository<T> $repo */
|
||||
$repo = $this->em->getRepository($type);
|
||||
|
||||
$path_delimiter = $context['path_delimiter'] ?? '->';
|
||||
|
||||
@@ -46,7 +46,7 @@ class MoneyFormatter
|
||||
public function format(string|float $value, ?Currency $currency = null, int $decimals = 5, bool $show_all_digits = false): string
|
||||
{
|
||||
$iso_code = $this->base_currency;
|
||||
if ($currency instanceof Currency && ($currency->getIsoCode() !== null && $currency->getIsoCode() !== '')) {
|
||||
if ($currency instanceof Currency && ($currency->getIsoCode() !== '')) {
|
||||
$iso_code = $currency->getIsoCode();
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ class BOMImporter
|
||||
break;
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line We want to keep this check just to be safe when something changes
|
||||
$new_index = self::MAP_KICAD_PCB_FIELDS[$index] ?? throw new \UnexpectedValueException('Invalid field index!');
|
||||
$out[$new_index] = $field;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
*/
|
||||
class EntityImporter
|
||||
{
|
||||
|
||||
/**
|
||||
* The encodings that are supported by the importer, and that should be autodeceted.
|
||||
*/
|
||||
private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"];
|
||||
|
||||
public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator)
|
||||
{
|
||||
}
|
||||
@@ -58,13 +64,16 @@ class EntityImporter
|
||||
* @phpstan-param class-string<T> $class_name
|
||||
* @param AbstractStructuralDBElement|null $parent the element which will be used as parent element for new elements
|
||||
* @param array $errors an associative array containing all validation errors
|
||||
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
|
||||
* @param-out list<array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
|
||||
*
|
||||
* @return AbstractNamedDBElement[] An array containing all valid imported entities (with the type $class_name)
|
||||
* @return T[]
|
||||
*/
|
||||
public function massCreation(string $lines, string $class_name, ?AbstractStructuralDBElement $parent = null, array &$errors = []): array
|
||||
{
|
||||
//Try to detect the text encoding of the data and convert it to UTF-8
|
||||
$lines = mb_convert_encoding($lines, 'UTF-8', mb_detect_encoding($lines, self::ENCODINGS));
|
||||
|
||||
//Expand every line to a single entry:
|
||||
$names = explode("\n", $lines);
|
||||
|
||||
@@ -124,13 +133,15 @@ class EntityImporter
|
||||
if ($repo instanceof StructuralDBElementRepository) {
|
||||
$entities = $repo->getNewEntityFromPath($new_path);
|
||||
$entity = end($entities);
|
||||
if ($entity === false) {
|
||||
throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!');
|
||||
}
|
||||
} else { //Otherwise just create a new entity
|
||||
$entity = new $class_name;
|
||||
$entity->setName($name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Validate entity
|
||||
$tmp = $this->validator->validate($entity);
|
||||
//If no error occured, write entry to DB:
|
||||
@@ -159,6 +170,9 @@ class EntityImporter
|
||||
*/
|
||||
public function importString(string $data, array $options = [], array &$errors = []): array
|
||||
{
|
||||
//Try to detect the text encoding of the data and convert it to UTF-8
|
||||
$data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data, self::ENCODINGS));
|
||||
|
||||
$resolver = new OptionsResolver();
|
||||
$this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
@@ -215,6 +229,11 @@ class EntityImporter
|
||||
|
||||
//Iterate over each $entity write it to DB.
|
||||
foreach ($entities as $key => $entity) {
|
||||
//Ensure that entity is a NamedDBElement
|
||||
if (!$entity instanceof AbstractNamedDBElement) {
|
||||
throw new \RuntimeException("Encountered an entity that is not a NamedDBElement!");
|
||||
}
|
||||
|
||||
//Validate entity
|
||||
$tmp = $this->validator->validate($entity);
|
||||
|
||||
@@ -269,7 +288,7 @@ class EntityImporter
|
||||
*
|
||||
* @param File $file the file that should be used for importing
|
||||
* @param array $options options for the import process
|
||||
* @param AbstractNamedDBElement[] $entities The imported entities are returned in this array
|
||||
* @param-out AbstractNamedDBElement[] $entities The imported entities are returned in this array
|
||||
*
|
||||
* @return array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> An associative array containing an ConstraintViolationList and the entity name as key are returned,
|
||||
* if an error happened during validation. When everything was successfully, the array should be empty.
|
||||
@@ -305,7 +324,7 @@ class EntityImporter
|
||||
* @param array $options options for the import process
|
||||
* @param-out array<string, array{'entity': object, 'violations': ConstraintViolationListInterface}> $errors
|
||||
*
|
||||
* @return array an array containing the deserialized elements
|
||||
* @return AbstractNamedDBElement[] an array containing the deserialized elements
|
||||
*/
|
||||
public function importFile(File $file, array $options = [], array &$errors = []): array
|
||||
{
|
||||
|
||||
@@ -205,10 +205,6 @@ trait PKImportHelperTrait
|
||||
*/
|
||||
protected function setIDOfEntity(AbstractDBElement $element, int|string $id): void
|
||||
{
|
||||
if (!is_int($id) && !is_string($id)) {
|
||||
throw new \InvalidArgumentException('ID must be an integer or string');
|
||||
}
|
||||
|
||||
$id = (int) $id;
|
||||
|
||||
$metadata = $this->em->getClassMetadata($element::class);
|
||||
|
||||
@@ -27,6 +27,7 @@ use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Parameters\PartParameter;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\InfoProviderReference;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
@@ -36,6 +37,7 @@ use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Entity\PriceInformations\Pricedetail;
|
||||
use App\Repository\Parts\CategoryRepository;
|
||||
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
@@ -156,6 +158,12 @@ final class DTOtoEntityConverter
|
||||
|
||||
$entity->setMass($dto->mass);
|
||||
|
||||
//Try to map the category to an existing entity (but never create a new one)
|
||||
if ($dto->category) {
|
||||
//@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category
|
||||
$entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category));
|
||||
}
|
||||
|
||||
$entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer));
|
||||
$entity->setFootprint($this->getOrCreateEntity(Footprint::class, $dto->footprint));
|
||||
|
||||
@@ -166,9 +174,21 @@ final class DTOtoEntityConverter
|
||||
//Set the provider reference on the part
|
||||
$entity->setProviderReference(InfoProviderReference::fromPartDTO($dto));
|
||||
|
||||
$param_groups = [];
|
||||
|
||||
//Add parameters
|
||||
foreach ($dto->parameters ?? [] as $parameter) {
|
||||
$entity->addParameter($this->convertParameter($parameter));
|
||||
$new_param = $this->convertParameter($parameter);
|
||||
|
||||
$key = $new_param->getName() . '##' . $new_param->getGroup();
|
||||
//If there is already an parameter with the same name and group, rename the new parameter, by suffixing a number
|
||||
if (count($param_groups[$key] ?? []) > 0) {
|
||||
$new_param->setName($new_param->getName() . ' (' . (count($param_groups[$key]) + 1) . ')');
|
||||
}
|
||||
|
||||
$param_groups[$key][] = $new_param;
|
||||
|
||||
$entity->addParameter($new_param);
|
||||
}
|
||||
|
||||
//Add preview image
|
||||
@@ -184,6 +204,8 @@ final class DTOtoEntityConverter
|
||||
$entity->setMasterPictureAttachment($preview_image);
|
||||
}
|
||||
|
||||
$attachments_grouped = [];
|
||||
|
||||
//Add other images
|
||||
$images = $this->files_unique($dto->images ?? []);
|
||||
foreach ($images as $image) {
|
||||
@@ -192,14 +214,29 @@ final class DTOtoEntityConverter
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity->addAttachment($this->convertFile($image, $image_type));
|
||||
$attachment = $this->convertFile($image, $image_type);
|
||||
|
||||
$attachments_grouped[$attachment->getName()][] = $attachment;
|
||||
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
|
||||
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()]) + 1) . ')');
|
||||
}
|
||||
|
||||
|
||||
$entity->addAttachment($attachment);
|
||||
}
|
||||
|
||||
//Add datasheets
|
||||
$datasheet_type = $this->getDatasheetType();
|
||||
$datasheets = $this->files_unique($dto->datasheets ?? []);
|
||||
foreach ($datasheets as $datasheet) {
|
||||
$entity->addAttachment($this->convertFile($datasheet, $datasheet_type));
|
||||
$attachment = $this->convertFile($datasheet, $datasheet_type);
|
||||
|
||||
$attachments_grouped[$attachment->getName()][] = $attachment;
|
||||
if (count($attachments_grouped[$attachment->getName()] ?? []) > 1) {
|
||||
$attachment->setName($attachment->getName() . ' (' . (count($attachments_grouped[$attachment->getName()])) . ')');
|
||||
}
|
||||
|
||||
$entity->addAttachment($attachment);
|
||||
}
|
||||
|
||||
//Add orderdetails and prices
|
||||
|
||||
77
src/Services/InfoProviderSystem/ExistingPartFinder.php
Normal file
77
src/Services/InfoProviderSystem/ExistingPartFinder.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\InfoProviderSystem;
|
||||
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* This service assists in finding existing local parts for a SearchResultDTO, so that the user
|
||||
* does not accidentally add a duplicate.
|
||||
*
|
||||
* A part is considered to be a duplicate, if the provider reference matches, or if the manufacturer and the MPN of the
|
||||
* DTO and the local part match. This checks also for alternative names of the manufacturer and the part name (as alternative
|
||||
* for the MPN).
|
||||
*/
|
||||
final class ExistingPartFinder
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $em)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first existing local part, that matches the search result.
|
||||
* If no part is found, return null.
|
||||
* @param SearchResultDTO $dto
|
||||
* @return Part|null
|
||||
*/
|
||||
public function findFirstExisting(SearchResultDTO $dto): ?Part
|
||||
{
|
||||
$results = $this->findAllExisting($dto);
|
||||
return count($results) > 0 ? $results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all existing local parts that match the search result.
|
||||
* If no part is found, return an empty array.
|
||||
* @param SearchResultDTO $dto
|
||||
* @return Part[]
|
||||
*/
|
||||
public function findAllExisting(SearchResultDTO $dto): array
|
||||
{
|
||||
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
|
||||
$qb->select('part')
|
||||
->leftJoin('part.manufacturer', 'manufacturer')
|
||||
->Orwhere($qb->expr()->andX(
|
||||
'part.providerReference.provider_key = :providerKey',
|
||||
'part.providerReference.provider_id = :providerId',
|
||||
))
|
||||
|
||||
//Or the manufacturer (allowing for alternative names) and the MPN (or part name) must match
|
||||
->OrWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
"ILIKE(manufacturer.name, :manufacturerName) = TRUE",
|
||||
"ILIKE(manufacturer.alternative_names, :manufacturerAltNames) = TRUE",
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
"ILIKE(part.manufacturer_product_number, :mpn) = TRUE",
|
||||
"ILIKE(part.name, :mpn) = TRUE",
|
||||
)
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
$qb->setParameter('providerKey', $dto->provider_key);
|
||||
$qb->setParameter('providerId', $dto->provider_id);
|
||||
|
||||
$qb->setParameter('manufacturerName', $dto->manufacturer);
|
||||
$qb->setParameter('manufacturerAltNames', '%'.$dto->manufacturer.'%');
|
||||
$qb->setParameter('mpn', $dto->mpn);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,9 @@ class LCSCProvider implements InfoProviderInterface
|
||||
private function getRealDatasheetUrl(?string $url): string
|
||||
{
|
||||
if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) {
|
||||
if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) {
|
||||
$url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1];
|
||||
}
|
||||
$response = $this->lcscClient->request('GET', $url, [
|
||||
'headers' => [
|
||||
'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'
|
||||
|
||||
@@ -205,7 +205,7 @@ class MouserProvider implements InfoProviderInterface
|
||||
if (isset($arr['SearchResults'])) {
|
||||
$products = $arr['SearchResults']['Parts'] ?? [];
|
||||
} else {
|
||||
throw new \RuntimeException('Unknown response format');
|
||||
throw new \RuntimeException('Unknown response format: ' .json_encode($arr, JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
@@ -771,11 +771,6 @@ class OEMSecretsProvider implements InfoProviderInterface
|
||||
// Logic to extract parameters from the description
|
||||
$extractedParameters = $this->parseDescriptionToParameters($description) ?? [];
|
||||
|
||||
// Ensure that $extractedParameters is an array
|
||||
if (!is_array($extractedParameters)) {
|
||||
$extractedParameters = [];
|
||||
}
|
||||
|
||||
foreach ($extractedParameters as $newParam) {
|
||||
$isDuplicate = false;
|
||||
foreach ($parameters as $existingParam) {
|
||||
|
||||
@@ -50,6 +50,16 @@ class TMEClient
|
||||
return $this->token !== '' && $this->secret !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the client is using a private (account related token) instead of a deprecated anonymous token
|
||||
* to authenticate with TME.
|
||||
* @return bool
|
||||
*/
|
||||
public function isUsingPrivateToken(): bool
|
||||
{
|
||||
//Private tokens are longer than anonymous ones (50 instead of 45 characters)
|
||||
return strlen($this->token) > 45;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the signature for the given action and parameters.
|
||||
|
||||
@@ -36,12 +36,19 @@ class TMEProvider implements InfoProviderInterface
|
||||
|
||||
private const VENDOR_NAME = 'TME';
|
||||
|
||||
/** @var bool If true, the prices are gross prices. If false, the prices are net prices. */
|
||||
private readonly bool $get_gross_prices;
|
||||
|
||||
public function __construct(private readonly TMEClient $tmeClient, private readonly string $country,
|
||||
private readonly string $language, private readonly string $currency,
|
||||
/** @var bool If true, the prices are gross prices. If false, the prices are net prices. */
|
||||
private readonly bool $get_gross_prices)
|
||||
bool $get_gross_prices)
|
||||
{
|
||||
|
||||
//If we have a private token, set get_gross_prices to false, as it is automatically determined by the account type then
|
||||
if ($this->tmeClient->isUsingPrivateToken()) {
|
||||
$this->get_gross_prices = false;
|
||||
} else {
|
||||
$this->get_gross_prices = $get_gross_prices;
|
||||
}
|
||||
}
|
||||
|
||||
public function getProviderInfo(): array
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user