diff --git a/module/Application/migrations/Version20250807174014.php b/module/Application/migrations/Version20250807174014.php new file mode 100644 index 0000000000..0b1157fc34 --- /dev/null +++ b/module/Application/migrations/Version20250807174014.php @@ -0,0 +1,42 @@ +addSql('CREATE TABLE MailingListMember (email VARCHAR(255) NOT NULL, member INT NOT NULL, mailingList VARCHAR(255) NOT NULL, INDEX IDX_3A8467A97B1AC3ED (mailingList), INDEX IDX_3A8467A970E4FA78 (member), UNIQUE INDEX mailinglistmember_unique_idx (mailingList, member, email), PRIMARY KEY(mailingList, member, email)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE MailingListMember ADD CONSTRAINT FK_3A8467A97B1AC3ED FOREIGN KEY (mailingList) REFERENCES MailingList (name)'); + $this->addSql('ALTER TABLE MailingListMember ADD CONSTRAINT FK_3A8467A970E4FA78 FOREIGN KEY (member) REFERENCES Member (lidnr)'); + $this->addSql('ALTER TABLE members_mailinglists DROP FOREIGN KEY FK_5AD357D95E237E06'); + $this->addSql('ALTER TABLE members_mailinglists DROP FOREIGN KEY FK_5AD357D9D665E01D'); + $this->addSql('DROP TABLE members_mailinglists'); + $this->addSql('ALTER TABLE MailingList DROP onForm, DROP defaultSub'); + } + + public function down(Schema $schema): void + { + $this->addSql('CREATE TABLE members_mailinglists (lidnr INT NOT NULL, name VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, INDEX IDX_5AD357D95E237E06 (name), INDEX IDX_5AD357D9D665E01D (lidnr), PRIMARY KEY(lidnr, name)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE members_mailinglists ADD CONSTRAINT FK_5AD357D95E237E06 FOREIGN KEY (name) REFERENCES MailingList (name)'); + $this->addSql('ALTER TABLE members_mailinglists ADD CONSTRAINT FK_5AD357D9D665E01D FOREIGN KEY (lidnr) REFERENCES Member (lidnr)'); + $this->addSql('ALTER TABLE MailingListMember DROP FOREIGN KEY FK_3A8467A97B1AC3ED'); + $this->addSql('ALTER TABLE MailingListMember DROP FOREIGN KEY FK_3A8467A970E4FA78'); + $this->addSql('DROP TABLE MailingListMember'); + $this->addSql('ALTER TABLE MailingList ADD onForm TINYINT(1) NOT NULL, ADD defaultSub TINYINT(1) NOT NULL'); + } +} diff --git a/module/Decision/src/Model/MailingList.php b/module/Decision/src/Model/MailingList.php index c9ccab7463..9a092e539f 100644 --- a/module/Decision/src/Model/MailingList.php +++ b/module/Decision/src/Model/MailingList.php @@ -9,10 +9,12 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; -use Doctrine\ORM\Mapping\ManyToMany; +use Doctrine\ORM\Mapping\OneToMany; /** - * Mailing List model. + * Mailing List model (partial) + * + * ReportDB does not know about mailman and doesn't need to know. * * @psalm-type MailingListGdprArrayType = array{ * name: string, @@ -24,7 +26,7 @@ class MailingList { /** - * Mailman-identifier / name. + * Name of the mailing list */ #[Id] #[Column(type: 'string')] @@ -42,34 +44,20 @@ class MailingList #[Column(type: 'text')] protected string $en_description; - /** - * If the mailing list should be on the form. - */ - #[Column(type: 'boolean')] - protected bool $onForm; - - /** - * If members should be subscribed by default. - * - * (when it is on the form, that means that the checkbox is checked by default) - */ - #[Column(type: 'boolean')] - protected bool $defaultSub; - /** * Mailing list members. * - * @var Collection + * @var Collection */ - #[ManyToMany( - targetEntity: Member::class, - mappedBy: 'lists', + #[OneToMany( + targetEntity: MailingListMember::class, + mappedBy: 'mailingList', )] - protected Collection $members; + protected Collection $mailingListMemberships; public function __construct() { - $this->members = new ArrayCollection(); + $this->mailingListMemberships = new ArrayCollection(); } /** @@ -120,78 +108,14 @@ public function setNlDescription(string $description): void $this->nl_description = $description; } - /** - * Get the description. - */ - public function getDescription(): string - { - return $this->getNlDescription(); - } - - /** - * Set the description. - */ - public function setDescription(string $description): void - { - $this->setNlDescription($description); - } - - /** - * Get if it should be on the form. - */ - public function getOnForm(): bool - { - return $this->onForm; - } - - /** - * Set if it should be on the form. - */ - public function setOnForm(bool $onForm): void - { - $this->onForm = $onForm; - } - - /** - * Get if it is a default list. - */ - public function getDefaultSub(): bool - { - return $this->defaultSub; - } - - /** - * Set if it is a default list. - */ - public function setDefaultSub(bool $default): void - { - $this->defaultSub = $default; - } - /** * Get subscribed members. * - * @return Collection - */ - public function getMembers(): Collection - { - return $this->members; - } - - /** - * Add a member. - */ - public function addMember(Member $member): void - { - $this->members[] = $member; - } - - /** - * Remove a member. + * @return Collection */ - public function removeMember(Member $member): void + public function getMailingListMemberships(): Collection { - $this->members->removeElement($member); + return $this->mailingListMemberships; } /** diff --git a/module/Decision/src/Model/MailingListMember.php b/module/Decision/src/Model/MailingListMember.php new file mode 100644 index 0000000000..ef82e46768 --- /dev/null +++ b/module/Decision/src/Model/MailingListMember.php @@ -0,0 +1,136 @@ +mailingList; + } + + /** + * Set the mailing list. + */ + public function setMailingList(MailingList $mailingList): void + { + $this->mailingList = $mailingList; + } + + /** + * Get the member. + */ + public function getMember(): Member + { + return $this->member; + } + + /** + * Set the member. + */ + public function setMember(Member $member): void + { + $this->member = $member; + } + + /** + * Get the email address of this subscription + */ + public function getEmail(): string + { + return $this->email; + } + + /** + * Set the email address of this subscription + */ + public function setEmail(string $email): void + { + $this->email = $email; + } + + /** + * @return MailingListMemberGdprArrayType + */ + public function toGdprArray(): array + { + return [ + 'list' => $this->mailingList->toGdprArray(), + 'email' => $this->getEmail(), + ]; + } +} diff --git a/module/Decision/src/Model/Member.php b/module/Decision/src/Model/Member.php index e015037363..4a71820ea1 100644 --- a/module/Decision/src/Model/Member.php +++ b/module/Decision/src/Model/Member.php @@ -13,10 +13,7 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; -use Doctrine\ORM\Mapping\InverseJoinColumn; use Doctrine\ORM\Mapping\JoinColumn; -use Doctrine\ORM\Mapping\JoinTable; -use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OneToOne; use Photo\Model\Tag as TagModel; @@ -207,22 +204,14 @@ enumType: MembershipTypes::class, /** * Memberships of mailing lists. * - * @var Collection + * @var Collection */ - #[ManyToMany( - targetEntity: MailingList::class, - inversedBy: 'members', - )] - #[JoinTable(name: 'members_mailinglists')] - #[JoinColumn( - name: 'lidnr', - referencedColumnName: 'lidnr', - )] - #[InverseJoinColumn( - name: 'name', - referencedColumnName: 'name', + #[OneToMany( + targetEntity: MailingListMember::class, + mappedBy: 'member', + cascade: ['persist'], )] - protected Collection $lists; + protected Collection $mailingListMemberships; /** * Organ memberships. @@ -295,7 +284,7 @@ public function __construct() $this->organInstallations = new ArrayCollection(); $this->boardInstallations = new ArrayCollection(); $this->keyGrantings = new ArrayCollection(); - $this->lists = new ArrayCollection(); + $this->mailingListMemberships = new ArrayCollection(); $this->tags = new ArrayCollection(); } @@ -687,28 +676,30 @@ static function ($c, $kg) { /** * Get mailing list subscriptions. * - * @return Collection + * @return Collection */ - public function getLists(): Collection + public function getMailingListMemberships(): Collection { - return $this->lists; + return $this->mailingListMemberships; } /** * Add a mailing list subscription. - * - * Note that this is the owning side. */ - public function addList(MailingList $list): void + public function addList(MailingListMember $list): void { - $list->addMember($this); - $this->lists[] = $list; + if ($this->mailingListMemberships->contains($list)) { + return; + } + + $list->setMember($this); + $this->mailingListMemberships->add($list); } /** * Add multiple mailing lists. * - * @param MailingList[] $lists + * @param MailingListMember[] $lists */ public function addLists(array $lists): void { @@ -717,25 +708,6 @@ public function addLists(array $lists): void } } - /** - * Remove a mailing list subscription. - * - * Note that this is the owning side. - */ - public function removeList(MailingList $list): void - { - $list->removeMember($this); - $this->lists->removeElement($list); - } - - /** - * Clear the lists. - */ - public function clearLists(): void - { - $this->lists = new ArrayCollection(); - } - /** * Get the organ installations of organs that the member is currently part of. * @@ -901,6 +873,6 @@ protected function isCurrentBoard(BoardMember $boardMember): bool public function isExpired(): bool { - return $this->getExpiration() < (new DateTime()); + return $this->getExpiration() < new DateTime(); } } diff --git a/module/Decision/src/Service/Gdpr.php b/module/Decision/src/Service/Gdpr.php index 902abee883..b0c9cd888b 100644 --- a/module/Decision/src/Service/Gdpr.php +++ b/module/Decision/src/Service/Gdpr.php @@ -15,7 +15,7 @@ use Decision\Mapper\SubDecision as SubDecisionMapper; use Decision\Model\Address as AddressModel; use Decision\Model\Authorization as AuthorizationModel; -use Decision\Model\MailingList as MailingListModel; +use Decision\Model\MailingListMember as MailingListMemberModel; use Decision\Model\Member as MemberModel; use Decision\Model\SubDecision as SubDecisionModel; use Education\Mapper\CourseDocument as CourseDocumentMapper; @@ -58,7 +58,7 @@ * @psalm-import-type AuthorizationGdprArrayType from AuthorizationModel as ImportedAuthorizationGdprArrayType * @psalm-import-type CourseDocumentGdprArrayType from CourseDocumentModel as ImportedCourseDocumentGdprArrayType * @psalm-import-type LoginAttemptGdprArrayType from LoginAttemptModel as ImportedLoginAttemptGdprArrayType - * @psalm-import-type MailingListGdprArrayType from MailingListModel as ImportedMailingListGdprArrayType + * @psalm-import-type MailingListMemberGdprArrayType from MailingListMemberModel as ImportedMailingListMemberGdprArrayType * @psalm-import-type MemberGdprArrayType from MemberModel as ImportedMemberGdprArrayType * @psalm-import-type PhotoGdprArrayType from PhotoModel as ImportedPhotoGdprArrayType * @psalm-import-type PollGdprArrayType from PollModel as ImportedPollGdprArrayType @@ -103,7 +103,7 @@ public function __construct( * user_information: ?ImportedUserGdprArrayType, * profile_photo: ?ImportedProfilePhotoGdprArrayType, * addresses: ImportedAddressGdprArrayType[], - * lists: ImportedMailingListGdprArrayType[], + * lists: ImportedMailingListMemberGdprArrayType[], * login_attempts: ImportedLoginAttemptGdprArrayType[], * app_authentications: ImportedApiAppAuthenticationGdprArrayType[], * }, @@ -163,9 +163,9 @@ public function getMemberData(int $lidnr): array $addresses[] = $address->toGdprArray(); } - /** @var ImportedMailingListGdprArrayType[] $lists */ + /** @var ImportedMailingListMemberGdprArrayType[] $lists */ $lists = []; - foreach ($member->getLists() as $list) { + foreach ($member->getMailingListMemberships() as $list) { $lists[] = $list->toGdprArray(); }