feat: mark a library document for a specific purpose
This commit is contained in:
1 parent
cb2d64f2cd
commit
19364df2db
8 files changed
+249
-6
No files matched your search
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
/**
|
||||
* The slots in the staff-wide document library (Upload::TYPE_DOCUMENT) that code looks a file up
|
||||
* by, rather than by id or name.
|
||||
*
|
||||
* A fixed list and not free text on purpose: something in the application has to consume a slot
|
||||
* for it to mean anything, so inventing one without a release would only produce a token nothing
|
||||
* reads - and a typo would look exactly the same as a document nobody has assigned yet, while
|
||||
* silently dropping an attachment.
|
||||
*
|
||||
* A case's value is what ends up in upload.token, so renaming one orphans the assignment and the
|
||||
* mail quietly loses its attachment. The column is unique: one document per slot.
|
||||
*/
|
||||
enum UploadToken: string
|
||||
{
|
||||
case APPLICATION_FORM_HANDOUT = 'application_form_handout';
|
||||
|
||||
/**
|
||||
* What the document library offers in its dropdown.
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::APPLICATION_FORM_HANDOUT => 'Handout für die 2. Mail an Bewerber:innen',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Controller\Administrative\System\Document;
|
||||
|
||||
use App\Config\UploadToken;
|
||||
use App\Entity\Upload;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Repository\UploadRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EnumType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -18,6 +21,7 @@ class EditController extends AbstractController
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly UploadRepository $uploadRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -26,6 +30,7 @@ class EditController extends AbstractController
|
||||
public function index(Upload $upload, Request $request): Response
|
||||
{
|
||||
$nameBefore = $upload->getDisplayName();
|
||||
$tokenBefore = $upload->getToken();
|
||||
|
||||
$form = $this
|
||||
->createFormBuilder($upload)
|
||||
@@ -35,19 +40,37 @@ class EditController extends AbstractController
|
||||
'placeholder' => $upload->getOriginalFilename(),
|
||||
],
|
||||
])
|
||||
->add('token', EnumType::class, [
|
||||
'label' => 'Verwendung',
|
||||
'class' => UploadToken::class,
|
||||
'required' => false,
|
||||
'placeholder' => 'keine besondere Verwendung',
|
||||
'choice_label' => fn (UploadToken $token) => $token->label(),
|
||||
'help' => 'Eine Verwendung gehört immer zu genau einem Dokument. Wird sie hier gesetzt, verliert das bisherige Dokument sie.',
|
||||
])
|
||||
->getForm()
|
||||
;
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->flush();
|
||||
$this->addFlash('success', 'Das Dokument wurden umbenannt');
|
||||
// The column is unique, so taking a slot has to free it first - otherwise the flush
|
||||
// dies on the index. Moving rather than refusing is what staff mean by assigning it:
|
||||
// "this is the handout now".
|
||||
$previousHolderId = null === $upload->getToken()
|
||||
? null
|
||||
: $this->uploadRepository->releaseToken($upload->getToken(), $upload);
|
||||
|
||||
$this->logger->info('Rename document', [
|
||||
$this->entityManager->flush();
|
||||
$this->addFlash('success', 'Das Dokument wurde aktualisiert');
|
||||
|
||||
$this->logger->info('Edit document', [
|
||||
'document_id' => $upload->getId(),
|
||||
'document_filename' => $upload->getOriginalFilename(),
|
||||
'document_name_before' => $nameBefore,
|
||||
'document_name_after' => $upload->getDisplayName(),
|
||||
'document_token_before' => $tokenBefore?->value,
|
||||
'document_token_after' => $upload->getToken()?->value,
|
||||
'document_token_taken_from' => $previousHolderId,
|
||||
]);
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_system_document_index'));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Config\UploadToken;
|
||||
use App\Entity\Traits\BlameableEntity;
|
||||
use App\Entity\Traits\TimestampableEntity;
|
||||
use App\Model\UploadDto;
|
||||
@@ -53,6 +54,14 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $displayName = null;
|
||||
|
||||
/**
|
||||
* Marks this document as the one a specific feature looks up - the handout a mail attaches,
|
||||
* and whatever else earns a slot later. Unique, so assigning a token that another document
|
||||
* holds moves it rather than duplicating it (see the document library's edit action).
|
||||
*/
|
||||
#[ORM\Column(length: 64, nullable: true, unique: true, enumType: UploadToken::class)]
|
||||
private ?UploadToken $token = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?int $size = null;
|
||||
|
||||
@@ -163,6 +172,18 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
return static::STATUS_REJECTED === $this->getStatus();
|
||||
}
|
||||
|
||||
public function getToken(): ?UploadToken
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setToken(?UploadToken $token): static
|
||||
{
|
||||
$this->token = $token;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFilename(): ?string
|
||||
{
|
||||
return $this->filename;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Config\UploadToken;
|
||||
use App\Entity\Upload;
|
||||
use App\Model\DocumentFilterDto;
|
||||
use App\Repository\Traits\QueryHelperTrait;
|
||||
@@ -231,4 +232,38 @@ class UploadRepository extends ServiceEntityRepository
|
||||
->getSingleScalarResult()
|
||||
;
|
||||
}
|
||||
|
||||
public function findByToken(UploadToken $token): ?Upload
|
||||
{
|
||||
return $this->findOneBy(['token' => $token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees a token slot, so the document taking it can be flushed without tripping the unique
|
||||
* index. The update runs immediately rather than through the unit of work on purpose: both
|
||||
* rows are Uploads, and nothing guarantees Doctrine writes the release before it writes the
|
||||
* row that takes the token.
|
||||
*
|
||||
* Returns the id of the document that lost the slot, for the log.
|
||||
*/
|
||||
public function releaseToken(UploadToken $token, Upload $takenBy): ?int
|
||||
{
|
||||
$holder = $this->findByToken($token);
|
||||
|
||||
if (null === $holder || $holder === $takenBy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this
|
||||
->getEntityManager()
|
||||
->createQuery('UPDATE App\Entity\Upload u SET u.token = NULL WHERE u.id = :id')
|
||||
->setParameter('id', $holder->getId())
|
||||
->execute()
|
||||
;
|
||||
|
||||
// Keeps the loaded entity in step with the row that was just written.
|
||||
$holder->setToken(null);
|
||||
|
||||
return $holder->getId();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user