First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,76 @@
<?php
namespace Civi\ActionSchedule\Event;
use Civi\ActionSchedule\MappingInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Class MailingQueryEvent
* @package Civi\ActionSchedule\Event
*
* This event allows listeners to modify the query which generates mailing data.
* If you want to fetch extra mail-merge data as part of an initial query, then
* modify the mailing-query to add extra JOINs/SELECTs.
*
* The basic mailing query looks a bit like this (depending on configuration):
*
* @code
* SELECT reminder.id AS reminderID, reminder.contact_id as contactID, ...
* FROM `civicrm_action_log` reminder
* ... JOIN `target_entity` e ON e.id = reminder.entity_id ...
* WHERE reminder.action_schedule_id = #casActionScheduleId
* @endcode
*
* Listeners may modify the query. For example, suppose we want to load
* additional fields from the related 'foo' entity:
*
* @code
* $event->query->join('foo', '!casMailingJoinType civicrm_foo foo ON foo.myentity_id = e.id')
* ->select('foo.bar_value AS bar');
* @endcode
*
* There are several parameters pre-set for use in queries:
* - 'casActionScheduleId'
* - 'casEntityJoinExpr' - eg 'e.id = reminder.entity_id'
* - 'casMailingJoinType' - eg 'LEFT JOIN' or 'INNER JOIN' (depending on configuration)
* - 'casMappingId'
* - 'casMappingEntity'
*
* (Note: When adding more JOINs, it seems typical to use !casMailingJoinType, although
* some hard-code a LEFT JOIN. Don't have an explanation for why.)
*/
class MailingQueryEvent extends Event {
/**
* The schedule record which produced this mailing.
*
* @var \CRM_Core_DAO_ActionSchedule
*/
public $actionSchedule;
/**
* The mapping record which produced this mailing.
*
* @var MappingInterface
*/
public $mapping;
/**
* The alterable query. For details, see the class description.
* @var \CRM_Utils_SQL_Select
* @see MailingQueryEvent
*/
public $query;
/**
* @param \CRM_Core_DAO_ActionSchedule $actionSchedule
* @param MappingInterface $mapping
* @param \CRM_Utils_SQL_Select $query
*/
public function __construct($actionSchedule, $mapping, $query) {
$this->actionSchedule = $actionSchedule;
$this->mapping = $mapping;
$this->query = $query;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Civi\ActionSchedule\Event;
use Civi\ActionSchedule\MappingInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Class ActionScheduleEvent
* @package Civi\ActionSchedule\Event
*
* Register any available mappings.
*/
class MappingRegisterEvent extends Event {
/**
* @var array
* Array(scalar $id => Mapping $mapping).
*/
protected $mappings = array();
/**
* Register a new mapping.
*
* @param MappingInterface $mapping
* The new mapping.
* @return MappingRegisterEvent
*/
public function register(MappingInterface $mapping) {
$this->mappings[$mapping->getId()] = $mapping;
return $this;
}
/**
* @return array
* Array(scalar $id => MappingInterface $mapping).
*/
public function getMappings() {
ksort($this->mappings);
return $this->mappings;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Civi\ActionSchedule;
class Events {
/**
* Register any available mappings.
*
* @see EntityListEvent
*/
const MAPPINGS = 'civi.actionSchedule.getMappings';
/**
* Prepare the pre-mailing query. This query loads details about
* the contact/entity so that they're available for mail-merge.
*/
const MAILING_QUERY = 'civi.actionSchedule.prepareMailingQuery';
}

View file

@ -0,0 +1,344 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\ActionSchedule;
/**
* Class Mapping
* @package Civi\ActionSchedule
*
* This is the initial implementation of MappingInterface; it was
* constructed by cutting out swaths from CRM_Core_BAO_ActionSchedule.
* New implementers should consider implementing MappingInterface on
* their own.
*
* Background: The original designers of ActionMappings intended that
* one could create and configure new mappings through the database.
* To, e.g., define the filtering options for CiviEvent, you
* would insert a record in "civicrm_action_mapping" with fields like
* "entity" (a table name, eg "civicrm_event"), "entity_value" (an
* option-group name, eg "event_types").
*
* Unfortunately, the metadata in "civicrm_action_mapping" proved
* inadequate and was not updated to cope. Instead, a number
* of work-arounds for specific entities were hard-coded into
* the core action-scheduling code. Ultimately, to add a new
* mapping type, one needed to run around and patch a dozen
* places.
*
* The new MappingInterface makes no pretense of database-driven
* configuration. The dozen places have been consolidated and
* replaced with functions in MappingInterface.
*
* This "Mapping" implementation is a refactoring of the old
* hard-coded bits. Internally, it uses the concepts from
* "civicrm_action_mapping". The resulting code is more
* convoluted than a clean implementation of MappingInterface, but
* it strictly matches the old behavior (based on logging/comparing
* the queries produced through ActionScheduleTest).
*/
abstract class Mapping implements MappingInterface {
private static $fields = array(
'id',
'entity',
'entity_label',
'entity_value',
'entity_value_label',
'entity_status',
'entity_status_label',
'entity_date_start',
'entity_date_end',
);
/**
* Create mapping.
*
* @param array $params
*
* @return static
*/
public static function create($params) {
return new static($params);
}
/**
* Class constructor.
*
* @param array $params
*/
public function __construct($params) {
foreach (self::$fields as $field) {
if (isset($params[$field])) {
$this->{$field} = $params[$field];
}
}
}
protected $id;
/**
* The basic entity to query (table name).
*
* @var string
* Ex: 'civicrm_activity', 'civicrm_event'.
*/
protected $entity;
/**
* The basic entity to query (label).
*
* @var
* Ex: 'Activity', 'Event'
*/
private $entity_label;
/**
* Level 1 filter -- the field/option-list to filter on.
*
* @var string
* Ex: 'activity_type', 'civicrm_event', 'event_template'.
*/
private $entity_value;
/**
* Level 1 filter -- The field label.
*
* @var string
* Ex: 'Activity Type', 'Event Name', 'Event Template'.
*/
private $entity_value_label;
/**
* Level 2 filter -- the field/option-list to filter on.
* @var string
* Ex: 'activity_status, 'civicrm_participant_status_type', 'auto_renew_options'.
*/
private $entity_status;
/**
* Level 2 filter -- the field label.
* @var string
* Ex: 'Activity Status', 'Participant Status', 'Auto Rewnewal Options'.
*/
private $entity_status_label;
/**
* Date filter -- the field name.
* @var string|NULL
* Ex: 'event_start_date'
*/
private $entity_date_start;
/**
* Date filter -- the field name.
* @var string|NULL
* Ex: 'event_end_date'.
*/
private $entity_date_end;
/**
* @return mixed
*/
public function getId() {
return $this->id;
}
/**
* @return string
*/
public function getEntity() {
return $this->entity;
}
/**
* Get a printable label for this mapping type.
*
* @return string
*/
public function getLabel() {
return $this->entity_label;
}
/**
* Get a printable label to use a header on the 'value' filter.
*
* @return string
*/
public function getValueHeader() {
return $this->entity_value_label;
}
/**
* Get a printable label to use a header on the 'status' filter.
*
* @return string
*/
public function getStatusHeader() {
return $this->entity_status_label;
}
/**
* Get a list of value options.
*
* @return array
* Array(string $value => string $label).
* Ex: array(123 => 'Phone Call', 456 => 'Meeting').
*/
public function getValueLabels() {
return self::getValueLabelMap($this->entity_value);
}
/**
* Get a list of status options.
*
* @param string|int $value
* The list of status options may be contingent upon the selected filter value.
* This is the selected filter value.
* @return array
* Array(string $value => string $label).
* Ex: Array(123 => 'Completed', 456 => 'Scheduled').
*/
public function getStatusLabels($value) {
if ($this->entity_status === 'auto_renew_options') {
if ($value && \CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $value, 'auto_renew')) {
return \CRM_Core_OptionGroup::values('auto_renew_options');
}
else {
return array();
}
}
return self::getValueLabelMap($this->entity_status);
}
/**
* Get a list of available date fields.
*
* @return array
* Array(string $fieldName => string $fieldLabel).
*/
public function getDateFields() {
$dateFieldLabels = array();
if (!empty($this->entity_date_start)) {
$dateFieldLabels[$this->entity_date_start] = ucwords(str_replace('_', ' ', $this->entity_date_start));
}
if (!empty($this->entity_date_end)) {
$dateFieldLabels[$this->entity_date_end] = ucwords(str_replace('_', ' ', $this->entity_date_end));
}
return $dateFieldLabels;
}
/**
* Get a list of recipient types.
*
* Note: A single schedule may filter on *zero* or *one* recipient types.
* When an admin chooses a value, it's stored in $schedule->recipient.
*
* @return array
* array(string $value => string $label).
* Ex: array('assignee' => 'Activity Assignee').
*/
public function getRecipientTypes() {
return array();
}
/**
* Get a list of recipients which match the given type.
*
* Note: A single schedule may filter on *multiple* recipients.
* When an admin chooses value(s), it's stored in $schedule->recipient_listing.
*
* @param string $recipientType
* Ex: 'participant_role'.
* @return array
* Array(mixed $name => string $label).
* Ex: array(1 => 'Attendee', 2 => 'Volunteer').
* @see getRecipientTypes
*/
public function getRecipientListing($recipientType) {
return array();
}
protected static function getValueLabelMap($name) {
static $valueLabelMap = NULL;
if ($valueLabelMap === NULL) {
// CRM-20510: Include CiviCampaign activity types along with CiviCase IF component is enabled
$valueLabelMap['activity_type'] = \CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'label', TRUE);
asort($valueLabelMap['activity_type']);
$valueLabelMap['activity_status'] = \CRM_Core_PseudoConstant::activityStatus();
$valueLabelMap['event_type'] = \CRM_Event_PseudoConstant::eventType();
$valueLabelMap['civicrm_event'] = \CRM_Event_PseudoConstant::event(NULL, FALSE, "( is_template IS NULL OR is_template != 1 )");
$valueLabelMap['civicrm_participant_status_type'] = \CRM_Event_PseudoConstant::participantStatus(NULL, NULL, 'label');
$valueLabelMap['event_template'] = \CRM_Event_PseudoConstant::eventTemplates();
$valueLabelMap['auto_renew_options'] = \CRM_Core_OptionGroup::values('auto_renew_options');
$valueLabelMap['contact_date_reminder_options'] = \CRM_Core_OptionGroup::values('contact_date_reminder_options');
$valueLabelMap['civicrm_membership_type'] = \CRM_Member_PseudoConstant::membershipType();
$allCustomFields = \CRM_Core_BAO_CustomField::getFields('');
$dateFields = array(
'birth_date' => ts('Birth Date'),
'created_date' => ts('Created Date'),
'modified_date' => ts('Modified Date'),
);
foreach ($allCustomFields as $fieldID => $field) {
if ($field['data_type'] == 'Date') {
$dateFields["custom_$fieldID"] = $field['label'];
}
}
$valueLabelMap['civicrm_contact'] = $dateFields;
}
return $valueLabelMap[$name];
}
/**
* Determine whether a schedule based on this mapping is sufficiently
* complete.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @return array
* Array (string $code => string $message).
* List of error messages.
*/
public function validateSchedule($schedule) {
return array();
}
/**
* Generate a query to locate contacts who match the given
* schedule.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @param string $phase
* See, e.g., RecipientBuilder::PHASE_RELATION_FIRST.
* @param array $defaultParams
* @return \CRM_Utils_SQL_Select
*/
public abstract function createQuery($schedule, $phase, $defaultParams);
}

View file

@ -0,0 +1,148 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\ActionSchedule;
/**
* Interface MappingInterface
* @package Civi\ActionSchedule
*/
interface MappingInterface {
/**
* @return mixed
*/
public function getId();
/**
* @return string
*/
public function getEntity();
/**
* Get a printable label for this mapping type.
*
* @return string
*/
public function getLabel();
/**
* Get a printable label to use as the header on the 'value' filter.
*
* @return string
*/
public function getValueHeader();
/**
* Get a printable label to use as the header on the 'status' filter.
*
* @return string
*/
public function getStatusHeader();
/**
* Get a list of value options.
*
* @return array
* Array(string $value => string $label).
* Ex: array(123 => 'Phone Call', 456 => 'Meeting').
*/
public function getValueLabels();
/**
* Get a list of status options.
*
* @param string|int $value
* The list of status options may be contingent upon the selected filter value.
* This is the selected filter value.
* @return array
* Array(string $value => string $label).
* Ex: Array(123 => 'Completed', 456 => 'Scheduled').
*/
public function getStatusLabels($value);
/**
* Get a list of available date fields.
*
* @return array
* Array(string $fieldName => string $fieldLabel).
*/
public function getDateFields();
/**
* Get a list of recipient types.
*
* Note: A single schedule may filter on *zero* or *one* recipient types.
* When an admin chooses a value, it's stored in $schedule->recipient.
*
* @return array
* array(string $value => string $label).
* Ex: array('assignee' => 'Activity Assignee').
*/
public function getRecipientTypes();
/**
* Get a list of recipients which match the given type.
*
* Note: A single schedule may filter on *multiple* recipients.
* When an admin chooses value(s), it's stored in $schedule->recipient_listing.
*
* @param string $recipientType
* Ex: 'participant_role'.
* @return array
* Array(mixed $name => string $label).
* Ex: array(1 => 'Attendee', 2 => 'Volunteer').
* @see getRecipientTypes
*/
public function getRecipientListing($recipientType);
/**
* Determine whether a schedule based on this mapping is sufficiently
* complete.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @return array
* Array (string $code => string $message).
* List of error messages.
*/
public function validateSchedule($schedule);
/**
* Generate a query to locate contacts who match the given
* schedule.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @param string $phase
* See, e.g., RecipientBuilder::PHASE_RELATION_FIRST.
* @param array $defaultParams
* Default parameters that should be included with query.
* @return \CRM_Utils_SQL_Select
* @see RecipientBuilder
*/
public function createQuery($schedule, $phase, $defaultParams);
}

View file

@ -0,0 +1,668 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\ActionSchedule;
/**
* Class RecipientBuilder
* @package Civi\ActionSchedule
*
* The RecipientBuilder prepares a list of recipients based on an action-schedule.
*
* This is a four-step process, with different steps depending on:
*
* (a) How the recipient is identified. Sometimes recipients are identified based
* on their relations (e.g. selecting the assignees of an activity or the
* participants of an event), and sometimes they are manually added using
* a flat contact list (e.g. with a contact ID or group ID).
* (b) Whether this is the first reminder or a follow-up/repeated reminder.
*
* The permutations of these (a)+(b) produce four phases -- RELATION_FIRST,
* RELATION_REPEAT, ADDITION_FIRST, ADDITION_REPEAT.
*
* Each phase requires running a complex query. As a general rule,
* MappingInterface::createQuery() produces a base query, and the RecipientBuilder
* appends extra bits (JOINs/WHEREs/GROUP BYs) depending on which step is running.
*
* For example, suppose we want to send reminders to anyone who registers for
* a "Conference" or "Exhibition" event with the 'pay later' option, and we want
* to fire the reminders X days after the registration date. The
* MappingInterface::createQuery() could return a query like:
*
* @code
* CRM_Utils_SQL_Select::from('civicrm_participant e')
* ->join('event', 'INNER JOIN civicrm_event event ON e.event_id = event.id')
* ->where('e.is_pay_later = 1')
* ->where('event.event_type_id IN (#myEventTypes)')
* ->param('myEventTypes', array(2, 5))
* ->param('casDateField', 'e.register_date')
* ->param($defaultParams)
* ...etc...
* @endcode
*
* In the RELATION_FIRST phase, RecipientBuilder adds a LEFT-JOIN+WHERE to find
* participants who have *not* yet received any reminder, and filters those
* participants based on whether X days have passed since "e.register_date".
*
* Notice that the query may define several SQL elements directly (eg
* via `from()`, `where()`, `join()`, `groupBy()`). Additionally, it
* must define some parameters (eg `casDateField`). These parameters will be
* read by RecipientBuilder and used in other parts of the query.
*
* At time of writing, these parameters are required:
* - casAddlCheckFrom: string, SQL FROM expression
* - casContactIdField: string, SQL column expression
* - casDateField: string, SQL column expression
* - casEntityIdField: string, SQL column expression
*
* Some parameters are optional:
* - casContactTableAlias: string, SQL table alias
* - casAnniversaryMode: bool
* - casUseReferenceDate: bool
*
* Additionally, some parameters are automatically predefined:
* - casNow
* - casMappingEntity: string, SQL table name
* - casMappingId: int
* - casActionScheduleId: int
*
* Note: Any parameters defined by the core Civi\ActionSchedule subsystem
* use the prefix `cas`. If you define new parameters (like `myEventTypes`
* above), then use a different name (to avoid conflicts).
*/
class RecipientBuilder {
private $now;
/**
* Generate action_log's for new, first-time alerts to related contacts.
*
* @see buildRelFirstPass
*/
const PHASE_RELATION_FIRST = 'rel-first';
/**
* Generate action_log's for new, first-time alerts to additional contacts.
*
* @see buildAddlFirstPass
*/
const PHASE_ADDITION_FIRST = 'addl-first';
/**
* Generate action_log's for repeated, follow-up alerts to related contacts.
*
* @see buildRelRepeatPass
*/
const PHASE_RELATION_REPEAT = 'rel-repeat';
/**
* Generate action_log's for repeated, follow-up alerts to additional contacts.
*
* @see buildAddlRepeatPass
*/
const PHASE_ADDITION_REPEAT = 'addl-repeat';
/**
* @var \CRM_Core_DAO_ActionSchedule
*/
private $actionSchedule;
/**
* @var MappingInterface
*/
private $mapping;
/**
* @param $now
* @param \CRM_Core_DAO_ActionSchedule $actionSchedule
* @param MappingInterface $mapping
*/
public function __construct($now, $actionSchedule, $mapping) {
$this->now = $now;
$this->actionSchedule = $actionSchedule;
$this->mapping = $mapping;
}
/**
* Fill the civicrm_action_log with any new/missing TODOs.
*
* @throws \CRM_Core_Exception
*/
public function build() {
$this->buildRelFirstPass();
if ($this->prepareAddlFilter('c.id')) {
$this->buildAddlFirstPass();
}
if ($this->actionSchedule->is_repeat) {
$this->buildRelRepeatPass();
}
if ($this->actionSchedule->is_repeat && $this->prepareAddlFilter('c.id')) {
$this->buildAddlRepeatPass();
}
}
/**
* Generate action_log's for new, first-time alerts to related contacts.
*
* @throws \Exception
*/
protected function buildRelFirstPass() {
$query = $this->prepareQuery(self::PHASE_RELATION_FIRST);
$startDateClauses = $this->prepareStartDateClauses();
// In some cases reference_date got outdated due to many reason e.g. In Membership renewal end_date got extended
// which means reference date mismatches with the end_date where end_date may be used as the start_action_date
// criteria for some schedule reminder so in order to send new reminder we INSERT new reminder with new reference_date
// value via UNION operation
$referenceReminderIDs = array();
$referenceDate = NULL;
if (!empty($query['casUseReferenceDate'])) {
// First retrieve all the action log's ids which are outdated or in other words reference_date now don't match with entity date.
// And the retrieve the updated entity date which will later used below to update all other outdated action log records
$sql = $query->copy()
->select('reminder.id as id')
->select($query['casDateField'] . ' as reference_date')
->merge($this->joinReminder('INNER JOIN', 'rel', $query))
->where("reminder.id IS NOT NULL AND reminder.reference_date IS NOT NULL AND reminder.reference_date <> !casDateField")
->where($startDateClauses)
->orderBy("reminder.id desc")
->strict()
->toSQL();
$dao = \CRM_Core_DAO::executeQuery($sql);
while ($dao->fetch()) {
$referenceReminderIDs[] = $dao->id;
$referenceDate = $dao->reference_date;
}
}
if (empty($referenceReminderIDs)) {
$firstQuery = $query->copy()
->merge($this->selectIntoActionLog(self::PHASE_RELATION_FIRST, $query))
->merge($this->joinReminder('LEFT JOIN', 'rel', $query))
->where("reminder.id IS NULL")
->where($startDateClauses)
->strict()
->toSQL();
\CRM_Core_DAO::executeQuery($firstQuery);
}
else {
// INSERT new log to send reminder as desired entity date got updated
$referenceQuery = $query->copy()
->merge($this->selectIntoActionLog(self::PHASE_RELATION_FIRST, $query))
->merge($this->joinReminder('LEFT JOIN', 'rel', $query))
->where("reminder.id = !reminderID")
->where($startDateClauses)
->param('reminderID', $referenceReminderIDs[0])
->strict()
->toSQL();
\CRM_Core_DAO::executeQuery($referenceQuery);
// Update all the previous outdated reference date valued, action_log rows to the latest changed entity date
$updateQuery = "UPDATE civicrm_action_log SET reference_date = '" . $referenceDate . "' WHERE id IN (" . implode(', ', $referenceReminderIDs) . ")";
\CRM_Core_DAO::executeQuery($updateQuery);
}
}
/**
* Generate action_log's for new, first-time alerts to additional contacts.
*
* @throws \Exception
*/
protected function buildAddlFirstPass() {
$query = $this->prepareQuery(self::PHASE_ADDITION_FIRST);
$insertAdditionalSql = \CRM_Utils_SQL_Select::from("civicrm_contact c")
->merge($query, array('params'))
->merge($this->selectIntoActionLog(self::PHASE_ADDITION_FIRST, $query))
->merge($this->joinReminder('LEFT JOIN', 'addl', $query))
->where('reminder.id IS NULL')
->where("c.is_deleted = 0 AND c.is_deceased = 0")
->merge($this->prepareAddlFilter('c.id'))
->where("c.id NOT IN (
SELECT rem.contact_id
FROM civicrm_action_log rem INNER JOIN {$this->mapping->getEntity()} e ON rem.entity_id = e.id
WHERE rem.action_schedule_id = {$this->actionSchedule->id}
AND rem.entity_table = '{$this->mapping->getEntity()}'
)")
// Where does e.id come from here? ^^^
->groupBy("c.id")
->strict()
->toSQL();
\CRM_Core_DAO::executeQuery($insertAdditionalSql);
}
/**
* Generate action_log's for repeated, follow-up alerts to related contacts.
*
* @throws \CRM_Core_Exception
* @throws \Exception
*/
protected function buildRelRepeatPass() {
$query = $this->prepareQuery(self::PHASE_RELATION_REPEAT);
$startDateClauses = $this->prepareStartDateClauses();
// CRM-15376 - do not send our reminders if original criteria no longer applies
// the first part of the startDateClause array is the earliest the reminder can be sent. If the
// event (e.g membership_end_date) has changed then the reminder may no longer apply
// @todo - this only handles events that get moved later. Potentially they might get moved earlier
$repeatInsert = $query
->merge($this->joinReminder('INNER JOIN', 'rel', $query))
->merge($this->selectActionLogFields(self::PHASE_RELATION_REPEAT, $query))
->select("MAX(reminder.action_date_time) as latest_log_time")
->merge($this->prepareRepetitionEndFilter($query['casDateField']))
->where($this->actionSchedule->start_action_date ? $startDateClauses[0] : array())
->groupBy("reminder.contact_id, reminder.entity_id, reminder.entity_table")
// @todo replace use of timestampdiff with a direct comparison as TIMESTAMPDIFF cannot use an index.
->having("TIMESTAMPDIFF(HOUR, latest_log_time, CAST(!casNow AS datetime)) >= TIMESTAMPDIFF(HOUR, latest_log_time, DATE_ADD(latest_log_time, INTERVAL !casRepetitionInterval))")
->param(array(
'casRepetitionInterval' => $this->parseRepetitionInterval(),
))
->strict()
->toSQL();
// For unknown reasons, we manually insert each row. Why not change
// selectActionLogFields() to selectIntoActionLog() above?
$arrValues = \CRM_Core_DAO::executeQuery($repeatInsert)->fetchAll();
if ($arrValues) {
\CRM_Core_DAO::executeQuery(
\CRM_Utils_SQL_Insert::into('civicrm_action_log')
->columns(array('contact_id', 'entity_id', 'entity_table', 'action_schedule_id'))
->rows($arrValues)
->toSQL()
);
}
}
/**
* Generate action_log's for repeated, follow-up alerts to additional contacts.
*
* @throws \CRM_Core_Exception
* @throws \Exception
*/
protected function buildAddlRepeatPass() {
$query = $this->prepareQuery(self::PHASE_ADDITION_REPEAT);
$addlCheck = \CRM_Utils_SQL_Select::from($query['casAddlCheckFrom'])
->select('*')
->merge($query, array('params', 'wheres'))// why only where? why not the joins?
->merge($this->prepareRepetitionEndFilter($query['casDateField']))
->limit(1)
->strict()
->toSQL();
$daoCheck = \CRM_Core_DAO::executeQuery($addlCheck);
if ($daoCheck->fetch()) {
$repeatInsertAddl = \CRM_Utils_SQL_Select::from('civicrm_contact c')
->merge($this->selectActionLogFields(self::PHASE_ADDITION_REPEAT, $query))
->merge($this->joinReminder('INNER JOIN', 'addl', $query))
->select("MAX(reminder.action_date_time) as latest_log_time")
->merge($this->prepareAddlFilter('c.id'), array('params'))
->where("c.is_deleted = 0 AND c.is_deceased = 0")
->groupBy("reminder.contact_id")
// @todo replace use of timestampdiff with a direct comparison as TIMESTAMPDIFF cannot use an index.
->having("TIMESTAMPDIFF(HOUR, latest_log_time, CAST(!casNow AS datetime)) >= TIMESTAMPDIFF(HOUR, latest_log_time, DATE_ADD(latest_log_time, INTERVAL !casRepetitionInterval))")
->param(array(
'casRepetitionInterval' => $this->parseRepetitionInterval(),
))
->strict()
->toSQL();
// For unknown reasons, we manually insert each row. Why not change
// selectActionLogFields() to selectIntoActionLog() above?
$addValues = \CRM_Core_DAO::executeQuery($repeatInsertAddl)->fetchAll();
if ($addValues) {
\CRM_Core_DAO::executeQuery(
\CRM_Utils_SQL_Insert::into('civicrm_action_log')
->columns(array('contact_id', 'entity_id', 'entity_table', 'action_schedule_id'))
->rows($addValues)
->toSQL()
);
}
}
}
/**
* @param string $phase
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function prepareQuery($phase) {
$defaultParams = array(
'casActionScheduleId' => $this->actionSchedule->id,
'casMappingId' => $this->mapping->getId(),
'casMappingEntity' => $this->mapping->getEntity(),
'casNow' => $this->now,
);
/** @var \CRM_Utils_SQL_Select $query */
$query = $this->mapping->createQuery($this->actionSchedule, $phase, $defaultParams);
if ($this->actionSchedule->limit_to /*1*/) {
$query->merge($this->prepareContactFilter($query['casContactIdField']));
}
if (empty($query['casContactTableAlias'])) {
$query['casContactTableAlias'] = 'c';
$query->join('c', "INNER JOIN civicrm_contact c ON c.id = !casContactIdField AND c.is_deleted = 0 AND c.is_deceased = 0 ");
}
$multilingual = \CRM_Core_I18n::isMultilingual();
if ($multilingual && !empty($this->actionSchedule->filter_contact_language)) {
$query->where($this->prepareLanguageFilter($query['casContactTableAlias']));
}
return $query;
}
/**
* Parse repetition interval.
*
* @return int|string
*/
protected function parseRepetitionInterval() {
$actionSchedule = $this->actionSchedule;
if ($actionSchedule->repetition_frequency_unit == 'day') {
$interval = "{$actionSchedule->repetition_frequency_interval} DAY";
}
elseif ($actionSchedule->repetition_frequency_unit == 'week') {
$interval = "{$actionSchedule->repetition_frequency_interval} WEEK";
}
elseif ($actionSchedule->repetition_frequency_unit == 'month') {
$interval = "{$actionSchedule->repetition_frequency_interval} MONTH";
}
elseif ($actionSchedule->repetition_frequency_unit == 'year') {
$interval = "{$actionSchedule->repetition_frequency_interval} YEAR";
}
else {
$interval = "{$actionSchedule->repetition_frequency_interval} HOUR";
}
return $interval;
}
/**
* Prepare filter options for limiting by contact ID or group ID.
*
* @param string $contactIdField
* @return \CRM_Utils_SQL_Select
*/
protected function prepareContactFilter($contactIdField) {
$actionSchedule = $this->actionSchedule;
if ($actionSchedule->group_id) {
if ($this->isSmartGroup($actionSchedule->group_id)) {
// Check that the group is in place in the cache and up to date
\CRM_Contact_BAO_GroupContactCache::check($actionSchedule->group_id);
return \CRM_Utils_SQL_Select::fragment()
->join('grp', "INNER JOIN civicrm_group_contact_cache grp ON {$contactIdField} = grp.contact_id")
->where(" grp.group_id IN ({$actionSchedule->group_id})");
}
else {
return \CRM_Utils_SQL_Select::fragment()
->join('grp', " INNER JOIN civicrm_group_contact grp ON {$contactIdField} = grp.contact_id AND grp.status = 'Added'")
->where(" grp.group_id IN ({$actionSchedule->group_id})");
}
}
elseif (!empty($actionSchedule->recipient_manual)) {
$rList = \CRM_Utils_Type::escape($actionSchedule->recipient_manual, 'String');
return \CRM_Utils_SQL_Select::fragment()
->where("{$contactIdField} IN ({$rList})");
}
return NULL;
}
/**
* Prepare language filter.
*
* @param string $contactTableAlias
* @return string
*/
protected function prepareLanguageFilter($contactTableAlias) {
$actionSchedule = $this->actionSchedule;
// get language filter for the schedule
$filter_contact_language = explode(\CRM_Core_DAO::VALUE_SEPARATOR, $actionSchedule->filter_contact_language);
$w = '';
if (($key = array_search(\CRM_Core_I18n::NONE, $filter_contact_language)) !== FALSE) {
$w .= "{$contactTableAlias}.preferred_language IS NULL OR {$contactTableAlias}.preferred_language = '' OR ";
unset($filter_contact_language[$key]);
}
if (count($filter_contact_language) > 0) {
$w .= "{$contactTableAlias}.preferred_language IN ('" . implode("','", $filter_contact_language) . "')";
}
$w = "($w)";
return $w;
}
/**
* @return array
*/
protected function prepareStartDateClauses() {
$actionSchedule = $this->actionSchedule;
$startDateClauses = array();
if ($actionSchedule->start_action_date) {
$op = ($actionSchedule->start_action_condition == 'before' ? '<=' : '>=');
$operator = ($actionSchedule->start_action_condition == 'before' ? 'DATE_SUB' : 'DATE_ADD');
$date = $operator . "(!casDateField, INTERVAL {$actionSchedule->start_action_offset} {$actionSchedule->start_action_unit})";
$startDateClauses[] = "'!casNow' >= {$date}";
// This is weird. Waddupwidat?
if ($this->mapping->getEntity() == 'civicrm_participant') {
$startDateClauses[] = $operator . "(!casNow, INTERVAL 1 DAY ) {$op} " . '!casDateField';
}
else {
$startDateClauses[] = "DATE_SUB(!casNow, INTERVAL 1 DAY ) <= {$date}";
}
}
elseif ($actionSchedule->absolute_date) {
$startDateClauses[] = "DATEDIFF(DATE('!casNow'),'{$actionSchedule->absolute_date}') = 0";
}
return $startDateClauses;
}
/**
* @param int $groupId
* @return bool
*/
protected function isSmartGroup($groupId) {
// Then decide which table to join onto the query
$group = \CRM_Contact_DAO_Group::getTableName();
// Get the group information
$sql = "
SELECT $group.id, $group.cache_date, $group.saved_search_id, $group.children
FROM $group
WHERE $group.id = {$groupId}
";
$groupDAO = \CRM_Core_DAO::executeQuery($sql);
if (
$groupDAO->fetch() &&
!empty($groupDAO->saved_search_id)
) {
return TRUE;
}
return FALSE;
}
/**
* @param string $dateField
* @return \CRM_Utils_SQL_Select
*/
protected function prepareRepetitionEndFilter($dateField) {
$repeatEventDateExpr = ($this->actionSchedule->end_action == 'before' ? 'DATE_SUB' : 'DATE_ADD')
. "({$dateField}, INTERVAL {$this->actionSchedule->end_frequency_interval} {$this->actionSchedule->end_frequency_unit})";
return \CRM_Utils_SQL_Select::fragment()
->where("@casNow <= !repetitionEndDate")
->param(array(
'!repetitionEndDate' => $repeatEventDateExpr,
));
}
/**
* @param string $contactIdField
* @return \CRM_Utils_SQL_Select|null
*/
protected function prepareAddlFilter($contactIdField) {
$contactAddlFilter = NULL;
if ($this->actionSchedule->limit_to !== NULL && !$this->actionSchedule->limit_to /*0*/) {
$contactAddlFilter = $this->prepareContactFilter($contactIdField);
}
return $contactAddlFilter;
}
/**
* Generate a query fragment like for populating
* action logs, e.g.
*
* "SELECT contact_id, entity_id, entity_table, action schedule_id"
*
* @param string $phase
* @param \CRM_Utils_SQL_Select $query
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function selectActionLogFields($phase, $query) {
switch ($phase) {
case self::PHASE_RELATION_FIRST:
case self::PHASE_RELATION_REPEAT:
$fragment = \CRM_Utils_SQL_Select::fragment();
// CRM-15376: We are not tracking the reference date for 'repeated' schedule reminders.
if (!empty($query['casUseReferenceDate'])) {
$fragment->select($query['casDateField']);
}
$fragment->select(
array(
"!casContactIdField as contact_id",
"!casEntityIdField as entity_id",
"@casMappingEntity as entity_table",
"#casActionScheduleId as action_schedule_id",
)
);
break;
case self::PHASE_ADDITION_FIRST:
case self::PHASE_ADDITION_REPEAT:
//CRM-19017: Load default params for fragment query object.
$params = array(
'casActionScheduleId' => $this->actionSchedule->id,
'casNow' => $this->now,
);
$fragment = \CRM_Utils_SQL_Select::fragment()->param($params);
$fragment->select(
array(
"c.id as contact_id",
"c.id as entity_id",
"'civicrm_contact' as entity_table",
"#casActionScheduleId as action_schedule_id",
)
);
break;
default:
throw new \CRM_Core_Exception("Unrecognized phase: $phase");
}
return $fragment;
}
/**
* Generate a query fragment like for populating
* action logs, e.g.
*
* "INSERT INTO civicrm_action_log (...) SELECT (...)"
*
* @param string $phase
* @param \CRM_Utils_SQL_Select $query
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function selectIntoActionLog($phase, $query) {
$actionLogColumns = array(
"contact_id",
"entity_id",
"entity_table",
"action_schedule_id",
);
if ($phase === self::PHASE_RELATION_FIRST || $phase === self::PHASE_RELATION_REPEAT) {
if (!empty($query['casUseReferenceDate'])) {
array_unshift($actionLogColumns, 'reference_date');
}
}
return $this->selectActionLogFields($phase, $query)
->insertInto('civicrm_action_log', $actionLogColumns);
}
/**
* Add a JOIN clause like "INNER JOIN civicrm_action_log reminder ON...".
*
* @param string $joinType
* Join type (eg INNER JOIN, LEFT JOIN).
* @param string $for
* Ex: 'rel', 'addl'.
* @param \CRM_Utils_SQL_Select $query
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function joinReminder($joinType, $for, $query) {
switch ($for) {
case 'rel':
$contactIdField = $query['casContactIdField'];
$entityName = $this->mapping->getEntity();
$entityIdField = $query['casEntityIdField'];
break;
case 'addl':
$contactIdField = 'c.id';
$entityName = 'civicrm_contact';
$entityIdField = 'c.id';
break;
default:
throw new \CRM_Core_Exception("Unrecognized 'for': $for");
}
$joinClause = "civicrm_action_log reminder ON reminder.contact_id = {$contactIdField} AND
reminder.entity_id = {$entityIdField} AND
reminder.entity_table = '{$entityName}' AND
reminder.action_schedule_id = {$this->actionSchedule->id}";
// Why do we only include anniversary clause for 'rel' queries?
if ($for === 'rel' && !empty($query['casAnniversaryMode'])) {
// only consider reminders less than 11 months ago
$joinClause .= " AND reminder.action_date_time > DATE_SUB(!casNow, INTERVAL 11 MONTH)";
}
return \CRM_Utils_SQL_Select::fragment()->join("reminder", "$joinType $joinClause");
}
}