diff --git a/CHANGELOG.md b/CHANGELOG.md index abeaf3a1..a4adfcfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ See [keep a changelog] for information about writing changes to this log. - Added daily occurrence factory - Added indexing service and helper commands to populate and create indexes - Added Easy admin and event fixtures +- Make data imported from feeds read-only in easy admin [keep a changelog]: https://keepachangelog.com/en/1.1.0/ [unreleased]: https://github.com/itk-dev/event-database-imports/compare/main...develop diff --git a/baseline.xml b/baseline.xml index 14610079..100848a6 100644 --- a/baseline.xml +++ b/baseline.xml @@ -7,6 +7,8 @@ + getReference(ImagesFixtures::AAK)]]> + getReference(ImagesFixtures::ITK)]]> getReference(LocationFixture::ITKDEV)]]> getReference(LocationFixture::ITKDEV)]]> getReference(OrganizationFixtures::ITK)]]> @@ -14,6 +16,7 @@ getReference(TagsFixtures::AROS)]]> getReference(TagsFixtures::AROS)]]> getReference(TagsFixtures::CONCERT)]]> + getReference(TagsFixtures::ITKDEV)]]> getReference(TagsFixtures::RACE)]]> @@ -30,6 +33,12 @@ getReference(UserFixtures::USER)]]> + + + getSource()]]> + getSource()]]> + + getReference(AddressFixture::ITKDEV)]]> @@ -54,6 +63,7 @@ getReference(VocabularyFixtures::MANAGED)]]> getReference(VocabularyFixtures::MANAGED)]]> getReference(VocabularyFixtures::MANAGED)]]> + getReference(VocabularyFixtures::MANAGED)]]> diff --git a/docs/adr/006-user handling and login b/docs/adr/006-user handling and login deleted file mode 100644 index 377e86f3..00000000 --- a/docs/adr/006-user handling and login +++ /dev/null @@ -1,17 +0,0 @@ -ty Provider# User handling and login - -Date: 28-09-2023 - -## Status - -Accepted - -## Context -The idea is that each organization/institution have an appointed admin that is allowed to send user invitation out for his/her organization. Each user can be part of more than one organization, it just requires that they get an invitation and thereby link their account to a given organization. - -## Decision -For this first phase of the project users login by using username and password. If there needs to be a higher level of security sorounding user login in the future an identity Provider such as MitId can be connected to the project, but for now the customer has specifically requested (due to experiences with login in the existing solution) that we continue using username and password. -Using Single Sign On (SSO) would be a more professional login method for the future but for now it's not a part of the project. - -## Consequences -Login using username and password is not as secure as using SSO diff --git a/docs/adr/006-user-handling-and-login.md b/docs/adr/006-user-handling-and-login.md new file mode 100644 index 00000000..ab8881a8 --- /dev/null +++ b/docs/adr/006-user-handling-and-login.md @@ -0,0 +1,27 @@ +# User handling and login + +Date: 28-09-2023 + +## Status + +Accepted + +## Context + +The idea is that each organization/institution has an appointed admin that is allowed to send users invitation out for +his/her organization. Each user can be part of more than one organization, it just requires that they get an invitation +and thereby link their account to a given organization. + +## Decision + +For this first phase of the project, users login by using username and password. If there needs to be a higher level of +security surrounding user login in the future, an identity Provider such as MitId can be connected to the project, but +for now the customer has specifically requested (due to experiences with login in the existing solution) that we +continue using username and password. + +Using Single Sign On (SSO) would be a more professional login method for the future, but for now it's not a part of the +project. + +## Consequences + +Login using username and password is not as secure as using SSO. diff --git a/docs/adr/007-editability-of-content.md b/docs/adr/007-editability-of-content.md new file mode 100644 index 00000000..3b007c44 --- /dev/null +++ b/docs/adr/007-editability-of-content.md @@ -0,0 +1,24 @@ +# Editability of content + +Date: 27-11-2023 + +## Status + +Accepted + +## Context + +The forms UI (administrative user interface) allows users to add new content and change existing data. + +## Decision + +All data imported through feeds are marked as read-only as changes made in the administrative UI are overwritten by +changes in the feed. + +All other data is editable and will trigger a reindex of the data changes and all related data. + +## Consequences + +Imported data from feeds are not editable, and some changes to data not from feeds will trigger a reindex of data, +which may trigger a lager job to be processed based on which data is changed. E.g. if an address is changed, that will +trigger an update of all content in indexes that contains that address. Which may be a large number of events. diff --git a/migrations/Version20231127121531.php b/migrations/Version20231127121531.php new file mode 100644 index 00000000..015b3f0a --- /dev/null +++ b/migrations/Version20231127121531.php @@ -0,0 +1,41 @@ +addSql('ALTER TABLE address ADD editable TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE event ADD editable TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE image ADD editable TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE location ADD editable TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE occurrence ADD editable TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE tag ADD editable TINYINT(1) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE occurrence DROP editable'); + $this->addSql('ALTER TABLE location DROP editable'); + $this->addSql('ALTER TABLE event DROP editable'); + $this->addSql('ALTER TABLE tag DROP editable'); + $this->addSql('ALTER TABLE address DROP editable'); + $this->addSql('ALTER TABLE image DROP editable'); + } +} diff --git a/src/Controller/Admin/AbstractBaseCrudController.php b/src/Controller/Admin/AbstractBaseCrudController.php new file mode 100644 index 00000000..bff9b7b3 --- /dev/null +++ b/src/Controller/Admin/AbstractBaseCrudController.php @@ -0,0 +1,28 @@ +update(Crud::PAGE_INDEX, Action::EDIT, static function (Action $action) { + return $action->displayIf(static function (object $entity) { + return !($entity instanceof EditableEntityInterface) || $entity->isEditable(); + }); + }) + ->add(Crud::PAGE_INDEX, Action::DETAIL); + } +} diff --git a/src/Controller/Admin/AddressCrudController.php b/src/Controller/Admin/AddressCrudController.php index 95dc2842..276a654e 100644 --- a/src/Controller/Admin/AddressCrudController.php +++ b/src/Controller/Admin/AddressCrudController.php @@ -5,7 +5,6 @@ use App\Entity\Address; use Doctrine\Common\Collections\Criteria; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; @@ -13,7 +12,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use Symfony\Component\Translation\TranslatableMessage; -class AddressCrudController extends AbstractCrudController +class AddressCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/EventCrudController.php b/src/Controller/Admin/EventCrudController.php index a5cdf384..ec8f5eb1 100644 --- a/src/Controller/Admin/EventCrudController.php +++ b/src/Controller/Admin/EventCrudController.php @@ -5,7 +5,6 @@ use App\Entity\Event; use Doctrine\Common\Collections\Criteria; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; @@ -15,7 +14,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField; use Symfony\Component\Translation\TranslatableMessage; -class EventCrudController extends AbstractCrudController +class EventCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/FeedCrudController.php b/src/Controller/Admin/FeedCrudController.php index b0bd49a3..29779806 100644 --- a/src/Controller/Admin/FeedCrudController.php +++ b/src/Controller/Admin/FeedCrudController.php @@ -3,7 +3,6 @@ namespace App\Controller\Admin; use App\Entity\Feed; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\CodeEditorField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; @@ -11,7 +10,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use Symfony\Component\Translation\TranslatableMessage; -class FeedCrudController extends AbstractCrudController +class FeedCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/ImageCrudController.php b/src/Controller/Admin/ImageCrudController.php index 4e7179cd..e4914cf7 100644 --- a/src/Controller/Admin/ImageCrudController.php +++ b/src/Controller/Admin/ImageCrudController.php @@ -3,7 +3,6 @@ namespace App\Controller\Admin; use App\Entity\Image; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; @@ -11,7 +10,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField; use Symfony\Component\Translation\TranslatableMessage; -class ImageCrudController extends AbstractCrudController +class ImageCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/LocationCrudController.php b/src/Controller/Admin/LocationCrudController.php index aca78fbe..3d637809 100644 --- a/src/Controller/Admin/LocationCrudController.php +++ b/src/Controller/Admin/LocationCrudController.php @@ -5,7 +5,6 @@ use App\Entity\Location; use Doctrine\Common\Collections\Criteria; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; @@ -17,7 +16,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField; use Symfony\Component\Translation\TranslatableMessage; -class LocationCrudController extends AbstractCrudController +class LocationCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/OccurrenceCrudController.php b/src/Controller/Admin/OccurrenceCrudController.php index e8fb2763..9d9e3e05 100644 --- a/src/Controller/Admin/OccurrenceCrudController.php +++ b/src/Controller/Admin/OccurrenceCrudController.php @@ -5,7 +5,6 @@ use App\Entity\Occurrence; use Doctrine\Common\Collections\Criteria; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; @@ -13,7 +12,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use Symfony\Component\Translation\TranslatableMessage; -class OccurrenceCrudController extends AbstractCrudController +class OccurrenceCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/OrganizationCrudController.php b/src/Controller/Admin/OrganizationCrudController.php index a6f6ddc6..93f9eb22 100644 --- a/src/Controller/Admin/OrganizationCrudController.php +++ b/src/Controller/Admin/OrganizationCrudController.php @@ -3,7 +3,6 @@ namespace App\Controller\Admin; use App\Entity\Organization; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; @@ -12,7 +11,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField; use Symfony\Component\Translation\TranslatableMessage; -class OrganizationCrudController extends AbstractCrudController +class OrganizationCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/TagCrudController.php b/src/Controller/Admin/TagCrudController.php index 3e10c3fc..ff40cfcf 100644 --- a/src/Controller/Admin/TagCrudController.php +++ b/src/Controller/Admin/TagCrudController.php @@ -3,7 +3,6 @@ namespace App\Controller\Admin; use App\Entity\Tag; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\FormField; @@ -11,7 +10,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use Symfony\Component\Translation\TranslatableMessage; -class TagCrudController extends AbstractCrudController +class TagCrudController extends AbstractBaseCrudController { public static function getEntityFqcn(): string { diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php index fca772f7..6b81218d 100644 --- a/src/Controller/Admin/UserCrudController.php +++ b/src/Controller/Admin/UserCrudController.php @@ -6,7 +6,6 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore; use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; -use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; @@ -21,7 +20,7 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Translation\TranslatableMessage; -class UserCrudController extends AbstractCrudController +class UserCrudController extends AbstractBaseCrudController { public function __construct( private readonly UserPasswordHasherInterface $userPasswordHasher, diff --git a/src/DataFixtures/AddressFixture.php b/src/DataFixtures/AddressFixture.php index b22a2efc..c2b1dd1e 100644 --- a/src/DataFixtures/AddressFixture.php +++ b/src/DataFixtures/AddressFixture.php @@ -20,7 +20,8 @@ public function load(ObjectManager $manager): void ->setCountry('Danmark') ->setCity('Aarhus') ->setLatitude(56.1507645) - ->setLongitude(10.2112699); + ->setLongitude(10.2112699) + ->setEditable(true); $manager->persist($address); $this->addReference(self::ITKDEV, $address); diff --git a/src/DataFixtures/EventFixture.php b/src/DataFixtures/EventFixture.php index 5cf91bf6..3597500e 100644 --- a/src/DataFixtures/EventFixture.php +++ b/src/DataFixtures/EventFixture.php @@ -30,7 +30,10 @@ public function load(ObjectManager $manager): void ->setLocation($this->getReference(LocationFixture::ITKDEV)) ->addTag($this->getReference(TagsFixtures::AROS)) ->addTag($this->getReference(TagsFixtures::RACE)) - ->setHash('4936efebda146f6775fb7e429d884fef'); + ->addTag($this->getReference(TagsFixtures::ITKDEV)) + ->setImage($this->getReference(ImagesFixtures::ITK)) + ->setEditable(true) + ->setHash('4936efebda146f6775fb7e429d884fef'); $manager->persist($event); $this->addReference(self::EVENT2, $event); @@ -45,6 +48,8 @@ public function load(ObjectManager $manager): void ->setLocation($this->getReference(LocationFixture::ITKDEV)) ->addTag($this->getReference(TagsFixtures::CONCERT)) ->addTag($this->getReference(TagsFixtures::AROS)) + ->setEditable(true) + ->setImage($this->getReference(ImagesFixtures::AAK)) ->setHash('16d48c26d38f6d59b3d081e596b4d0e8'); $manager->persist($event); $this->addReference(self::EVENT1, $event); @@ -60,6 +65,7 @@ public function getDependencies(): array OrganizationFixtures::class, LocationFixture::class, TagsFixtures::class, + ImagesFixtures::class, ]; } } diff --git a/src/DataFixtures/ImagesFixtures.php b/src/DataFixtures/ImagesFixtures.php new file mode 100644 index 00000000..cda26696 --- /dev/null +++ b/src/DataFixtures/ImagesFixtures.php @@ -0,0 +1,41 @@ +setEditable(true) + ->setTitle('ITK Test image') + ->setSource('https://itk.aarhus.dk/media/79711/itk-4f-10.png'); + $image->setLocal($this->imageHandler->fetch($image->getSource())); + $manager->persist($image); + $this->addReference(self::ITK, $image); + + $image = new Image(); + $image->setEditable(false) + ->setTitle('AAK Test image') + ->setSource('https://placehold.co/600x400/0FF0FF/FF0000.png?text=AAK-Test'); + $image->setLocal($this->imageHandler->fetch($image->getSource())); + $manager->persist($image); + $this->addReference(self::AAK, $image); + + // Make it stick. + $manager->flush(); + } +} diff --git a/src/DataFixtures/LocationFixture.php b/src/DataFixtures/LocationFixture.php index c0f7a20f..eddac353 100644 --- a/src/DataFixtures/LocationFixture.php +++ b/src/DataFixtures/LocationFixture.php @@ -18,7 +18,8 @@ public function load(ObjectManager $manager): void ->setMail('itkdev@mkb.aarhus.dk') ->setUrl('https://itk.aarhus.dk/om-itk/afdelinger/development/') ->setAddress($this->getReference(AddressFixture::ITKDEV)) - ->setDisabilityAccess(true); + ->setDisabilityAccess(true) + ->setEditable(true); $manager->persist($location); $this->addReference(self::ITKDEV, $location); diff --git a/src/DataFixtures/OccurrenceFixture.php b/src/DataFixtures/OccurrenceFixture.php index 0dbc960e..2eacd2d9 100644 --- a/src/DataFixtures/OccurrenceFixture.php +++ b/src/DataFixtures/OccurrenceFixture.php @@ -16,14 +16,17 @@ public function load(ObjectManager $manager): void ->setStart(new \DateTimeImmutable('2024-12-07T14:30:00+02:00')) ->setEnd(new \DateTimeImmutable('2024-12-07T15:30:00+02:00')) ->setTicketPriceRange('10.000 Kr.') - ->setRoom('M2-5'); + ->setRoom('M2-5') + ->setEditable(true); $manager->persist($occurrence); + $occurrence = new Occurrence(); $occurrence->setEvent($this->getReference(EventFixture::EVENT1)) ->setStart(new \DateTimeImmutable('2024-11-08T10:30:00+02:00')) ->setEnd(new \DateTimeImmutable('2024-11-08T16:30:00+02:00')) ->setTicketPriceRange('Free or 100') - ->setRoom('M2-6'); + ->setRoom('M2-6') + ->setEditable(true); $manager->persist($occurrence); $occurrence = new Occurrence(); @@ -31,7 +34,8 @@ public function load(ObjectManager $manager): void ->setStart(new \DateTimeImmutable('2024-12-08T12:30:00+02:00')) ->setEnd(new \DateTimeImmutable('2024-12-08T14:30:00+02:00')) ->setTicketPriceRange('Free in December') - ->setRoom('M2-5'); + ->setRoom('M2-5') + ->setEditable(true); $manager->persist($occurrence); // Make it stick. diff --git a/src/DataFixtures/TagsFixtures.php b/src/DataFixtures/TagsFixtures.php index fa0553eb..bf7d9617 100644 --- a/src/DataFixtures/TagsFixtures.php +++ b/src/DataFixtures/TagsFixtures.php @@ -13,6 +13,7 @@ final class TagsFixtures extends Fixture implements DependentFixtureInterface public const KIDS = 'tags_kids'; public const RACE = 'tags_race'; public const AROS = 'tags_aros'; + public const ITKDEV = 'tags_itkdev'; public function load(ObjectManager $manager): void { @@ -40,6 +41,13 @@ public function load(ObjectManager $manager): void $manager->persist($tag); $this->addReference(self::CONCERT, $tag); + $tag = new Tag(); + $tag->setName('ITKDev') + ->addVocabulary($this->getReference(VocabularyFixtures::MANAGED)) + ->setEditable(true); + $manager->persist($tag); + $this->addReference(self::ITKDEV, $tag); + // Make it stick. $manager->flush(); } diff --git a/src/Entity/Address.php b/src/Entity/Address.php index 1ef4eb3a..10a05353 100644 --- a/src/Entity/Address.php +++ b/src/Entity/Address.php @@ -12,10 +12,11 @@ use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity(repositoryClass: AddressRepository::class)] -class Address +class Address implements EditableEntityInterface { use TimestampableEntity; use SoftDeleteableEntity; + use EditableEntity; #[ORM\Id] #[ORM\GeneratedValue] @@ -57,6 +58,9 @@ class Address #[ORM\OneToMany(mappedBy: 'address', targetEntity: Location::class)] private Collection $locations; + #[ORM\Column] + private bool $editable = false; + public function __construct() { $this->locations = new ArrayCollection(); @@ -197,4 +201,16 @@ public function removeLocation(Location $location): static return $this; } + + public function isEditable(): bool + { + return $this->editable; + } + + public function setEditable(bool $editable): static + { + $this->editable = $editable; + + return $this; + } } diff --git a/src/Entity/EditableEntity.php b/src/Entity/EditableEntity.php new file mode 100644 index 00000000..94826f55 --- /dev/null +++ b/src/Entity/EditableEntity.php @@ -0,0 +1,26 @@ +editable; + } + + public function setEditable(bool $editable): static + { + $this->editable = $editable; + + return $this; + } +} diff --git a/src/Entity/EditableEntityInterface.php b/src/Entity/EditableEntityInterface.php new file mode 100644 index 00000000..696f8df8 --- /dev/null +++ b/src/Entity/EditableEntityInterface.php @@ -0,0 +1,10 @@ +exists($dest)) { $size = intval(reset($headers['content-length'])); if ($size === filesize($dest)) { - // File exist with the same file size. + // File exists with the same file size. $fetchFile = false; } } @@ -192,7 +192,7 @@ private function generateLocalFilename(string $url, string $mimetype): string } /** - * Try to detect mime type based on http headers. + * Try to detect mime-type based on http headers. * * @param array $headers * Array of http headers