Added an table to show all defined attachments.

This commit is contained in:
Jan Böhmer 2019-10-04 18:06:37 +02:00
parent 2f0dc600e2
commit f53cc08f52
8 changed files with 437 additions and 17 deletions

View file

@ -32,10 +32,15 @@
namespace App\Controller; namespace App\Controller;
use App\DataTables\AttachmentDataTable;
use App\DataTables\PartsDataTable;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\PartAttachment;
use App\Services\AttachmentHelper; use App\Services\AttachmentHelper;
use Omines\DataTablesBundle\DataTableFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@ -104,4 +109,26 @@ class AttachmentFileController extends AbstractController
return $response; return $response;
} }
/**
* @Route("/attachment/list", name="attachment_list")
* @param DataTableFactory $dataTable
* @param Request $request
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function attachmentsTable(DataTableFactory $dataTable, Request $request)
{
$this->denyAccessUnlessGranted('read', new PartAttachment());
$table = $dataTable->createFromType(AttachmentDataTable::class)
->handleRequest($request);
if ($table->isCallback()) {
return $table->getResponse();
}
return $this->render('attachment_list.html.twig', [
'datatable' => $table
]);
}
} }

View file

@ -0,0 +1,222 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 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 General Public License
* as published by the Free Software Foundation; either version 2
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\DataTables;
use App\DataTables\Column\LocaleDateTimeColumn;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\FootprintAttachment;
use App\Entity\Parts\Part;
use App\Services\AttachmentHelper;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use Doctrine\ORM\QueryBuilder;
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
use Omines\DataTablesBundle\Column\BoolColumn;
use Omines\DataTablesBundle\Column\TextColumn;
use Omines\DataTablesBundle\DataTable;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class AttachmentDataTable implements DataTableTypeInterface
{
protected $translator;
protected $entityURLGenerator;
protected $attachmentHelper;
protected $elementTypeNameGenerator;
public function __construct(TranslatorInterface $translator, EntityURLGenerator $entityURLGenerator,
AttachmentHelper $attachmentHelper, ElementTypeNameGenerator $elementTypeNameGenerator)
{
$this->translator = $translator;
$this->entityURLGenerator = $entityURLGenerator;
$this->attachmentHelper = $attachmentHelper;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
}
protected function getQuery(QueryBuilder $builder)
{
$builder->distinct()->select('attachment')
->addSelect('attachment_type')
//->addSelect('element')
->from(Attachment::class, 'attachment')
->leftJoin('attachment.attachment_type', 'attachment_type');
//->leftJoin('attachment.element', 'element');
}
/**
* @param DataTable $dataTable
* @param array $options
*/
public function configure(DataTable $dataTable, array $options)
{
$dataTable->add('picture', TextColumn::class, [
'label' => '',
'render' => function ($value, Attachment $context) {
if ($context->isPicture()
&& !$context->isExternal()
&& $this->attachmentHelper->isFileExisting($context)) {
return sprintf(
'<img alt="%s" src="%s" class="%s">',
'Part image',
$this->entityURLGenerator->viewURL($context),
'img-fluid hoverpic'
);
}
return '';
}
]);
$dataTable->add('name', TextColumn::class, [
'label' => $this->translator->trans('attachment.edit.name'),
'render' => function ($value, Attachment $context) {
//Link to external source
if ($context->isExternal()) {
return sprintf(
'<a href="%s" class="link-external">%s</a>',
htmlspecialchars($context->getURL()),
htmlspecialchars($value)
);
}
if ($this->attachmentHelper->isFileExisting($context)) {
return sprintf(
'<a href="%s" target="_blank" data-no-ajax>%s</a>',
$this->entityURLGenerator->viewURL($context),
htmlspecialchars($value)
);
}
return $value;
}
]);
$dataTable->add('attachment_type', TextColumn::class, [
'field' => 'attachment_type.name',
'render' => function ($value, Attachment $context) {
return sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->editURL($context->getAttachmentType()),
htmlspecialchars($value)
);
}
]);
$dataTable->add('element', TextColumn::class, [
'label' => $this->translator->trans('attachment.table.element'),
//'propertyPath' => 'element.name',
'render' => function ($value, Attachment $context) {
return sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($context->getElement()),
$this->elementTypeNameGenerator->getTypeNameCombination($context->getElement(), true)
);
}
]);
$dataTable->add('filename', TextColumn::class, [
'propertyPath' => 'filename'
]);
$dataTable->add('filesize', TextColumn::class, [
'render' => function ($value, Attachment $context) {
if ($this->attachmentHelper->isFileExisting($context)) {
return $this->attachmentHelper->getHumanFileSize($context);
}
if ($context->isExternal()) {
return '<i>' . $this->translator->trans('attachment.external') . '</i>';
}
return sprintf(
'<span class="badge badge-warning">
<i class="fas fa-exclamation-circle fa-fw"></i>%s
</span>',
$this->translator->trans('attachment.file_not_found')
);
}
]);
$dataTable
->add('addedDate', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.addedDate'),
'visible' => false
])
->add('lastModified', LocaleDateTimeColumn::class, [
'label' => $this->translator->trans('part.table.lastModified'),
'visible' => false
]);
$dataTable->add('show_in_table', BoolColumn::class, [
'label' => $this->translator->trans('attachment.edit.show_in_table'),
'trueValue' => $this->translator->trans('true'),
'falseValue' => $this->translator->trans('false'),
'nullValue' => '',
'visible' => false
]);
$dataTable->add('isPicture', BoolColumn::class, [
'label' => $this->translator->trans('attachment.edit.isPicture'),
'trueValue' => $this->translator->trans('true'),
'falseValue' => $this->translator->trans('false'),
'nullValue' => '',
'visible' => false,
'propertyPath' => 'picture'
]);
$dataTable->add('is3DModel', BoolColumn::class, [
'label' => $this->translator->trans('attachment.edit.is3DModel'),
'trueValue' => $this->translator->trans('true'),
'falseValue' => $this->translator->trans('false'),
'nullValue' => '',
'visible' => false,
'propertyPath' => '3dmodel'
]);
$dataTable->add('isBuiltin', BoolColumn::class, [
'label' => $this->translator->trans('attachment.edit.isBuiltin'),
'trueValue' => $this->translator->trans('true'),
'falseValue' => $this->translator->trans('false'),
'nullValue' => '',
'visible' => false,
'propertyPath' => 'builtin'
]);
$dataTable->createAdapter(ORMAdapter::class, [
'entity' => Attachment::class,
'query' => function (QueryBuilder $builder) {
$this->getQuery($builder);
},
]);
}
}

View file

@ -29,6 +29,6 @@
namespace App\Exceptions; namespace App\Exceptions;
class EntityNotSupported extends \Exception class EntityNotSupportedException extends \InvalidArgumentException
{ {
} }

View file

@ -0,0 +1,132 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 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 General Public License
* as published by the Free Software Foundation; either version 2
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\Services;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\DBElement;
use App\Entity\Base\NamedDBElement;
use App\Entity\Devices\Device;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use App\Exceptions\EntityNotSupportedException;
use Proxies\__CG__\App\Entity\Parts\Supplier;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Contracts\Translation\TranslatorInterface;
class ElementTypeNameGenerator
{
protected $translator;
protected $mapping;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
//Child classes has to become before parent classes
$this->mapping = [
Attachment::class => $this->translator->trans('attachment.label'),
Category::class => $this->translator->trans('category.label'),
AttachmentType::class => $this->translator->trans('attachment_type.label'),
Device::class => $this->translator->trans('device.label'),
Footprint::class => $this->translator->trans('footprint.label'),
Manufacturer::class => $this->translator->trans('manufacturer.label'),
MeasurementUnit::class => $this->translator->trans('measurement_unit.label'),
Part::class => $this->translator->trans('part.label'),
PartLot::class => $this->translator->trans('part_lot.label'),
Storelocation::class => $this->translator->trans('storelocation.label'),
Supplier::class => $this->translator->trans('supplier.label'),
Currency::class => $this->translator->trans('currency.label'),
Orderdetail::class => $this->translator->trans('orderdetail.label'),
Pricedetail::class => $this->translator->trans('pricedetail.label'),
Group::class => $this->translator->trans('group.label'),
User::class => $this->translator->trans('user.label'),
];
}
/**
* Gets an localized label for the type of the entity.
* A part element becomes "Part" ("Bauteil" in german) and a category object becomes "Category".
* Useful when the type should be shown to user.
* Throws an exception if the class is not supported.
* @param DBElement $entity The element for which the label should be generated
* @return string The locatlized label for the entity type.
* @throws EntityNotSupportedException When the passed entity is not supported.
*/
public function getLocalizedTypeLabel(DBElement $entity) : string
{
//Check if we have an direct array entry for our entity class, then we can use it
if (isset($this->mapping[get_class($entity)])) {
return $this->mapping[get_class($entity)];
}
//Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed)
foreach ($this->mapping as $class => $translation) {
if ($entity instanceof $class) {
return $translation;
}
}
//When nothing was found throw an exception
throw new EntityNotSupportedException(
sprintf('No localized label for the element with type %s was found!', get_class($entity))
);
}
/**
* Returns a string like in the format ElementType: ElementName.
* For example this could be something like: "Part: BC547".
* It uses getLocalizedLabel to determine the type.
* @param NamedDBElement $entity The entity for which the string should be generated.
* @param bool $use_html If set to true, a html string is returned, where the type is set italic
* @return string The localized string
*/
public function getTypeNameCombination(NamedDBElement $entity, bool $use_html = false) : string
{
$type = $this->getLocalizedTypeLabel($entity);
if ($use_html) {
return '<i>' . $type . ':</i> ' . $entity->getName();
}
return $type . ": " . htmlspecialchars($entity->getName());
}
}

View file

@ -44,7 +44,7 @@ use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use App\Entity\UserSystem\Group; use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Exceptions\EntityNotSupported; use App\Exceptions\EntityNotSupportedException;
use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@ -72,7 +72,7 @@ class EntityURLGenerator
* @param array $map The map that should be used for determing the controller * @param array $map The map that should be used for determing the controller
* @param $entity mixed The entity for which the controller name should be determined. * @param $entity mixed The entity for which the controller name should be determined.
* @return string The name of the controller fitting the entity class * @return string The name of the controller fitting the entity class
* @throws EntityNotSupported * @throws EntityNotSupportedException
*/ */
protected function mapToController(array $map, $entity): string protected function mapToController(array $map, $entity): string
{ {
@ -87,7 +87,7 @@ class EntityURLGenerator
} }
} }
throw new EntityNotSupported(sprintf( throw new EntityNotSupportedException(sprintf(
'The given entity is not supported yet! Passed class type: %s', 'The given entity is not supported yet! Passed class type: %s',
get_class($entity) get_class($entity)
)); ));
@ -104,7 +104,7 @@ class EntityURLGenerator
* @param $entity mixed The element for which the page should be generated. * @param $entity mixed The element for which the page should be generated.
* @param string $type The page type. Currently supported: 'info', 'edit', 'create', 'clone', 'list'/'list_parts' * @param string $type The page type. Currently supported: 'info', 'edit', 'create', 'clone', 'list'/'list_parts'
* @return string The link to the desired page. * @return string The link to the desired page.
* @throws EntityNotSupported Thrown if the entity is not supported for the given type. * @throws EntityNotSupportedException Thrown if the entity is not supported for the given type.
* @throws \InvalidArgumentException Thrown if the givent type is not existing. * @throws \InvalidArgumentException Thrown if the givent type is not existing.
*/ */
public function getURL($entity, string $type) public function getURL($entity, string $type)
@ -142,7 +142,7 @@ class EntityURLGenerator
} }
//Otherwise throw an error //Otherwise throw an error
throw new EntityNotSupported('The given entity is not supported yet!'); throw new EntityNotSupportedException('The given entity is not supported yet!');
} }
public function downloadURL($entity): string public function downloadURL($entity): string
@ -155,7 +155,7 @@ class EntityURLGenerator
} }
//Otherwise throw an error //Otherwise throw an error
throw new EntityNotSupported(sprintf( throw new EntityNotSupportedException(sprintf(
'The given entity is not supported yet! Passed class type: %s', 'The given entity is not supported yet! Passed class type: %s',
get_class($entity) get_class($entity)
)); ));
@ -166,12 +166,25 @@ class EntityURLGenerator
* *
* @param $entity mixed The entity for which the info should be generated. * @param $entity mixed The entity for which the info should be generated.
* @return string The URL to the info page * @return string The URL to the info page
* @throws EntityNotSupported If the method is not supported for the given Entity * @throws EntityNotSupportedException If the method is not supported for the given Entity
*/ */
public function infoURL(DBElement $entity): string public function infoURL(DBElement $entity): string
{ {
$map = [ $map = [
Part::class => 'part_info' Part::class => 'part_info',
//As long we does not have own things for it use edit page
AttachmentType::class => 'attachment_type_edit',
Category::class => 'category_edit',
Device::class => 'device_edit',
Supplier::class => 'supplier_edit',
Manufacturer::class => 'manufacturer_edit',
Storelocation::class => 'store_location_edit',
Footprint::class => 'footprint_edit',
User::class => 'user_edit',
Currency::class => 'currency_edit',
MeasurementUnit::class => 'measurement_unit_edit',
Group::class => 'group_edit'
]; ];
return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]); return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]);
@ -182,7 +195,7 @@ class EntityURLGenerator
* *
* @param $entity mixed The entity for which the edit link should be generated. * @param $entity mixed The entity for which the edit link should be generated.
* @return string The URL to the edit page. * @return string The URL to the edit page.
* @throws EntityNotSupported If the method is not supported for the given Entity * @throws EntityNotSupportedException If the method is not supported for the given Entity
*/ */
public function editURL($entity): string public function editURL($entity): string
{ {
@ -209,7 +222,7 @@ class EntityURLGenerator
* *
* @param $entity mixed The entity for which the link should be generated. * @param $entity mixed The entity for which the link should be generated.
* @return string The URL to the page. * @return string The URL to the page.
* @throws EntityNotSupported If the method is not supported for the given Entity * @throws EntityNotSupportedException If the method is not supported for the given Entity
*/ */
public function createURL($entity): string public function createURL($entity): string
{ {
@ -237,7 +250,7 @@ class EntityURLGenerator
* *
* @param $entity mixed The entity for which the link should be generated. * @param $entity mixed The entity for which the link should be generated.
* @return string The URL to the page. * @return string The URL to the page.
* @throws EntityNotSupported If the method is not supported for the given Entity * @throws EntityNotSupportedException If the method is not supported for the given Entity
*/ */
public function cloneURL(DBElement $entity): string public function cloneURL(DBElement $entity): string
{ {
@ -253,7 +266,7 @@ class EntityURLGenerator
* *
* @param $entity mixed The entity for which the link should be generated. * @param $entity mixed The entity for which the link should be generated.
* @return string The URL to the page. * @return string The URL to the page.
* @throws EntityNotSupported If the method is not supported for the given Entity * @throws EntityNotSupportedException If the method is not supported for the given Entity
*/ */
public function listPartsURL(DBElement $entity): string public function listPartsURL(DBElement $entity): string
{ {

View file

@ -30,6 +30,7 @@
namespace App\Services; namespace App\Services;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Devices\Device; use App\Entity\Devices\Device;
use App\Entity\Parts\Category; use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint; use App\Entity\Parts\Footprint;
@ -50,6 +51,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* This Service generates the tree structure for the tools. * This Service generates the tree structure for the tools.
* Whenever you change something here, you has to clear the cache, because the results are cached for performance reasons.
* @package App\Services * @package App\Services
*/ */
class ToolsTreeBuilder class ToolsTreeBuilder
@ -78,7 +80,7 @@ class ToolsTreeBuilder
/** /**
* Generates the tree for the tools menu. * Generates the tree for the tools menu.
* The result is cached. * The result is cached.
* @return TreeViewNode The array containing all Nodes for the tools menu. * @return TreeViewNode[] The array containing all Nodes for the tools menu.
*/ */
public function getTree() : array public function getTree() : array
{ {
@ -175,10 +177,18 @@ class ToolsTreeBuilder
protected function getShowNodes() : array protected function getShowNodes() : array
{ {
$show_nodes = array(); $show_nodes = array();
$show_nodes[] = new TreeViewNode($this->translator->trans('tree.tools.show.all_parts'), $show_nodes[] = new TreeViewNode(
$this->translator->trans('tree.tools.show.all_parts'),
$this->urlGenerator->generate('parts_show_all') $this->urlGenerator->generate('parts_show_all')
); );
if ($this->security->isGranted('read', new PartAttachment())) {
$show_nodes[] = new TreeViewNode(
$this->translator->trans('tree.tools.show.all_attachments'),
$this->urlGenerator->generate('attachment_list')
);
}
return $show_nodes; return $show_nodes;
} }

View file

@ -80,7 +80,7 @@ class TreeBuilder
* @param DBElement|null $selectedElement When a element is given here, its tree node will be marked as selected in * @param DBElement|null $selectedElement When a element is given here, its tree node will be marked as selected in
* the resulting tree. When $selectedElement is not existing in the tree, then nothing happens. * the resulting tree. When $selectedElement is not existing in the tree, then nothing happens.
* @return TreeViewNode The Node for the given Element. * @return TreeViewNode The Node for the given Element.
* @throws \App\Exceptions\EntityNotSupported * @throws \App\Exceptions\EntityNotSupportedException
*/ */
public function elementToTreeNode(NamedDBElement $element, ?string $href_type = 'list_parts', DBElement $selectedElement = null) : TreeViewNode public function elementToTreeNode(NamedDBElement $element, ?string $href_type = 'list_parts', DBElement $selectedElement = null) : TreeViewNode
{ {
@ -123,7 +123,7 @@ class TreeBuilder
* @param DBElement|null $selectedElement When a element is given here, its tree node will be marked as selected in * @param DBElement|null $selectedElement When a element is given here, its tree node will be marked as selected in
* the resulting tree. When $selectedElement is not existing in the tree, then nothing happens. * the resulting tree. When $selectedElement is not existing in the tree, then nothing happens.
* @return TreeViewNode[] Returns an array, containing all nodes. It is empty if the given class has no elements. * @return TreeViewNode[] Returns an array, containing all nodes. It is empty if the given class has no elements.
* @throws \App\Exceptions\EntityNotSupported * @throws \App\Exceptions\EntityNotSupportedException
*/ */
public function typeToTree(string $class_name, ?string $href_type = 'list_parts', DBElement $selectedElement = null) : array public function typeToTree(string $class_name, ?string $href_type = 'list_parts', DBElement $selectedElement = null) : array
{ {

View file

@ -0,0 +1,16 @@
{% extends "base.html.twig" %}
{% block title %}{% trans %}attachment.list.title{% endtrans %}{% endblock %}
{% block content %}
<div id="part_list" class="" data-datatable data-settings='{{ datatable_settings(datatable) }}'>
<div class="card-body">
<div class="card">
<div class="card-body">
<h4>{% trans %}part_list.loading.caption{% endtrans %}</h4>
<h6>{% trans %}part_list.loading.message{% endtrans %}</h6>
</div>
</div>
</div>
</div>
{% endblock %}