mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Merge remote-tracking branch 'origin/l10n_master'
This commit is contained in:
commit
dd54c46a29
317 changed files with 37151 additions and 8990 deletions
|
@ -39,8 +39,8 @@ if [ -d /var/www/html/var/db ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Start PHP-FPM
|
||||
service php8.1-fpm start
|
||||
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile)
|
||||
service phpPHP_VERSION-fpm start
|
||||
|
||||
# first arg is `-f` or `--some-option` (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/docker-php-entrypoint)
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
|
|
|
@ -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
|
||||
|
|
34
.env
34
.env
|
@ -181,6 +181,40 @@ PROVIDER_LCSC_ENABLED=0
|
|||
# The currency to get prices in (e.g. EUR, USD, etc.)
|
||||
PROVIDER_LCSC_CURRENCY=EUR
|
||||
|
||||
# Oemsecrets Provider API 3.0.1:
|
||||
# You can get your API key from https://www.oemsecrets.com/api
|
||||
PROVIDER_OEMSECRETS_KEY=
|
||||
# The country you want the output for
|
||||
PROVIDER_OEMSECRETS_COUNTRY_CODE=DE
|
||||
# Available country code are:
|
||||
# AD, AE, AQ, AR, AT, AU, BE, BO, BR, BV, BY, CA, CH, CL, CN, CO, CZ, DE, DK, EC, EE, EH,
|
||||
# ES, FI, FK, FO, FR, GB, GE, GF, GG, GI, GL, GR, GS, GY, HK, HM, HR, HU, IE, IM, IN, IS,
|
||||
# IT, JM, JP, KP, KR, KZ, LI, LK, LT, LU, MC, MD, ME, MK, MT, NL, NO, NZ, PE, PH, PL, PT,
|
||||
# PY, RO, RS, RU, SB, SD, SE, SG, SI, SJ, SK, SM, SO, SR, SY, SZ, TC, TF, TG, TH, TJ, TK,
|
||||
# TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VE, VG, VI, VN, VU, WF, YE,
|
||||
# ZA, ZM, ZW
|
||||
#
|
||||
# The currency you want the prices to be displayed in
|
||||
PROVIDER_OEMSECRETS_CURRENCY=EUR
|
||||
# Available currency are:AUD, CAD, CHF, CNY, DKK, EUR, GBP, HKD, ILS, INR, JPY, KRW, NOK,
|
||||
# NZD, RUB, SEK, SGD, TWD, USD
|
||||
#
|
||||
# If PROVIDER_OEMSECRETS_ZERO_PRICE is set to 0, distributors with zero prices
|
||||
# will be discarded from the creation of a new part (set to 1 otherwise)
|
||||
PROVIDER_OEMSECRETS_ZERO_PRICE=0
|
||||
#
|
||||
# When PROVIDER_OEMSECRETS_SET_PARAM is set to 1 the parameters for the part are generated
|
||||
# from the description transforming unstructured descriptions into structured parameters;
|
||||
# each parameter in description should have the form: "...;name1:value1;name2:value2"
|
||||
PROVIDER_OEMSECRETS_SET_PARAM=1
|
||||
#
|
||||
# This environment variable determines the sorting criteria for product results.
|
||||
# The sorting process first arranges items based on the provided keyword.
|
||||
# Then, if set to 'C', it further sorts by completeness (prioritizing items with the most
|
||||
# detailed information). If set to 'M', it further sorts by manufacturer name.
|
||||
#If unset or set to any other value, no sorting is performed.
|
||||
PROVIDER_OEMSECRETS_SORT_CRITERIA=C
|
||||
|
||||
##################################################################################
|
||||
# EDA integration related settings
|
||||
##################################################################################
|
||||
|
|
182
Dockerfile
182
Dockerfile
|
@ -1,22 +1,64 @@
|
|||
FROM debian:bullseye-slim
|
||||
ARG BASE_IMAGE=debian:bookworm-slim
|
||||
ARG PHP_VERSION=8.2
|
||||
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
ARG PHP_VERSION
|
||||
|
||||
# Install needed dependencies for PHP build
|
||||
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
||||
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
||||
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get -y install apt-transport-https lsb-release ca-certificates curl zip mariadb-client \
|
||||
RUN apt-get update && apt-get -y install \
|
||||
apt-transport-https \
|
||||
lsb-release \
|
||||
ca-certificates \
|
||||
curl \
|
||||
zip \
|
||||
mariadb-client \
|
||||
postgresql-client \
|
||||
&& curl -sSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg \
|
||||
&& sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' \
|
||||
&& apt-get update && apt-get upgrade -y \
|
||||
&& apt-get install -y apache2 php8.1 php8.1-fpm php8.1-opcache php8.1-curl php8.1-gd php8.1-mbstring php8.1-xml php8.1-bcmath php8.1-intl php8.1-zip php8.1-xsl php8.1-sqlite3 php8.1-mysql gpg sudo \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
ENV APACHE_CONFDIR /etc/apache2
|
||||
ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars
|
||||
|
||||
&& apt-get install -y \
|
||||
apache2 \
|
||||
php${PHP_VERSION} \
|
||||
php${PHP_VERSION}-fpm \
|
||||
php${PHP_VERSION}-opcache \
|
||||
php${PHP_VERSION}-curl \
|
||||
php${PHP_VERSION}-gd \
|
||||
php${PHP_VERSION}-mbstring \
|
||||
php${PHP_VERSION}-xml \
|
||||
php${PHP_VERSION}-bcmath \
|
||||
php${PHP_VERSION}-intl \
|
||||
php${PHP_VERSION}-zip \
|
||||
php${PHP_VERSION}-xsl \
|
||||
php${PHP_VERSION}-sqlite3 \
|
||||
php${PHP_VERSION}-mysql \
|
||||
php${PHP_VERSION}-pgsql \
|
||||
gpg \
|
||||
sudo \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \
|
||||
# Create workdir and set permissions if directory does not exists
|
||||
RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html
|
||||
&& mkdir -p /var/www/html \
|
||||
&& chown -R www-data:www-data /var/www/html \
|
||||
# delete the "index.html" that installing Apache drops in here
|
||||
&& rm -rvf /var/www/html/*
|
||||
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
yarn \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
ENV APACHE_CONFDIR=/etc/apache2
|
||||
ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
|
||||
|
||||
# Configure apache 2 (taken from https://github.com/docker-library/php/blob/master/8.2/bullseye/apache/Dockerfile)
|
||||
# generically convert lines like
|
||||
|
@ -27,8 +69,6 @@ RUN mkdir -p /var/www/html && chown -R www-data:www-data /var/www/html
|
|||
# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...")
|
||||
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
|
||||
set -eux; . "$APACHE_ENVVARS"; \
|
||||
# delete the "index.html" that installing Apache drops in here
|
||||
rm -rvf /var/www/html/*; \
|
||||
\
|
||||
# logs should go to stdout / stderr
|
||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
||||
|
@ -36,82 +76,86 @@ RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"
|
|||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
|
||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
|
||||
|
||||
# Enable php-fpm
|
||||
RUN a2enmod proxy_fcgi setenvif && a2enconf php8.1-fpm
|
||||
# ---
|
||||
|
||||
FROM scratch AS apache-config
|
||||
ARG PHP_VERSION
|
||||
# Configure php-fpm to log to stdout of the container (stdout of PID 1)
|
||||
# We have to use /proc/1/fd/1 because /dev/stdout or /proc/self/fd/1 does not point to the container stdout (because we use apache as entrypoint)
|
||||
# We also disable the clear_env option to allow the use of environment variables in php-fpm
|
||||
RUN { \
|
||||
echo '[global]'; \
|
||||
echo 'error_log = /proc/1/fd/1'; \
|
||||
echo; \
|
||||
echo '[www]'; \
|
||||
echo 'access.log = /proc/1/fd/1'; \
|
||||
echo 'catch_workers_output = yes'; \
|
||||
echo 'decorate_workers_output = no'; \
|
||||
echo 'clear_env = no'; \
|
||||
} | tee "/etc/php/8.1/fpm/pool.d/zz-docker.conf"
|
||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/pool.d/zz-docker.conf
|
||||
[global]
|
||||
error_log = /proc/1/fd/1
|
||||
|
||||
[www]
|
||||
access.log = /proc/1/fd/1
|
||||
catch_workers_output = yes
|
||||
decorate_workers_output = no
|
||||
clear_env = no
|
||||
EOF
|
||||
|
||||
# PHP files should be handled by PHP, and should be preferred over any other file type
|
||||
RUN { \
|
||||
echo '<FilesMatch \.php$>'; \
|
||||
echo '\tSetHandler application/x-httpd-php'; \
|
||||
echo '</FilesMatch>'; \
|
||||
echo; \
|
||||
echo 'DirectoryIndex disabled'; \
|
||||
echo 'DirectoryIndex index.php index.html'; \
|
||||
echo; \
|
||||
echo '<Directory /var/www/>'; \
|
||||
echo '\tOptions -Indexes'; \
|
||||
echo '\tAllowOverride All'; \
|
||||
echo '</Directory>'; \
|
||||
} | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \
|
||||
&& a2enconf docker-php
|
||||
COPY <<EOF /etc/apache2/conf-available/docker-php.conf
|
||||
<FilesMatch \\.php$>
|
||||
SetHandler application/x-httpd-php
|
||||
</FilesMatch>
|
||||
|
||||
DirectoryIndex disabled
|
||||
DirectoryIndex index.php index.html
|
||||
|
||||
<Directory /var/www/>
|
||||
Options -Indexes
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
EOF
|
||||
|
||||
# Enable opcache and configure it recommended for symfony (see https://symfony.com/doc/current/performance.html)
|
||||
RUN \
|
||||
{ \
|
||||
echo 'opcache.memory_consumption=256'; \
|
||||
echo 'opcache.max_accelerated_files=20000'; \
|
||||
echo 'opcache.validate_timestamp=0'; \
|
||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/symfony-recommended.ini
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.validate_timestamp=0
|
||||
# Configure Realpath cache for performance
|
||||
echo 'realpath_cache_size=4096K'; \
|
||||
echo 'realpath_cache_ttl=600'; \
|
||||
} > /etc/php/8.1/fpm/conf.d/symfony-recommended.ini
|
||||
realpath_cache_size=4096K
|
||||
realpath_cache_ttl=600
|
||||
EOF
|
||||
|
||||
# Increase upload limit and enable preloading
|
||||
RUN \
|
||||
{ \
|
||||
echo 'upload_max_filesize=256M'; \
|
||||
echo 'post_max_size=300M'; \
|
||||
echo 'opcache.preload_user=www-data'; \
|
||||
echo 'opcache.preload=/var/www/html/config/preload.php'; \
|
||||
} > /etc/php/8.1/fpm/conf.d/partdb.ini
|
||||
COPY <<EOF /etc/php/${PHP_VERSION}/fpm/conf.d/partdb.ini
|
||||
upload_max_filesize=256M
|
||||
post_max_size=300M
|
||||
opcache.preload_user=www-data
|
||||
opcache.preload=/var/www/html/config/preload.php
|
||||
EOF
|
||||
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
# ---
|
||||
|
||||
FROM base
|
||||
ARG PHP_VERSION
|
||||
|
||||
# Set working dir
|
||||
WORKDIR /var/www/html
|
||||
COPY --from=apache-config / /
|
||||
COPY --chown=www-data:www-data . .
|
||||
|
||||
# Setup apache2
|
||||
RUN a2dissite 000-default.conf
|
||||
COPY ./.docker/symfony.conf /etc/apache2/sites-available/symfony.conf
|
||||
RUN a2ensite symfony.conf
|
||||
RUN a2enmod rewrite
|
||||
RUN a2dissite 000-default.conf && \
|
||||
a2ensite symfony.conf && \
|
||||
# Enable php-fpm
|
||||
a2enmod proxy_fcgi setenvif && \
|
||||
a2enconf php${PHP_VERSION}-fpm && \
|
||||
a2enconf docker-php && \
|
||||
a2enmod rewrite
|
||||
|
||||
# Install composer and yarn dependencies for Part-DB
|
||||
USER www-data
|
||||
RUN composer install -a --no-dev && composer clear-cache
|
||||
RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
|
||||
RUN composer install -a --no-dev && \
|
||||
composer clear-cache
|
||||
RUN yarn install --network-timeout 600000 && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf node_modules/
|
||||
|
||||
# Use docker env to output logs to stdout
|
||||
ENV APP_ENV=docker
|
||||
|
@ -119,10 +163,12 @@ ENV DATABASE_URL="sqlite:///%kernel.project_dir%/uploads/app.db"
|
|||
|
||||
USER root
|
||||
|
||||
# Copy entrypoint to /usr/local/bin and make it executable
|
||||
RUN cp ./.docker/partdb-entrypoint.sh /usr/local/bin/partdb-entrypoint.sh && chmod +x /usr/local/bin/partdb-entrypoint.sh
|
||||
# Copy apache2-foreground to /usr/local/bin and make it executable
|
||||
RUN cp ./.docker/apache2-foreground /usr/local/bin/apache2-foreground && chmod +x /usr/local/bin/apache2-foreground
|
||||
# Replace the php version placeholder in the entry point, with our php version
|
||||
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh
|
||||
|
||||
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable
|
||||
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
|
||||
install ./.docker/apache2-foreground /usr/local/bin
|
||||
ENTRYPOINT ["partdb-entrypoint.sh"]
|
||||
CMD ["apache2-foreground"]
|
||||
|
||||
|
|
|
@ -1,11 +1,25 @@
|
|||
FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
|
||||
|
||||
RUN apt-get update && apt-get -y install curl zip mariadb-client file acl git gettext ca-certificates gnupg \
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl \
|
||||
ca-certificates \
|
||||
mariadb-client \
|
||||
postgresql-client \
|
||||
file \
|
||||
acl \
|
||||
git \
|
||||
gettext \
|
||||
gnupg \
|
||||
zip \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
# Create workdir and set permissions if directory does not exists
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get update && apt-get install -y \
|
||||
nodejs yarn \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP
|
||||
RUN set -eux; \
|
||||
|
@ -17,6 +31,7 @@ RUN set -eux; \
|
|||
zip \
|
||||
pdo_mysql \
|
||||
pdo_sqlite \
|
||||
pdo_pgsql \
|
||||
gd \
|
||||
bcmath \
|
||||
xsl \
|
||||
|
@ -32,15 +47,13 @@ ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
|
|||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install composer
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
#COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Create workdir and set permissions if directory does not exists
|
||||
WORKDIR /app
|
||||
|
||||
# prevent the reinstallation of vendors at every changes in the source code
|
||||
COPY --link composer.* symfony.* ./
|
||||
RUN set -eux; \
|
||||
|
@ -57,7 +70,10 @@ RUN set -eux; \
|
|||
composer run-script --no-dev post-install-cmd; \
|
||||
chmod +x bin/console; sync;
|
||||
|
||||
RUN yarn install --network-timeout 600000 && yarn build && yarn cache clean && rm -rf node_modules/
|
||||
RUN yarn install --network-timeout 600000 && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf node_modules/
|
||||
|
||||
# Use docker env to output logs to stdout
|
||||
ENV APP_ENV=docker
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.13.0-dev
|
||||
1.14.4
|
||||
|
|
|
@ -88,5 +88,8 @@ export default class extends Controller {
|
|||
} else {
|
||||
this.hideSidebar();
|
||||
}
|
||||
|
||||
//Hide the tootip on the button
|
||||
this._toggle_button.blur();
|
||||
}
|
||||
}
|
|
@ -186,5 +186,15 @@ export default class extends Controller {
|
|||
];
|
||||
},
|
||||
});
|
||||
|
||||
//Try to find the input field and register a defocus handler. This is necessarry, as by default the autocomplete
|
||||
//lib has problems when multiple inputs are present on the page. (see https://github.com/algolia/autocomplete/issues/1216)
|
||||
const inputs = this.element.getElementsByClassName('aa-Input');
|
||||
for (const input of inputs) {
|
||||
input.addEventListener('blur', () => {
|
||||
this._autocomplete.setIsOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -24,7 +24,6 @@ import {Controller} from "@hotwired/stimulus";
|
|||
|
||||
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
|
||||
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
|
@ -58,7 +57,21 @@ export default class extends Controller {
|
|||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
option: this.renderOption.bind(this),
|
||||
option_create: function(data, escape) {
|
||||
option_create: (data, escape) => {
|
||||
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
|
||||
//This here handles the display part, while the createItem function handles the actual creation
|
||||
if (data.input.startsWith("->")) {
|
||||
//Get current selected value
|
||||
const current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
|
||||
//Prepend it to the input
|
||||
if (current) {
|
||||
data.input = current + " " + data.input;
|
||||
} else {
|
||||
//If there is no current value, we remove the "->"
|
||||
data.input = data.input.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
return '<div class="create"><i class="fa-solid fa-plus fa-fw"></i> <strong>' + escape(data.input) + '</strong>… ' +
|
||||
'<small class="text-muted float-end">(' + addHint +')</small>' +
|
||||
'</div>';
|
||||
|
@ -76,6 +89,22 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
createItem(input, callback) {
|
||||
|
||||
//If the input starts with "->", we prepend the current selected value, for easier extension of existing values
|
||||
if (input.startsWith("->")) {
|
||||
//Get current selected value
|
||||
let current = this._tomSelect.getItem(this._tomSelect.getValue()).textContent.replaceAll("→", "->").trim();
|
||||
//Replace no break spaces with normal spaces
|
||||
current = current.replaceAll("\u00A0", " ");
|
||||
//Prepend it to the input
|
||||
if (current) {
|
||||
input = current + " " + input;
|
||||
} else {
|
||||
//If there is no current value, we remove the "->"
|
||||
input = input.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
callback({
|
||||
//$%$ is a special value prefix, that is used to identify items, that are not yet in the DB
|
||||
value: '$%$' + input,
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
.part-table-image {
|
||||
max-height: 40px;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.part-info-image {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -63,10 +63,6 @@ table.dataTable > tbody > tr.selected > td > a {
|
|||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.card-footer-table {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import {Dropdown} from "bootstrap";
|
||||
import ClipboardJS from "clipboard";
|
||||
import {Modal} from "bootstrap";
|
||||
|
||||
class RegisterEventHelper {
|
||||
constructor() {
|
||||
|
@ -31,9 +32,11 @@ class RegisterEventHelper {
|
|||
//Initialize ClipboardJS
|
||||
this.registerLoadHandler(() => {
|
||||
new ClipboardJS('.btn');
|
||||
})
|
||||
});
|
||||
|
||||
this.registerModalDropRemovalOnFormSubmit();
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerModalDropRemovalOnFormSubmit() {
|
||||
|
@ -43,6 +46,15 @@ class RegisterEventHelper {
|
|||
if (back_drop) {
|
||||
back_drop.remove();
|
||||
}
|
||||
|
||||
//Remove scroll-lock if it is still active
|
||||
if (document.body.classList.contains('modal-open')) {
|
||||
document.body.classList.remove('modal-open');
|
||||
|
||||
//Remove the padding-right and overflow:hidden from the body
|
||||
document.body.style.paddingRight = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
"nyholm/psr7": "^1.1",
|
||||
"ocramius/proxy-manager": "2.2.*",
|
||||
"omines/datatables-bundle": "^0.8.0",
|
||||
"paragonie/sodium_compat": "^1.21",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"php-translation/symfony-bundle": "^0.14.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"phpstan/phpdoc-parser": "^1.23",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
|
@ -98,13 +98,14 @@
|
|||
"dama/doctrine-test-bundle": "^v8.0.0",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.2",
|
||||
"ekino/phpstan-banned-code": "^v1.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",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"rector/rector": "^0.18.0",
|
||||
"rector/rector": "^1.1.1",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
|
|
3001
composer.lock
generated
3001
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -18,7 +18,6 @@ return [
|
|||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true],
|
||||
Translation\Bundle\TranslationBundle::class => ['all' => true],
|
||||
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
|
||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
|
@ -32,4 +31,5 @@ return [
|
|||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
|
||||
];
|
||||
|
|
|
@ -10,13 +10,12 @@ datatables:
|
|||
options:
|
||||
lengthMenu : [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
|
||||
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>>>
|
||||
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' B l> <'col mt-2' <'pull-right' p>>>"
|
||||
<'row' <'col mt-2 input-group flex-nowrap' B l > <'col-auto mt-2' < p >>>"
|
||||
pagingType: 'simple_numbers'
|
||||
searching: true
|
||||
stateSave: true
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
translation:
|
||||
symfony_profiler:
|
||||
enabled: true
|
||||
webui:
|
||||
enabled: true
|
|
@ -9,10 +9,17 @@ doctrine:
|
|||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
|
||||
types:
|
||||
# UTC datetimes
|
||||
datetime:
|
||||
class: App\Doctrine\Types\UTCDateTimeType
|
||||
date:
|
||||
class: App\Doctrine\Types\UTCDateTimeType
|
||||
|
||||
datetime_immutable:
|
||||
class: App\Doctrine\Types\UTCDateTimeImmutableType
|
||||
date_immutable:
|
||||
class: App\Doctrine\Types\UTCDateTimeImmutableType
|
||||
|
||||
big_decimal:
|
||||
class: App\Doctrine\Types\BigDecimalType
|
||||
tinyint:
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
translation:
|
||||
locales: ["en", "de"]
|
||||
edit_in_place:
|
||||
enabled: false
|
||||
config_name: app
|
||||
configs:
|
||||
app:
|
||||
dirs: ["%kernel.project_dir%/templates", "%kernel.project_dir%/src"]
|
||||
output_dir: "%kernel.project_dir%/translations"
|
||||
excluded_names: ["*TestCase.php", "*Test.php"]
|
||||
excluded_dirs: [cache, data, logs]
|
|
@ -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
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
_translation_webui:
|
||||
resource: '@TranslationBundle/Resources/config/routing_webui.yaml'
|
||||
prefix: /admin
|
||||
|
||||
_translation_profiler:
|
||||
resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yaml'
|
3
config/routes/jbtronics_translation_editor.yaml
Normal file
3
config/routes/jbtronics_translation_editor.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
when@dev:
|
||||
translation_editor:
|
||||
resource: '@JbtronicsTranslationEditorBundle/config/routes.php'
|
|
@ -1,3 +0,0 @@
|
|||
_translation_edit_in_place:
|
||||
resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml'
|
||||
prefix: /admin
|
|
@ -76,18 +76,12 @@ services:
|
|||
# Only the event classes specified here are saved to DB (set to []) to log all events
|
||||
$whitelist: []
|
||||
|
||||
App\EventSubscriber\LogSystem\EventLoggerSubscriber:
|
||||
App\EventListener\LogSystem\EventLoggerListener:
|
||||
arguments:
|
||||
$save_changed_fields: '%env(bool:HISTORY_SAVE_CHANGED_FIELDS)%'
|
||||
$save_changed_data: '%env(bool:HISTORY_SAVE_CHANGED_DATA)%'
|
||||
$save_removed_data: '%env(bool:HISTORY_SAVE_REMOVED_DATA)%'
|
||||
$save_new_data: '%env(bool:HISTORY_SAVE_NEW_DATA)%'
|
||||
tags:
|
||||
- { name: 'doctrine.event_subscriber' }
|
||||
|
||||
App\EventSubscriber\LogSystem\LogDBMigrationSubscriber:
|
||||
tags:
|
||||
- { name: 'doctrine.event_subscriber' }
|
||||
|
||||
App\Form\AttachmentFormType:
|
||||
arguments:
|
||||
|
@ -312,6 +306,16 @@ services:
|
|||
$enabled: '%env(bool:PROVIDER_LCSC_ENABLED)%'
|
||||
$currency: '%env(string:PROVIDER_LCSC_CURRENCY)%'
|
||||
|
||||
App\Services\InfoProviderSystem\Providers\OEMSecretsProvider:
|
||||
arguments:
|
||||
$api_key: '%env(string:PROVIDER_OEMSECRETS_KEY)%'
|
||||
$country_code: '%env(string:PROVIDER_OEMSECRETS_COUNTRY_CODE)%'
|
||||
$currency: '%env(PROVIDER_OEMSECRETS_CURRENCY)%'
|
||||
$zero_price: '%env(PROVIDER_OEMSECRETS_ZERO_PRICE)%'
|
||||
$set_param: '%env(PROVIDER_OEMSECRETS_SET_PARAM)%'
|
||||
$sort_criteria: '%env(PROVIDER_OEMSECRETS_SORT_CRITERIA)%'
|
||||
|
||||
|
||||
####################################################################################################################
|
||||
# API system
|
||||
####################################################################################################################
|
||||
|
|
|
@ -32,11 +32,16 @@ options listed, see `.env` file for the full list of possible env variables.
|
|||
|
||||
### General options
|
||||
|
||||
* `DATABASE_URL`: Configures the database which Part-DB uses. For mysql use a string in the form
|
||||
of `mysql://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<TABLE_NAME>` here
|
||||
(e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`). For SQLite use the following format to specify the
|
||||
* `DATABASE_URL`: Configures the database which Part-DB uses:
|
||||
* For MySQL (or MariaDB) use a string in the form of `mysql://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<TABLE_NAME>` here
|
||||
(e.g. `DATABASE_URL=mysql://user:password@127.0.0.1:3306/part-db`).
|
||||
* For SQLite use the following format to specify the
|
||||
absolute path where it should be located `sqlite:///path/part/app.db`. You can use `%kernel.project_dir%` as
|
||||
placeholder for the Part-DB root folder (e.g. `sqlite:///%kernel.project_dir%/var/app.db`)
|
||||
* For Postgresql use a string in the form of `DATABASE_URL=postgresql://user:password@127.0.0.1:5432/part-db?serverVersion=x.y`.
|
||||
|
||||
Please note that **`serverVersion=x.y`** variable is required due to dependency of Symfony framework.
|
||||
|
||||
* `DATABASE_MYSQL_USE_SSL_CA`: If this value is set to `1` or `true` and a MySQL connection is used, then the connection
|
||||
is encrypted by SSL/TLS and the server certificate is verified against the system CA certificates or the CA certificate
|
||||
bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept all certificates.
|
||||
|
@ -86,6 +91,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
|||
* `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...)
|
||||
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new
|
||||
versions, or if your server can not connect to the internet.
|
||||
* `APP_SECRET`: This variable is a configuration parameter used for various security-related purposes,
|
||||
particularly for securing and protecting various aspects of your application. It's a secret key that is used for
|
||||
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
|
||||
value should be handled as confidential data and not shared publicly.
|
||||
|
||||
### E-Mail settings
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ services:
|
|||
container_name: partdb_database
|
||||
image: mysql:8.0
|
||||
restart: unless-stopped
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
command: --default-authentication-plugin=mysql_native_password --log-bin-trust-function-creators=1
|
||||
environment:
|
||||
# Change this Password
|
||||
MYSQL_ROOT_PASSWORD: SECRET_ROOT_PASSWORD
|
||||
|
|
|
@ -212,6 +212,26 @@ An API key is not required, it is enough to enable the provider using the follow
|
|||
* `PROVIDER_LCSC_ENABLED`: Set this to `1` to enable the LCSC provider
|
||||
* `PROVIDER_LCSC_CURRENCY`: The currency you want to get prices in (see LCSC webshop for available currencies, default: `EUR`)
|
||||
|
||||
### OEMsecrets
|
||||
|
||||
The oemsecrets provider uses the [oemsecrets API](https://www.oemsecrets.com/) to search for parts and getting shopping
|
||||
information from them. Similar to octopart it aggregates offers from different distributors.
|
||||
|
||||
You can apply for a free API key on the [oemsecrets API page](https://www.oemsecrets.com/api/) and put the key you get
|
||||
in the Part-DB env configuration (see below).
|
||||
|
||||
The following env configuration options are available:
|
||||
|
||||
* `PROVIDER_OEMSECRETS_KEY`: The API key you got from oemsecrets (mandatory)
|
||||
* `PROVIDER_OEMSECRETS_COUNTRY_CODE`: The two-letter code of the country you want to get the prices for
|
||||
* `PROVIDER_OEMSECRETS_CURRENCY`: The currency you want to get prices in (optional, default: `EUR`)
|
||||
* `PROVIDER_OEMSECRETS_ZERO_PRICE`: If set to `1`, parts with a price of 0 will be included in the search results, otherwise
|
||||
they will be excluded (optional, default: `0`)
|
||||
* `PROVIDER_OEMSECRETS_SET_PARAM`: If set to `1`, the provider will try to extract parameters from the part description
|
||||
* `PROVIDER_OEMSECRETS_SORT_CRITERIA`: The criteria to sort the search results by. If set to 'C', it further sorts by
|
||||
completeness (prioritizing items with the most detailed information). If set to 'M', it further sorts by manufacturer name.
|
||||
If set to any other value, no sorting is performed.
|
||||
|
||||
### Custom provider
|
||||
|
||||
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long
|
||||
|
|
|
@ -343,6 +343,7 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme
|
|||
$this->addSql('ALTER TABLE webauthn_keys CHANGE public_key_credential_id public_key_credential_id LONGTEXT NOT NULL, CHANGE transports transports LONGTEXT NOT NULL, CHANGE trust_path trust_path JSON NOT NULL, CHANGE aaguid aaguid TINYTEXT NOT NULL, CHANGE credential_public_key credential_public_key LONGTEXT NOT NULL, CHANGE other_ui other_ui LONGTEXT DEFAULT NULL, CHANGE last_time_used last_time_used DATETIME DEFAULT NULL');
|
||||
|
||||
// Add the natural sort emulation function to the database (based on this stackoverflow: https://stackoverflow.com/questions/153633/natural-sort-in-mysql/58154535#58154535)
|
||||
//This version here is wrong, and will be replaced by the correct version in the next migration (we need to use nowdoc instead of heredoc, otherwise the slashes will be wrongly escaped!!)
|
||||
$this->addSql(<<<EOD
|
||||
CREATE DEFINER=CURRENT_USER FUNCTION `NatSortKey`(`s` VARCHAR(1000) CHARSET utf8mb4, `n` INT) RETURNS varchar(3500) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
|
||||
DETERMINISTIC
|
||||
|
@ -411,7 +412,7 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme
|
|||
|
||||
DECLARE x,y varchar(1000); # need to be same length as input s
|
||||
DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type
|
||||
DECLARE suf varchar(101); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
|
||||
DECLARE suf varchar(1001); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
|
||||
DECLARE i,j,k int UNSIGNED;
|
||||
IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers"
|
||||
LOOP
|
||||
|
|
165
migrations/Version20240728145604.php
Normal file
165
migrations/Version20240728145604.php
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20240728145604 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Update the Natural Sorting function for MySQL';
|
||||
}
|
||||
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
//Remove the old function
|
||||
$this->addSql('DROP FUNCTION IF EXISTS NatSortKey');
|
||||
|
||||
//The difference to the original function is the correct length of the suf variable and correct escaping
|
||||
//We now use heredoc syntax to avoid escaping issues with the \ (which resulted in "range out of order in character class").
|
||||
$this->addSql(<<<'EOD'
|
||||
CREATE DEFINER=CURRENT_USER FUNCTION `NatSortKey`(`s` VARCHAR(1000) CHARSET utf8mb4, `n` INT) RETURNS varchar(3500) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
|
||||
DETERMINISTIC
|
||||
SQL SECURITY INVOKER
|
||||
BEGIN
|
||||
/****
|
||||
Converts numbers in the input string s into a format such that sorting results in a nat-sort.
|
||||
Numbers of up to 359 digits (before the decimal point, if one is present) are supported. Sort results are undefined if the input string contains numbers longer than this.
|
||||
For n>0, only the first n numbers in the input string will be converted for nat-sort (so strings that differ only after the first n numbers will not nat-sort amongst themselves).
|
||||
Total sort-ordering is preserved, i.e. if s1!=s2, then NatSortKey(s1,n)!=NatSortKey(s2,n), for any given n.
|
||||
Numbers may contain ',' as a thousands separator, and '.' as a decimal point. To reverse these (as appropriate for some European locales), the code would require modification.
|
||||
Numbers preceded by '+' sort with numbers not preceded with either a '+' or '-' sign.
|
||||
Negative numbers (preceded with '-') sort before positive numbers, but are sorted in order of ascending absolute value (so -7 sorts BEFORE -1001).
|
||||
Numbers with leading zeros sort after the same number with no (or fewer) leading zeros.
|
||||
Decimal-part-only numbers (like .75) are recognised, provided the decimal point is not immediately preceded by either another '.', or by a letter-type character.
|
||||
Numbers with thousand separators sort after the same number without them.
|
||||
Thousand separators are only recognised in numbers with no leading zeros that don't immediately follow a ',', and when they format the number correctly.
|
||||
(When not recognised as a thousand separator, a ',' will instead be treated as separating two distinct numbers).
|
||||
Version-number-like sequences consisting of 3 or more numbers separated by '.' are treated as distinct entities, and each component number will be nat-sorted.
|
||||
The entire entity will sort after any number beginning with the first component (so e.g. 10.2.1 sorts after both 10 and 10.995, but before 11)
|
||||
Note that The first number component in an entity like this is also permitted to contain thousand separators.
|
||||
|
||||
To achieve this, numbers within the input string are prefixed and suffixed according to the following format:
|
||||
- The number is prefixed by a 2-digit base-36 number representing its length, excluding leading zeros. If there is a decimal point, this length only includes the integer part of the number.
|
||||
- A 3-character suffix is appended after the number (after the decimals if present).
|
||||
- The first character is a space, or a '+' sign if the number was preceded by '+'. Any preceding '+' sign is also removed from the front of the number.
|
||||
- This is followed by a 2-digit base-36 number that encodes the number of leading zeros and whether the number was expressed in comma-separated form (e.g. 1,000,000.25 vs 1000000.25)
|
||||
- The value of this 2-digit number is: (number of leading zeros)*2 + (1 if comma-separated, 0 otherwise)
|
||||
- For version number sequences, each component number has the prefix in front of it, and the separating dots are removed.
|
||||
Then there is a single suffix that consists of a ' ' or '+' character, followed by a pair base-36 digits for each number component in the sequence.
|
||||
|
||||
e.g. here is how some simple sample strings get converted:
|
||||
'Foo055' --> 'Foo0255 02'
|
||||
'Absolute zero is around -273 centigrade' --> 'Absolute zero is around -03273 00 centigrade'
|
||||
'The $1,000,000 prize' --> 'The $071000000 01 prize'
|
||||
'+99.74 degrees' --> '0299.74+00 degrees'
|
||||
'I have 0 apples' --> 'I have 00 02 apples'
|
||||
'.5 is the same value as 0000.5000' --> '00.5 00 is the same value as 00.5000 08'
|
||||
'MariaDB v10.3.0018' --> 'MariaDB v02100130218 000004'
|
||||
|
||||
The restriction to numbers of up to 359 digits comes from the fact that the first character of the base-36 prefix MUST be a decimal digit, and so the highest permitted prefix value is '9Z' or 359 decimal.
|
||||
The code could be modified to handle longer numbers by increasing the size of (both) the prefix and suffix.
|
||||
A higher base could also be used (by replacing CONV() with a custom function), provided that the collation you are using sorts the "digits" of the base in the correct order, starting with 0123456789.
|
||||
However, while the maximum number length may be increased this way, note that the technique this function uses is NOT applicable where strings may contain numbers of unlimited length.
|
||||
|
||||
The function definition does not specify the charset or collation to be used for string-type parameters or variables: The default database charset & collation at the time the function is defined will be used.
|
||||
This is to make the function code more portable. However, there are some important restrictions:
|
||||
|
||||
- Collation is important here only when comparing (or storing) the output value from this function, but it MUST order the characters " +0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" in that order for the natural sort to work.
|
||||
This is true for most collations, but not all of them, e.g. in Lithuanian 'Y' comes before 'J' (according to Wikipedia).
|
||||
To adapt the function to work with such collations, replace CONV() in the function code with a custom function that emits "digits" above 9 that are characters ordered according to the collation in use.
|
||||
|
||||
- For efficiency, the function code uses LENGTH() rather than CHAR_LENGTH() to measure the length of strings that consist only of digits 0-9, '.', and ',' characters.
|
||||
This works for any single-byte charset, as well as any charset that maps standard ASCII characters to single bytes (such as utf8 or utf8mb4).
|
||||
If using a charset that maps these characters to multiple bytes (such as, e.g. utf16 or utf32), you MUST replace all instances of LENGTH() in the function definition with CHAR_LENGTH()
|
||||
|
||||
Length of the output:
|
||||
|
||||
Each number converted adds 5 characters (2 prefix + 3 suffix) to the length of the string. n is the maximum count of numbers to convert;
|
||||
This parameter is provided as a means to limit the maximum output length (to input length + 5*n).
|
||||
If you do not require the total-ordering property, you could edit the code to use suffixes of 1 character (space or plus) only; this would reduce the maximum output length for any given n.
|
||||
Since a string of length L has at most ((L+1) DIV 2) individual numbers in it (every 2nd character a digit), for n<=0 the maximum output length is (inputlength + 5*((inputlength+1) DIV 2))
|
||||
So for the current input length of 100, the maximum output length is 350.
|
||||
If changing the input length, the output length must be modified according to the above formula. The DECLARE statements for x,y,r, and suf must also be modified, as the code comments indicate.
|
||||
****/
|
||||
|
||||
DECLARE x,y varchar(1000); # need to be same length as input s
|
||||
DECLARE r varchar(3500) DEFAULT ''; # return value: needs to be same length as return type
|
||||
DECLARE suf varchar(1001); # suffix for a number or version string. Must be (((inputlength+1) DIV 2)*2 + 1) chars to support version strings (e.g. '1.2.33.5'), though it's usually just 3 chars. (Max version string e.g. 1.2. ... .5 has ((length of input + 1) DIV 2) numeric components)
|
||||
DECLARE i,j,k int UNSIGNED;
|
||||
IF n<=0 THEN SET n := -1; END IF; # n<=0 means "process all numbers"
|
||||
LOOP
|
||||
SET i := REGEXP_INSTR(s,'\\d'); # find position of next digit
|
||||
IF i=0 OR n=0 THEN RETURN CONCAT(r,s); END IF; # no more numbers to process -> we're done
|
||||
SET n := n-1, suf := ' ';
|
||||
IF i>1 THEN
|
||||
IF SUBSTRING(s,i-1,1)='.' AND (i=2 OR SUBSTRING(s,i-2,1) RLIKE '[^.\\p{L}\\p{N}\\p{M}\\x{608}\\x{200C}\\x{200D}\\x{2100}-\\x{214F}\\x{24B6}-\\x{24E9}\\x{1F130}-\\x{1F149}\\x{1F150}-\\x{1F169}\\x{1F170}-\\x{1F189}]') AND (SUBSTRING(s,i) NOT RLIKE '^\\d++\\.\\d') THEN SET i:=i-1; END IF; # Allow decimal number (but not version string) to begin with a '.', provided preceding char is neither another '.', nor a member of the unicode character classes: "Alphabetic", "Letter", "Block=Letterlike Symbols" "Number", "Mark", "Join_Control"
|
||||
IF i>1 AND SUBSTRING(s,i-1,1)='+' THEN SET suf := '+', j := i-1; ELSE SET j := i; END IF; # move any preceding '+' into the suffix, so equal numbers with and without preceding "+" signs sort together
|
||||
SET r := CONCAT(r,SUBSTRING(s,1,j-1)); SET s = SUBSTRING(s,i); # add everything before the number to r and strip it from the start of s; preceding '+' is dropped (not included in either r or s)
|
||||
END IF;
|
||||
SET x := REGEXP_SUBSTR(s,IF(SUBSTRING(s,1,1) IN ('0','.') OR (SUBSTRING(r,-1)=',' AND suf=' '),'^\\d*+(?:\\.\\d++)*','^(?:[1-9]\\d{0,2}(?:,\\d{3}(?!\\d))++|\\d++)(?:\\.\\d++)*+')); # capture the number + following decimals (including multiple consecutive '.<digits>' sequences)
|
||||
SET s := SUBSTRING(s,CHAR_LENGTH(x)+1); # NOTE: CHAR_LENGTH() can be safely used instead of CHAR_LENGTH() here & below PROVIDED we're using a charset that represents digits, ',' and '.' characters using single bytes (e.g. latin1, utf8)
|
||||
SET i := INSTR(x,'.');
|
||||
IF i=0 THEN SET y := ''; ELSE SET y := SUBSTRING(x,i); SET x := SUBSTRING(x,1,i-1); END IF; # move any following decimals into y
|
||||
SET i := CHAR_LENGTH(x);
|
||||
SET x := REPLACE(x,',','');
|
||||
SET j := CHAR_LENGTH(x);
|
||||
SET x := TRIM(LEADING '0' FROM x); # strip leading zeros
|
||||
SET k := CHAR_LENGTH(x);
|
||||
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294) + IF(i=j,0,1),10,36),2,'0')); # (j-k)*2 + IF(i=j,0,1) = (count of leading zeros)*2 + (1 if there are thousands-separators, 0 otherwise) Note the first term is bounded to <= base-36 'ZY' as it must fit within 2 characters
|
||||
SET i := LOCATE('.',y,2);
|
||||
IF i=0 THEN
|
||||
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x,y,suf); # k = count of digits in number, bounded to be <= '9Z' base-36
|
||||
ELSE # encode a version number (like 3.12.707, etc)
|
||||
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36
|
||||
WHILE CHAR_LENGTH(y)>0 AND n!=0 DO
|
||||
IF i=0 THEN SET x := SUBSTRING(y,2); SET y := ''; ELSE SET x := SUBSTRING(y,2,i-2); SET y := SUBSTRING(y,i); SET i := LOCATE('.',y,2); END IF;
|
||||
SET j := CHAR_LENGTH(x);
|
||||
SET x := TRIM(LEADING '0' FROM x); # strip leading zeros
|
||||
SET k := CHAR_LENGTH(x);
|
||||
SET r := CONCAT(r,LPAD(CONV(LEAST(k,359),10,36),2,'0'),x); # k = count of digits in number, bounded to be <= '9Z' base-36
|
||||
SET suf := CONCAT(suf,LPAD(CONV(LEAST((j-k)*2,1294),10,36),2,'0')); # (j-k)*2 = (count of leading zeros)*2, bounded to fit within 2 base-36 digits
|
||||
SET n := n-1;
|
||||
END WHILE;
|
||||
SET r := CONCAT(r,y,suf);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
EOD
|
||||
);
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
//Not needed
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
//Not needed
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
//Not needed
|
||||
}
|
||||
|
||||
public function postgreSQLUp(Schema $schema): void
|
||||
{
|
||||
//Not needed
|
||||
}
|
||||
|
||||
public function postgreSQLDown(Schema $schema): void
|
||||
{
|
||||
//Not needed
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ parameters:
|
|||
- src/Configuration/*
|
||||
- src/Doctrine/Purger/*
|
||||
- src/DataTables/Adapters/TwoStepORMAdapter.php
|
||||
- src/Form/Fixes/*
|
||||
- src/Translation/Fixes/*
|
||||
|
||||
|
||||
|
||||
|
|
22
rector.php
22
rector.php
|
@ -2,12 +2,17 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\CodeQuality\Rector\Identical\FlipTypeControlToUseExclusiveTypeRector;
|
||||
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Doctrine\Set\DoctrineSetList;
|
||||
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector;
|
||||
use Rector\PHPUnit\Set\PHPUnitSetList;
|
||||
use Rector\Set\ValueObject\LevelSetList;
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
use Rector\Symfony\CodeQuality\Rector\Class_\EventListenerToEventSubscriberRector;
|
||||
use Rector\Symfony\CodeQuality\Rector\ClassMethod\ActionSuffixRemoverRector;
|
||||
use Rector\Symfony\CodeQuality\Rector\MethodCall\LiteralGetToRequestClassConstantRector;
|
||||
use Rector\Symfony\Set\SymfonySetList;
|
||||
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
|
||||
|
||||
|
@ -55,5 +60,22 @@ return static function (RectorConfig $rectorConfig): void {
|
|||
|
||||
$rectorConfig->skip([
|
||||
CountArrayToEmptyArrayComparisonRector::class,
|
||||
//Leave our !== null checks alone
|
||||
FlipTypeControlToUseExclusiveTypeRector::class,
|
||||
//Leave our PartList TableAction alone
|
||||
ActionSuffixRemoverRector::class,
|
||||
//We declare event listeners via attributes, therefore no need to migrate them to subscribers
|
||||
EventListenerToEventSubscriberRector::class,
|
||||
PreferPHPUnitThisCallRector::class,
|
||||
//Do not replace 'GET' with class constant,
|
||||
LiteralGetToRequestClassConstantRector::class,
|
||||
]);
|
||||
|
||||
//Do not apply rules to Symfony own files
|
||||
$rectorConfig->skip([
|
||||
__DIR__ . '/public/index.php',
|
||||
__DIR__ . '/src/Kernel.php',
|
||||
__DIR__ . '/config/preload.php',
|
||||
__DIR__ . '/config/bundles.php',
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -81,12 +81,12 @@ class EntityFilterHelper
|
|||
|
||||
public function getDescription(array $properties): array
|
||||
{
|
||||
if (!$properties) {
|
||||
if ($properties === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$description = [];
|
||||
foreach ($properties as $property => $strategy) {
|
||||
foreach (array_keys($properties) as $property) {
|
||||
$description[(string)$property] = [
|
||||
'property' => $property,
|
||||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
|
|
|
@ -61,7 +61,7 @@ final class LikeFilter extends AbstractFilter
|
|||
}
|
||||
|
||||
$description = [];
|
||||
foreach ($this->properties as $property => $strategy) {
|
||||
foreach (array_keys($this->properties) as $property) {
|
||||
$description[(string)$property] = [
|
||||
'property' => $property,
|
||||
'type' => Type::BUILTIN_TYPE_STRING,
|
||||
|
|
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(
|
||||
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_1'),
|
||||
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_2'),
|
||||
$expr->like('o.'.$property, ':' . $tag_identifier_prefix . '_3'),
|
||||
$expr->eq('o.'.$property, ':' . $tag_identifier_prefix . '_4'),
|
||||
);
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@ class BackupCommand extends Command
|
|||
$backup_attachments = $input->getOption('attachments');
|
||||
$backup_config = $input->getOption('config');
|
||||
$backup_full = $input->getOption('full');
|
||||
$overwrite = $input->getOption('overwrite');
|
||||
|
||||
if ($backup_full) {
|
||||
$backup_database = true;
|
||||
|
@ -70,7 +71,9 @@ class BackupCommand extends Command
|
|||
|
||||
//Check if the file already exists
|
||||
//Then ask the user, if he wants to overwrite the file
|
||||
if (file_exists($output_filepath) && !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
|
||||
if (!$overwrite
|
||||
&& file_exists($output_filepath)
|
||||
&& !$io->confirm('The file '.realpath($output_filepath).' already exists. Do you want to overwrite it?', false)) {
|
||||
$io->error('Backup aborted!');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -35,6 +35,7 @@ use App\Entity\LabelSystem\LabelProcessMode;
|
|||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use App\Exceptions\TwigModeException;
|
||||
use App\Form\AdminPages\ImportType;
|
||||
use App\Form\AdminPages\MassCreationForm;
|
||||
use App\Repository\AbstractPartsContainingRepository;
|
||||
|
@ -52,8 +53,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
use InvalidArgumentException;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
@ -61,6 +62,8 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
@ -74,15 +77,10 @@ abstract class BaseAdminController extends AbstractController
|
|||
protected string $attachment_class = '';
|
||||
protected ?string $parameter_class = '';
|
||||
|
||||
/**
|
||||
* @var EventDispatcher|EventDispatcherInterface
|
||||
*/
|
||||
protected EventDispatcher|EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(protected TranslatorInterface $translator, protected UserPasswordHasherInterface $passwordEncoder,
|
||||
protected AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
protected EventCommentHelper $commentHelper, protected HistoryHelper $historyHelper, protected TimeTravel $timeTravel,
|
||||
protected DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator,
|
||||
protected DataTableFactory $dataTableFactory, protected EventDispatcherInterface $eventDispatcher, protected LabelExampleElementsGenerator $barcodeExampleGenerator,
|
||||
protected LabelGenerator $labelGenerator, protected EntityManagerInterface $entityManager)
|
||||
{
|
||||
if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
|
||||
|
@ -96,7 +94,6 @@ abstract class BaseAdminController extends AbstractController
|
|||
if ('' === $this->parameter_class || ($this->parameter_class && !is_a($this->parameter_class, AbstractParameter::class, true))) {
|
||||
throw new InvalidArgumentException('You have to override the $parameter_class value with a valid Parameter class in your subclass!');
|
||||
}
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?DateTime
|
||||
|
@ -192,11 +189,9 @@ abstract class BaseAdminController extends AbstractController
|
|||
}
|
||||
|
||||
//Ensure that the master picture is still part of the attachments
|
||||
if ($entity instanceof AttachmentContainingDBElement) {
|
||||
if ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment())) {
|
||||
if ($entity instanceof AttachmentContainingDBElement && ($entity->getMasterPictureAttachment() !== null && !$entity->getAttachments()->contains($entity->getMasterPictureAttachment()))) {
|
||||
$entity->setMasterPictureAttachment(null);
|
||||
}
|
||||
}
|
||||
|
||||
$this->commentHelper->setMessage($form['log_comment']->getData());
|
||||
|
||||
|
@ -218,7 +213,12 @@ abstract class BaseAdminController extends AbstractController
|
|||
//Show preview for LabelProfile if needed.
|
||||
if ($entity instanceof LabelProfile) {
|
||||
$example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
|
||||
$pdf_data = null;
|
||||
try {
|
||||
$pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
|
||||
} catch (TwigModeException $exception) {
|
||||
$form->get('options')->get('lines')->addError(new FormError($exception->getSafeMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/** @var AbstractPartsContainingRepository $repo */
|
||||
|
@ -283,11 +283,9 @@ abstract class BaseAdminController extends AbstractController
|
|||
}
|
||||
|
||||
//Ensure that the master picture is still part of the attachments
|
||||
if ($new_entity instanceof AttachmentContainingDBElement) {
|
||||
if ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment())) {
|
||||
if ($new_entity instanceof AttachmentContainingDBElement && ($new_entity->getMasterPictureAttachment() !== null && !$new_entity->getAttachments()->contains($new_entity->getMasterPictureAttachment()))) {
|
||||
$new_entity->setMasterPictureAttachment(null);
|
||||
}
|
||||
}
|
||||
|
||||
$this->commentHelper->setMessage($form['log_comment']->getData());
|
||||
$em->persist($new_entity);
|
||||
|
@ -333,8 +331,8 @@ abstract class BaseAdminController extends AbstractController
|
|||
try {
|
||||
$errors = $importer->importFileAndPersistToDB($file, $options);
|
||||
|
||||
foreach ($errors as $name => $error) {
|
||||
foreach ($error as $violation) {
|
||||
foreach ($errors as $name => ['violations' => $violations]) {
|
||||
foreach ($violations as $violation) {
|
||||
$this->addFlash('error', $name.': '.$violation->getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -344,6 +342,7 @@ abstract class BaseAdminController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
ret:
|
||||
//Mass creation form
|
||||
$mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
|
||||
$mass_creation_form->handleRequest($request);
|
||||
|
@ -356,11 +355,14 @@ abstract class BaseAdminController extends AbstractController
|
|||
$results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors);
|
||||
|
||||
//Show errors to user:
|
||||
foreach ($errors as $error) {
|
||||
if ($error['entity'] instanceof AbstractStructuralDBElement) {
|
||||
$this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
|
||||
foreach ($errors as ['entity' => $new_entity, 'violations' => $violations]) {
|
||||
/** @var ConstraintViolationInterface $violation */
|
||||
foreach ($violations as $violation) {
|
||||
if ($new_entity instanceof AbstractStructuralDBElement) {
|
||||
$this->addFlash('error', $new_entity->getFullPath().':'.$violation->getMessage());
|
||||
} else { //When we don't have a structural element, we can only show the name
|
||||
$this->addFlash('error', $error['entity']->getName().':'.$error['violations']);
|
||||
$this->addFlash('error', $new_entity->getName().':'.$violation->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,7 +377,6 @@ abstract class BaseAdminController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
ret:
|
||||
return $this->render($this->twig_template, [
|
||||
'entity' => $new_entity,
|
||||
'form' => $form,
|
||||
|
|
|
@ -52,11 +52,11 @@ class AttachmentFileController extends AbstractController
|
|||
}
|
||||
|
||||
if ($attachment->isExternal()) {
|
||||
throw new RuntimeException('You can not download external attachments!');
|
||||
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
|
||||
}
|
||||
|
||||
if (!$helper->isFileExisting($attachment)) {
|
||||
throw new RuntimeException('The file associated with the attachment is not existing!');
|
||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||
}
|
||||
|
||||
$file_path = $helper->toAbsoluteFilePath($attachment);
|
||||
|
@ -81,11 +81,11 @@ class AttachmentFileController extends AbstractController
|
|||
}
|
||||
|
||||
if ($attachment->isExternal()) {
|
||||
throw new RuntimeException('You can not download external attachments!');
|
||||
throw $this->createNotFoundException('The file for this attachment is external and can not stored locally!');
|
||||
}
|
||||
|
||||
if (!$helper->isFileExisting($attachment)) {
|
||||
throw new RuntimeException('The file associated with the attachment is not existing!');
|
||||
throw $this->createNotFoundException('The file associated with the attachment is not existing!');
|
||||
}
|
||||
|
||||
$file_path = $helper->toAbsoluteFilePath($attachment);
|
||||
|
|
|
@ -30,6 +30,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Controller\KiCadApiControllerTest
|
||||
*/
|
||||
#[Route('/kicad-api/v1')]
|
||||
class KiCadApiController extends AbstractController
|
||||
{
|
||||
|
@ -62,7 +65,7 @@ class KiCadApiController extends AbstractController
|
|||
#[Route('/parts/category/{category}.json', name: 'kicad_api_category')]
|
||||
public function categoryParts(?Category $category): Response
|
||||
{
|
||||
if ($category) {
|
||||
if ($category !== null) {
|
||||
$this->denyAccessUnlessGranted('read', $category);
|
||||
} else {
|
||||
$this->denyAccessUnlessGranted('@categories.read');
|
||||
|
|
|
@ -117,7 +117,7 @@ class LabelController extends AbstractController
|
|||
$pdf_data = $this->labelGenerator->generateLabel($form_options, $targets);
|
||||
$filename = $this->getLabelName($targets[0], $profile);
|
||||
} catch (TwigModeException $exception) {
|
||||
$form->get('options')->get('lines')->addError(new FormError($exception->getMessage()));
|
||||
$form->get('options')->get('lines')->addError(new FormError($exception->getSafeMessage()));
|
||||
}
|
||||
} else {
|
||||
//$this->addFlash('warning', 'label_generator.no_entities_found');
|
||||
|
|
|
@ -51,7 +51,7 @@ class OAuthClientController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route('/{name}/check', name: 'oauth_client_check')]
|
||||
public function check(string $name, Request $request): Response
|
||||
public function check(string $name): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.manage_oauth_tokens');
|
||||
|
||||
|
|
|
@ -219,7 +219,7 @@ class ProjectController extends AbstractController
|
|||
'project' => $project,
|
||||
'part' => $part
|
||||
]);
|
||||
if ($bom_entry) {
|
||||
if ($bom_entry !== null) {
|
||||
$preset_data->add($bom_entry);
|
||||
} else { //Otherwise create an empty one
|
||||
$entry = new ProjectBOMEntry();
|
||||
|
|
|
@ -54,6 +54,9 @@ use Symfony\Component\HttpFoundation\Response;
|
|||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Controller\ScanControllerTest
|
||||
*/
|
||||
#[Route(path: '/scan')]
|
||||
class ScanController extends AbstractController
|
||||
{
|
||||
|
|
|
@ -25,12 +25,14 @@ namespace App\Controller;
|
|||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use App\Services\Attachments\AttachmentURLGenerator;
|
||||
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
||||
use App\Services\Doctrine\DBInfoHelper;
|
||||
use App\Services\Doctrine\NatsortDebugHelper;
|
||||
use App\Services\Misc\GitVersionInfo;
|
||||
use App\Services\Misc\DBInfoHelper;
|
||||
use App\Services\System\UpdateAvailableManager;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Runtime\SymfonyRuntime;
|
||||
|
||||
#[Route(path: '/tools')]
|
||||
class ToolsController extends AbstractController
|
||||
|
@ -44,7 +46,7 @@ class ToolsController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route(path: '/server_infos', name: 'tools_server_infos')]
|
||||
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper,
|
||||
public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper,
|
||||
AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@system.server_infos');
|
||||
|
@ -59,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'),
|
||||
|
@ -84,7 +86,7 @@ class ToolsController extends AbstractController
|
|||
'php_post_max_size' => ini_get('post_max_size'),
|
||||
'kernel_runtime_environment' => $this->getParameter('kernel.runtime_environment'),
|
||||
'kernel_runtime_mode' => $this->getParameter('kernel.runtime_mode'),
|
||||
'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime',
|
||||
'kernel_runtime' => $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? SymfonyRuntime::class,
|
||||
|
||||
//DB section
|
||||
'db_type' => $DBInfoHelper->getDatabaseType() ?? 'Unknown',
|
||||
|
@ -92,6 +94,8 @@ class ToolsController extends AbstractController
|
|||
'db_size' => $DBInfoHelper->getDatabaseSize(),
|
||||
'db_name' => $DBInfoHelper->getDatabaseName() ?? 'Unknown',
|
||||
'db_user' => $DBInfoHelper->getDatabaseUsername() ?? 'Unknown',
|
||||
'db_natsort_method' => $natsortDebugHelper->getNaturalSortMethod(),
|
||||
'db_natsort_slow_allowed' => $natsortDebugHelper->isSlowNaturalSortAllowed(),
|
||||
|
||||
//New version section
|
||||
'new_version_available' => $updateAvailableManager->isUpdateAvailable(),
|
||||
|
|
|
@ -38,7 +38,6 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
use RuntimeException;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
|
@ -59,11 +58,8 @@ use Symfony\Component\Validator\Constraints\Length;
|
|||
#[Route(path: '/user')]
|
||||
class UserSettingsController extends AbstractController
|
||||
{
|
||||
protected EventDispatcher|EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(protected bool $demo_mode, EventDispatcherInterface $eventDispatcher)
|
||||
public function __construct(protected bool $demo_mode, protected EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
#[Route(path: '/2fa_backup_codes', name: 'show_backup_codes')]
|
||||
|
|
|
@ -75,7 +75,7 @@ class APITokenFixtures extends Fixture implements DependentFixtureInterface
|
|||
$expired_token->setUser($admin_user);
|
||||
$expired_token->setLevel(ApiTokenLevel::FULL);
|
||||
$expired_token->setName('expired');
|
||||
$expired_token->setValidUntil(new \DateTime('-1 day'));
|
||||
$expired_token->setValidUntil(new \DateTimeImmutable('-1 day'));
|
||||
$this->setTokenSecret($expired_token, self::TOKEN_EXPIRED);
|
||||
$manager->persist($expired_token);
|
||||
|
||||
|
|
|
@ -74,30 +74,37 @@ class DataStructureFixtures extends Fixture implements DependentFixtureInterface
|
|||
/** @var AbstractStructuralDBElement $node1 */
|
||||
$node1 = new $class();
|
||||
$node1->setName('Node 1');
|
||||
$this->addReference($class . '_1', $node1);
|
||||
|
||||
/** @var AbstractStructuralDBElement $node2 */
|
||||
$node2 = new $class();
|
||||
$node2->setName('Node 2');
|
||||
$this->addReference($class . '_2', $node2);
|
||||
|
||||
/** @var AbstractStructuralDBElement $node3 */
|
||||
$node3 = new $class();
|
||||
$node3->setName('Node 3');
|
||||
$this->addReference($class . '_3', $node3);
|
||||
|
||||
$node1_1 = new $class();
|
||||
$node1_1->setName('Node 1.1');
|
||||
$node1_1->setParent($node1);
|
||||
$this->addReference($class . '_4', $node1_1);
|
||||
|
||||
$node1_2 = new $class();
|
||||
$node1_2->setName('Node 1.2');
|
||||
$node1_2->setParent($node1);
|
||||
$this->addReference($class . '_5', $node1_2);
|
||||
|
||||
$node2_1 = new $class();
|
||||
$node2_1->setName('Node 2.1');
|
||||
$node2_1->setParent($node2);
|
||||
$this->addReference($class . '_6', $node2_1);
|
||||
|
||||
$node1_1_1 = new $class();
|
||||
$node1_1_1->setName('Node 1.1.1');
|
||||
$node1_1_1->setParent($node1_1);
|
||||
$this->addReference($class . '_7', $node1_1_1);
|
||||
|
||||
$manager->persist($node1);
|
||||
$manager->persist($node2);
|
||||
|
|
106
src/DataFixtures/LogEntryFixtures.php
Normal file
106
src/DataFixtures/LogEntryFixtures.php
Normal file
|
@ -0,0 +1,106 @@
|
|||
<?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\DataFixtures;
|
||||
|
||||
use App\Entity\LogSystem\ElementCreatedLogEntry;
|
||||
use App\Entity\LogSystem\ElementDeletedLogEntry;
|
||||
use App\Entity\LogSystem\ElementEditedLogEntry;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LogEntryFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->createCategoryEntries($manager);
|
||||
$this->createDeletedCategory($manager);
|
||||
}
|
||||
|
||||
public function createCategoryEntries(ObjectManager $manager): void
|
||||
{
|
||||
$category = $this->getReference(Category::class . '_1', Category::class);
|
||||
|
||||
$logEntry = new ElementCreatedLogEntry($category);
|
||||
$logEntry->setTimestamp(new \DateTimeImmutable("+1 second"));
|
||||
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
|
||||
$logEntry->setComment('Test');
|
||||
$manager->persist($logEntry);
|
||||
|
||||
$logEntry = new ElementEditedLogEntry($category);
|
||||
$logEntry->setTimestamp(new \DateTimeImmutable("+2 second"));
|
||||
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
|
||||
$logEntry->setComment('Test');
|
||||
|
||||
$logEntry->setOldData(['name' => 'Test']);
|
||||
$logEntry->setNewData(['name' => 'Node 1.1']);
|
||||
|
||||
$manager->persist($logEntry);
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
public function createDeletedCategory(ObjectManager $manager): void
|
||||
{
|
||||
//We create a fictive category to test the deletion
|
||||
$category = new Category();
|
||||
$category->setName('Node 100');
|
||||
|
||||
//Assume a category with id 100 was deleted
|
||||
$reflClass = new \ReflectionClass($category);
|
||||
$reflClass->getProperty('id')->setValue($category, 100);
|
||||
|
||||
//The whole lifecycle from creation to deletion
|
||||
$logEntry = new ElementCreatedLogEntry($category);
|
||||
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
|
||||
$logEntry->setComment('Creation');
|
||||
$manager->persist($logEntry);
|
||||
|
||||
$logEntry = new ElementEditedLogEntry($category);
|
||||
$logEntry->setTimestamp(new \DateTimeImmutable("+1 second"));
|
||||
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
|
||||
$logEntry->setComment('Edit');
|
||||
$logEntry->setOldData(['name' => 'Test']);
|
||||
$logEntry->setNewData(['name' => 'Node 100']);
|
||||
$manager->persist($logEntry);
|
||||
|
||||
$logEntry = new ElementDeletedLogEntry($category);
|
||||
$logEntry->setTimestamp(new \DateTimeImmutable("+2 second"));
|
||||
$logEntry->setUser($this->getReference(UserFixtures::ADMIN, User::class));
|
||||
$logEntry->setOldData(['name' => 'Node 100', 'id' => 100, 'comment' => 'Test comment']);
|
||||
$manager->persist($logEntry);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [
|
||||
UserFixtures::class,
|
||||
DataStructureFixtures::class
|
||||
];
|
||||
}
|
||||
}
|
|
@ -73,6 +73,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
$part = new Part();
|
||||
$part->setName('Part 1');
|
||||
$part->setCategory($manager->find(Category::class, 1));
|
||||
$this->addReference(Part::class . '_1', $part);
|
||||
$manager->persist($part);
|
||||
|
||||
/** More complex part */
|
||||
|
@ -86,6 +87,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
$part->setIpn('IPN123');
|
||||
$part->setNeedsReview(true);
|
||||
$part->setManufacturingStatus(ManufacturingStatus::ACTIVE);
|
||||
$this->addReference(Part::class . '_2', $part);
|
||||
$manager->persist($part);
|
||||
|
||||
/** Part with orderdetails, storelocations and Attachments */
|
||||
|
@ -98,8 +100,9 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
$partLot1->setStorageLocation($manager->find(StorageLocation::class, 1));
|
||||
$part->addPartLot($partLot1);
|
||||
|
||||
|
||||
$partLot2 = new PartLot();
|
||||
$partLot2->setExpirationDate(new DateTime());
|
||||
$partLot2->setExpirationDate(new \DateTimeImmutable());
|
||||
$partLot2->setComment('Test');
|
||||
$partLot2->setNeedsRefill(true);
|
||||
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
|
||||
|
@ -133,6 +136,8 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
|
|||
$attachment->setAttachmentType($manager->find(AttachmentType::class, 1));
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->addReference(Part::class . '_3', $part);
|
||||
|
||||
$manager->persist($part);
|
||||
$manager->flush();
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\DataTables\Adapters;
|
||||
|
||||
use Doctrine\ORM\Query\Expr\From;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
|
@ -51,12 +52,12 @@ class TwoStepORMAdapter extends ORMAdapter
|
|||
|
||||
private bool $use_simple_total = false;
|
||||
|
||||
private \Closure|null $query_modifier;
|
||||
private \Closure|null $query_modifier = null;
|
||||
|
||||
public function __construct(ManagerRegistry $registry = null)
|
||||
{
|
||||
parent::__construct($registry);
|
||||
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids) {
|
||||
$this->detailQueryCallable = static function (QueryBuilder $qb, array $ids): never {
|
||||
throw new \RuntimeException('You need to set the detail_query option to use the TwoStepORMAdapter');
|
||||
};
|
||||
}
|
||||
|
@ -66,9 +67,7 @@ class TwoStepORMAdapter extends ORMAdapter
|
|||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setRequired('filter_query');
|
||||
$resolver->setDefault('query', function (Options $options) {
|
||||
return $options['filter_query'];
|
||||
});
|
||||
$resolver->setDefault('query', fn(Options $options) => $options['filter_query']);
|
||||
|
||||
$resolver->setRequired('detail_query');
|
||||
$resolver->setAllowedTypes('detail_query', \Closure::class);
|
||||
|
@ -108,7 +107,7 @@ class TwoStepORMAdapter extends ORMAdapter
|
|||
}
|
||||
}
|
||||
|
||||
/** @var Query\Expr\From $fromClause */
|
||||
/** @var From $fromClause */
|
||||
$fromClause = $builder->getDQLPart('from')[0];
|
||||
$identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}";
|
||||
|
||||
|
@ -201,7 +200,7 @@ class TwoStepORMAdapter extends ORMAdapter
|
|||
/** The paginator count queries can be rather slow, so when query for total count (100ms or longer),
|
||||
* just return the entity count.
|
||||
*/
|
||||
/** @var Query\Expr\From $from_expr */
|
||||
/** @var From $from_expr */
|
||||
$from_expr = $queryBuilder->getDQLPart('from')[0];
|
||||
|
||||
return $this->manager->getRepository($from_expr->getFrom())->count([]);
|
||||
|
|
|
@ -34,6 +34,7 @@ use App\Services\EntityURLGenerator;
|
|||
use Doctrine\ORM\QueryBuilder;
|
||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
|
||||
use Omines\DataTablesBundle\Column\NumberColumn;
|
||||
use Omines\DataTablesBundle\Column\TextColumn;
|
||||
use Omines\DataTablesBundle\DataTable;
|
||||
use Omines\DataTablesBundle\DataTableTypeInterface;
|
||||
|
@ -84,6 +85,11 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
},
|
||||
]);
|
||||
|
||||
$dataTable->add('id', NumberColumn::class, [
|
||||
'label' => $this->translator->trans('part.table.id'),
|
||||
'visible' => false,
|
||||
]);
|
||||
|
||||
$dataTable->add('name', TextColumn::class, [
|
||||
'label' => 'attachment.edit.name',
|
||||
'orderField' => 'NATSORT(attachment.name)',
|
||||
|
@ -92,7 +98,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
if ($context->isExternal()) {
|
||||
return sprintf(
|
||||
'<a href="%s" class="link-external">%s</a>',
|
||||
htmlspecialchars($context->getURL()),
|
||||
htmlspecialchars((string) $context->getURL()),
|
||||
htmlspecialchars($value)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\DataTables\Column;
|
||||
|
||||
use Omines\DataTablesBundle\Column\AbstractColumn;
|
||||
|
|
|
@ -47,7 +47,7 @@ class LocaleDateTimeColumn extends AbstractColumn
|
|||
}
|
||||
|
||||
if (!$value instanceof DateTimeInterface) {
|
||||
$value = new DateTime((string) $value);
|
||||
$value = new \DateTimeImmutable((string) $value);
|
||||
}
|
||||
|
||||
$formatValues = [
|
||||
|
|
|
@ -22,9 +22,92 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An alias of NumberConstraint to use to filter on a DateTime
|
||||
* Similar to NumberConstraint but for DateTime values
|
||||
*/
|
||||
class DateTimeConstraint extends NumberConstraint
|
||||
class DateTimeConstraint extends AbstractConstraint
|
||||
{
|
||||
protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN'];
|
||||
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
protected \DateTimeInterface|null $value1 = null,
|
||||
protected ?string $operator = null,
|
||||
/**
|
||||
* The second value used when operator is RANGE; this is the upper bound of the range
|
||||
*/
|
||||
protected \DateTimeInterface|null $value2 = null)
|
||||
{
|
||||
parent::__construct($property, $identifier);
|
||||
}
|
||||
|
||||
public function getValue1(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->value1;
|
||||
}
|
||||
|
||||
public function setValue1(\DateTimeInterface|null $value1): void
|
||||
{
|
||||
$this->value1 = $value1;
|
||||
}
|
||||
|
||||
public function getValue2(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->value2;
|
||||
}
|
||||
|
||||
public function setValue2(?\DateTimeInterface $value2): void
|
||||
{
|
||||
$this->value2 = $value2;
|
||||
}
|
||||
|
||||
public function getOperator(): string|null
|
||||
{
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $operator
|
||||
*/
|
||||
public function setOperator(?string $operator): void
|
||||
{
|
||||
$this->operator = $operator;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->value1 !== null
|
||||
&& ($this->operator !== null && $this->operator !== '');
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
//If no value is provided then we do not apply a filter
|
||||
if (!$this->isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Ensure we have an valid operator
|
||||
if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) {
|
||||
throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
|
||||
}
|
||||
|
||||
if ($this->operator !== 'BETWEEN') {
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value1);
|
||||
} else {
|
||||
if ($this->value2 === null) {
|
||||
throw new RuntimeException("Cannot use operator BETWEEN without value2!");
|
||||
}
|
||||
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1);
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ class EntityConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
//We need to handle null values differently, as they can not be compared with == or !=
|
||||
if (!$this->value instanceof AbstractDBElement) {
|
||||
if ($this->value === null) {
|
||||
if($this->operator === '=' || $this->operator === 'INCLUDING_CHILDREN') {
|
||||
$queryBuilder->andWhere(sprintf("%s IS NULL", $this->property));
|
||||
return;
|
||||
|
@ -152,7 +152,8 @@ class EntityConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
if($this->operator === '=' || $this->operator === '!=') {
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value);
|
||||
//Include null values on != operator, so that really all values are returned that are not equal to the given value
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value, $this->operator === '!=');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -168,7 +169,8 @@ class EntityConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
if ($this->operator === 'EXCLUDING_CHILDREN') {
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list);
|
||||
//Include null values in the result, so that all elements that are not in the list are returned
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list, true);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -56,8 +56,14 @@ trait FilterTrait
|
|||
|
||||
/**
|
||||
* Adds a simple constraint in the form of (property OPERATOR value) (e.g. "part.name = :name") to the given query builder.
|
||||
* @param QueryBuilder $queryBuilder The query builder to add the constraint to
|
||||
* @param string $property The property to compare
|
||||
* @param string $parameterIdentifier The identifier for the parameter
|
||||
* @param string $comparison_operator The comparison operator to use
|
||||
* @param mixed $value The value to compare to
|
||||
* @param bool $include_null If true, the result of this constraint will also include null values of this property (useful for exclusion filters)
|
||||
*/
|
||||
protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, mixed $value): void
|
||||
protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, mixed $value, bool $include_null = false): void
|
||||
{
|
||||
if ($comparison_operator === 'IN' || $comparison_operator === 'NOT IN') {
|
||||
$expression = sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier);
|
||||
|
@ -65,6 +71,10 @@ trait FilterTrait
|
|||
$expression = sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier);
|
||||
}
|
||||
|
||||
if ($include_null) {
|
||||
$expression = sprintf("(%s OR %s IS NULL)", $expression, $property);
|
||||
}
|
||||
|
||||
if($this->useHaving || $this->isAggregateFunctionString($property)) { //If the property is an aggregate function, we have to use the "having" instead of the "where"
|
||||
$queryBuilder->andHaving($expression);
|
||||
} else {
|
||||
|
|
|
@ -29,12 +29,28 @@ class NumberConstraint extends AbstractConstraint
|
|||
{
|
||||
protected const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN'];
|
||||
|
||||
public function getValue1(): float|int|null|\DateTimeInterface
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
protected float|int|null $value1 = null,
|
||||
protected ?string $operator = null,
|
||||
/**
|
||||
* The second value used when operator is RANGE; this is the upper bound of the range
|
||||
*/
|
||||
protected float|int|null $value2 = null)
|
||||
{
|
||||
parent::__construct($property, $identifier);
|
||||
}
|
||||
|
||||
public function getValue1(): float|int|null
|
||||
{
|
||||
return $this->value1;
|
||||
}
|
||||
|
||||
public function setValue1(float|int|\DateTimeInterface|null $value1): void
|
||||
public function setValue1(float|int|null $value1): void
|
||||
{
|
||||
$this->value1 = $value1;
|
||||
}
|
||||
|
@ -63,22 +79,6 @@ class NumberConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
|
||||
public function __construct(
|
||||
string $property,
|
||||
string $identifier = null,
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
*/
|
||||
protected float|int|\DateTimeInterface|null $value1 = null,
|
||||
protected ?string $operator = null,
|
||||
/**
|
||||
* The second value used when operator is RANGE; this is the upper bound of the range
|
||||
*/
|
||||
protected float|int|\DateTimeInterface|null $value2 = null)
|
||||
{
|
||||
parent::__construct($property, $identifier);
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->value1 !== null
|
||||
|
@ -105,7 +105,13 @@ class NumberConstraint extends AbstractConstraint
|
|||
}
|
||||
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1);
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2);
|
||||
|
||||
//Workaround for the amountSum which we need to add twice on postgres. Replace one of the __ with __2 to make it work
|
||||
//Otherwise we get an error, that __partLot was already defined
|
||||
|
||||
$property2 = str_replace('__', '__2', $this->property);
|
||||
|
||||
$this->addSimpleAndConstraint($queryBuilder, $property2, $this->identifier . '2', '<=', $this->value2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,15 +30,8 @@ class TagsConstraint extends AbstractConstraint
|
|||
{
|
||||
final public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE'];
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct(string $property, string $identifier = null, /**
|
||||
* @var string The value to compare to
|
||||
*/
|
||||
protected $value = null, /**
|
||||
* @var string|null The operator to use
|
||||
*/
|
||||
public function __construct(string $property, string $identifier = null,
|
||||
protected ?string $value = null,
|
||||
protected ?string $operator = '')
|
||||
{
|
||||
parent::__construct($property, $identifier);
|
||||
|
@ -61,12 +54,12 @@ class TagsConstraint extends AbstractConstraint
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string $value): self
|
||||
public function setValue(?string $value): self
|
||||
{
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
|
@ -92,6 +85,9 @@ 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();
|
||||
|
|
|
@ -33,9 +33,9 @@ class TextConstraint extends AbstractConstraint
|
|||
* @param string $value
|
||||
*/
|
||||
public function __construct(string $property, string $identifier = null, /**
|
||||
* @var string The value to compare to
|
||||
* @var string|null The value to compare to
|
||||
*/
|
||||
protected $value = null, /**
|
||||
protected ?string $value = null, /**
|
||||
* @var string|null The operator to use
|
||||
*/
|
||||
protected ?string $operator = '')
|
||||
|
@ -60,12 +60,12 @@ class TextConstraint extends AbstractConstraint
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string $value): self
|
||||
public function setValue(?string $value): self
|
||||
{
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
|
|
|
@ -109,7 +109,7 @@ class ColumnSortHelper
|
|||
}
|
||||
|
||||
//and the remaining non-visible columns
|
||||
foreach ($this->columns as $col_id => $col_data) {
|
||||
foreach (array_keys($this->columns) as $col_id) {
|
||||
if (in_array($col_id, $processed_columns, true)) {
|
||||
// column already processed
|
||||
continue;
|
||||
|
|
|
@ -162,7 +162,7 @@ class LogDataTable implements DataTableTypeInterface
|
|||
if (!$user instanceof User) {
|
||||
if ($context->isCLIEntry()) {
|
||||
return sprintf('%s [%s]',
|
||||
htmlentities($context->getCLIUsername()),
|
||||
htmlentities((string) $context->getCLIUsername()),
|
||||
$this->translator->trans('log.cli_user')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -137,7 +137,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')
|
||||
|
||||
|
@ -156,7 +157,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
'orderField' => 'NATSORT(_partUnit.name)',
|
||||
'render' => function($value, Part $context): string {
|
||||
$partUnit = $context->getPartUnit();
|
||||
if (!$partUnit) {
|
||||
if ($partUnit === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -184,7 +185,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
'label' => $this->translator->trans('part.table.manufacturingStatus'),
|
||||
'class' => ManufacturingStatus::class,
|
||||
'render' => function (?ManufacturingStatus $status, Part $context): string {
|
||||
if (!$status) {
|
||||
if ($status === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
'orderField' => 'NATSORT(part.name)',
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
if(!$context->getPart() instanceof Part) {
|
||||
return htmlspecialchars($context->getName());
|
||||
return htmlspecialchars((string) $context->getName());
|
||||
}
|
||||
if($context->getPart() instanceof Part) {
|
||||
$tmp = $this->partDataTableHelper->renderName($context->getPart());
|
||||
|
@ -154,7 +154,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
'label' => 'project.bom.instockAmount',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
if ($context->getPart()) {
|
||||
if ($context->getPart() !== null) {
|
||||
return $this->partDataTableHelper->renderAmount($context->getPart());
|
||||
}
|
||||
|
||||
|
@ -165,7 +165,7 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
|
|||
'label' => 'part.table.storeLocations',
|
||||
'visible' => false,
|
||||
'render' => function ($value, ProjectBOMEntry $context) {
|
||||
if ($context->getPart()) {
|
||||
if ($context->getPart() !== null) {
|
||||
return $this->partDataTableHelper->renderStorageLocations($context->getPart());
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Doctrine\Functions;
|
||||
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\TokenType;
|
||||
|
||||
|
@ -36,7 +38,7 @@ class Field2 extends FunctionNode
|
|||
|
||||
private $values = [];
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser): void
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
|
@ -58,15 +60,16 @@ class Field2 extends FunctionNode
|
|||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker): string
|
||||
public function getSql(SqlWalker $sqlWalker): string
|
||||
{
|
||||
$query = 'FIELD2(';
|
||||
|
||||
$query .= $this->field->dispatch($sqlWalker);
|
||||
|
||||
$query .= ', ';
|
||||
$counter = count($this->values);
|
||||
|
||||
for ($i = 0; $i < count($this->values); $i++) {
|
||||
for ($i = 0; $i < $counter; $i++) {
|
||||
if ($i > 0) {
|
||||
$query .= ', ';
|
||||
}
|
||||
|
@ -74,8 +77,6 @@ class Field2 extends FunctionNode
|
|||
$query .= $this->values[$i]->dispatch($sqlWalker);
|
||||
}
|
||||
|
||||
$query .= ')';
|
||||
|
||||
return $query;
|
||||
return $query . ')';
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Doctrine\Functions;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
|
@ -54,27 +55,59 @@ class Natsort extends FunctionNode
|
|||
self::$allowSlowNaturalSort = $allow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the slow natural sort is allowed
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSlowNaturalSortAllowed(): bool
|
||||
{
|
||||
return self::$allowSlowNaturalSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the MariaDB version which is connected to supports the natural sort (meaning it has a version of 10.7.0 or higher)
|
||||
* The result is cached in memory.
|
||||
* @param Connection $connection
|
||||
* @return bool
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function mariaDBSupportsNaturalSort(Connection $connection): bool
|
||||
private function mariaDBSupportsNaturalSort(Connection $connection): bool
|
||||
{
|
||||
if (self::$supportsNaturalSort !== null) {
|
||||
return self::$supportsNaturalSort;
|
||||
}
|
||||
|
||||
$version = $connection->getServerVersion();
|
||||
//Remove the -MariaDB suffix
|
||||
$version = str_replace('-MariaDB', '', $version);
|
||||
|
||||
//Get the effective MariaDB version number
|
||||
$version = $this->getMariaDbMysqlVersionNumber($version);
|
||||
|
||||
//We need at least MariaDB 10.7.0 to support the natural sort
|
||||
self::$supportsNaturalSort = version_compare($version, '10.7.0', '>=');
|
||||
return self::$supportsNaturalSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from Doctrine\DBAL\Driver\AbstractMySQLDriver
|
||||
*
|
||||
* Detect MariaDB server version, including hack for some mariadb distributions
|
||||
* that starts with the prefix '5.5.5-'
|
||||
*
|
||||
* @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial'
|
||||
*/
|
||||
private function getMariaDbMysqlVersionNumber(string $versionString) : string
|
||||
{
|
||||
if ( ! preg_match(
|
||||
'/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
|
||||
$versionString,
|
||||
$versionParts
|
||||
)) {
|
||||
throw new \RuntimeException('Could not detect MariaDB version from version string ' . $versionString);
|
||||
}
|
||||
|
||||
return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
|
||||
}
|
||||
|
||||
public function parse(Parser $parser): void
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
|
@ -95,7 +128,7 @@ class Natsort extends FunctionNode
|
|||
return $this->field->dispatch($sqlWalker) . ' COLLATE numeric';
|
||||
}
|
||||
|
||||
if ($platform instanceof MariaDBPlatform && self::mariaDBSupportsNaturalSort($sqlWalker->getConnection())) {
|
||||
if ($platform instanceof MariaDBPlatform && $this->mariaDBSupportsNaturalSort($sqlWalker->getConnection())) {
|
||||
return 'NATURAL_SORT_KEY(' . $this->field->dispatch($sqlWalker) . ')';
|
||||
}
|
||||
|
||||
|
|
|
@ -98,10 +98,9 @@ class SQLiteRegexExtensionMiddlewareDriver extends AbstractDriverMiddleware
|
|||
* This function returns the index (position) of the first argument in the subsequent arguments.
|
||||
* If the first argument is not found or is NULL, 0 is returned.
|
||||
* @param string|int|null $value
|
||||
* @param mixed ...$array
|
||||
* @return int
|
||||
*/
|
||||
final public static function field(string|int|null $value, ...$array): int
|
||||
final public static function field(string|int|null $value, mixed ...$array): int
|
||||
{
|
||||
if ($value === null) {
|
||||
return 0;
|
||||
|
|
97
src/Doctrine/Types/UTCDateTimeImmutableType.php
Normal file
97
src/Doctrine/Types/UTCDateTimeImmutableType.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Doctrine\Types;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\DateTimeImmutableType;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
use Doctrine\DBAL\Types\Exception\InvalidFormat;
|
||||
|
||||
/**
|
||||
* This DateTimeImmutableType all dates to UTC, so it can be later used with the timezones.
|
||||
* Taken (and adapted) from here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/cookbook/working-with-datetime.html.
|
||||
*/
|
||||
class UTCDateTimeImmutableType extends DateTimeImmutableType
|
||||
{
|
||||
private static ?DateTimeZone $utc_timezone = null;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return (T is null ? null : string)
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
if (!self::$utc_timezone instanceof \DateTimeZone) {
|
||||
self::$utc_timezone = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeImmutable) {
|
||||
$value = $value->setTimezone(self::$utc_timezone);
|
||||
}
|
||||
|
||||
return parent::convertToDatabaseValue($value, $platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTimeImmutable
|
||||
{
|
||||
if (!self::$utc_timezone instanceof \DateTimeZone) {
|
||||
self::$utc_timezone = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
if (null === $value || $value instanceof \DateTimeImmutable) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$converted = \DateTimeImmutable::createFromFormat(
|
||||
$platform->getDateTimeFormatString(),
|
||||
$value,
|
||||
self::$utc_timezone
|
||||
);
|
||||
|
||||
if (!$converted) {
|
||||
throw InvalidFormat::new(
|
||||
$value,
|
||||
static::class,
|
||||
$platform->getDateTimeFormatString(),
|
||||
);
|
||||
}
|
||||
|
||||
return $converted;
|
||||
}
|
||||
}
|
|
@ -147,7 +147,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
* @var string|null the original filename the file had, when the user uploaded it
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'attachment:read'])]
|
||||
#[Groups(['attachment:read', 'import'])]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected ?string $original_filename = null;
|
||||
|
||||
|
@ -161,7 +161,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
* @var string the name of this element
|
||||
*/
|
||||
#[Assert\NotBlank(message: 'validator.attachment.name_not_blank')]
|
||||
#[Groups(['simple', 'extended', 'full', 'attachment:read', 'attachment:write'])]
|
||||
#[Groups(['simple', 'extended', 'full', 'attachment:read', 'attachment:write', 'import'])]
|
||||
protected string $name = '';
|
||||
|
||||
/**
|
||||
|
@ -173,21 +173,21 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
protected ?AttachmentContainingDBElement $element = null;
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
#[Groups(['attachment:read', 'attachment_write'])]
|
||||
#[Groups(['attachment:read', 'attachment_write', 'full', 'import'])]
|
||||
protected bool $show_in_table = false;
|
||||
|
||||
#[Assert\NotNull(message: 'validator.attachment.must_not_be_null')]
|
||||
#[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'attachments_with_type')]
|
||||
#[ORM\JoinColumn(name: 'type_id', nullable: false)]
|
||||
#[Selectable]
|
||||
#[Groups(['attachment:read', 'attachment:write'])]
|
||||
#[Groups(['attachment:read', 'attachment:write', 'import', 'full'])]
|
||||
#[ApiProperty(readableLink: false)]
|
||||
protected ?AttachmentType $attachment_type = null;
|
||||
|
||||
#[Groups(['attachment:read'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['attachment:read'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
|
||||
public function __construct()
|
||||
|
@ -385,7 +385,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
return null;
|
||||
}
|
||||
|
||||
return parse_url($this->getURL(), PHP_URL_HOST);
|
||||
return parse_url((string) $this->getURL(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -477,7 +477,8 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
*/
|
||||
public function setElement(AttachmentContainingDBElement $element): self
|
||||
{
|
||||
if (!is_a($element, static::ALLOWED_ELEMENT_CLASS)) {
|
||||
//Do not allow Rector to replace this check with a instanceof. It will not work!!
|
||||
if (!is_a($element, static::ALLOWED_ELEMENT_CLASS, true)) {
|
||||
throw new InvalidArgumentException(sprintf('The element associated with a %s must be a %s!', static::class, static::ALLOWED_ELEMENT_CLASS));
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ abstract class AttachmentContainingDBElement extends AbstractNamedDBElement impl
|
|||
* @phpstan-var Collection<int, AT>
|
||||
* ORM Mapping is done in subclasses (e.g. Part)
|
||||
*/
|
||||
#[Groups(['full'])]
|
||||
#[Groups(['full', 'import'])]
|
||||
protected Collection $attachments;
|
||||
|
||||
public function __construct()
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Attachments;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
|
@ -86,7 +87,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
class AttachmentType extends AbstractStructuralDBElement
|
||||
{
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: AttachmentType::class, cascade: ['persist'])]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $children;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: AttachmentType::class, inversedBy: 'children')]
|
||||
|
@ -102,7 +103,7 @@ class AttachmentType extends AbstractStructuralDBElement
|
|||
*/
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
#[ValidFileFilter]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write'])]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'extended'])]
|
||||
protected string $filetype_filter = '';
|
||||
|
||||
/**
|
||||
|
@ -110,21 +111,21 @@ class AttachmentType extends AbstractStructuralDBElement
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: AttachmentTypeAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'full'])]
|
||||
protected Collection $attachments;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: AttachmentTypeAttachment::class)]
|
||||
#[ORM\JoinColumn(name: 'id_preview_attachment', onDelete: 'SET NULL')]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write'])]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write', 'full'])]
|
||||
protected ?Attachment $master_picture_attachment = null;
|
||||
|
||||
/** @var Collection<int, AttachmentTypeParameter>
|
||||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: AttachmentTypeParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write'])]
|
||||
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
|
||||
#[Groups(['attachment_type:read', 'attachment_type:write', 'import', 'full'])]
|
||||
protected Collection $parameters;
|
||||
|
||||
/**
|
||||
|
@ -134,9 +135,9 @@ class AttachmentType extends AbstractStructuralDBElement
|
|||
protected Collection $attachments_with_type;
|
||||
|
||||
#[Groups(['attachment_type:read'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['attachment_type:read'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
|
||||
public function __construct()
|
||||
|
|
|
@ -41,14 +41,14 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
||||
{
|
||||
#[Groups(['company:read'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['company:read'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
/**
|
||||
* @var string The address of the company
|
||||
*/
|
||||
#[Groups(['full', 'company:read', 'company:write'])]
|
||||
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $address = '';
|
||||
|
@ -56,7 +56,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
/**
|
||||
* @var string The phone number of the company
|
||||
*/
|
||||
#[Groups(['full', 'company:read', 'company:write'])]
|
||||
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $phone_number = '';
|
||||
|
@ -64,7 +64,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
/**
|
||||
* @var string The fax number of the company
|
||||
*/
|
||||
#[Groups(['full', 'company:read', 'company:write'])]
|
||||
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $fax_number = '';
|
||||
|
@ -73,7 +73,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
* @var string The email address of the company
|
||||
*/
|
||||
#[Assert\Email]
|
||||
#[Groups(['full', 'company:read', 'company:write'])]
|
||||
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $email_address = '';
|
||||
|
@ -82,12 +82,12 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
* @var string The website of the company
|
||||
*/
|
||||
#[Assert\Url]
|
||||
#[Groups(['full', 'company:read', 'company:write'])]
|
||||
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $website = '';
|
||||
|
||||
#[Groups(['company:read', 'company:write'])]
|
||||
#[Groups(['company:read', 'company:write', 'import', 'full', 'extended'])]
|
||||
protected string $comment = '';
|
||||
|
||||
/**
|
||||
|
@ -95,6 +95,7 @@ abstract class AbstractCompany extends AbstractPartsContainingDBElement
|
|||
*/
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
#[Groups(['full', 'company:read', 'company:write', 'import', 'extended'])]
|
||||
protected string $auto_product_url = '';
|
||||
|
||||
/********************************************************************************
|
||||
|
|
|
@ -38,7 +38,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
#[ORM\MappedSuperclass(repositoryClass: AbstractPartsContainingRepository::class)]
|
||||
abstract class AbstractPartsContainingDBElement extends AbstractStructuralDBElement
|
||||
{
|
||||
#[Groups(['full'])]
|
||||
#[Groups(['full', 'import'])]
|
||||
protected Collection $parameters;
|
||||
|
||||
public function __construct()
|
||||
|
|
|
@ -34,28 +34,28 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
trait TimestampTrait
|
||||
{
|
||||
/**
|
||||
* @var \DateTime|null the date when this element was modified the last time
|
||||
* @var \DateTimeImmutable|null the date when this element was modified the last time
|
||||
*/
|
||||
#[Groups(['extended', 'full'])]
|
||||
#[ApiProperty(writable: false)]
|
||||
#[ORM\Column(name: 'last_modified', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
#[ORM\Column(name: 'last_modified', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
/**
|
||||
* @var \DateTime|null the date when this element was created
|
||||
* @var \DateTimeImmutable|null the date when this element was created
|
||||
*/
|
||||
#[Groups(['extended', 'full'])]
|
||||
#[ApiProperty(writable: false)]
|
||||
#[ORM\Column(name: 'datetime_added', type: Types::DATETIME_MUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
#[ORM\Column(name: 'datetime_added', type: Types::DATETIME_IMMUTABLE, options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
|
||||
/**
|
||||
* Returns the last time when the element was modified.
|
||||
* Returns null if the element was not yet saved to DB yet.
|
||||
*
|
||||
* @return \DateTimeInterface|null the time of the last edit
|
||||
* @return \DateTimeImmutable|null the time of the last edit
|
||||
*/
|
||||
public function getLastModified(): ?\DateTimeInterface
|
||||
public function getLastModified(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->lastModified;
|
||||
}
|
||||
|
@ -64,9 +64,9 @@ trait TimestampTrait
|
|||
* Returns the date/time when the element was created.
|
||||
* Returns null if the element was not yet saved to DB yet.
|
||||
*
|
||||
* @return \DateTimeInterface|null the creation time of the part
|
||||
* @return \DateTimeImmutable|null the creation time of the part
|
||||
*/
|
||||
public function getAddedDate(): ?\DateTimeInterface
|
||||
public function getAddedDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->addedDate;
|
||||
}
|
||||
|
@ -78,9 +78,9 @@ trait TimestampTrait
|
|||
#[ORM\PreUpdate]
|
||||
public function updateTimestamps(): void
|
||||
{
|
||||
$this->lastModified = new DateTime('now');
|
||||
$this->lastModified = new \DateTimeImmutable('now');
|
||||
if (null === $this->addedDate) {
|
||||
$this->addedDate = new DateTime('now');
|
||||
$this->addedDate = new \DateTimeImmutable('now');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,33 +36,33 @@ class EDACategoryInfo
|
|||
* @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors.
|
||||
*/
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $reference_prefix = null;
|
||||
|
||||
/** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
|
||||
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||
private ?bool $visibility = null;
|
||||
|
||||
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
|
||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||
private ?bool $exclude_from_bom = null;
|
||||
|
||||
/** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */
|
||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||
private ?bool $exclude_from_board = null;
|
||||
|
||||
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
|
||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||
private ?bool $exclude_from_sim = true;
|
||||
|
||||
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[Groups(['full', 'category:read', 'category:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $kicad_symbol = null;
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class EDAFootprintInfo
|
|||
{
|
||||
/** @var string|null The KiCAD footprint, which should be used (the path to the library) */
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'footprint:read', 'footprint:write'])]
|
||||
#[Groups(['full', 'footprint:read', 'footprint:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $kicad_footprint = null;
|
||||
|
||||
|
|
|
@ -36,45 +36,45 @@ class EDAPartInfo
|
|||
* @var string|null The reference prefix of the Part in the schematic. E.g. "R" for resistors, or "C" for capacitors.
|
||||
*/
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $reference_prefix = null;
|
||||
|
||||
/** @var string|null The value, which should be shown together with the part (e.g. 470 for a 470 Ohm resistor) */
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $value = null;
|
||||
|
||||
/** @var bool|null Visibility of this part to EDA software in trinary logic. True=Visible, False=Invisible, Null=Auto */
|
||||
#[Column(name: 'invisible', type: Types::BOOLEAN, nullable: true)] //TODO: Rename column to visibility
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
private ?bool $visibility = null;
|
||||
|
||||
/** @var bool|null If this is set to true, then this part will be excluded from the BOM */
|
||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
private ?bool $exclude_from_bom = null;
|
||||
|
||||
/** @var bool|null If this is set to true, then this part will be excluded from the board/the PCB */
|
||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
private ?bool $exclude_from_board = null;
|
||||
|
||||
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
|
||||
#[Column(type: Types::BOOLEAN, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
private ?bool $exclude_from_sim = null;
|
||||
|
||||
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $kicad_symbol = null;
|
||||
|
||||
/** @var string|null The KiCAD footprint, which should be used (the path to the library) */
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write'])]
|
||||
#[Groups(['full', 'eda_info:read', 'eda_info:write', 'import'])]
|
||||
#[Length(max: 255)]
|
||||
private ?string $kicad_footprint = null;
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
enum BarcodeType: string
|
||||
|
|
|
@ -43,6 +43,7 @@ namespace App\Entity\LabelSystem;
|
|||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Embeddable]
|
||||
|
@ -53,6 +54,7 @@ class LabelOptions
|
|||
*/
|
||||
#[Assert\Positive]
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected float $width = 50.0;
|
||||
|
||||
/**
|
||||
|
@ -60,38 +62,45 @@ class LabelOptions
|
|||
*/
|
||||
#[Assert\Positive]
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected float $height = 30.0;
|
||||
|
||||
/**
|
||||
* @var BarcodeType The type of the barcode that should be used in the label (e.g. 'qr')
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, enumType: BarcodeType::class)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected BarcodeType $barcode_type = BarcodeType::NONE;
|
||||
|
||||
/**
|
||||
* @var LabelPictureType What image should be shown along the label
|
||||
*/
|
||||
#[ORM\Column(type: Types::STRING, enumType: LabelPictureType::class)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected LabelPictureType $picture_type = LabelPictureType::NONE;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, enumType: LabelSupportedElement::class)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected LabelSupportedElement $supported_element = LabelSupportedElement::PART;
|
||||
|
||||
/**
|
||||
* @var string any additional CSS for the label
|
||||
*/
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
#[Groups([ "full", "import"])]
|
||||
protected string $additional_css = '';
|
||||
|
||||
/** @var LabelProcessMode The mode that will be used to interpret the lines
|
||||
*/
|
||||
#[ORM\Column(name: 'lines_mode', type: Types::STRING, enumType: LabelProcessMode::class)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected LabelProcessMode $process_mode = LabelProcessMode::PLACEHOLDER;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected string $lines = '';
|
||||
|
||||
public function getWidth(): float
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
enum LabelPictureType: string
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
enum LabelProcessMode: string
|
||||
|
|
|
@ -41,6 +41,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Repository\LabelProfileRepository;
|
||||
use App\EntityListeners\TreeCacheInvalidationListener;
|
||||
|
@ -51,6 +52,7 @@ use App\Entity\Attachments\LabelAttachment;
|
|||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
|
@ -66,7 +68,7 @@ class LabelProfile extends AttachmentContainingDBElement
|
|||
* @var Collection<int, LabelAttachment>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: LabelAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $attachments;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: LabelAttachment::class)]
|
||||
|
@ -78,6 +80,7 @@ class LabelProfile extends AttachmentContainingDBElement
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\Embedded(class: 'LabelOptions')]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected LabelOptions $options;
|
||||
|
||||
/**
|
||||
|
@ -90,6 +93,7 @@ class LabelProfile extends AttachmentContainingDBElement
|
|||
* @var bool determines, if this label profile should be shown in the dropdown quick menu
|
||||
*/
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
#[Groups(["extended", "full", "import"])]
|
||||
protected bool $show_in_dropdown = true;
|
||||
|
||||
public function __construct()
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LabelSystem;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace App\Entity\LogSystem;
|
|||
use Doctrine\DBAL\Types\Types;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\UserSystem\User;
|
||||
use DateTime;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use App\Repository\LogEntryRepository;
|
||||
|
||||
|
@ -55,10 +55,11 @@ abstract class AbstractLogEntry extends AbstractDBElement
|
|||
#[ORM\Column(type: Types::STRING)]
|
||||
protected string $username = '';
|
||||
|
||||
/** @var \DateTime The datetime the event associated with this log entry has occured
|
||||
/**
|
||||
* @var \DateTimeImmutable The datetime the event associated with this log entry has occured
|
||||
*/
|
||||
#[ORM\Column(name: 'datetime', type: Types::DATETIME_MUTABLE)]
|
||||
protected \DateTime $timestamp;
|
||||
#[ORM\Column(name: 'datetime', type: Types::DATETIME_IMMUTABLE)]
|
||||
protected \DateTimeImmutable $timestamp;
|
||||
|
||||
/**
|
||||
* @var LogLevel The priority level of the associated level. 0 is highest, 7 lowest
|
||||
|
@ -89,7 +90,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
|
|||
|
||||
public function __construct()
|
||||
{
|
||||
$this->timestamp = new DateTime();
|
||||
$this->timestamp = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -164,7 +165,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
|
|||
/**
|
||||
* Returns the timestamp when the event that caused this log entry happened.
|
||||
*/
|
||||
public function getTimestamp(): \DateTimeInterface
|
||||
public function getTimestamp(): \DateTimeImmutable
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
@ -174,7 +175,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
|
|||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTimestamp(\DateTime $timestamp): self
|
||||
public function setTimestamp(\DateTimeImmutable $timestamp): self
|
||||
{
|
||||
$this->timestamp = $timestamp;
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use Psr\Log\LogLevel as PSRLogLevel;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
|
@ -120,7 +122,7 @@ enum LogTargetType: int
|
|||
}
|
||||
}
|
||||
|
||||
$elementClass = is_object($element) ? get_class($element) : $element;
|
||||
$elementClass = is_object($element) ? $element::class : $element;
|
||||
//If no matching type was found, throw an exception
|
||||
throw new \InvalidArgumentException("The given class $elementClass is not a valid log target type.");
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
use App\Entity\Contracts\LogWithEventUndoInterface;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
|
@ -17,7 +20,6 @@
|
|||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity\LogSystem;
|
||||
|
||||
enum PartStockChangeType: string
|
||||
|
|
|
@ -52,8 +52,6 @@ class PartStockChangedLogEntry extends AbstractLogEntry
|
|||
$this->level = LogLevel::INFO;
|
||||
|
||||
$this->setTargetElement($lot);
|
||||
|
||||
$this->typeString = 'part_stock_changed';
|
||||
$this->extra = array_merge($this->extra, [
|
||||
't' => $type->toExtraShortType(),
|
||||
'o' => $old_stock,
|
||||
|
|
|
@ -127,7 +127,7 @@ 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
|
||||
*/
|
||||
|
|
|
@ -52,7 +52,7 @@ 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
|
||||
*/
|
||||
|
|
|
@ -49,7 +49,7 @@ 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
|
||||
*/
|
||||
|
|
|
@ -38,7 +38,7 @@ use League\OAuth2\Client\Token\AccessTokenInterface;
|
|||
class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
|
||||
{
|
||||
/** @var string|null The short-term usable OAuth2 token */
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $token = null;
|
||||
|
||||
/** @var \DateTimeImmutable|null The date when the token expires */
|
||||
|
@ -46,7 +46,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
|
|||
private ?\DateTimeImmutable $expires_at = null;
|
||||
|
||||
/** @var string|null The refresh token for the OAuth2 auth */
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $refresh_token = null;
|
||||
|
||||
/**
|
||||
|
@ -92,7 +92,7 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface
|
|||
return $this->token;
|
||||
}
|
||||
|
||||
public function getExpirationDate(): ?\DateTimeInterface
|
||||
public function getExpirationDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->expires_at;
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
* @var string The mathematical symbol for this specification. Can be rendered pretty later. Should be short
|
||||
*/
|
||||
#[Assert\Length(max: 20)]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
protected string $symbol = '';
|
||||
|
||||
|
@ -126,7 +126,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
#[Assert\Type(['float', null])]
|
||||
#[Assert\LessThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.min_lesser_typical')]
|
||||
#[Assert\LessThan(propertyPath: 'value_max', message: 'parameters.validator.min_lesser_max')]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::FLOAT, nullable: true)]
|
||||
protected ?float $value_min = null;
|
||||
|
||||
|
@ -134,7 +134,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
* @var float|null the typical value of this property
|
||||
*/
|
||||
#[Assert\Type([null, 'float'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::FLOAT, nullable: true)]
|
||||
protected ?float $value_typical = null;
|
||||
|
||||
|
@ -143,14 +143,14 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
*/
|
||||
#[Assert\Type(['float', null])]
|
||||
#[Assert\GreaterThanOrEqual(propertyPath: 'value_typical', message: 'parameters.validator.max_greater_typical')]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::FLOAT, nullable: true)]
|
||||
protected ?float $value_max = null;
|
||||
|
||||
/**
|
||||
* @var string The unit in which the value values are given (e.g. V)
|
||||
*/
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 50)]
|
||||
protected string $unit = '';
|
||||
|
@ -158,7 +158,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
/**
|
||||
* @var string a text value for the given property
|
||||
*/
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $value_text = '';
|
||||
|
@ -166,7 +166,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
/**
|
||||
* @var string the group this parameter belongs to
|
||||
*/
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write'])]
|
||||
#[Groups(['full', 'parameter:read', 'parameter:write', 'import'])]
|
||||
#[ORM\Column(name: 'param_group', type: Types::STRING)]
|
||||
#[Assert\Length(max: 255)]
|
||||
protected string $group = '';
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Parts;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
|
@ -91,7 +92,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
class Category extends AbstractPartsContainingDBElement
|
||||
{
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $children;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||
|
@ -165,7 +166,7 @@ class Category extends AbstractPartsContainingDBElement
|
|||
#[Assert\Valid]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $attachments;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: CategoryAttachment::class)]
|
||||
|
@ -178,13 +179,13 @@ class Category extends AbstractPartsContainingDBElement
|
|||
#[Assert\Valid]
|
||||
#[Groups(['full', 'category:read', 'category:write'])]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: CategoryParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
|
||||
protected Collection $parameters;
|
||||
|
||||
#[Groups(['category:read'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['category:read'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
#[Assert\Valid]
|
||||
#[ORM\Embedded(class: EDACategoryInfo::class)]
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Parts;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
|
@ -96,7 +97,7 @@ class Footprint extends AbstractPartsContainingDBElement
|
|||
protected ?AbstractStructuralDBElement $parent = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $children;
|
||||
|
||||
#[Groups(['footprint:read', 'footprint:write'])]
|
||||
|
@ -107,7 +108,7 @@ class Footprint extends AbstractPartsContainingDBElement
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
#[Groups(['footprint:read', 'footprint:write'])]
|
||||
protected Collection $attachments;
|
||||
|
||||
|
@ -128,14 +129,14 @@ class Footprint extends AbstractPartsContainingDBElement
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: FootprintParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
|
||||
#[Groups(['footprint:read', 'footprint:write'])]
|
||||
protected Collection $parameters;
|
||||
|
||||
#[Groups(['footprint:read'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['footprint:read'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
#[Assert\Valid]
|
||||
#[ORM\Embedded(class: EDAFootprintInfo::class)]
|
||||
|
|
|
@ -31,31 +31,32 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
|
||||
/**
|
||||
* This class represents a reference to a info provider inside a part.
|
||||
* @see \App\Tests\Entity\Parts\InfoProviderReferenceTest
|
||||
*/
|
||||
#[Embeddable]
|
||||
class InfoProviderReference
|
||||
{
|
||||
|
||||
/** @var string|null The key referencing the provider used to get this part, or null if it was not provided by a data provider */
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
#[Groups(['provider_reference:read'])]
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['provider_reference:read', 'full'])]
|
||||
private ?string $provider_key = null;
|
||||
|
||||
/** @var string|null The id of this part inside the provider system or null if the part was not provided by a data provider */
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
#[Groups(['provider_reference:read'])]
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['provider_reference:read', 'full'])]
|
||||
private ?string $provider_id = null;
|
||||
|
||||
/**
|
||||
* @var string|null The url of this part inside the provider system or null if this info is not existing
|
||||
*/
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
#[Groups(['provider_reference:read'])]
|
||||
#[Column(type: Types::STRING, nullable: true)]
|
||||
#[Groups(['provider_reference:read', 'full'])]
|
||||
private ?string $provider_url = null;
|
||||
|
||||
#[Column(type: Types::DATETIME_MUTABLE, nullable: true, options: ['default' => null])]
|
||||
#[Groups(['provider_reference:read'])]
|
||||
private ?\DateTime $last_updated = null;
|
||||
#[Column(type: Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
|
||||
#[Groups(['provider_reference:read', 'full'])]
|
||||
private ?\DateTimeImmutable $last_updated = null;
|
||||
|
||||
/**
|
||||
* Constructing is forbidden from outside.
|
||||
|
@ -94,9 +95,8 @@ class InfoProviderReference
|
|||
|
||||
/**
|
||||
* Gets the time, when the part was last time updated by the provider.
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getLastUpdated(): ?\DateTimeInterface
|
||||
public function getLastUpdated(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->last_updated;
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ class InfoProviderReference
|
|||
$ref->provider_key = $provider_key;
|
||||
$ref->provider_id = $provider_id;
|
||||
$ref->provider_url = $provider_url;
|
||||
$ref->last_updated = new \DateTime();
|
||||
$ref->last_updated = new \DateTimeImmutable();
|
||||
return $ref;
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,7 @@ class InfoProviderReference
|
|||
$ref->provider_key = $dto->provider_key;
|
||||
$ref->provider_id = $dto->provider_id;
|
||||
$ref->provider_url = $dto->provider_url;
|
||||
$ref->last_updated = new \DateTime();
|
||||
$ref->last_updated = new \DateTimeImmutable();
|
||||
return $ref;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Parts;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
|
@ -95,7 +96,7 @@ class Manufacturer extends AbstractCompany
|
|||
protected ?AbstractStructuralDBElement $parent = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $children;
|
||||
|
||||
/**
|
||||
|
@ -103,7 +104,7 @@ class Manufacturer extends AbstractCompany
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: ManufacturerAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
#[Groups(['manufacturer:read', 'manufacturer:write'])]
|
||||
#[ApiProperty(readableLink: false, writableLink: true)]
|
||||
protected Collection $attachments;
|
||||
|
@ -118,7 +119,7 @@ class Manufacturer extends AbstractCompany
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: ManufacturerParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
|
||||
#[Groups(['manufacturer:read', 'manufacturer:write'])]
|
||||
#[ApiProperty(readableLink: false, writableLink: true)]
|
||||
protected Collection $parameters;
|
||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Entity\Parts;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
|
@ -98,7 +99,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
|
|||
* or m (for meters).
|
||||
*/
|
||||
#[Assert\Length(max: 10)]
|
||||
#[Groups(['extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
|
||||
#[Groups(['simple', 'extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
|
||||
#[ORM\Column(name: 'unit', type: Types::STRING, nullable: true)]
|
||||
protected ?string $unit = null;
|
||||
|
||||
|
@ -109,7 +110,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
|
|||
* @var bool Determines if the amount value associated with this unit should be treated as integer.
|
||||
* Set to false, to measure continuous sizes likes masses or lengths.
|
||||
*/
|
||||
#[Groups(['extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
|
||||
#[Groups(['simple', 'extended', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
|
||||
#[ORM\Column(name: 'is_integer', type: Types::BOOLEAN)]
|
||||
protected bool $is_integer = false;
|
||||
|
||||
|
@ -118,12 +119,12 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
|
|||
* Useful for sizes like meters. For this the unit must be set
|
||||
*/
|
||||
#[Assert\Expression('this.isUseSIPrefix() == false or this.getUnit() != null', message: 'validator.measurement_unit.use_si_prefix_needs_unit')]
|
||||
#[Groups(['full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
|
||||
#[Groups(['simple', 'full', 'import', 'measurement_unit:read', 'measurement_unit:write'])]
|
||||
#[ORM\Column(name: 'use_si_prefix', type: Types::BOOLEAN)]
|
||||
protected bool $use_si_prefix = false;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class, cascade: ['persist'])]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
protected Collection $children;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
|
||||
|
@ -137,7 +138,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: MeasurementUnitAttachment::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['name' => Criteria::ASC])]
|
||||
#[Groups(['measurement_unit:read', 'measurement_unit:write'])]
|
||||
protected Collection $attachments;
|
||||
|
||||
|
@ -150,14 +151,14 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
|
|||
*/
|
||||
#[Assert\Valid]
|
||||
#[ORM\OneToMany(mappedBy: 'element', targetEntity: MeasurementUnitParameter::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['group' => 'ASC', 'name' => 'ASC'])]
|
||||
#[ORM\OrderBy(['group' => Criteria::ASC, 'name' => 'ASC'])]
|
||||
#[Groups(['measurement_unit:read', 'measurement_unit:write'])]
|
||||
protected Collection $parameters;
|
||||
|
||||
#[Groups(['measurement_unit:read'])]
|
||||
protected ?\DateTime $addedDate = null;
|
||||
protected ?\DateTimeImmutable $addedDate = null;
|
||||
#[Groups(['measurement_unit:read'])]
|
||||
protected ?\DateTime $lastModified = null;
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
|
||||
/**
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue