Implement voting for editor permissions (draft)
This commit is contained in:
parent
adee0f6d0f
commit
097bf6a272
@ -28,6 +28,11 @@ class BibliographyController extends AbstractController
|
||||
$bibliography->setCollections($collections);
|
||||
$bibliography->setCollectors($collectors);
|
||||
|
||||
$repo = $em->getRepository(Bibliography::class);
|
||||
$isEditable = $repo->hasCreatorEditor($bibliography->getCreator());
|
||||
|
||||
$bibliography->setEditableStatus($isEditable);
|
||||
|
||||
return $this->render('bibliography/index.html.twig', [
|
||||
'controller_name' => 'BibliographyController',
|
||||
'record' => $bibliography,
|
||||
@ -85,10 +90,11 @@ class BibliographyController extends AbstractController
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
$em->remove($bibliography);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('notice', 'Record deleted successfully');
|
||||
if ($bibliography) {
|
||||
$em->remove($bibliography);
|
||||
$em->flush();
|
||||
$this->addFlash('notice', 'Record deleted successfully');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_bibliography_landing');
|
||||
}
|
||||
@ -100,7 +106,7 @@ class BibliographyController extends AbstractController
|
||||
public function copy(Bibliography $bibliography, EntityManagerInterface $em): Response
|
||||
{
|
||||
try {
|
||||
$this->denyAccessUnlessGranted(RecordVoter::EDIT, $bibliography);
|
||||
$this->denyAccessUnlessGranted(RecordVoter::COPY, $bibliography);
|
||||
}
|
||||
catch (AccessDeniedException) {
|
||||
$this->addFlash('warning', 'You are not authorized to copy this record');
|
||||
|
@ -20,9 +20,12 @@ class CollectionController extends AbstractController
|
||||
{
|
||||
$repo = $em->getRepository(Bibliography::class);
|
||||
$bibliographies = $repo->findAllByCollection($collection->getId());
|
||||
|
||||
$collection->setBibliographies($bibliographies);
|
||||
|
||||
$repo = $em->getRepository(Collection::class);
|
||||
$isEditable = $repo->hasCreatorEditor($collection->getCreator());
|
||||
$collection->setEditableStatus($isEditable);
|
||||
|
||||
return $this->render('collection/index.html.twig', [
|
||||
'controller_name' => 'CollectionController',
|
||||
'record' => $collection,
|
||||
@ -56,8 +59,10 @@ class CollectionController extends AbstractController
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
$em->remove($collection);
|
||||
$em->flush();
|
||||
if ($collection) {
|
||||
$em->remove($collection);
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
$this->addFlash('notice', 'Record deleted successfully');
|
||||
|
||||
|
@ -5,6 +5,7 @@ namespace App\Controller;
|
||||
use App\Entity\Collector;
|
||||
use App\Entity\Collection;
|
||||
use App\Entity\Bibliography;
|
||||
use App\RecordStatus;
|
||||
use App\Security\Voter\RecordVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@ -21,6 +22,10 @@ class CollectorController extends AbstractController
|
||||
$bibliographies = $repo->findAllByCollector($collector->getId());
|
||||
$collector->setBibliographies($bibliographies);
|
||||
|
||||
$repo = $em->getRepository(Collector::class);
|
||||
$isEditable = $repo->hasCreatorEditor($collector->getCreator());
|
||||
$collector->setEditableStatus($isEditable);
|
||||
|
||||
return $this->render('collector/index.html.twig', [
|
||||
'controller_name' => 'CollectorController',
|
||||
'record' => $collector,
|
||||
@ -61,4 +66,40 @@ class CollectorController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute('app_collector_landing');
|
||||
}
|
||||
/**
|
||||
* @todo Move clone logic to __clone() in Entity or Repository
|
||||
*/
|
||||
#[Route('/collector/copy/{id<\d+>}', name: 'app_collector_copy')]
|
||||
public function copy(Collector $collector, EntityManagerInterface $em): Response
|
||||
{
|
||||
try {
|
||||
$this->denyAccessUnlessGranted(RecordVoter::EDIT, $collector);
|
||||
}
|
||||
catch (AccessDeniedException) {
|
||||
$this->addFlash('warning', 'You are not authorized to copy this record');
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$editor = "{$user->getFirstname()} {$user->getLastName()}";
|
||||
|
||||
$copy = clone $collector;
|
||||
$copy->setEditor($editor);
|
||||
$copy->setOwner($editor);
|
||||
$copy->setCreator($user->getUsername());
|
||||
$repo = $em->getRepository(Bibliography::class);
|
||||
$copy->setBibliographies(
|
||||
$repo->findAllByCollector($collector->getId())
|
||||
);
|
||||
$copy->setName("{$collector->getName()} - Copy");
|
||||
$copy->setModifiedAt(new \DateTimeImmutable());
|
||||
$copy->setStatus(RecordStatus::Draft->value);
|
||||
|
||||
$em->persist($copy);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('notice', 'Record copied successfully');
|
||||
|
||||
return $this->redirectToRoute('app_collector', ['id' => $copy->getId()]);
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,14 @@ namespace App\Entity;
|
||||
|
||||
use App\RecordInterface;
|
||||
use App\Repository\BibliographyRepository;
|
||||
use App\Repository\CollectionRepository;
|
||||
use App\Repository\CollectorRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use App\RecordStatus;
|
||||
use ContainerBNNizmi\getBibliographyRepositoryService;
|
||||
use Doctrine\Common\Collections\Collection as DoctrineCollection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder as QueryQueryBuilder;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
#[ORM\Entity(repositoryClass: BibliographyRepository::class)]
|
||||
#[ORM\Table(name: 'bibliography')]
|
||||
@ -64,6 +64,9 @@ class Bibliography implements RecordInterface
|
||||
|
||||
private DoctrineCollection $sites;
|
||||
|
||||
// Checks if the record can be edited by an 'editor' user
|
||||
private bool $isEditable = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@ -189,4 +192,16 @@ class Bibliography implements RecordInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEditableStatus(): bool
|
||||
{
|
||||
return $this->isEditable;
|
||||
}
|
||||
|
||||
public function setEditableStatus(bool $status): static
|
||||
{
|
||||
$this->isEditable = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,8 @@ class Collection implements RecordInterface
|
||||
#[ORM\ManyToMany(targetEntity: Bibliography::class, inversedBy: 'collection')]
|
||||
private DoctrineCollection $bibliographies;
|
||||
|
||||
private bool $isEditable = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@ -292,4 +294,16 @@ class Collection implements RecordInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEditableStatus(): bool
|
||||
{
|
||||
return $this->isEditable;
|
||||
}
|
||||
|
||||
public function setEditableStatus(bool $status): static
|
||||
{
|
||||
$this->isEditable = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,8 @@ class Collector implements RecordInterface
|
||||
#[ORM\ManyToMany(targetEntity: Bibliography::class, inversedBy: 'collector')]
|
||||
private DoctrineCollection $bibliographies;
|
||||
|
||||
private bool $isEditable = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@ -292,4 +294,16 @@ class Collector implements RecordInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEditableStatus(): bool
|
||||
{
|
||||
return $this->isEditable;
|
||||
}
|
||||
|
||||
public function setEditableStatus(bool $status): static
|
||||
{
|
||||
$this->isEditable = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Bibliography;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@ -18,6 +19,28 @@ class BibliographyRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, Bibliography::class);
|
||||
}
|
||||
|
||||
public function hasCreatorEditor(string $creator): bool
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$repo = $em->getRepository(User::class);
|
||||
|
||||
$creator = $repo->findOneBy(['username' => $creator]);
|
||||
|
||||
return in_array('ROLE_EDITOR', $creator->getRoles());
|
||||
}
|
||||
|
||||
/*
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$qb = $this->createQueryBuilder('b')
|
||||
->delete(Bibliography::class, 'bib')
|
||||
->where('bib.id = :id')
|
||||
->setParameter('id', $id);
|
||||
|
||||
$qb->getQuery()->execute();
|
||||
}
|
||||
*/
|
||||
|
||||
public function findAllByCollection(int $collectionId): ?ArrayCollection
|
||||
{
|
||||
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Collection;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@ -18,6 +19,16 @@ class CollectionRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, Collection::class);
|
||||
}
|
||||
|
||||
public function hasCreatorEditor(string $creator): bool
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$repo = $em->getRepository(User::class);
|
||||
|
||||
$creator = $repo->findOneBy(['username' => $creator]);
|
||||
|
||||
return in_array('ROLE_EDITOR', $creator->getRoles());
|
||||
}
|
||||
|
||||
public function findAllByBibliography(int $biblioId): ?ArrayCollection
|
||||
{
|
||||
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Collector;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
@ -18,6 +19,16 @@ class CollectorRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, Collector::class);
|
||||
}
|
||||
|
||||
public function hasCreatorEditor(string $creator): bool
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$repo = $em->getRepository(User::class);
|
||||
|
||||
$creator = $repo->findOneBy(['username' => $creator]);
|
||||
|
||||
return in_array('ROLE_EDITOR', $creator->getRoles());
|
||||
}
|
||||
|
||||
public function findAllByBibliography(int $biblioId): ?ArrayCollection
|
||||
{
|
||||
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
||||
|
@ -5,16 +5,21 @@ namespace App\Security\Voter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use App\Entity\User;
|
||||
use App\RecordStatus;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
final class RecordVoter extends Voter
|
||||
{
|
||||
public const EDIT = 'RECORD_EDIT';
|
||||
public const DELETE = 'RECORD_DELETE';
|
||||
public const COPY = 'RECORD_COPY';
|
||||
public const VIEW = 'RECORD_VIEW';
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE])
|
||||
return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE, self::COPY])
|
||||
&& $subject instanceof \App\RecordInterface;
|
||||
}
|
||||
|
||||
@ -32,11 +37,37 @@ final class RecordVoter extends Voter
|
||||
// TODO: Better way to check roles?
|
||||
switch ($attribute) {
|
||||
case self::EDIT:
|
||||
case self::DELETE:
|
||||
$editorAllowed = true;
|
||||
if (in_array('ROLE_EDITOR', $roles)) {
|
||||
$creator = $subject->getCreator();
|
||||
$isCreator = $creator === $user->getUsername();
|
||||
$creatorIsEditor = $subject->getEditableStatus();
|
||||
|
||||
$editorAllowed &= $isCreator || $creatorIsEditor;
|
||||
}
|
||||
|
||||
return in_array('ROLE_ADMIN', $roles)
|
||||
|| in_array('ROLE_REVISOR', $roles);
|
||||
|| in_array('ROLE_REVISOR', $roles)
|
||||
|| $editorAllowed;
|
||||
case self::DELETE:
|
||||
$editorAllowed = true;
|
||||
if (in_array('ROLE_EDITOR', $roles)) {
|
||||
$isCreator = $subject->getCreator() === $user->getUsername();
|
||||
$isValidStatus = match($subject->getStatus()) {
|
||||
RecordStatus::Draft => true,
|
||||
RecordStatus::Complete => true,
|
||||
default => false
|
||||
};
|
||||
|
||||
$editorAllowed &= $isCreator && $isValidStatus;
|
||||
}
|
||||
|
||||
return in_array('ROLE_ADMIN', $roles)
|
||||
|| in_array('ROLE_REVISOR', $roles)
|
||||
|| $editorAllowed;
|
||||
break;
|
||||
|
||||
case self::COPY:
|
||||
case self::VIEW:
|
||||
return ! in_array('ROLE_READER', $roles);
|
||||
break;
|
||||
|
@ -27,16 +27,20 @@
|
||||
</article>
|
||||
|
||||
<div class="card p-5">
|
||||
{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_REVISOR') %}
|
||||
{% if not is_granted('ROLE_READER') %}
|
||||
<div class="columns">
|
||||
<div class="column is-half"></div>
|
||||
<div class="column has-text-right">
|
||||
{% if is_granted('ROLE_REVISOR') or
|
||||
is_granted('ROLE_ADMIN') or
|
||||
record.editableStatus %}
|
||||
<button class="button is-link">
|
||||
Edit
|
||||
<span class="icon ml-2">
|
||||
<i class="fa fa-edit"></i>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_bibliography_copy', {'id' : record.id}) }}"
|
||||
class="button is-link">
|
||||
Copy
|
||||
@ -44,14 +48,16 @@
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</a>
|
||||
<a href="{{ path('app_bibliography_del', {'id' : record.id}) }}"
|
||||
{% if is_granted('ROLE_REVISOR') or is_granted('ROLE_ADMIN') %}
|
||||
<button data-url="{{ path('app_bibliography_del', {'id' : record.id}) }}"
|
||||
class="button is-danger"
|
||||
data-delete-record-target="path" data-action="click->delete-record#show">
|
||||
data-delete-record-target="path" data-action="click->delete-record#warn">
|
||||
Delete
|
||||
<span class="icon ml-2">
|
||||
<i class="fa fa-trash"></i>
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -43,14 +43,14 @@
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</a>
|
||||
<a href="{{ path('app_collection_del', {'id' : record.id}) }}"
|
||||
<button data-url="{{ path('app_collection_del', {'id' : record.id}) }}"
|
||||
class="button is-danger"
|
||||
data-delete-record-target="path" data-action="click->delete-record#show">
|
||||
data-delete-record-target="path" data-action="click->delete-record#warn">
|
||||
Delete
|
||||
<span class="icon ml-2">
|
||||
<i class="fa fa-trash"></i>
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -74,7 +74,7 @@
|
||||
<tr><th>External identifier(s)</th><td>{{ record.externalIdentifier }}</td></tr>
|
||||
<tr><th>External link(s)</th><td>{{ record.link }}</td></tr>
|
||||
<tr><th>Subject headings</th><td>{{ record.subjectHeadings }}</td></tr>
|
||||
<tr><th>ArCOA URI</th><td>{{ record.uri }}</td></tr>
|
||||
<tr><th>ArCOA URI</th><td>arcoa.cnr.it/collection/{{ record.id }}</td></tr>
|
||||
<tr><th>Editorial notes</th><td>{{ record.notes }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -7,6 +7,15 @@
|
||||
<h1 class="is-size-1 mt-0 has-text-centered">Collector</h1>
|
||||
<h2 class="is-size-3 mt-3 has-text-centered">{{ record.name }}</h2>
|
||||
|
||||
{% for message in app.flashes('notice') %}
|
||||
<div class=" mt-4 notification is-success"
|
||||
data-controller="notification"
|
||||
data-notification-target="notif">
|
||||
<button class="delete" data-action="click->notification#close"></button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<article class="message is-info mt-3">
|
||||
<div class="message-body">
|
||||
<p>
|
||||
@ -27,20 +36,21 @@
|
||||
<i class="fa fa-edit"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-link">
|
||||
<a href="{{ path('app_collector_copy', {'id' : record.id}) }}"
|
||||
class="button is-link">
|
||||
Copy
|
||||
<span class="icon ml-2">
|
||||
<i class="fa fa-copy"></i>
|
||||
</span>
|
||||
</button>
|
||||
<a href=""
|
||||
</a>
|
||||
<button data-url="{{ path('app_collector_del', {'id' : record.id}) }}"
|
||||
class="button is-danger"
|
||||
data-delete-record-target="path" data-action="click->delete-record#show">
|
||||
data-delete-record-target="path" data-action="click->delete-record#warn">
|
||||
Delete
|
||||
<span class="icon ml-2">
|
||||
<i class="fa fa-trash"></i>
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -64,7 +74,7 @@
|
||||
<tr><th>External identifier(s)</th><td>{{ record.externalIdentifier }}</td></tr>
|
||||
<tr><th>External link(s)</th><td>{{ record.link }}</td></tr>
|
||||
<tr><th>Subject headings</th><td>{{ record.subjectHeadings }}</td></tr>
|
||||
<tr><th>ArCOA URI</th><td>{{ record.uri }}</td></tr>
|
||||
<tr><th>ArCOA URI</th><td>arcoa.cnr.it/collector/{{ record.id }}</td></tr>
|
||||
<tr><th>Editorial notes</th><td>{{ record.notes }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -4,10 +4,13 @@
|
||||
|
||||
{% block rightpanel %}
|
||||
{% for message in app.flashes('warning') %}
|
||||
<article class="message is-warning mb-6 mt-3 ml-auto mr-auto" style="max-width: 35vw">
|
||||
<article class="message is-warning mb-6 mt-3 ml-auto mr-auto" style="max-width: 35vw"
|
||||
data-controller="notification"
|
||||
data-notification-target="notif">
|
||||
<div class="message-header">
|
||||
<p>Warning</p>
|
||||
<button class="delete" aria-label="delete"></button>
|
||||
<button class="delete" aria-label="delete"
|
||||
data-action="click->notification#close"></button>
|
||||
</div>
|
||||
<div class="message-body">{{ message }}</div>
|
||||
</article>
|
||||
@ -40,13 +43,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" defer>
|
||||
const warning = document.querySelector('.is-warning');
|
||||
|
||||
if (warning) {
|
||||
warning.querySelector('.delete').addEventListener('click', () => {
|
||||
warning.classList.add('is-hidden');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user