Skip to content
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

dev/core#2486 Add v4 Mailing api (+ MailingJob) #22624

Merged
merged 5 commits into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 47 additions & 35 deletions CRM/Mailing/BAO/Mailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,47 @@ protected static function processWorkflowPermissions(array $params): array {
return $safeParams;
}

/**
* Do Submit actions.
*
* When submitting (as opposed to creating or updating) a mailing it should
* be scheduled.
*
* This function creates the initial job and the recipient list.
*
* @param array $params
* @param \CRM_Mailing_DAO_Mailing $mailing
*
* @return array
*/
protected static function doSubmitActions(array $params, CRM_Mailing_DAO_Mailing $mailing): array {
// Create parent job if not yet created.
// Condition on the existence of a scheduled date.
if (!empty($params['scheduled_date']) && $params['scheduled_date'] != 'null' && empty($params['_skip_evil_bao_auto_schedule_'])) {
$job = new CRM_Mailing_BAO_MailingJob();
$job->mailing_id = $mailing->id;
// If we are creating a new Completed mailing (e.g. import from another system) set the job to completed.
// Keeping former behaviour when an id is present is precautionary and may warrant reconsideration later.
$job->status = ((empty($params['is_completed']) || !empty($params['id'])) ? 'Scheduled' : 'Complete');
$job->is_test = 0;

if (!$job->find(TRUE)) {
// Don't schedule job until we populate the recipients.
$job->scheduled_date = NULL;
$job->save();
}
// Schedule the job now that it has recipients.
$job->scheduled_date = $params['scheduled_date'];
$job->save();
}

// Populate the recipients.
if (empty($params['_skip_evil_bao_auto_recipients_'])) {
self::getRecipients($mailing->id);
}
return $params;
}

/**
* Returns the regex patterns that are used for preparing the text and html templates.
*
Expand Down Expand Up @@ -1478,7 +1519,6 @@ public static function add(&$params, $ids = []) {
*
* - _skip_evil_bao_auto_recipients_: bool
* - _skip_evil_bao_auto_schedule_: bool
* - _evil_bao_validator_: string|callable
*
* </twowrongsmakesaright>
*
Expand Down Expand Up @@ -1612,42 +1652,14 @@ public static function create(array $params) {
// check and attach and files as needed
CRM_Core_BAO_File::processAttachment($params, 'civicrm_mailing', $mailing->id);

// If we're going to autosend, then check validity before saving.
if (empty($params['is_completed']) && !empty($params['scheduled_date']) && $params['scheduled_date'] != 'null' && !empty($params['_evil_bao_validator_'])) {
$cb = Civi\Core\Resolver::singleton()
->get($params['_evil_bao_validator_']);
$errors = call_user_func($cb, $mailing);
if (!empty($errors)) {
$fields = implode(',', array_keys($errors));
throw new CRM_Core_Exception("Mailing cannot be sent. There are missing or invalid fields ($fields).", 'cannot-send', $errors);
}
}

$transaction->commit();

// Create parent job if not yet created.
// Condition on the existence of a scheduled date.
if (!empty($params['scheduled_date']) && $params['scheduled_date'] != 'null' && empty($params['_skip_evil_bao_auto_schedule_'])) {
$job = new CRM_Mailing_BAO_MailingJob();
$job->mailing_id = $mailing->id;
// If we are creating a new Completed mailing (e.g. import from another system) set the job to completed.
// Keeping former behaviour when an id is present is precautionary and may warrant reconsideration later.
$job->status = ((empty($params['is_completed']) || !empty($params['id'])) ? 'Scheduled' : 'Complete');
$job->is_test = 0;

if (!$job->find(TRUE)) {
// Don't schedule job until we populate the recipients.
$job->scheduled_date = NULL;
$job->save();
}
// Schedule the job now that it has recipients.
$job->scheduled_date = $params['scheduled_date'];
$job->save();
}

// Populate the recipients.
if (empty($params['_skip_evil_bao_auto_recipients_'])) {
self::getRecipients($mailing->id);
// These actions are really 'submit' not create actions.
// In v4 of the api they are not available via CRUD. At some
// point we will create a 'submit' function which will do the crud+submit
// but for now only CRUD is available via v4 api.
if (($params['version'] ?? '') !== 4) {
$params = self::doSubmitActions($params, $mailing);
}

return $mailing;
Expand Down
25 changes: 25 additions & 0 deletions Civi/Api4/Mailing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\Api4;

/**
* Mailing.
*
* Mailing entities store the contents and settings for bulk mails.
*
* @searchable secondary
* @see https://docs.civicrm.org/user/en/latest/email/what-is-civimail/
* @since 5.47
* @package Civi\Api4
*/
class Mailing extends Generic\DAOEntity {

}
29 changes: 29 additions & 0 deletions Civi/Api4/MailingJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\Api4;

use Civi\Api4\Generic\Traits\ReadOnlyEntity;

/**
* Mailing job.
*
* Mailing job tracks the batching of CiviMails.
*
* @searchable none
*
* @see https://docs.civicrm.org/user/en/latest/email/what-is-civimail/
* @since 5.47
* @package Civi\Api4
*/
class MailingJob extends Generic\DAOEntity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make MailingJob readonly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton the @readonly annotation isn't right. Instead you have to use the ReadOnlyEntity trait.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colemanw thanks - fixed

use ReadOnlyEntity;

}
17 changes: 15 additions & 2 deletions api/v3/Mailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,22 @@ function civicrm_api3_mailing_create($params) {
if (!$timestampCheck) {
throw new API_Exception("Mailing has not been saved, Content maybe out of date, please refresh the page and try again");
}
// If we're going to autosend, then check validity before saving.
if (empty($params['is_completed']) && !empty($params['scheduled_date'])
&& $params['scheduled_date'] !== 'null'
// This might have been passed in as empty to prevent us validating, is set skip.
&& !isset($params['_evil_bao_validator_'])) {

// FlexMailer is a refactoring of CiviMail which provides new hooks/APIs/docs. If the sysadmin has opted to enable it, then use that instead of CiviMail.
$function = \CRM_Utils_Constant::value('CIVICRM_FLEXMAILER_HACK_SENDABLE', 'CRM_Mailing_BAO_Mailing::checkSendable');
$validationFunction = Civi\Core\Resolver::singleton()->get($function);
$errors = call_user_func($validationFunction, $params);
if (!empty($errors)) {
$fields = implode(',', array_keys($errors));
throw new CiviCRM_API3_Exception("Mailing cannot be sent. There are missing or invalid fields ($fields).", 'cannot-send', $errors);
}
}

// FlexMailer is a refactoring of CiviMail which provides new hooks/APIs/docs. If the sysadmin has opted to enable it, then use that instead of CiviMail.
$safeParams['_evil_bao_validator_'] = \CRM_Utils_Constant::value('CIVICRM_FLEXMAILER_HACK_SENDABLE', 'CRM_Mailing_BAO_Mailing::checkSendable');
$result = _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $safeParams, 'Mailing');
return _civicrm_api3_mailing_get_formatResult($result);
}
Expand Down
14 changes: 10 additions & 4 deletions ext/flexmailer/src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ class Validator {
const EVENT_CHECK_SENDABLE = 'civi.flexmailer.checkSendable';

/**
* @param \CRM_Mailing_DAO_Mailing $mailing
* @param array $params
* The mailing which may or may not be sendable.
* @return array
* List of error messages.
*/
public static function createAndRun($mailing) {
$validator = new \Civi\FlexMailer\Validator();
return $validator->run(array(
public static function createAndRun(array $params): array {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colemanw @eileenmcnaughton - The failures in ValidatorTest are complaining because the signature changed (DAO => array).

It should be straight-forward to update the test:

diff --git a/ext/flexmailer/tests/phpunit/Civi/FlexMailer/ValidatorTest.php b/ext/flexmailer/tests/phpunit/Civi/FlexMailer/ValidatorTest.php
index 35541e5e67..642d4527b2 100644
--- a/ext/flexmailer/tests/phpunit/Civi/FlexMailer/ValidatorTest.php
+++ b/ext/flexmailer/tests/phpunit/Civi/FlexMailer/ValidatorTest.php
@@ -86,9 +86,7 @@ class ValidatorTest extends \CiviUnitTestCase {
    * @dataProvider getExamples
    */
   public function testExamples($mailingData, $expectedErrors): void {
-    $mailing = new \CRM_Mailing_DAO_Mailing();
-    $mailing->copyValues($mailingData);
-    $actualErrors = Validator::createAndRun($mailing);
+    $actualErrors = Validator::createAndRun($mailingData);
     $this->assertEquals(
       array_keys($actualErrors),
       array_keys($expectedErrors)

Grepped universe for createAndRun -- I don't see anything outside of civicrm-core that uses it.

$mailing = new \CRM_Mailing_BAO_Mailing();
$mailing->id = $params['id'] ?? NULL;
if ($mailing->id) {
$mailing->find(TRUE);
}
$mailing->copyValues($params);

return (new Validator())->run(array(
'mailing' => $mailing,
'attachments' => \CRM_Core_BAO_File::getEntityFile('civicrm_mailing', $mailing->id),
));
Expand Down
21 changes: 15 additions & 6 deletions tests/phpunit/api/v3/MailingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,21 @@ public function tearDown(): void {

/**
* Test civicrm_mailing_create.
*
* @dataProvider versionThreeAndFour
*
* @param int $version
*/
public function testMailerCreateSuccess(): void {
public function testMailerCreateSuccess(int $version): void {
$this->_apiversion = $version;
$this->enableCiviCampaign();
$this->callAPISuccess('Campaign', 'create', ['name' => 'big campaign', 'title' => 'abc']);
$result = $this->callAPIAndDocument('mailing', 'create', $this->_params + ['scheduled_date' => 'now', 'campaign_id' => 'big campaign'], __FUNCTION__, __FILE__);
$jobs = $this->callAPISuccess('mailing_job', 'get', ['mailing_id' => $result['id']]);
$jobs = $this->callAPISuccess('MailingJob', 'get', ['mailing_id' => $result['id']]);
$this->assertEquals(1, $jobs['count']);
// return isn't working on this in getAndCheck so lets not check it for now
unset($this->_params['created_id']);
$this->getAndCheck($this->_params, $result['id'], 'mailing');
$this->getAndCheck($this->_params, $result['id'], 'Mailing');
}

/**
Expand All @@ -103,9 +109,12 @@ public function testSkipAutoSchedule() {
/**
* Create a completed mailing (e.g when importing from a provider).
*
* @throws \CRM_Core_Exception
* @dataProvider versionThreeAndFour
*
* @param int $version
*/
public function testMailerCreateCompleted() {
public function testMailerCreateCompleted(int $version): void {
$this->_apiversion = $version;
$this->_params['body_html'] = 'I am completed so it does not matter if there is an opt out link since I have already been sent by another system';
$this->_params['is_completed'] = 1;
$result = $this->callAPIAndDocument('mailing', 'create', $this->_params + ['scheduled_date' => 'now'], __FUNCTION__, __FILE__);
Expand All @@ -120,7 +129,7 @@ public function testMailerCreateCompleted() {
/**
* Per CRM-20316 the mailing should still create without created_id (not mandatory).
*/
public function testMailerCreateSuccessNoCreatedID() {
public function testMailerCreateSuccessNoCreatedID(): void {
unset($this->_params['created_id']);
$result = $this->callAPIAndDocument('mailing', 'create', $this->_params + ['scheduled_date' => 'now'], __FUNCTION__, __FILE__);
$this->getAndCheck($this->_params, $result['id'], 'mailing');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,12 @@ private function getFkID(string $fkEntity) {
$params['where'] = [['contact_type', '=', 'Individual']];
}
$entityList = civicrm_api4($fkEntity, 'get', $params);
// If no existing entities, create one
if ($entityList->count() < 1) {
$msg = sprintf('At least one %s is required in test', $fkEntity);
throw new \API_Exception($msg);
$entityList = civicrm_api4($fkEntity, 'create', [
'checkPermissions' => FALSE,
'values' => $this->getRequired($fkEntity),
]);
}

return $entityList->last()['id'];
Expand Down