mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-03 05:49:36 +00:00
Compare commits
6 commits
03e1105a8e
...
956ece60af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956ece60af | ||
|
|
53da45d7d7 | ||
|
|
57f0432a87 | ||
|
|
fb535ec6f7 | ||
|
|
4e1b1a4ffa | ||
|
|
5b111d80f1 |
11 changed files with 226 additions and 21 deletions
|
|
@ -25,9 +25,20 @@ import "katex/dist/katex.css";
|
|||
export default class extends Controller {
|
||||
static targets = ["input", "preview"];
|
||||
|
||||
static values = {
|
||||
unit: {type: Boolean, default: false} //Render as upstanding (non-italic) text, useful for units
|
||||
}
|
||||
|
||||
updatePreview()
|
||||
{
|
||||
katex.render(this.inputTarget.value, this.previewTarget, {
|
||||
let value = "";
|
||||
if (this.unitValue) {
|
||||
value = "\\mathrm{" + this.inputTarget.value + "}";
|
||||
} else {
|
||||
value = this.inputTarget.value;
|
||||
}
|
||||
|
||||
katex.render(value, this.previewTarget, {
|
||||
throwOnError: false,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,4 +111,11 @@ ul.structural_link li a:hover {
|
|||
.permission-checkbox:checked {
|
||||
background-color: var(--bs-success);
|
||||
border-color: var(--bs-success);
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* Katex rendering with same height as text
|
||||
***********************************************/
|
||||
.katex-same-height-as-text .katex {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
|
@ -71,3 +71,9 @@ docker exec --user=www-data partdb php bin/console cache:clear
|
|||
|
||||
* `php bin/console doctrine:migrations:migrate`: Migrate the database to the latest version
|
||||
* `php bin/console doctrine:migrations:up-to-date`: Check if the database is up-to-date
|
||||
|
||||
## Attachment commands
|
||||
|
||||
* `php bin/console partdb:attachments:download`: Download all attachments, which are not already downloaded, to the
|
||||
local filesystem. This is useful to create local backups of the attachments, no matter what happens on the remote and
|
||||
also makes pictures thumbnails available for the frontend for them
|
||||
136
src/Command/Attachments/DownloadAttachmentsCommand.php
Normal file
136
src/Command/Attachments/DownloadAttachmentsCommand.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Command\Attachments;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentUpload;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand('partdb:attachments:download', "Downloads all attachments which have only an external URL to the local filesystem.")]
|
||||
class DownloadAttachmentsCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly AttachmentSubmitHandler $attachmentSubmitHandler,
|
||||
private EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setHelp('This command downloads all attachments, which only have an external URL, to the local filesystem, so that you have an offline copy of the attachments.');
|
||||
$this->addOption('--private', null, null, 'If set, the attachments will be downloaded to the private storage.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$qb = $this->entityManager->createQueryBuilder();
|
||||
$qb->select('attachment')
|
||||
->from(Attachment::class, 'attachment')
|
||||
->where('attachment.external_path IS NOT NULL')
|
||||
->andWhere('attachment.external_path != \'\'')
|
||||
->andWhere('attachment.internal_path IS NULL');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
$attachments = $query->getResult();
|
||||
|
||||
if (count($attachments) === 0) {
|
||||
$io->success('No attachments with external URL found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->note('Found ' . count($attachments) . ' attachments with external URL, that will be downloaded.');
|
||||
|
||||
//If the option --private is set, the attachments will be downloaded to the private storage.
|
||||
$private = $input->getOption('private');
|
||||
if ($private) {
|
||||
if (!$io->confirm('Attachments will be downloaded to the private storage. Continue?')) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
} else {
|
||||
if (!$io->confirm('Attachments will be downloaded to the public storage, where everybody knowing the correct URL can access it. Continue?')){
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar = $io->createProgressBar(count($attachments));
|
||||
$progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:16s%/%estimated:-16s% \n%message%");
|
||||
|
||||
$progressBar->setMessage('Starting download...');
|
||||
$progressBar->start();
|
||||
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
/** @var Attachment $attachment */
|
||||
$progressBar->setMessage(sprintf('%s (ID: %s) from %s', $attachment->getName(), $attachment->getID(), $attachment->getHost()));
|
||||
$progressBar->advance();
|
||||
|
||||
try {
|
||||
$attachmentUpload = new AttachmentUpload(file: null, downloadUrl: true, private: $private);
|
||||
$this->attachmentSubmitHandler->handleUpload($attachment, $attachmentUpload);
|
||||
|
||||
//Write changes to the database
|
||||
$this->entityManager->flush();
|
||||
} catch (AttachmentDownloadException $e) {
|
||||
$errors[] = [
|
||||
'attachment' => $attachment,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
//Fix the line break after the progress bar
|
||||
$io->newLine();
|
||||
$io->newLine();
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$io->warning('Some attachments could not be downloaded:');
|
||||
foreach ($errors as $error) {
|
||||
$io->warning(sprintf("Attachment %s (ID %s) could not be downloaded from %s:\n%s",
|
||||
$error['attachment']->getName(),
|
||||
$error['attachment']->getID(),
|
||||
$error['attachment']->getExternalPath(),
|
||||
$error['error'])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$io->success('All attachments downloaded successfully.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
*/
|
||||
#[Groups(['parameter:read', 'full'])]
|
||||
#[SerializedName('formatted')]
|
||||
public function getFormattedValue(): string
|
||||
public function getFormattedValue(bool $latex_formatted = false): string
|
||||
{
|
||||
//If we just only have text value, return early
|
||||
if (null === $this->value_typical && null === $this->value_min && null === $this->value_max) {
|
||||
|
|
@ -218,7 +218,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
$str = '';
|
||||
$bracket_opened = false;
|
||||
if ($this->value_typical) {
|
||||
$str .= $this->getValueTypicalWithUnit();
|
||||
$str .= $this->getValueTypicalWithUnit($latex_formatted);
|
||||
if ($this->value_min || $this->value_max) {
|
||||
$bracket_opened = true;
|
||||
$str .= ' (';
|
||||
|
|
@ -226,11 +226,11 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
}
|
||||
|
||||
if ($this->value_max && $this->value_min) {
|
||||
$str .= $this->getValueMinWithUnit().' ... '.$this->getValueMaxWithUnit();
|
||||
$str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted);
|
||||
} elseif ($this->value_max) {
|
||||
$str .= 'max. '.$this->getValueMaxWithUnit();
|
||||
$str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted);
|
||||
} elseif ($this->value_min) {
|
||||
$str .= 'min. '.$this->getValueMinWithUnit();
|
||||
$str .= 'min. '.$this->getValueMinWithUnit($latex_formatted);
|
||||
}
|
||||
|
||||
//Add closing bracket
|
||||
|
|
@ -344,25 +344,25 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
/**
|
||||
* Return a formatted version with the minimum value with the unit of this parameter.
|
||||
*/
|
||||
public function getValueTypicalWithUnit(): string
|
||||
public function getValueTypicalWithUnit(bool $with_latex = false): string
|
||||
{
|
||||
return $this->formatWithUnit($this->value_typical);
|
||||
return $this->formatWithUnit($this->value_typical, with_latex: $with_latex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted version with the maximum value with the unit of this parameter.
|
||||
*/
|
||||
public function getValueMaxWithUnit(): string
|
||||
public function getValueMaxWithUnit(bool $with_latex = false): string
|
||||
{
|
||||
return $this->formatWithUnit($this->value_max);
|
||||
return $this->formatWithUnit($this->value_max, with_latex: $with_latex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted version with the typical value with the unit of this parameter.
|
||||
*/
|
||||
public function getValueMinWithUnit(): string
|
||||
public function getValueMinWithUnit(bool $with_latex = false): string
|
||||
{
|
||||
return $this->formatWithUnit($this->value_min);
|
||||
return $this->formatWithUnit($this->value_min, with_latex: $with_latex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -441,11 +441,18 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
|
|||
/**
|
||||
* Return a string representation and (if possible) with its unit.
|
||||
*/
|
||||
protected function formatWithUnit(float $value, string $format = '%g'): string
|
||||
protected function formatWithUnit(float $value, string $format = '%g', bool $with_latex = false): string
|
||||
{
|
||||
$str = sprintf($format, $value);
|
||||
if ($this->unit !== '') {
|
||||
return $str.' '.$this->unit;
|
||||
|
||||
if (!$with_latex) {
|
||||
$unit = $this->unit;
|
||||
} else {
|
||||
$unit = '$\mathrm{'.$this->unit.'}$';
|
||||
}
|
||||
|
||||
return $str.' '.$unit;
|
||||
}
|
||||
|
||||
return $str;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class AttachmentRepository extends DBElementRepository
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the count of all external attachments (attachments containing an external path).
|
||||
* Gets the count of all external attachments (attachments containing only an external path).
|
||||
*
|
||||
* @throws NoResultException
|
||||
* @throws NonUniqueResultException
|
||||
|
|
@ -75,8 +75,9 @@ class AttachmentRepository extends DBElementRepository
|
|||
{
|
||||
$qb = $this->createQueryBuilder('attachment');
|
||||
$qb->select('COUNT(attachment)')
|
||||
->andWhere('attaachment.internal_path IS NULL')
|
||||
->where('attachment.external_path IS NOT NULL');
|
||||
->where('attachment.external_path IS NOT NULL')
|
||||
->andWhere('attachment.internal_path IS NULL');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
|
||||
return (int) $query->getSingleScalarResult();
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ class KiCadHelper
|
|||
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class);
|
||||
$item->tag($secure_class_name);
|
||||
|
||||
//Invalidate the cache on part changes (as the visibility depends on parts, and the parts can change)
|
||||
$secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Part::class);
|
||||
$item->tag($secure_class_name);
|
||||
|
||||
//If the category depth is smaller than 0, create only one dummy category
|
||||
if ($this->category_depth < 0) {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
<td class="col-sm-2">{{ form_widget(form.name, {"attr": {"data-pages--parameters-autocomplete-target": "name"}}) }}</td>
|
||||
<td {{ stimulus_controller('pages/latex_preview') }}>{{ form_widget(form.symbol, {"attr": {"data-pages--parameters-autocomplete-target": "symbol", "data-pages--latex-preview-target": "input"}}) }}<span {{ stimulus_target('pages/latex_preview', 'preview') }}></span></td>
|
||||
<td>{{ form_widget(form.value) }}</td>
|
||||
<td {{ stimulus_controller('pages/latex_preview') }}>{{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}<span {{ stimulus_target('pages/latex_preview', 'preview') }}></span></td>
|
||||
<td {{ stimulus_controller('pages/latex_preview', {"unit": true}) }}>{{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}<span {{ stimulus_target('pages/latex_preview', 'preview') }}></span></td>
|
||||
<td>{{ form_widget(form.value_text) }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-sm" {{ collection.delete_btn() }} title="{% trans %}orderdetail.delete{% endtrans %}">
|
||||
|
|
|
|||
|
|
@ -218,14 +218,16 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}specifications.property{% endtrans %}</th>
|
||||
<th>{% trans %}specifications.symbol{% endtrans %}</th>
|
||||
<th>{% trans %}specifications.value{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in parameters %}
|
||||
<tr>
|
||||
<td>{{ param.name }} {% if param.symbol is not empty %}<span class="latex" data-controller="common--latex">${{ param.symbol }}$</span>{% endif %}</td>
|
||||
<td>{{ param.formattedValue }}</td>
|
||||
<td>{{ param.name }}</td>
|
||||
<td>{% if param.symbol is not empty %}<span class="latex" {{ stimulus_controller('common/latex') }}>${{ param.symbol }}$</span>{% endif %}</td>
|
||||
<td {{ stimulus_controller('common/latex') }} class="katex-same-height-as-text">{{ param.formattedValue(true) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
<td>{{ form_widget(form.value_min) }}{{ form_errors(form.value_min) }}</td>
|
||||
<td>{{ form_widget(form.value_typical) }}{{ form_errors(form.value_typical) }}</td>
|
||||
<td>{{ form_widget(form.value_max) }}{{ form_errors(form.value_max) }}</td>
|
||||
<td {{ stimulus_controller('pages/latex_preview') }}>{{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }}<span {{ stimulus_target('pages/latex_preview', 'preview') }}></span></td>
|
||||
<td {{ stimulus_controller('pages/latex_preview', {"unit": true}) }}>{{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit", "data-pages--latex-preview-target": "input"}}) }}{{ form_errors(form.unit) }}<span {{ stimulus_target('pages/latex_preview', 'preview') }}></span></td>
|
||||
<td>{{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }}</td>
|
||||
<td>{{ form_widget(form.group) }}{{ form_errors(form.group) }}</td>
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,19 @@ class PartParameterTest extends TestCase
|
|||
yield ['10.23 V (9 V ... 11 V) [Test]', 9, 10.23, 11, 'V', 'Test'];
|
||||
}
|
||||
|
||||
public function formattedValueWithLatexDataProvider(): \Iterator
|
||||
{
|
||||
yield ['Text Test', null, null, null, 'V', 'Text Test'];
|
||||
yield ['10.23 $\mathrm{V}$', null, 10.23, null, 'V', ''];
|
||||
yield ['10.23 $\mathrm{V}$ [Text]', null, 10.23, null, 'V', 'Text'];
|
||||
yield ['max. 10.23 $\mathrm{V}$', null, null, 10.23, 'V', ''];
|
||||
yield ['max. 10.23 [Text]', null, null, 10.23, '', 'Text'];
|
||||
yield ['min. 10.23 $\mathrm{V}$', 10.23, null, null, 'V', ''];
|
||||
yield ['10.23 $\mathrm{V}$ ... 11 $\mathrm{V}$', 10.23, null, 11, 'V', ''];
|
||||
yield ['10.23 $\mathrm{V}$ (9 $\mathrm{V}$ ... 11 $\mathrm{V}$)', 9, 10.23, 11, 'V', ''];
|
||||
yield ['10.23 $\mathrm{V}$ (9 $\mathrm{V}$ ... 11 $\mathrm{V}$) [Test]', 9, 10.23, 11, 'V', 'Test'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider valueWithUnitDataProvider
|
||||
*/
|
||||
|
|
@ -117,4 +130,22 @@ class PartParameterTest extends TestCase
|
|||
$param->setValueText($text);
|
||||
$this->assertSame($expected, $param->getFormattedValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider formattedValueWithLatexDataProvider
|
||||
*
|
||||
* @param float $min
|
||||
* @param float $typical
|
||||
* @param float $max
|
||||
*/
|
||||
public function testGetFormattedValueWithLatex(string $expected, ?float $min, ?float $typical, ?float $max, string $unit, string $text): void
|
||||
{
|
||||
$param = new PartParameter();
|
||||
$param->setUnit($unit);
|
||||
$param->setValueMin($min);
|
||||
$param->setValueTypical($typical);
|
||||
$param->setValueMax($max);
|
||||
$param->setValueText($text);
|
||||
$this->assertSame($expected, $param->getFormattedValue(true));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue