Compare commits

..

No commits in common. "956ece60afad9e06312ea64dfb02af98101684d5" and "03e1105a8efcc3495ac34e9b60f68173b1ea37b2" have entirely different histories.

11 changed files with 21 additions and 226 deletions

View file

@ -25,20 +25,9 @@ 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()
{
let value = "";
if (this.unitValue) {
value = "\\mathrm{" + this.inputTarget.value + "}";
} else {
value = this.inputTarget.value;
}
katex.render(value, this.previewTarget, {
katex.render(this.inputTarget.value, this.previewTarget, {
throwOnError: false,
});
}

View file

@ -111,11 +111,4 @@ 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;
}

View file

@ -71,9 +71,3 @@ 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

View file

@ -1,136 +0,0 @@
<?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;
}
}

View file

@ -208,7 +208,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
*/
#[Groups(['parameter:read', 'full'])]
#[SerializedName('formatted')]
public function getFormattedValue(bool $latex_formatted = false): string
public function getFormattedValue(): 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($latex_formatted);
$str .= $this->getValueTypicalWithUnit();
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($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted);
$str .= $this->getValueMinWithUnit().' ... '.$this->getValueMaxWithUnit();
} elseif ($this->value_max) {
$str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted);
$str .= 'max. '.$this->getValueMaxWithUnit();
} elseif ($this->value_min) {
$str .= 'min. '.$this->getValueMinWithUnit($latex_formatted);
$str .= 'min. '.$this->getValueMinWithUnit();
}
//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(bool $with_latex = false): string
public function getValueTypicalWithUnit(): string
{
return $this->formatWithUnit($this->value_typical, with_latex: $with_latex);
return $this->formatWithUnit($this->value_typical);
}
/**
* Return a formatted version with the maximum value with the unit of this parameter.
*/
public function getValueMaxWithUnit(bool $with_latex = false): string
public function getValueMaxWithUnit(): string
{
return $this->formatWithUnit($this->value_max, with_latex: $with_latex);
return $this->formatWithUnit($this->value_max);
}
/**
* Return a formatted version with the typical value with the unit of this parameter.
*/
public function getValueMinWithUnit(bool $with_latex = false): string
public function getValueMinWithUnit(): string
{
return $this->formatWithUnit($this->value_min, with_latex: $with_latex);
return $this->formatWithUnit($this->value_min);
}
/**
@ -441,18 +441,11 @@ 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', bool $with_latex = false): string
protected function formatWithUnit(float $value, string $format = '%g'): string
{
$str = sprintf($format, $value);
if ($this->unit !== '') {
if (!$with_latex) {
$unit = $this->unit;
} else {
$unit = '$\mathrm{'.$this->unit.'}$';
}
return $str.' '.$unit;
return $str.' '.$this->unit;
}
return $str;

View file

@ -66,7 +66,7 @@ class AttachmentRepository extends DBElementRepository
}
/**
* Gets the count of all external attachments (attachments containing only an external path).
* Gets the count of all external attachments (attachments containing an external path).
*
* @throws NoResultException
* @throws NonUniqueResultException
@ -75,9 +75,8 @@ class AttachmentRepository extends DBElementRepository
{
$qb = $this->createQueryBuilder('attachment');
$qb->select('COUNT(attachment)')
->where('attachment.external_path IS NOT NULL')
->andWhere('attachment.internal_path IS NULL');
->andWhere('attaachment.internal_path IS NULL')
->where('attachment.external_path IS NOT NULL');
$query = $qb->getQuery();
return (int) $query->getSingleScalarResult();

View file

@ -66,10 +66,6 @@ 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 [

View file

@ -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', {"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 {{ 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>{{ form_widget(form.value_text) }}</td>
<td>
<button type="button" class="btn btn-danger btn-sm" {{ collection.delete_btn() }} title="{% trans %}orderdetail.delete{% endtrans %}">

View file

@ -218,16 +218,14 @@
<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 }}</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>
<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>
</tr>
{% endfor %}
</tbody>

View file

@ -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', {"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 {{ 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>{{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }}</td>
<td>{{ form_widget(form.group) }}{{ form_errors(form.group) }}</td>
<td>

View file

@ -67,19 +67,6 @@ 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
*/
@ -130,22 +117,4 @@ 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));
}
}