diff --git a/src/Campaigns/Actions/ConvertQueryDataToCampaign.php b/src/Campaigns/Actions/ConvertQueryDataToCampaign.php index 405786561d..7d6b2957bb 100644 --- a/src/Campaigns/Actions/ConvertQueryDataToCampaign.php +++ b/src/Campaigns/Actions/ConvertQueryDataToCampaign.php @@ -19,7 +19,6 @@ public function __invoke(object $queryObject): Campaign { return new Campaign([ 'id' => (int)$queryObject->id, - 'pageId' => (int)$queryObject->pageId, 'type' => new CampaignType($queryObject->type), 'title' => $queryObject->title, 'shortDescription' => $queryObject->shortDescription, diff --git a/src/Campaigns/Actions/EditCampaignPageRedirect.php b/src/Campaigns/Actions/EditCampaignPageRedirect.php new file mode 100644 index 0000000000..d25734f138 --- /dev/null +++ b/src/Campaigns/Actions/EditCampaignPageRedirect.php @@ -0,0 +1,30 @@ +page() ?: CampaignPage::create([ + 'campaignId' => $campaign->id, + ]); + + wp_safe_redirect($page->getEditLinkUrl(), 303); + exit(); + } +} diff --git a/src/Campaigns/Actions/RegisterCampaignPagePostType.php b/src/Campaigns/Actions/RegisterCampaignPagePostType.php new file mode 100644 index 0000000000..e8dd4f189f --- /dev/null +++ b/src/Campaigns/Actions/RegisterCampaignPagePostType.php @@ -0,0 +1,32 @@ + __('Campaign Page', 'give'), + 'public' => true, + 'show_ui' => true, + 'show_in_menu' => false, + 'show_in_rest' => true, + 'supports' => [ + 'editor' + ], + 'rewrite' => [ + 'slug' => 'campaign' + ], + 'template' => [ + // TODO: Add default blocks template. + ], + ] ); + } +} diff --git a/src/Campaigns/Factories/CampaignFactory.php b/src/Campaigns/Factories/CampaignFactory.php index 9f8b0c633e..e74e6d32e5 100644 --- a/src/Campaigns/Factories/CampaignFactory.php +++ b/src/Campaigns/Factories/CampaignFactory.php @@ -20,7 +20,6 @@ public function definition(): array $currentDate = Temporal::getCurrentDateTime(); return [ - 'pageId' => 1, 'type' => CampaignType::CORE(), 'title' => __('GiveWP Campaign', 'give'), 'shortDescription' => __('Campaign short description', 'give'), diff --git a/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php b/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php index 4ba4016fa9..5f8f529cd3 100644 --- a/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php +++ b/src/Campaigns/Migrations/MigrateFormsToCampaignForms.php @@ -49,7 +49,6 @@ public function run() public function createParentCampaignForDonationForm(DonationForm $form) { $campaign = Campaign::create([ - 'pageId' => 0, 'type' => CampaignType::CORE(), 'title' => $form->title, 'shortDescription' => $form->settings->formExcerpt, diff --git a/src/Campaigns/Models/Campaign.php b/src/Campaigns/Models/Campaign.php index 0fe0434291..3e4be445cd 100644 --- a/src/Campaigns/Models/Campaign.php +++ b/src/Campaigns/Models/Campaign.php @@ -5,6 +5,7 @@ use DateTime; use Give\Campaigns\Actions\ConvertQueryDataToCampaign; use Give\Campaigns\Factories\CampaignFactory; +use Give\Campaigns\Repositories\CampaignPageRepository; use Give\Campaigns\Repositories\CampaignRepository; use Give\Campaigns\ValueObjects\CampaignStatus; use Give\Campaigns\ValueObjects\CampaignType; @@ -21,7 +22,6 @@ * @unreleased * * @property int $id - * @property int $pageId * @property CampaignType $type * @property string $title * @property string $url @@ -44,7 +44,6 @@ class Campaign extends Model implements ModelCrud, ModelHasFactory */ protected $properties = [ 'id' => 'int', - 'pageId' => 'int', 'type' => CampaignType::class, 'title' => 'string', 'shortDescription' => 'string', @@ -69,6 +68,14 @@ public function forms() })->where('campaign_forms.campaign_id', $this->id); } + /** + * @unreleased + */ + public function page() + { + return give(CampaignPageRepository::class)->findByCampaignId($this->id); + } + /** * @unreleased */ diff --git a/src/Campaigns/Models/CampaignPage.php b/src/Campaigns/Models/CampaignPage.php new file mode 100644 index 0000000000..0f4f0ef9c5 --- /dev/null +++ b/src/Campaigns/Models/CampaignPage.php @@ -0,0 +1,117 @@ + 'int', + 'campaignId' => 'int', + 'createdAt' => DateTime::class, + 'updatedAt' => DateTime::class, + ]; + + public $relationships = [ + 'campaign' => Relationship::BELONGS_TO, + ]; + + /** + * @unreleased + */ + public function getEditLinkUrl(): string + { + // By default, the URL is encoded for display purposes. + // Setting any other value prevents encoding the URL. + return get_edit_post_link($this->id, 'redirect'); + } + + /** + * @unreleased + */ + public function campaign() + { + return Campaign::find($this->campaignId); + } + + /** + * @unreleased + */ + public static function find($id) + { + return give(CampaignPageRepository::class) + ->prepareQuery() + ->where('ID', $id) + ->get(); + } + + /** + * @unreleased + */ + public static function create(array $attributes): CampaignPage + { + $campaignPage = new static($attributes); + + give(CampaignPageRepository::class)->insert($campaignPage); + + return $campaignPage; + } + + /** + * @unreleased + */ + public function save(): void + { + if (!$this->id) { + give(CampaignPageRepository::class)->insert($this); + } else { + give(CampaignPageRepository::class)->update($this); + } + } + + /** + * @unreleased + */ + public function delete(): bool + { + return give(CampaignPageRepository::class)->delete($this); + } + + /** + * @unreleased + * + * @return ModelQueryBuilder + */ + public static function query(): ModelQueryBuilder + { + return give(CampaignPageRepository::class)->prepareQuery(); + } + + /** + * @unreleased + */ + public static function fromQueryBuilderObject($object): CampaignPage + { + return new CampaignPage([ + 'id' => (int) $object->id, + 'campaignId' => (int) $object->campaignId, + 'createdAt' => Temporal::toDateTime($object->createdAt), + 'updatedAt' => Temporal::toDateTime($object->updatedAt), + ]); + } +} diff --git a/src/Campaigns/Repositories/CampaignPageRepository.php b/src/Campaigns/Repositories/CampaignPageRepository.php new file mode 100644 index 0000000000..f88b8b0beb --- /dev/null +++ b/src/Campaigns/Repositories/CampaignPageRepository.php @@ -0,0 +1,211 @@ +prepareQuery() + ->where('id', $id) + ->get(); + } + + /** + * @unreleased + */ + public function findByCampaignId(int $campaignId): ?CampaignPage + { + return $this->prepareQuery() + ->where('postmeta_attach_meta_campaignId.meta_value', $campaignId) + ->get(); + } + + /** + * @unreleased + */ + public function insert(CampaignPage $campaignPage): void + { + $this->validate($campaignPage); + + Hooks::doAction('givewp_campaign_page_creating', $campaignPage); + + $dateCreated = Temporal::withoutMicroseconds($campaignPage->createdAt ?: Temporal::getCurrentDateTime()); + $dateCreatedFormatted = Temporal::getFormattedDateTime($dateCreated); + $dateUpdated = $campaignPage->updatedAt ?? $dateCreated; + $dateUpdatedFormatted = Temporal::getFormattedDateTime($dateUpdated); + + DB::query('START TRANSACTION'); + + try { + DB::table('posts') + ->insert([ + 'post_date' => $dateCreatedFormatted, + 'post_date_gmt' => get_gmt_from_date($dateCreatedFormatted), + 'post_modified' => $dateUpdatedFormatted, + 'post_modified_gmt' => get_gmt_from_date($dateUpdatedFormatted), + 'post_status' => 'publish', // TODO: Update to value object + 'post_type' => 'give_campaign_page', + ]); + + $campaignPage->id = DB::last_insert_id();; + $campaignPage->createdAt = $dateCreated; + $campaignPage->updatedAt = $dateUpdated; + + DB::table('postmeta') + ->insert([ + 'post_id' => $campaignPage->id, + 'meta_key' => 'campaignId', + 'meta_value' => $campaignPage->campaignId, + ]); + + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed creating a campaign page', [$campaignPage]); + + throw new $exception('Failed creating a campaign page'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_page_created', $campaignPage); + } + + /** + * @unreleased + */ + public function update(CampaignPage $campaignPage): void + { + $this->validate($campaignPage); + + Hooks::doAction('givewp_campaign_page_updating', $campaignPage); + + $now = Temporal::withoutMicroseconds(Temporal::getCurrentDateTime()); + $nowFormatted = Temporal::getFormattedDateTime($now); + + DB::query('START TRANSACTION'); + + try { + DB::table('posts') + ->where('ID', $campaignPage->id) + ->update([ + 'post_modified' => $nowFormatted, + 'post_modified_gmt' => get_gmt_from_date($nowFormatted), + 'post_status' => 'publish', // TODO: Update to value object + 'post_type' => 'give_campaign_page', + ]); + + $campaignPage->updatedAt = $now; + + DB::table('postmeta') + ->where('post_id', $campaignPage->id) + ->where('meta_key', 'campaignId') + ->update([ + 'meta_value' => $campaignPage->campaignId, + ]); + + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed updating a campaign page', [$campaignPage]); + + throw new $exception('Failed updating a campaign page'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_page_updated', $campaignPage); + } + + /** + * @unreleased + */ + public function delete(CampaignPage $campaignPage): bool + { + DB::query('START TRANSACTION'); + + Hooks::doAction('givewp_campaign_page_deleting', $campaignPage); + + try { + DB::table('posts') + ->where('id', $campaignPage->id) + ->delete(); + + DB::table('postmeta') + ->where('post_id', $campaignPage->id) + ->delete(); + } catch (Exception $exception) { + DB::query('ROLLBACK'); + + Log::error('Failed deleting a campaign page', [$campaignPage]); + + throw new $exception('Failed deleting a campaign page'); + } + + DB::query('COMMIT'); + + Hooks::doAction('givewp_campaign_page_deleted', $campaignPage); + + return true; + } + + /** + * @unreleased + * + * @return ModelQueryBuilder + */ + public function prepareQuery(): ModelQueryBuilder + { + $builder = new ModelQueryBuilder(CampaignPage::class); + + return $builder->from('posts') + ->select( + ['ID', 'id'], + ['post_date', 'createdAt'], + ['post_modified', 'updatedAt'], + ['post_status', 'status'] + ) + ->attachMeta( + 'postmeta', + 'ID', + 'post_id', + 'campaignId' + ) + ->where('post_type', 'give_campaign_page'); + } + + /** + * @unreleased + */ + public function validate(CampaignPage $campaignPage) + { + foreach ($this->requiredProperties as $key) { + if (!isset($campaignPage->$key)) { + throw new InvalidArgumentException("'$key' is required."); + } + } + } +} diff --git a/src/Campaigns/Repositories/CampaignRepository.php b/src/Campaigns/Repositories/CampaignRepository.php index 1f79b36c47..b34de1ca54 100644 --- a/src/Campaigns/Repositories/CampaignRepository.php +++ b/src/Campaigns/Repositories/CampaignRepository.php @@ -57,7 +57,6 @@ public function insert(Campaign $campaign): void try { DB::table('give_campaigns') ->insert([ - 'campaign_page_id' => $campaign->pageId, 'campaign_type' => $campaign->type->getValue(), 'campaign_title' => $campaign->title, 'short_desc' => $campaign->shortDescription, @@ -111,7 +110,6 @@ public function update(Campaign $campaign): void ->where('id', $campaign->id) ->update([ 'campaign_type' => $campaign->type->getValue(), - 'campaign_page_id' => $campaign->pageId, 'campaign_title' => $campaign->title, 'short_desc' => $campaign->shortDescription, 'long_desc' => $campaign->longDescription, @@ -192,7 +190,6 @@ public function prepareQuery(): ModelQueryBuilder return $builder->from('give_campaigns') ->select( 'id', - ['campaign_page_id', 'pageId'], ['campaign_type', 'type'], ['campaign_title', 'title'], ['short_desc', 'shortDescription'], diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php index 83534d0236..9ccefd7611 100644 --- a/src/Campaigns/ServiceProvider.php +++ b/src/Campaigns/ServiceProvider.php @@ -33,6 +33,7 @@ public function boot(): void { $this->registerMenus(); $this->registerActions(); + $this->setupCampaignPages(); $this->registerMigrations(); } @@ -77,4 +78,10 @@ private function registerMenus() { Hooks::addAction('admin_menu', CampaignsAdminPage::class, 'addCampaignsSubmenuPage', 999); } + + private function setupCampaignPages() + { + Hooks::addAction('init', Actions\RegisterCampaignPagePostType::class); + Hooks::addAction('admin_action_edit_campaign_page', Actions\EditCampaignPageRedirect::class); + } } diff --git a/tests/Unit/Campaigns/Models/CampaignPageTest.php b/tests/Unit/Campaigns/Models/CampaignPageTest.php new file mode 100644 index 0000000000..d122c84bbb --- /dev/null +++ b/tests/Unit/Campaigns/Models/CampaignPageTest.php @@ -0,0 +1,32 @@ +create(); + $campaignPage = CampaignPage::create([ + 'campaignId' => $campaign->id, + ]); + + $campaignPageFresh = CampaignPage::find($campaignPage->id); + + $this->assertInstanceOf(CampaignPage::class, $campaignPageFresh); + $this->assertEquals($campaignPage->id, $campaignPageFresh->id); + } +} diff --git a/tests/Unit/Campaigns/Repositories/CampaignPageRepositoryTest.php b/tests/Unit/Campaigns/Repositories/CampaignPageRepositoryTest.php new file mode 100644 index 0000000000..2e770846c0 --- /dev/null +++ b/tests/Unit/Campaigns/Repositories/CampaignPageRepositoryTest.php @@ -0,0 +1,131 @@ +create(); + $campaignPage = CampaignPage::create([ + 'campaignId' => $campaign->id, + ]); + + $campaignPageFresh = give(CampaignPageRepository::class)->getById($campaignPage->id); + + $this->assertInstanceOf(CampaignPage::class, $campaignPageFresh); + $this->assertEquals($campaignPage->id, $campaignPageFresh->id); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function testInsertShouldAddCampaignPageToDatabase() + { + $campaign = Campaign::factory()->create(); + $campaignPage = new CampaignPage([ + 'campaignId' => $campaign->id, + ]); + + give(CampaignPageRepository::class)->insert($campaignPage); + + $campaignPageFresh = give(CampaignPageRepository::class)->getById($campaignPage->id); + + $this->assertEquals($campaignPage->getAttributes(), $campaignPageFresh->getAttributes()); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function testCampaignPageInsertShouldFailValidationWhenMissingKeyAndThrowException() + { + $this->expectException(InvalidArgumentException::class); + + $campaignPageMissingCampaignId = new CampaignPage([ + // Note: `campaignId` intentionally not set. + ]); + + (new CampaignPageRepository())->insert($campaignPageMissingCampaignId); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function testCampaignPageUpdateShouldFailValidationWhenMissingKeyAndThrowException() + { + $this->expectException(InvalidArgumentException::class); + + $campaignPageMissingCampaignId = new CampaignPage([ + // Note: `campaignId` intentionally not set. + ]); + + (new CampaignPageRepository())->update($campaignPageMissingCampaignId); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function testCampaignPageUpdateShouldUpdateCampaignPageValuesInTheDatabase() + { + $campaign1 = Campaign::factory()->create(); + $campaignPage = CampaignPage::create([ + 'campaignId' => $campaign1->id, + ]); + + $campaign2 = Campaign::factory()->create(); + $campaignPage->campaignId = $campaign2->id; + give(CampaignPageRepository::class)->update($campaignPage); + + $campaignPageFresh = give(CampaignPageRepository::class)->getById($campaignPage->id); + + $this->assertEquals($campaign2->id, $campaignPageFresh->campaignId); + $this->assertNotEquals($campaign1->id, $campaignPageFresh->campaignId); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function testCampaignPageDeleteShouldRemoveCampaignPageFromTheDatabase() + { + $campaign = Campaign::factory()->create(); + $campaignPage = CampaignPage::create([ + 'campaignId' => $campaign->id, + ]); + + give(CampaignPageRepository::class)->delete($campaignPage); + + $campaignPageFresh = CampaignPage::find($campaignPage->id); + + $this->assertNull($campaignPageFresh); + } +}