-
Notifications
You must be signed in to change notification settings - Fork 65
Implement trip notifications and read state #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
42bfe60
5e78ffd
11f79d1
385b10c
003901a
bf65c23
357e4f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php | ||
|
|
||
| namespace DoctrineMigrations; | ||
|
|
||
| use Doctrine\DBAL\Schema\Schema; | ||
| use Doctrine\Migrations\AbstractMigration; | ||
| use Override; | ||
|
|
||
| final class Version20260626100000 extends AbstractMigration | ||
| { | ||
| #[Override] | ||
| public function getDescription(): string | ||
| { | ||
| return 'Add subtrip hidden state and trip notification preference'; | ||
| } | ||
|
|
||
| public function up(Schema $schema): void | ||
| { | ||
| $this->addSql('CREATE TABLE IF NOT EXISTS member_subtrip_hidden (id INT AUTO_INCREMENT NOT NULL, member_id INT NOT NULL, subtrip_id INT NOT NULL, created DATETIME NOT NULL, INDEX IDX_B1EC0277597D3FE (member_id), INDEX IDX_B1EC027F2BD7DD7 (subtrip_id), UNIQUE INDEX member_subtrip_hidden_unique (member_id, subtrip_id), CONSTRAINT FK_B1EC0277597D3FE FOREIGN KEY (member_id) REFERENCES member (id) ON DELETE CASCADE, CONSTRAINT FK_B1EC027F2BD7DD7 FOREIGN KEY (subtrip_id) REFERENCES sub_trips (id) ON DELETE CASCADE, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); | ||
| $this->addSql('CREATE TABLE IF NOT EXISTS member_trip_notification_sent (id INT AUTO_INCREMENT NOT NULL, member_id INT NOT NULL, trip_id INT NOT NULL, created DATETIME NOT NULL, INDEX IDX_9D9311917597D3FE (member_id), INDEX IDX_9D931191A5BC2E0E (trip_id), UNIQUE INDEX member_trip_notification_sent_unique (member_id, trip_id), CONSTRAINT FK_9D9311917597D3FE FOREIGN KEY (member_id) REFERENCES member (id) ON DELETE CASCADE, CONSTRAINT FK_9D931191A5BC2E0E FOREIGN KEY (trip_id) REFERENCES trips (id) ON DELETE CASCADE, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); | ||
| $this->addSql("INSERT INTO preferences (position, codeName, codeDescription, Description, created, DefaultValue, PossibleValues, Status) SELECT 65, 'TripsNotifications', 'trips.notifications', 'How often the member wants notifications for trips in their area', NOW(), 'No', 'No;Immediately;Daily;Weekly;Monthly', 'Normal' WHERE NOT EXISTS (SELECT 1 FROM preferences WHERE codeName = 'TripsNotifications')"); | ||
| $this->addSql("UPDATE preferences SET PossibleValues = 'No;Immediately;Daily;Weekly;Monthly' WHERE codeName = 'TripsNotifications'"); | ||
| } | ||
|
|
||
| #[Override] | ||
| public function down(Schema $schema): void | ||
| { | ||
| $this->addSql("DELETE mp FROM memberspreferences mp INNER JOIN preferences p ON p.id = mp.IdPreference WHERE p.codeName = 'TripsNotifications'"); | ||
| $this->addSql("DELETE FROM preferences WHERE codeName = 'TripsNotifications'"); | ||
| $this->addSql('DROP TABLE IF EXISTS member_trip_notification_sent'); | ||
| $this->addSql('DROP TABLE IF EXISTS member_subtrip_hidden'); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| <?php | ||
|
|
||
| namespace App\Command; | ||
|
|
||
| use App\Entity\Preference; | ||
| use App\Model\TripModel; | ||
| use DateInterval; | ||
| use DateTimeImmutable; | ||
| use Symfony\Component\Console\Attribute\AsCommand; | ||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Input\InputArgument; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
| use Symfony\Component\Console\Style\SymfonyStyle; | ||
|
|
||
| #[AsCommand( | ||
| name: 'trips:notifications:send', | ||
| description: 'Send scheduled trip notification emails', | ||
| )] | ||
| class SendTripNotificationsCommand extends Command | ||
| { | ||
| private const array FREQUENCIES = [ | ||
| 'daily' => [Preference::TRIP_NOTIFICATIONS_DAILY, 'P1D'], | ||
| 'weekly' => [Preference::TRIP_NOTIFICATIONS_WEEKLY, 'P7D'], | ||
| 'monthly' => [Preference::TRIP_NOTIFICATIONS_MONTHLY, 'P31D'], | ||
| ]; | ||
|
|
||
| public function __construct( | ||
| private readonly TripModel $tripModel, | ||
| ) { | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| protected function configure(): void | ||
| { | ||
| $this->addArgument('frequency', InputArgument::REQUIRED, 'daily, weekly or monthly'); | ||
| } | ||
|
|
||
| protected function execute(InputInterface $input, OutputInterface $output): int | ||
| { | ||
| $io = new SymfonyStyle($input, $output); | ||
| $frequency = strtolower((string) $input->getArgument('frequency')); | ||
|
|
||
| if (!isset(self::FREQUENCIES[$frequency])) { | ||
| $io->error('Frequency must be daily, weekly or monthly.'); | ||
|
|
||
| return Command::INVALID; | ||
| } | ||
|
|
||
| [$preferenceValue, $period] = self::FREQUENCIES[$frequency]; | ||
| $until = new DateTimeImmutable(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I always thought about trip notifications as information about upcoming trips. This seems to target trips in the past. |
||
| $since = $until->sub(new DateInterval($period)); | ||
| $sent = $this->tripModel->sendScheduledTripNotifications($preferenceValue, $since, $until); | ||
|
|
||
| $io->success(\sprintf('Sent %d trip notification emails.', $sent)); | ||
|
|
||
| return Command::SUCCESS; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?php | ||
|
|
||
| namespace App\Entity; | ||
|
|
||
| use DateTime; | ||
| use Doctrine\ORM\Mapping as ORM; | ||
|
|
||
| #[ORM\Table(name: 'member_subtrip_hidden')] | ||
| #[ORM\UniqueConstraint(name: 'member_subtrip_hidden_unique', columns: ['member_id', 'subtrip_id'])] | ||
| #[ORM\Entity] | ||
| #[ORM\HasLifecycleCallbacks] | ||
| class MemberSubtripHidden | ||
| { | ||
| #[ORM\JoinColumn(name: 'member_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] | ||
| #[ORM\ManyToOne(targetEntity: Member::class)] | ||
| private Member $member; | ||
|
|
||
| #[ORM\JoinColumn(name: 'subtrip_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] | ||
| #[ORM\ManyToOne(targetEntity: Subtrip::class)] | ||
| private Subtrip $subtrip; | ||
|
|
||
| #[ORM\Column(name: 'created', type: 'datetime', nullable: false)] | ||
| private DateTime $created; | ||
|
|
||
| #[ORM\Column(name: 'id', type: 'integer')] | ||
| #[ORM\Id] | ||
| #[ORM\GeneratedValue(strategy: 'IDENTITY')] | ||
| private int $id; | ||
|
|
||
| public function __construct(Member $member, Subtrip $subtrip) | ||
| { | ||
| $this->member = $member; | ||
| $this->subtrip = $subtrip; | ||
| } | ||
|
|
||
| public function getMember(): Member | ||
| { | ||
| return $this->member; | ||
| } | ||
|
|
||
| public function getSubtrip(): Subtrip | ||
| { | ||
| return $this->subtrip; | ||
| } | ||
|
|
||
| public function getCreated(): DateTime | ||
| { | ||
| return $this->created; | ||
| } | ||
|
|
||
| #[ORM\PrePersist] | ||
| public function onPrePersist(): void | ||
| { | ||
| $this->created = new DateTime('now'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?php | ||
|
|
||
| namespace App\Entity; | ||
|
|
||
| use DateTime; | ||
| use Doctrine\ORM\Mapping as ORM; | ||
|
|
||
| #[ORM\Table(name: 'member_trip_notification_sent')] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you hide legs of a trip notifications should be sent for legs as well, shouldn't they? |
||
| #[ORM\UniqueConstraint(name: 'member_trip_notification_sent_unique', columns: ['member_id', 'trip_id'])] | ||
| #[ORM\Entity] | ||
| #[ORM\HasLifecycleCallbacks] | ||
| class MemberTripNotificationSent | ||
| { | ||
| #[ORM\JoinColumn(name: 'member_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] | ||
| #[ORM\ManyToOne(targetEntity: Member::class)] | ||
| private Member $member; | ||
|
|
||
| #[ORM\JoinColumn(name: 'trip_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] | ||
| #[ORM\ManyToOne(targetEntity: Trip::class)] | ||
| private Trip $trip; | ||
|
|
||
| #[ORM\Column(name: 'created', type: 'datetime', nullable: false)] | ||
| private DateTime $created; | ||
|
|
||
| #[ORM\Column(name: 'id', type: 'integer')] | ||
| #[ORM\Id] | ||
| #[ORM\GeneratedValue(strategy: 'IDENTITY')] | ||
| private int $id; | ||
|
|
||
| public function __construct(Member $member, Trip $trip) | ||
| { | ||
| $this->member = $member; | ||
| $this->trip = $trip; | ||
| } | ||
|
|
||
| public function getMember(): Member | ||
| { | ||
| return $this->member; | ||
| } | ||
|
|
||
| public function getTrip(): Trip | ||
| { | ||
| return $this->trip; | ||
| } | ||
|
|
||
| public function getCreated(): DateTime | ||
| { | ||
| return $this->created; | ||
| } | ||
|
|
||
| #[ORM\PrePersist] | ||
| public function onPrePersist(): void | ||
| { | ||
| $this->created = new DateTime('now'); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,12 @@ class Preference | |
| public const READ_COMMENT_GUIDELINES = 'ReadCommentGuidelines'; | ||
| public const FORUM_ORDER_LIST_ASC = 'PreferenceForumOrderListAsc'; | ||
| public const TRIPS_VICINITY_RADIUS = 'TripLegsVicinityRadius'; | ||
| public const TRIP_NOTIFICATIONS = 'TripsNotifications'; | ||
| public const TRIP_NOTIFICATIONS_NEVER = 'No'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be Never. |
||
| public const TRIP_NOTIFICATIONS_IMMEDIATELY = 'Immediately'; | ||
| public const TRIP_NOTIFICATIONS_DAILY = 'Daily'; | ||
| public const TRIP_NOTIFICATIONS_WEEKLY = 'Weekly'; | ||
| public const TRIP_NOTIFICATIONS_MONTHLY = 'Monthly'; | ||
| public const SHOW_PROFILE_VISITORS = 'PreferenceShowProfileVisits'; | ||
| public const SHOW_FORUMS_POSTS = 'MyForumPostsPagePublic'; | ||
| public const SEARCH_OPTIONS = 'SearchOptions'; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only change the database if the previous statement didn't. For new setups that will always be the case (the situation that needs cleanup can only arise during development).