diff --git a/README.md b/README.md
index 0b0757264a1..b6d85da3f20 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,10 @@ Installing from source is only recommended if you are comfortable using the comm
## Requirements
+#### Contributors Agreement
+
+By contributing to this project, you accept and agree to the [Contributors Agreement](https://www.mautic.org/contributors-agreement) in its entirety.
+
#### Development / Build process requirements
1. Mautic uses Git as a version control system. Download and install git for your OS from https://git-scm.com/.
diff --git a/app/AppKernel.php b/app/AppKernel.php
index 604dbdd31af..72e00f6c9ff 100644
--- a/app/AppKernel.php
+++ b/app/AppKernel.php
@@ -41,7 +41,7 @@ class AppKernel extends Kernel
*
* @const integer
*/
- const PATCH_VERSION = 1;
+ const PATCH_VERSION = 2;
/**
* Extra version identifier.
@@ -527,7 +527,9 @@ protected function initializeContainer()
// Warm up the cache if classes.php is missing or in dev mode
if (!$fresh && $this->container->has('cache_warmer')) {
- $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'));
+ $warmer = $this->container->get('cache_warmer');
+ $warmer->enableOptionalWarmers();
+ $warmer->warmUp($this->container->getParameter('kernel.cache_dir'));
}
}
diff --git a/app/bundles/CampaignBundle/Assets/js/campaign.js b/app/bundles/CampaignBundle/Assets/js/campaign.js
index b3778bccea8..4c63543e844 100644
--- a/app/bundles/CampaignBundle/Assets/js/campaign.js
+++ b/app/bundles/CampaignBundle/Assets/js/campaign.js
@@ -1868,21 +1868,23 @@ Mautic.campaignBuilderValidateConnection = function (epDetails, targetType, targ
*/
Mautic.updateScheduledCampaignEvent = function(eventId, contactId) {
// Convert scheduled date/time to an input
- mQuery('#timeline-campaign-event-'+eventId+' .btn-edit').prop('disabled', true).addClass('disabled');
+ mQuery('#timeline-campaign-event-'+eventId+' .btn-reschedule').addClass('disabled');
var converting = false;
var eventWrapper = '#timeline-campaign-event-'+eventId;
var eventSpan = '.timeline-campaign-event-date-'+eventId;
var eventText = '#timeline-campaign-event-text-'+eventId;
+ var saveButton = '#timeline-campaign-event-save-' + eventId;
var originalDate = mQuery(eventWrapper+' '+eventSpan).first().text();
var revertInput = function(input) {
converting = true;
mQuery(input).datetimepicker('destroy');
mQuery(eventSpan).text(originalDate);
- mQuery(eventWrapper+' .btn').prop('disabled', false).removeClass('disabled');
+ mQuery(eventWrapper+' .btn-reschedule').removeClass('disabled');
};
var date = mQuery(eventSpan).attr('data-date');
+ mQuery(saveButton).show();
var input = mQuery('')
.css('height', '20px')
.css('color', '#000000')
@@ -1902,8 +1904,8 @@ Mautic.updateScheduledCampaignEvent = function(eventId, contactId) {
originalDate: date
}, function (response) {
mQuery(eventSpan).text(response.formattedDate);
- mQuery(eventSpan).attr('data-data', response.date);
- mQuery(eventWrapper+' .btn').prop('disabled', false).removeClass('disabled');
+ mQuery(eventSpan).attr('data-date', response.date);
+ mQuery(eventWrapper+' .btn-reschedule').removeClass('disabled');
if (response.success) {
mQuery(eventText).removeClass('text-warning').addClass('text-info');
@@ -1911,21 +1913,63 @@ Mautic.updateScheduledCampaignEvent = function(eventId, contactId) {
mQuery('.fa.timeline-campaign-event-cancelled-'+eventId).remove();
mQuery('.timeline-campaign-event-scheduled-'+eventId).removeClass('hide');
mQuery('.timeline-campaign-event-cancelled-'+eventId).addClass('hide');
+ mQuery(saveButton).hide();
}
}, false
);
} else if (code == 27) {
e.preventDefault();
revertInput(input);
+ mQuery(saveButton).hide();
}
})
.on('blur', function (e) {
if (!converting) {
revertInput(input);
}
+ mQuery(saveButton).hide();
});
mQuery('#timeline-campaign-event-'+eventId+' '+eventSpan).html(input);
Mautic.activateDateTimeInputs('#timeline-reschedule');
+ mQuery('#timeline-reschedule').focus();
+};
+
+/**
+ *
+ * @param eventId
+ * @param contactId
+ */
+Mautic.saveScheduledCampaignEvent = function (eventId, contactId) {
+ var saveButton = '#timeline-campaign-event-save-' + eventId;
+ mQuery(saveButton).addClass('disabled');
+
+ // Convert scheduled date/time to an input
+ var eventWrapper = '#timeline-campaign-event-' + eventId;
+ var eventSpan = '.timeline-campaign-event-date-' + eventId;
+ var eventText = '#timeline-campaign-event-text-' + eventId;
+
+ var date = mQuery(eventSpan).attr('data-date');
+ Mautic.ajaxActionRequest('campaign:updateScheduledCampaignEvent',
+ {
+ eventId: eventId,
+ contactId: contactId,
+ date: mQuery('#timeline-reschedule').val(),
+ originalDate: date
+ }, function (response) {
+ mQuery(eventSpan).text(response.formattedDate);
+ mQuery(eventSpan).attr('data-date', response.date);
+
+ if (response.success) {
+ mQuery(eventText).removeClass('text-warning').addClass('text-info');
+ mQuery(eventSpan).css('textDecoration', 'inherit');
+ mQuery('.fa.timeline-campaign-event-cancelled-' + eventId).remove();
+ mQuery('.timeline-campaign-event-scheduled-' + eventId).removeClass('hide');
+ mQuery('.timeline-campaign-event-cancelled-' + eventId).addClass('hide');
+ }
+
+ mQuery(saveButton).removeClass('disabled').hide();
+ mQuery(eventWrapper + ' .btn-reschedule').removeClass('disabled');
+ }, false);
};
/**
diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php
index c9edeb46f38..a052fd486f0 100644
--- a/app/bundles/CampaignBundle/Config/config.php
+++ b/app/bundles/CampaignBundle/Config/config.php
@@ -274,6 +274,7 @@
'mautic.campaign.model.event',
'mautic.campaign.model.campaign',
'mautic.helper.ip_lookup',
+ 'mautic.campaign.scheduler',
],
],
],
diff --git a/app/bundles/CampaignBundle/Controller/CampaignController.php b/app/bundles/CampaignBundle/Controller/CampaignController.php
index bed237cecfd..7563ec3a290 100644
--- a/app/bundles/CampaignBundle/Controller/CampaignController.php
+++ b/app/bundles/CampaignBundle/Controller/CampaignController.php
@@ -680,33 +680,54 @@ protected function getViewArguments(array $args, $action)
$dateRangeForm = $this->get('form.factory')->create('daterange', $dateRangeValues, ['action' => $action]);
/** @var LeadEventLogRepository $eventLogRepo */
- $eventLogRepo = $this->getDoctrine()->getManager()->getRepository('MauticCampaignBundle:LeadEventLog');
- $events = $this->getCampaignModel()->getEventRepository()->getCampaignEvents($entity->getId());
- $leadCount = $this->getCampaignModel()->getRepository()->getCampaignLeadCount($entity->getId());
- $campaignLogCounts = $eventLogRepo->getCampaignLogCounts($entity->getId(), false, false);
- $sortedEvents = [
+ $eventLogRepo = $this->getDoctrine()->getManager()->getRepository('MauticCampaignBundle:LeadEventLog');
+ $events = $this->getCampaignModel()->getEventRepository()->getCampaignEvents($entity->getId());
+ $leadCount = $this->getCampaignModel()->getRepository()->getCampaignLeadCount($entity->getId());
+ $campaignLogCounts = $eventLogRepo->getCampaignLogCounts($entity->getId(), false, false, true);
+ $pendingCampaignLogCounts = $eventLogRepo->getCampaignLogCounts($entity->getId(), false, false);
+ $sortedEvents = [
'decision' => [],
'action' => [],
'condition' => [],
];
-
- foreach ($events as $event) {
- $event['logCount'] =
- $event['percent'] =
- $event['yesPercent'] =
- $event['noPercent'] = 0;
- $event['leadCount'] = $leadCount;
+ foreach ($events as &$event) {
+ $event['logCount'] =
+ $event['logCountForPending'] =
+ $event['percent'] =
+ $event['yesPercent'] =
+ $event['noPercent'] = 0;
+ $event['leadCount'] = $leadCount;
if (isset($campaignLogCounts[$event['id']])) {
- $event['logCount'] = array_sum($campaignLogCounts[$event['id']]);
+ $event['logCount'] = array_sum($campaignLogCounts[$event['id']]);
+ $event['logCountForPending'] = isset($pendingCampaignLogCounts[$event['id']]) ? array_sum($pendingCampaignLogCounts[$event['id']]) : 0;
+ $pending = $event['leadCount'] - $event['logCountForPending'];
+ $totalYes = $campaignLogCounts[$event['id']][1];
+ $totalNo = $campaignLogCounts[$event['id']][0];
+ $total = $totalYes + $totalNo + $pending;
if ($leadCount) {
- $event['percent'] = round(($event['logCount'] / $leadCount) * 100, 1);
- $event['yesPercent'] = round(($campaignLogCounts[$event['id']][1] / $leadCount) * 100, 1);
- $event['noPercent'] = round(($campaignLogCounts[$event['id']][0] / $leadCount) * 100, 1);
+ $event['percent'] = round(($event['logCount'] / $total) * 100, 1);
+ $event['yesPercent'] = round(($campaignLogCounts[$event['id']][1] / $total) * 100, 1);
+ $event['noPercent'] = round(($campaignLogCounts[$event['id']][0] / $total) * 100, 1);
}
}
+ }
+ // rewrite stats data from parent condition if exist
+ foreach ($events as &$event) {
+ if (!empty($event['decisionPath']) && !empty($event['parent_id']) && isset($events[$event['parent_id']])) {
+ $parentEvent = $events[$event['parent_id']];
+ $event['logCountForPending'] = $parentEvent['logCountForPending'];
+ $event['percent'] = $parentEvent['percent'];
+ $event['yesPercent'] = $parentEvent['yesPercent'];
+ $event['noPercent'] = $parentEvent['noPercent'];
+ if ($event['decisionPath'] == 'yes') {
+ $event['noPercent'] = 0;
+ } else {
+ $event['yesPercent'] = 0;
+ }
+ }
$sortedEvents[$event['eventType']][] = $event;
}
diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php
index 54cd3b2196d..7f2a3ac01f4 100644
--- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php
+++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php
@@ -215,19 +215,26 @@ public function getUpcomingEvents(array $options = null)
/**
* @param $campaignId
* @param bool $excludeScheduled
+ * @param bool $excludeNegative
+ * @param bool $all
*
* @return array
*/
- public function getCampaignLogCounts($campaignId, $excludeScheduled = false, $excludeNegative = true)
+ public function getCampaignLogCounts($campaignId, $excludeScheduled = false, $excludeNegative = true, $all = false)
{
$q = $this->getSlaveConnection()->createQueryBuilder()
- ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'o')
- ->innerJoin(
- 'o',
- MAUTIC_TABLE_PREFIX.'campaign_leads',
- 'l',
- 'l.campaign_id = '.(int) $campaignId.' and l.manually_removed = 0 and o.lead_id = l.lead_id and l.rotation = o.rotation'
- );
+ ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'o');
+
+ $join = 'innerJoin';
+ if ($all === true) {
+ $join = 'leftJoin';
+ }
+ $q->$join(
+ 'o',
+ MAUTIC_TABLE_PREFIX.'campaign_leads',
+ 'l',
+ 'l.campaign_id = '.(int) $campaignId.' and l.manually_removed = 0 and o.lead_id = l.lead_id and l.rotation = o.rotation'
+ );
$expr = $q->expr()->andX(
$q->expr()->eq('o.campaign_id', (int) $campaignId)
diff --git a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php
index 3525bda3b3c..7f89fc9c382 100644
--- a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php
+++ b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php
@@ -233,8 +233,8 @@ public function wasLogUpdatedByListener()
}
/**
- * @param $channel
- * @param null $channelId
+ * @param string $channel
+ * @param string|int|null $channelId
*
* @return $this
*/
diff --git a/app/bundles/CampaignBundle/Model/CampaignModel.php b/app/bundles/CampaignBundle/Model/CampaignModel.php
index d39c6a7431c..de95b2362be 100644
--- a/app/bundles/CampaignBundle/Model/CampaignModel.php
+++ b/app/bundles/CampaignBundle/Model/CampaignModel.php
@@ -653,7 +653,7 @@ public function saveCampaignLead(CampaignLead $campaignLead)
return true;
} catch (\Exception $exception) {
- $this->logger->log('error', $exception->getMessage());
+ $this->logger->log('error', $exception->getMessage(), ['exception' => $exception]);
return false;
}
diff --git a/app/bundles/CampaignBundle/Model/EventLogModel.php b/app/bundles/CampaignBundle/Model/EventLogModel.php
index 2378705bd84..d337d50a79a 100644
--- a/app/bundles/CampaignBundle/Model/EventLogModel.php
+++ b/app/bundles/CampaignBundle/Model/EventLogModel.php
@@ -11,10 +11,9 @@
namespace Mautic\CampaignBundle\Model;
-use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Entity\LeadEventLog;
-use Mautic\CampaignBundle\Event\CampaignScheduledEvent;
+use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Model\AbstractCommonModel;
@@ -40,6 +39,11 @@ class EventLogModel extends AbstractCommonModel
*/
protected $ipLookupHelper;
+ /**
+ * @var EventScheduler
+ */
+ protected $eventScheduler;
+
/**
* EventLogModel constructor.
*
@@ -47,11 +51,12 @@ class EventLogModel extends AbstractCommonModel
* @param CampaignModel $campaignModel
* @param IpLookupHelper $ipLookupHelper
*/
- public function __construct(EventModel $eventModel, CampaignModel $campaignModel, IpLookupHelper $ipLookupHelper)
+ public function __construct(EventModel $eventModel, CampaignModel $campaignModel, IpLookupHelper $ipLookupHelper, EventScheduler $eventScheduler)
{
$this->eventModel = $eventModel;
$this->campaignModel = $campaignModel;
$this->ipLookupHelper = $ipLookupHelper;
+ $this->eventScheduler = $eventScheduler;
}
/**
@@ -198,26 +203,16 @@ public function updateContactEvent(Event $event, Lead $contact, array $parameter
}
/**
- * @param $entity
+ * @param LeadEventLog $entity
*/
public function saveEntity(LeadEventLog $entity)
{
- $eventSettings = $this->campaignModel->getEvents();
- if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_SCHEDULED)) {
- $event = $entity->getEvent();
- $args = [
- 'eventSettings' => $eventSettings[$event->getEventType()][$event->getType()],
- 'eventDetails' => null,
- 'event' => $event->convertToArray(),
- 'lead' => $entity->getLead(),
- 'systemTriggered' => false,
- 'dateScheduled' => $entity->getTriggerDate(),
- ];
-
- $scheduledEvent = new CampaignScheduledEvent($args, $entity);
- $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_SCHEDULED, $scheduledEvent);
+ $triggerDate = $entity->getTriggerDate();
+ if (null === $triggerDate) {
+ // Reschedule for now
+ $triggerDate = new \DateTime();
}
- $this->getRepository()->saveEntity($entity);
+ $this->eventScheduler->reschedule($entity, $triggerDate);
}
}
diff --git a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql
index 9cab8235b2a..4bdbab62f39 100644
--- a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql
+++ b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql
@@ -39,9 +39,9 @@ VALUES
(15,1,3,'Tag EmailNotOpen Again',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1612\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:22:\"Tag EmailNotOpen Again\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"6\";s:19:\"triggerIntervalUnit\";s:1:\"i\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"Wd8bGtv2HJ6Nyf3K90Efoo2Rn2VkDWwXhwzCIPMiD-M\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,6,'i','interval','no','newf16dfec5f2a65aa9c527675e7be516020a90daa6',NULL,NULL),
(16,1,12,'Tag ChainedAction',NULL,'lead.changetags','action',4,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"168\";s:8:\"droppedY\";s:3:\"439\";}s:4:\"name\";s:14:\"Chained Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:6:\"bottom\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:2:\"10\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:6:\"action\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"6xgHe74aRnc1V7AGzdang3-iJ0Ub5BKfbdU5NsxQmv0\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:13:\"ChainedAction\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate',NULL,'new60f74507aeccf217f78647e41ae29af51debe666',NULL,NULL);
-INSERT INTO `#__lead_lists` (`id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`alias`,`filters`,`is_global`)
+INSERT INTO `#__lead_lists` (`id`,`is_preference_center`, `is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`alias`,`filters`,`is_global`)
VALUES
- (1,1,'2018-01-04 23:41:20',1,'Admin User',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test',NULL,'campaign-test','a:0:{}',1);
+ (1,0,1,'2018-01-04 23:41:20',1,'Admin User',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test',NULL,'campaign-test','a:0:{}',1);
INSERT INTO `#__campaign_leadlist_xref` (`campaign_id`,`leadlist_id`)
VALUES
diff --git a/app/bundles/CampaignBundle/Translations/en_US/messages.ini b/app/bundles/CampaignBundle/Translations/en_US/messages.ini
index 88324607b26..09e305bcf28 100644
--- a/app/bundles/CampaignBundle/Translations/en_US/messages.ini
+++ b/app/bundles/CampaignBundle/Translations/en_US/messages.ini
@@ -48,6 +48,7 @@ mautic.campaign.event.timed.choice.custom="Custom"
mautic.campaign.event.leadchange="contact changed campaigns"
mautic.campaign.event.leadchange_descr="Trigger actions when a contact is added/removed from a campaign."
mautic.campaign.event.reschedule="Reschedule this event."
+mautic.campaign.event.save="Save"
mautic.campaign.event.cancel="Cancel this event (it can be rescheduled later)."
mautic.campaign.event.cancelled="This event has been cancelled. Reschedule it to restore."
mautic.campaign.event.cancelled.time="This event was scheduled for %date% but has been cancelled."
diff --git a/app/bundles/CampaignBundle/Views/Campaign/events.html.php b/app/bundles/CampaignBundle/Views/Campaign/events.html.php
index c4e3dd26d39..9f8d0fcf1f5 100644
--- a/app/bundles/CampaignBundle/Views/Campaign/events.html.php
+++ b/app/bundles/CampaignBundle/Views/Campaign/events.html.php
@@ -29,7 +29,7 @@
- = $event['logCount']; ?> = $event['leadCount'] - $event['logCount']; ?>
+ = $event['logCount']; ?> = $event['leadCount'] - $event['logCountForPending']; ?>
diff --git a/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php b/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php
index 63297031a47..830c26d80b4 100644
--- a/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php
+++ b/app/bundles/CampaignBundle/Views/SubscribedEvents/Timeline/index.html.php
@@ -48,7 +48,10 @@
hasEntityAccess('lead:leads:editown', 'lead:leads:editother', $lead->getPermissionUser())): ?>
-
@@ -90,6 +91,7 @@
rowIfExists($fields, 'ip_lookup_service', $template); ?>
rowIfExists($fields, 'ip_lookup_auth', $template); ?>
+ rowIfExists($fields, 'ip_lookup_create_organization', $template); ?>
rowIfExists($fields, 'ip_lookup_config', '
{content}
'); ?>
diff --git a/app/bundles/CoreBundle/Views/Helper/pagination.html.php b/app/bundles/CoreBundle/Views/Helper/pagination.html.php
index f4ae2ff6c52..f1bf7ab5829 100644
--- a/app/bundles/CoreBundle/Views/Helper/pagination.html.php
+++ b/app/bundles/CoreBundle/Views/Helper/pagination.html.php
@@ -102,7 +102,7 @@
-