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#2644 Add Scheduled Communications extension #27081

Merged
merged 7 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
!/ext/civi_member
!/ext/civi_pledge
!/ext/civi_report
!/ext/scheduled_communications
backdrop/
bower_components
CRM/Case/xml/configuration
Expand Down
2 changes: 1 addition & 1 deletion CRM/Activity/ActionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public function createQuery($schedule, $phase, $defaultParams): CRM_Utils_SQL_Se
$selectedValues = (array) \CRM_Utils_Array::explodePadded($schedule->entity_value);
$selectedStatuses = (array) \CRM_Utils_Array::explodePadded($schedule->entity_status);

$query = \CRM_Utils_SQL_Select::from("{$this->getEntityTable()} e")->param($defaultParams);
$query = \CRM_Utils_SQL_Select::from("civicrm_activity e")->param($defaultParams);
$query['casAddlCheckFrom'] = 'civicrm_activity e';
$query['casContactIdField'] = 'r.contact_id';
$query['casEntityIdField'] = 'e.id';
Expand Down
2 changes: 1 addition & 1 deletion CRM/Activity/Tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected function getApiEntityName(): string {
* @inheritDoc
*/
public function alterActionScheduleQuery(MailingQueryEvent $e): void {
if ($e->mapping->getEntityTable() !== $this->getExtendableTableName()) {
if ($e->mapping->getEntityTable($e->actionSchedule) !== $this->getExtendableTableName()) {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions CRM/Contact/ActionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@ public function createQuery($schedule, $phase, $defaultParams): CRM_Utils_SQL_Se
$selectedValues = (array) \CRM_Utils_Array::explodePadded($schedule->entity_value);
$selectedStatuses = (array) \CRM_Utils_Array::explodePadded($schedule->entity_status);

// FIXME: This assumes that $values only has one field, but UI shows multiselect.
// Properly supporting multiselect would require total rewrite of this function.
// Only one value is allowed for this mapping type.
// The form and API both enforce this, so this error should never happen.
if (count($selectedValues) != 1 || !isset($selectedValues[0])) {
throw new \CRM_Core_Exception("Error: Scheduled reminders may only have one contact field.");
}
elseif (in_array($selectedValues[0], $this->contactDateFields)) {
$dateDBField = $selectedValues[0];
$query = \CRM_Utils_SQL_Select::from("{$this->getEntityTable()} e")->param($defaultParams);
$query = \CRM_Utils_SQL_Select::from('civicrm_contact e')->param($defaultParams);
$query->param([
'casAddlCheckFrom' => 'civicrm_contact e',
'casContactIdField' => 'e.id',
Expand Down
41 changes: 15 additions & 26 deletions CRM/Core/BAO/ActionSchedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

use Civi\ActionSchedule\Event\MappingRegisterEvent;
use Civi\ActionSchedule\MappingBase;
use Civi\Core\HookInterface;

/**
Expand Down Expand Up @@ -86,10 +87,8 @@ public static function getMappingOptions(): array {
*/
public static function getEntityValueOptions(string $fieldName, array $params): array {
$values = self::fillValues($params['values'], ['mapping_id']);
if (!$values['mapping_id']) {
return [];
}
return self::getMapping($values['mapping_id'])->getValueLabels();
$mapping = self::getMapping($values['mapping_id']);
return $mapping ? $mapping->getValueLabels() : [];
}

/**
Expand All @@ -98,10 +97,8 @@ public static function getEntityValueOptions(string $fieldName, array $params):
*/
public static function getLimitToOptions(string $fieldName, array $params): ?array {
$values = self::fillValues($params['values'], ['mapping_id']);
if (!$values['mapping_id']) {
return Civi\ActionSchedule\MappingBase::getLimitToOptions();
}
return self::getMapping($values['mapping_id'])::getLimitToOptions();
$mapping = self::getMapping($values['mapping_id']);
return $mapping ? $mapping::getLimitToOptions() : MappingBase::getLimitToOptions();
}

/**
Expand All @@ -110,10 +107,8 @@ public static function getLimitToOptions(string $fieldName, array $params): ?arr
*/
public static function getRecipientOptions(string $fieldName, array $params): ?array {
$values = self::fillValues($params['values'], ['mapping_id']);
if (!$values['mapping_id']) {
return Civi\ActionSchedule\MappingBase::getRecipientTypes();
}
return self::getMapping($values['mapping_id'])::getRecipientTypes();
$mapping = self::getMapping($values['mapping_id']);
return $mapping ? $mapping::getRecipientTypes() : MappingBase::getRecipientTypes();
}

/**
Expand All @@ -122,10 +117,8 @@ public static function getRecipientOptions(string $fieldName, array $params): ?a
*/
public static function getRecipientListingOptions(string $fieldName, array $params): ?array {
$values = self::fillValues($params['values'], ['mapping_id', 'recipient']);
if (!$values['mapping_id']) {
return [];
}
return self::getMapping($values['mapping_id'])->getRecipientListing($values['recipient']);
$mapping = self::getMapping($values['mapping_id']);
return $mapping ? $mapping->getRecipientListing($values['recipient']) : [];
}

/**
Expand All @@ -134,10 +127,8 @@ public static function getRecipientListingOptions(string $fieldName, array $para
*/
public static function getEntityStatusOptions(string $fieldName, array $params): array {
$values = self::fillValues($params['values'], ['mapping_id', 'entity_value']);
if (!$values['mapping_id']) {
return [];
}
return self::getMapping($values['mapping_id'])->getStatusLabels($values['entity_value']);
$mapping = self::getMapping($values['mapping_id']);
return $mapping ? $mapping->getStatusLabels($values['entity_value']) : [];
}

/**
Expand All @@ -146,10 +137,8 @@ public static function getEntityStatusOptions(string $fieldName, array $params):
*/
public static function getActionDateOptions(string $fieldName, array $params): array {
$values = self::fillValues($params['values'], ['mapping_id', 'entity_value']);
if (!$values['mapping_id']) {
return [];
}
return self::getMapping($values['mapping_id'])->getDateFields($values['entity_value']);
$mapping = self::getMapping($values['mapping_id']);
return $mapping ? $mapping->getDateFields($values['entity_value']) : [];
}

/**
Expand Down Expand Up @@ -540,7 +529,7 @@ public static function pickLocale($communication_language, $preferred_language)
protected static function createMailingActivity($tokenRow, $mapping, $contactID, $entityID, $caseID) {
$session = CRM_Core_Session::singleton();

if ($mapping->getEntityTable() == 'civicrm_membership') {
if ($mapping->getEntityName() === 'Membership') {
// @todo - not required with api
$activityTypeID
= CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Membership Renewal Reminder');
Expand Down Expand Up @@ -585,7 +574,7 @@ protected static function prepareMailingQuery($mapping, $actionSchedule) {
'casActionScheduleId' => $actionSchedule->id,
'casMailingJoinType' => ($actionSchedule->limit_to == 2) ? 'LEFT JOIN' : 'INNER JOIN',
'casMappingId' => $mapping->getId(),
'casMappingEntity' => $mapping->getEntityTable(),
'casMappingEntity' => $mapping->getEntityTable($actionSchedule),
'casEntityJoinExpr' => 'e.id = IF(reminder.entity_table = "civicrm_contact", reminder.contact_id, reminder.entity_id)',
]);

Expand Down
5 changes: 2 additions & 3 deletions CRM/Core/EntityTokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,7 @@ public function __construct() {
public function checkActive(TokenProcessor $processor) {
return ((!empty($processor->context['actionMapping'])
// This makes the 'schema context compulsory - which feels accidental
// since recent discu
&& $processor->context['actionMapping']->getEntityTable()) || in_array($this->getEntityIDField(), $processor->context['schema'])) && in_array($this->getApiEntityName(), array_keys(\Civi::service('action_object_provider')->getEntities()));
&& $processor->context['actionMapping']->getEntityName()) || in_array($this->getEntityIDField(), $processor->context['schema'])) && in_array($this->getApiEntityName(), array_keys(\Civi::service('action_object_provider')->getEntities()));
}

/**
Expand All @@ -360,7 +359,7 @@ public function checkActive(TokenProcessor $processor) {
* @param \Civi\ActionSchedule\Event\MailingQueryEvent $e
*/
public function alterActionScheduleQuery(MailingQueryEvent $e): void {
if ($e->mapping->getEntityTable() !== $this->getExtendableTableName()) {
if ($e->mapping->getEntityTable($e->actionSchedule) !== $this->getExtendableTableName()) {
return;
}
$e->query->select('e.id AS tokenContext_' . $this->getEntityIDField());
Expand Down
2 changes: 1 addition & 1 deletion CRM/Event/ActionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public function createQuery($schedule, $phase, $defaultParams): CRM_Utils_SQL_Se
$selectedValues = (array) \CRM_Utils_Array::explodePadded($schedule->entity_value);
$selectedStatuses = (array) \CRM_Utils_Array::explodePadded($schedule->entity_status);

$query = \CRM_Utils_SQL_Select::from("{$this->getEntityTable()} e")->param($defaultParams);
$query = \CRM_Utils_SQL_Select::from('civicrm_participant e')->param($defaultParams);
$query['casAddlCheckFrom'] = 'civicrm_event r';
$query['casContactIdField'] = 'e.contact_id';
$query['casEntityIdField'] = 'e.id';
Expand Down
2 changes: 1 addition & 1 deletion CRM/Event/ParticipantTokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ protected function getBespokeTokens(): array {
public function alterActionScheduleQuery(\Civi\ActionSchedule\Event\MailingQueryEvent $e): void {
// When targeting `civicrm_participant` records, we enable both `{participant.*}` (per usual) and the related `{event.*}`.
parent::alterActionScheduleQuery($e);
if ($e->mapping->getEntityTable() === $this->getExtendableTableName()) {
if ($e->mapping->getEntityTable($e->actionSchedule) === $this->getExtendableTableName()) {
$e->query->select('e.event_id AS tokenContext_eventId');
}
}
Expand Down
2 changes: 1 addition & 1 deletion CRM/Member/ActionMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function createQuery($schedule, $phase, $defaultParams): CRM_Utils_SQL_Se
$selectedValues = (array) \CRM_Utils_Array::explodePadded($schedule->entity_value);
$selectedStatuses = (array) \CRM_Utils_Array::explodePadded($schedule->entity_status);

$query = \CRM_Utils_SQL_Select::from("{$this->getEntityTable()} e")->param($defaultParams);
$query = \CRM_Utils_SQL_Select::from('civicrm_membership e')->param($defaultParams);
$query['casAddlCheckFrom'] = 'civicrm_membership e';
$query['casContactIdField'] = 'e.contact_id';
$query['casEntityIdField'] = 'e.id';
Expand Down
7 changes: 7 additions & 0 deletions CRM/Utils/SQL/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -647,4 +647,11 @@ public function execute($daoName = NULL, $i18nRewrite = TRUE) {
$freeDAO, $i18nRewrite, $trapException);
}

/**
* @return string
*/
public function getFrom(): string {
return $this->from;
}

}
4 changes: 2 additions & 2 deletions Civi/ActionSchedule/MappingBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static function onRegisterActionMappings(\Civi\ActionSchedule\Event\Mappi
$registrations->register(new static());
}

public function getEntityTable(): string {
public function getEntityTable(\CRM_Core_DAO_ActionSchedule $actionSchedule): string {
return \CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getEntityName());
}

Expand All @@ -52,7 +52,7 @@ public function getEntityTable(): string {
*/
public function getEntity(): string {
\CRM_Core_Error::deprecatedFunctionWarning('getEntityTable');
return $this->getEntityTable();
return \CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getEntityName());
}

public function getLabel(): string {
Expand Down
3 changes: 2 additions & 1 deletion Civi/ActionSchedule/MappingInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ public function getName(): string;

/**
* Name of the table belonging to the main entity e.g. `civicrm_activity`
* @param \CRM_Core_DAO_ActionSchedule $actionSchedule
* @return string
*/
public function getEntityTable(): string;
public function getEntityTable(\CRM_Core_DAO_ActionSchedule $actionSchedule): string;

/**
* Main entity name e.g. `Activity`
Expand Down
14 changes: 9 additions & 5 deletions Civi/ActionSchedule/RecipientBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ protected function buildAddlFirstPass() {
->merge($this->prepareAddlFilter('c.id'))
->where("c.id NOT IN (
SELECT rem.contact_id
FROM civicrm_action_log rem INNER JOIN {$this->mapping->getEntityTable()} e ON rem.entity_id = e.id
FROM civicrm_action_log rem INNER JOIN {$this->getMappingTable()} e ON rem.entity_id = e.id
WHERE rem.action_schedule_id = {$this->actionSchedule->id}
AND rem.entity_table = '{$this->mapping->getEntityTable()}'
AND rem.entity_table = '{$this->getMappingTable()}'
)")
// Where does e.id come from here? ^^^
->groupBy("c.id")
Expand Down Expand Up @@ -276,7 +276,7 @@ protected function prepareQuery($phase) {
$defaultParams = [
'casActionScheduleId' => $this->actionSchedule->id,
'casMappingId' => $this->mapping->getId(),
'casMappingEntity' => $this->mapping->getEntityTable(),
'casMappingEntity' => $this->getMappingTable(),
'casNow' => $this->now,
];

Expand Down Expand Up @@ -402,7 +402,7 @@ protected function prepareStartDateClauses() {
$date = $operator . "(!casDateField, INTERVAL {$actionSchedule->start_action_offset} {$actionSchedule->start_action_unit})";
$startDateClauses[] = "'!casNow' >= {$date}";
// This is weird. Waddupwidat?
if ($this->mapping->getEntityTable() == 'civicrm_participant') {
if ($this->getMappingTable() == 'civicrm_participant') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Non-blocking but === would be better

$startDateClauses[] = $operator . "(!casNow, INTERVAL 1 DAY ) {$op} " . '!casDateField';
}
else {
Expand Down Expand Up @@ -566,7 +566,7 @@ protected function joinReminder($joinType, $for, $query) {
switch ($for) {
case 'rel':
$contactIdField = $query['casContactIdField'];
$entityName = $this->mapping->getEntityTable();
$entityName = $this->getMappingTable();
$entityIdField = $query['casEntityIdField'];
break;

Expand Down Expand Up @@ -608,4 +608,8 @@ protected function resetOnTriggerDateChange() {
return $this->mapping->resetOnTriggerDateChange($this->actionSchedule);
}

protected function getMappingTable(): string {
return $this->mapping->getEntityTable($this->actionSchedule);
}

}
80 changes: 76 additions & 4 deletions Civi/Api4/Generic/Traits/SavedSearchInspectorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

use Civi\API\Exception\UnauthorizedException;
use Civi\API\Request;
use Civi\Api4\Query\SqlEquation;
use Civi\Api4\Query\SqlExpression;
use Civi\Api4\Query\SqlField;
use Civi\Api4\Query\SqlFunction;
use Civi\Api4\SavedSearch;
use Civi\Api4\Utils\CoreUtil;

Expand Down Expand Up @@ -44,6 +47,11 @@ trait SavedSearchInspectorTrait {
*/
private $_searchEntityFields;

/**
* @var array
*/
private $_joinMap;

/**
* If SavedSearch is supplied as a string, this will load it as an array
* @param int|null $id
Expand All @@ -66,7 +74,7 @@ protected function loadSavedSearch(int $id = NULL) {
$this->savedSearch['api_params'] += ['version' => 4, 'select' => [], 'where' => []];
}
// Reset internal cached metadata
$this->_selectQuery = $this->_selectClause = $this->_searchEntityFields = NULL;
$this->_selectQuery = $this->_selectClause = $this->_searchEntityFields = $this->_joinMap = NULL;
$this->_apiParams = ($this->savedSearch['api_params'] ?? []) + ['select' => [], 'where' => []];
}

Expand Down Expand Up @@ -118,11 +126,12 @@ protected function getField($fieldName) {
}

/**
* @param $joinAlias
* @param string $joinAlias
* Alias of the join, with or without the trailing dot
* @return array{entity: string, alias: string, table: string, bridge: string|NULL}|NULL
*/
protected function getJoin($joinAlias) {
return $this->getQuery() ? $this->getQuery()->getExplicitJoin($joinAlias) : NULL;
protected function getJoin(string $joinAlias) {
return $this->getQuery() ? $this->getQuery()->getExplicitJoin(rtrim($joinAlias, '.')) : NULL;
}

/**
Expand Down Expand Up @@ -360,4 +369,67 @@ protected function checkPermissionToLoadSearch() {
}
}

/**
* @param \Civi\Api4\Query\SqlExpression $expr
* @return string
*/
protected function getColumnLabel(SqlExpression $expr) {
if ($expr instanceof SqlFunction) {
$args = [];
foreach ($expr->getArgs() as $arg) {
foreach ($arg['expr'] ?? [] as $ex) {
$args[] = $this->getColumnLabel($ex);
}
}
return '(' . $expr->getTitle() . ')' . ($args ? ' ' . implode(',', array_filter($args)) : '');
}
if ($expr instanceof SqlEquation) {
$args = [];
foreach ($expr->getArgs() as $arg) {
if (is_array($arg) && !empty($arg['expr'])) {
$args[] = $this->getColumnLabel(SqlExpression::convert($arg['expr']));
}
}
return '(' . implode(',', array_filter($args)) . ')';
}
elseif ($expr instanceof SqlField) {
$field = $this->getField($expr->getExpr());
$label = '';
if (!empty($field['explicit_join'])) {
$label = $this->getJoinLabel($field['explicit_join']) . ': ';
}
if (!empty($field['implicit_join']) && empty($field['custom_field_id'])) {
$field = $this->getField(substr($expr->getAlias(), 0, -1 - strlen($field['name'])));
}
return $label . $field['label'];
}
else {
return NULL;
}
}

/**
* @param string $joinAlias
* @return string
*/
protected function getJoinLabel($joinAlias) {
if (!isset($this->_joinMap)) {
$this->_joinMap = [];
$joinCount = [$this->savedSearch['api_entity'] => 1];
foreach ($this->savedSearch['api_params']['join'] ?? [] as $join) {
[$entityName, $alias] = explode(' AS ', $join[0]);
$num = '';
if (!empty($joinCount[$entityName])) {
$num = ' ' . (++$joinCount[$entityName]);
}
else {
$joinCount[$entityName] = 1;
}
$label = CoreUtil::getInfoItem($entityName, 'title');
$this->_joinMap[$alias] = $label . $num;
}
}
return $this->_joinMap[$joinAlias];
}

}
Loading